From bba4aa480c303d147bfb301e950fa56954907133 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Wed, 9 Sep 2026 22:49:05 -0400 Subject: [PATCH 001/136] Modularize map generation and add new generators --- SConstruct | 2 + data/texts.ar.txt | 36 + data/texts.br.txt | 36 + data/texts.ca.txt | 36 + data/texts.cz.txt | 36 + data/texts.de.txt | 36 + data/texts.dk.txt | 36 + data/texts.en.txt | 36 + data/texts.eo.txt | 36 + data/texts.es.txt | 36 + data/texts.eu.txt | 36 + data/texts.fa.txt | 36 + data/texts.fi.txt | 36 + data/texts.fr.txt | 36 + data/texts.gr.txt | 36 + data/texts.hu.txt | 36 + data/texts.id.txt | 36 + data/texts.it.txt | 36 + data/texts.ja.txt | 36 + data/texts.keys.txt | 18 + data/texts.ko.txt | 36 + data/texts.nl.txt | 36 + data/texts.pl.txt | 36 + data/texts.pt.txt | 36 + data/texts.ro.txt | 36 + data/texts.ru.txt | 36 + data/texts.si.txt | 36 + data/texts.sk.txt | 36 + data/texts.sr.txt | 36 + data/texts.sv.txt | 36 + data/texts.tr.txt | 36 + data/texts.uk.txt | 36 + data/texts.vi.txt | 36 + data/texts.zh-cn.txt | 36 + data/texts.zh-tw.txt | 36 + docs/map-generators/ADDING_A_GENERATOR.md | 80 + docs/map-generators/FRAMEWORK_UPGRADES.md | 40 + docs/map-generators/README.md | 97 +- docs/map-generators/refactor/RESULTS.md | 76 + docs/map-generators/refactor/REVIEW.md | 54 + .../baseline-confirmation-configs.json | 7 + .../baseline-confirmation-reproducibility.csv | 7 + .../refactor/baseline-confirmation.csv.gz | Bin 0 -> 255191 bytes docs/map-generators/refactor/catalog.json | 4753 +++++++++++++++++ .../map-generators/refactor/confirmation.json | 22 + docs/map-generators/refactor/coverage.png | Bin 0 -> 126170 bytes docs/map-generators/refactor/coverage.svg | 2832 ++++++++++ .../refactor/edges-configs.json | 1890 +++++++ .../refactor/edges-reproducibility.csv | 1249 +++++ docs/map-generators/refactor/edges.csv.gz | Bin 0 -> 40284 bytes .../refactor/framework-regression.json | 16 + docs/map-generators/refactor/generator-1.png | Bin 0 -> 42459 bytes docs/map-generators/refactor/generator-10.png | Bin 0 -> 42471 bytes docs/map-generators/refactor/generator-11.png | Bin 0 -> 39678 bytes docs/map-generators/refactor/generator-12.png | Bin 0 -> 35401 bytes docs/map-generators/refactor/generator-2.png | Bin 0 -> 38733 bytes docs/map-generators/refactor/generator-3.png | Bin 0 -> 44637 bytes docs/map-generators/refactor/generator-4.png | Bin 0 -> 42806 bytes docs/map-generators/refactor/generator-5.png | Bin 0 -> 44413 bytes docs/map-generators/refactor/generator-6.png | Bin 0 -> 44818 bytes docs/map-generators/refactor/generator-7.png | Bin 0 -> 52215 bytes docs/map-generators/refactor/generator-8.png | Bin 0 -> 43068 bytes docs/map-generators/refactor/generator-9.png | Bin 0 -> 38828 bytes .../modular-confirmation-configs.json | 7 + .../modular-confirmation-reproducibility.csv | 7 + .../refactor/modular-confirmation.csv.gz | Bin 0 -> 255104 bytes docs/map-generators/refactor/previews.json | 338 ++ .../refactor/source-hashes.json | 67 + docs/map-generators/refactor/summary.json | 364 ++ .../refactor/validation-configs.json | 42 + .../refactor/validation-reproducibility.csv | 49 + .../map-generators/refactor/validation.csv.gz | Bin 0 -> 590054 bytes src/CustomGameScreen.cpp | 111 +- src/CustomGameSetup.h | 32 +- src/EditorMainMenu.cpp | 107 +- src/GUIMapPreview.h | 1 - src/Game.h | 6 - src/Glob2.cpp | 17 +- src/NewMapScreen.cpp | 76 +- src/NewMapScreen.h | 54 +- src/PerlinNoise.cpp | 287 - src/PerlinNoise.h | 49 - src/SConscript | 44 +- src/map/Map.h | 8 +- src/map/MapTerrain.cpp | 80 +- src/map/generator/GameMaps.cpp | 85 - src/map/generator/Generator.cpp | 450 -- src/map/generator/GeneratorDivide.cpp | 237 - src/map/generator/GeneratorHeightmap.cpp | 144 - src/map/generator/GeneratorPoints.cpp | 198 - src/map/generator/GeneratorSplit.cpp | 262 - src/map/generator/HeightMapGenerator.cpp | 295 - src/map/generator/HeightMapGenerator.h | 52 - src/map/generator/MapGenerationDescriptor.cpp | 372 -- src/map/generator/MapGenerationDescriptor.h | 119 +- src/map/generator/MapGenerator.h | 125 +- src/map/generator/MapHomogen.cpp | 236 - src/map/generator/MapOldIslands.cpp | 287 - src/map/generator/MapOldIslandsRessources.cpp | 87 - src/map/generator/MapOldRandom.cpp | 454 -- src/map/generator/MapOldRandomRessources.cpp | 169 - src/map/generator/MapRandom.cpp | 308 -- .../LegacyGenerationDescriptor.cpp | 324 ++ .../LegacyGenerationDescriptor.h | 92 + src/map/generator/core/GenerationContext.cpp | 38 + src/map/generator/core/GenerationContext.h | 23 + src/map/generator/core/GenerationRequest.h | 64 + src/map/generator/core/GenerationResult.h | 29 + src/map/generator/core/GenerationService.cpp | 94 + src/map/generator/core/GenerationService.h | 17 + .../generator/core/GenerationValidation.cpp | 64 + src/map/generator/core/GenerationValidation.h | 9 + src/map/generator/core/GeneratorControls.cpp | 144 + src/map/generator/core/GeneratorControls.h | 34 + src/map/generator/core/GeneratorDefinition.h | 20 + src/map/generator/core/GeneratorRegistry.cpp | 79 + src/map/generator/core/GeneratorRegistry.h | 15 + .../generators/ConcreteIslandsGenerator.cpp | 186 + .../generators/ConcreteIslandsGenerator.h | 13 + .../generators/ContestedCommonsGenerator.cpp | 453 ++ .../generators/ContestedCommonsGenerator.h | 10 + .../generators/CraterLakesGenerator.cpp | 43 + .../generators/CraterLakesGenerator.h | 16 + .../generators/FjordContinentGenerator.cpp | 459 ++ .../generators/FjordContinentGenerator.h | 10 + .../generator/generators/IslandsGenerator.cpp | 44 + .../generator/generators/IslandsGenerator.h | 16 + .../generator/generators/IslesGenerator.cpp | 239 + src/map/generator/generators/IslesGenerator.h | 13 + .../generator/generators/LatticeGenerator.cpp | 103 + .../generator/generators/LatticeGenerator.h | 11 + .../generator/generators/MazeGenerator.cpp | 247 + src/map/generator/generators/MazeGenerator.h | 10 + .../generator/generators/RiverGenerator.cpp | 39 + src/map/generator/generators/RiverGenerator.h | 15 + .../generators/RuggedArchipelagoGenerator.cpp | 400 ++ .../generators/RuggedArchipelagoGenerator.h | 13 + .../generators/ShatteredCoastGenerator.cpp | 817 +++ .../generators/ShatteredCoastGenerator.h | 14 + .../generator/generators/SwampGenerator.cpp | 35 + src/map/generator/generators/SwampGenerator.h | 14 + .../generator/generators/UniformGenerator.cpp | 28 + .../generator/generators/UniformGenerator.h | 9 + src/map/generator/shared/Distances.cpp | 119 + src/map/generator/shared/Distances.h | 13 + src/map/generator/shared/Geometry.cpp | 81 + src/map/generator/shared/Geometry.h | 41 + src/map/generator/shared/HeightMap.cpp | 326 ++ src/map/generator/shared/HeightMap.h | 80 + src/map/generator/shared/Noise.cpp | 305 ++ src/map/generator/shared/Noise.h | 46 + src/map/generator/shared/Regions.cpp | 450 ++ src/map/generator/shared/Regions.h | 53 + src/map/generator/shared/Resources.cpp | 97 + src/map/generator/shared/Resources.h | 16 + src/map/generator/shared/Settlements.cpp | 88 + src/map/generator/shared/Settlements.h | 12 + .../generator/shared/StartingPositions.cpp | 351 ++ src/map/generator/shared/StartingPositions.h | 19 + src/map/generator/shared/Terrain.cpp | 313 ++ src/map/generator/shared/Terrain.h | 16 + src/map/generator/shared/Topology.cpp | 126 + src/map/generator/shared/Topology.h | 20 + test/CustomGameSetupHarness.cpp | 693 +-- test/MapGeneratorDefaultsTest.cpp | 217 +- test/MapGeneratorFrameworkChecks.h | 166 + test/MapGeneratorStudy.cpp | 158 +- test/PerlinNoiseTest.cpp | 8 +- test/SConstruct | 4 +- tools/map_generator_study.py | 33 +- tools/plot_map_generator_refactor.py | 159 + tools/plot_map_generator_study.py | 3 + 172 files changed, 21389 insertions(+), 4884 deletions(-) create mode 100644 docs/map-generators/ADDING_A_GENERATOR.md create mode 100644 docs/map-generators/FRAMEWORK_UPGRADES.md create mode 100644 docs/map-generators/refactor/RESULTS.md create mode 100644 docs/map-generators/refactor/REVIEW.md create mode 100644 docs/map-generators/refactor/baseline-confirmation-configs.json create mode 100644 docs/map-generators/refactor/baseline-confirmation-reproducibility.csv create mode 100644 docs/map-generators/refactor/baseline-confirmation.csv.gz create mode 100644 docs/map-generators/refactor/catalog.json create mode 100644 docs/map-generators/refactor/confirmation.json create mode 100644 docs/map-generators/refactor/coverage.png create mode 100644 docs/map-generators/refactor/coverage.svg create mode 100644 docs/map-generators/refactor/edges-configs.json create mode 100644 docs/map-generators/refactor/edges-reproducibility.csv create mode 100644 docs/map-generators/refactor/edges.csv.gz create mode 100644 docs/map-generators/refactor/framework-regression.json create mode 100644 docs/map-generators/refactor/generator-1.png create mode 100644 docs/map-generators/refactor/generator-10.png create mode 100644 docs/map-generators/refactor/generator-11.png create mode 100644 docs/map-generators/refactor/generator-12.png create mode 100644 docs/map-generators/refactor/generator-2.png create mode 100644 docs/map-generators/refactor/generator-3.png create mode 100644 docs/map-generators/refactor/generator-4.png create mode 100644 docs/map-generators/refactor/generator-5.png create mode 100644 docs/map-generators/refactor/generator-6.png create mode 100644 docs/map-generators/refactor/generator-7.png create mode 100644 docs/map-generators/refactor/generator-8.png create mode 100644 docs/map-generators/refactor/generator-9.png create mode 100644 docs/map-generators/refactor/modular-confirmation-configs.json create mode 100644 docs/map-generators/refactor/modular-confirmation-reproducibility.csv create mode 100644 docs/map-generators/refactor/modular-confirmation.csv.gz create mode 100644 docs/map-generators/refactor/previews.json create mode 100644 docs/map-generators/refactor/source-hashes.json create mode 100644 docs/map-generators/refactor/summary.json create mode 100644 docs/map-generators/refactor/validation-configs.json create mode 100644 docs/map-generators/refactor/validation-reproducibility.csv create mode 100644 docs/map-generators/refactor/validation.csv.gz delete mode 100644 src/PerlinNoise.cpp delete mode 100644 src/PerlinNoise.h delete mode 100644 src/map/generator/GameMaps.cpp delete mode 100644 src/map/generator/Generator.cpp delete mode 100644 src/map/generator/GeneratorDivide.cpp delete mode 100644 src/map/generator/GeneratorHeightmap.cpp delete mode 100644 src/map/generator/GeneratorPoints.cpp delete mode 100644 src/map/generator/GeneratorSplit.cpp delete mode 100644 src/map/generator/HeightMapGenerator.cpp delete mode 100644 src/map/generator/HeightMapGenerator.h delete mode 100644 src/map/generator/MapGenerationDescriptor.cpp delete mode 100644 src/map/generator/MapHomogen.cpp delete mode 100644 src/map/generator/MapOldIslands.cpp delete mode 100644 src/map/generator/MapOldIslandsRessources.cpp delete mode 100644 src/map/generator/MapOldRandom.cpp delete mode 100644 src/map/generator/MapOldRandomRessources.cpp delete mode 100644 src/map/generator/MapRandom.cpp create mode 100644 src/map/generator/compatibility/LegacyGenerationDescriptor.cpp create mode 100644 src/map/generator/compatibility/LegacyGenerationDescriptor.h create mode 100644 src/map/generator/core/GenerationContext.cpp create mode 100644 src/map/generator/core/GenerationContext.h create mode 100644 src/map/generator/core/GenerationRequest.h create mode 100644 src/map/generator/core/GenerationResult.h create mode 100644 src/map/generator/core/GenerationService.cpp create mode 100644 src/map/generator/core/GenerationService.h create mode 100644 src/map/generator/core/GenerationValidation.cpp create mode 100644 src/map/generator/core/GenerationValidation.h create mode 100644 src/map/generator/core/GeneratorControls.cpp create mode 100644 src/map/generator/core/GeneratorControls.h create mode 100644 src/map/generator/core/GeneratorDefinition.h create mode 100644 src/map/generator/core/GeneratorRegistry.cpp create mode 100644 src/map/generator/core/GeneratorRegistry.h create mode 100644 src/map/generator/generators/ConcreteIslandsGenerator.cpp create mode 100644 src/map/generator/generators/ConcreteIslandsGenerator.h create mode 100644 src/map/generator/generators/ContestedCommonsGenerator.cpp create mode 100644 src/map/generator/generators/ContestedCommonsGenerator.h create mode 100644 src/map/generator/generators/CraterLakesGenerator.cpp create mode 100644 src/map/generator/generators/CraterLakesGenerator.h create mode 100644 src/map/generator/generators/FjordContinentGenerator.cpp create mode 100644 src/map/generator/generators/FjordContinentGenerator.h create mode 100644 src/map/generator/generators/IslandsGenerator.cpp create mode 100644 src/map/generator/generators/IslandsGenerator.h create mode 100644 src/map/generator/generators/IslesGenerator.cpp create mode 100644 src/map/generator/generators/IslesGenerator.h create mode 100644 src/map/generator/generators/LatticeGenerator.cpp create mode 100644 src/map/generator/generators/LatticeGenerator.h create mode 100644 src/map/generator/generators/MazeGenerator.cpp create mode 100644 src/map/generator/generators/MazeGenerator.h create mode 100644 src/map/generator/generators/RiverGenerator.cpp create mode 100644 src/map/generator/generators/RiverGenerator.h create mode 100644 src/map/generator/generators/RuggedArchipelagoGenerator.cpp create mode 100644 src/map/generator/generators/RuggedArchipelagoGenerator.h create mode 100644 src/map/generator/generators/ShatteredCoastGenerator.cpp create mode 100644 src/map/generator/generators/ShatteredCoastGenerator.h create mode 100644 src/map/generator/generators/SwampGenerator.cpp create mode 100644 src/map/generator/generators/SwampGenerator.h create mode 100644 src/map/generator/generators/UniformGenerator.cpp create mode 100644 src/map/generator/generators/UniformGenerator.h create mode 100644 src/map/generator/shared/Distances.cpp create mode 100644 src/map/generator/shared/Distances.h create mode 100644 src/map/generator/shared/Geometry.cpp create mode 100644 src/map/generator/shared/Geometry.h create mode 100644 src/map/generator/shared/HeightMap.cpp create mode 100644 src/map/generator/shared/HeightMap.h create mode 100644 src/map/generator/shared/Noise.cpp create mode 100644 src/map/generator/shared/Noise.h create mode 100644 src/map/generator/shared/Regions.cpp create mode 100644 src/map/generator/shared/Regions.h create mode 100644 src/map/generator/shared/Resources.cpp create mode 100644 src/map/generator/shared/Resources.h create mode 100644 src/map/generator/shared/Settlements.cpp create mode 100644 src/map/generator/shared/Settlements.h create mode 100644 src/map/generator/shared/StartingPositions.cpp create mode 100644 src/map/generator/shared/StartingPositions.h create mode 100644 src/map/generator/shared/Terrain.cpp create mode 100644 src/map/generator/shared/Terrain.h create mode 100644 src/map/generator/shared/Topology.cpp create mode 100644 src/map/generator/shared/Topology.h create mode 100644 test/MapGeneratorFrameworkChecks.h create mode 100644 tools/plot_map_generator_refactor.py diff --git a/SConstruct b/SConstruct index f295acf2d..f6603dfcf 100644 --- a/SConstruct +++ b/SConstruct @@ -333,6 +333,8 @@ def main(): '#src/game/entities', '#src/gui', '#src/map', '#src/map/edit', '#src/map/generator', + '#src/map/generator/core', '#src/map/generator/generators', + '#src/map/generator/shared', '#src/map/generator/compatibility', '#src/map/gradient', '#src/map/io', '#src/map/pathfind', '#src/net', '#src/net/irc', '#src/net/message', '#src/sgsl', diff --git a/data/texts.ar.txt b/data/texts.ar.txt index a7496d3ef..6a018d6a5 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1770,3 +1770,39 @@ OpenGL غير متوفر في هذا الإصدار. عرض الجسر البري [Lake size] حجم البحيرة +[Contested commons] +الأراضي المشاع المتنازع عليها +[Lattice] +الشبكة +[Maze] +المتاهة +[Fjord continent] +قارة المضائق البحرية +[Islet size] +حجم الجزيرة الصغيرة +[Home radius] +نصف قطر القاعدة +[Cell size] +حجم الخلية +[Room size] +حجم الغرفة +[Corridor width] +عرض الممر +[Loopiness] +كثافة الحلقات +[Home island size] +حجم جزيرة البداية +[Commons size] +حجم الأراضي المشاع +[Moat width] +عرض الخندق المائي +[Bridge count] +عدد الجسور +[Continent size] +حجم القارة +[Coast roughness] +وعورة الساحل +[Fjord width] +عرض المضيق البحري +[Resource islands] +جزر الموارد diff --git a/data/texts.br.txt b/data/texts.br.txt index c6d1c4fea..6e8077fbf 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1768,3 +1768,39 @@ Largura do canal Largura da ponte de terra [Lake size] Tamanho do lago +[Contested commons] +Terras comuns disputadas +[Lattice] +Malha +[Maze] +Labirinto +[Fjord continent] +Continente de fiordes +[Islet size] +Tamanho da ilhota +[Home radius] +Raio da área inicial +[Cell size] +Tamanho da célula +[Room size] +Tamanho da sala +[Corridor width] +Largura do corredor +[Loopiness] +Densidade de ciclos +[Home island size] +Tamanho da ilha inicial +[Commons size] +Tamanho da área comum +[Moat width] +Largura do fosso +[Bridge count] +Número de pontes +[Continent size] +Tamanho do continente +[Coast roughness] +Irregularidade da costa +[Fjord width] +Largura do fiorde +[Resource islands] +Ilhas de recursos diff --git a/data/texts.ca.txt b/data/texts.ca.txt index e9e6c20ec..717726f77 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1780,3 +1780,39 @@ Amplada del canal Amplada del pont de terra [Lake size] Mida del llac +[Contested commons] +Terres comunals disputades +[Lattice] +Retícula +[Maze] +Laberint +[Fjord continent] +Continent de fiords +[Islet size] +Mida de l'illot +[Home radius] +Radi de l'àrea inicial +[Cell size] +Mida de la cel·la +[Room size] +Mida de la sala +[Corridor width] +Amplada del passadís +[Loopiness] +Densitat de bucles +[Home island size] +Mida de l'illa inicial +[Commons size] +Mida de l'àrea comuna +[Moat width] +Amplada del fossat +[Bridge count] +Nombre de ponts +[Continent size] +Mida del continent +[Coast roughness] +Irregularitat de la costa +[Fjord width] +Amplada del fiord +[Resource islands] +Illes de recursos diff --git a/data/texts.cz.txt b/data/texts.cz.txt index c07e91b77..9f8a075d1 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1772,3 +1772,39 @@ Automaticky zobrazovat přehled mapy ve tvaru torusu při pohybu po mapě (OpenG Šířka pevninského mostu [Lake size] Velikost jezera +[Contested commons] +Sporné společné území +[Lattice] +Mřížka +[Maze] +Bludiště +[Fjord continent] +Fjordový kontinent +[Islet size] +Velikost ostrůvku +[Home radius] +Poloměr domovské oblasti +[Cell size] +Velikost buňky +[Room size] +Velikost místnosti +[Corridor width] +Šířka chodby +[Loopiness] +Propojenost +[Home island size] +Velikost domovského ostrova +[Commons size] +Velikost společného území +[Moat width] +Šířka příkopu +[Bridge count] +Počet mostů +[Continent size] +Velikost kontinentu +[Coast roughness] +Členitost pobřeží +[Fjord width] +Šířka fjordu +[Resource islands] +Surovinové ostrovy diff --git a/data/texts.de.txt b/data/texts.de.txt index d80760c9d..2959a80e1 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1772,3 +1772,39 @@ Kanalbreite Breite der Landbrücke [Lake size] Seegröße +[Contested commons] +Umkämpftes Gemeinschaftsland +[Lattice] +Gitternetz +[Maze] +Labyrinth +[Fjord continent] +Fjordkontinent +[Islet size] +Inselchengröße +[Home radius] +Radius des Startgebiets +[Cell size] +Zellengröße +[Room size] +Raumgröße +[Corridor width] +Korridorbreite +[Loopiness] +Vernetzungsgrad +[Home island size] +Größe der Startinsel +[Commons size] +Größe des Gemeinschaftslands +[Moat width] +Grabenbreite +[Bridge count] +Anzahl der Brücken +[Continent size] +Kontinentgröße +[Coast roughness] +Küstenzerklüftung +[Fjord width] +Fjordbreite +[Resource islands] +Rohstoffinseln diff --git a/data/texts.dk.txt b/data/texts.dk.txt index 791b7b3ab..cffe067bb 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1848,3 +1848,39 @@ Kanalbredde Landbroens bredde [Lake size] Søstørrelse +[Contested commons] +Omstridt fællesområde +[Lattice] +Gitter +[Maze] +Labyrint +[Fjord continent] +Fjordkontinent +[Islet size] +Holmstørrelse +[Home radius] +Radius for startområde +[Cell size] +Cellestørrelse +[Room size] +Rumstørrelse +[Corridor width] +Korridorbredde +[Loopiness] +Løkketæthed +[Home island size] +Størrelse på startø +[Commons size] +Størrelse på fællesområde +[Moat width] +Voldgravsbredde +[Bridge count] +Antal broer +[Continent size] +Kontinentstørrelse +[Coast roughness] +Kystens ujævnhed +[Fjord width] +Fjordbredde +[Resource islands] +Ressourceøer diff --git a/data/texts.en.txt b/data/texts.en.txt index b3dfc3d06..dd8938dda 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1770,3 +1770,39 @@ Channel width Land bridge width [Lake size] Lake size +[Contested commons] +Contested commons +[Lattice] +Lattice +[Maze] +Maze +[Fjord continent] +Fjord continent +[Islet size] +Islet size +[Home radius] +Home radius +[Cell size] +Cell size +[Room size] +Room size +[Corridor width] +Corridor width +[Loopiness] +Loopiness +[Home island size] +Home island size +[Commons size] +Commons size +[Moat width] +Moat width +[Bridge count] +Bridge count +[Continent size] +Continent size +[Coast roughness] +Coast roughness +[Fjord width] +Fjord width +[Resource islands] +Resource islands diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 72f5c911d..a62fd49bd 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1770,3 +1770,39 @@ Larĝo de kanalo Larĝo de terponto [Lake size] Grandeco de lago +[Contested commons] +Pridisputata komunareo +[Lattice] +Krado +[Maze] +Labirinto +[Fjord continent] +Fjorda kontinento +[Islet size] +Grandeco de insuleto +[Home radius] +Radiuso de hejmareo +[Cell size] +Grandeco de ĉelo +[Room size] +Grandeco de ĉambro +[Corridor width] +Larĝo de koridoro +[Loopiness] +Bukleco +[Home island size] +Grandeco de hejminsulo +[Commons size] +Grandeco de komunareo +[Moat width] +Larĝo de fosaĵo +[Bridge count] +Nombro de pontoj +[Continent size] +Grandeco de kontinento +[Coast roughness] +Malglateco de marbordo +[Fjord width] +Larĝo de fjordo +[Resource islands] +Rimedinsuloj diff --git a/data/texts.es.txt b/data/texts.es.txt index f9ef275b7..a6270f2c6 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1772,3 +1772,39 @@ Anchura del canal Anchura del puente terrestre [Lake size] Tamaño del lago +[Contested commons] +Tierras comunes disputadas +[Lattice] +Retícula +[Maze] +Laberinto +[Fjord continent] +Continente de fiordos +[Islet size] +Tamaño del islote +[Home radius] +Radio del área inicial +[Cell size] +Tamaño de la celda +[Room size] +Tamaño de la sala +[Corridor width] +Anchura del pasillo +[Loopiness] +Densidad de bucles +[Home island size] +Tamaño de la isla inicial +[Commons size] +Tamaño del área común +[Moat width] +Anchura del foso +[Bridge count] +Número de puentes +[Continent size] +Tamaño del continente +[Coast roughness] +Irregularidad de la costa +[Fjord width] +Anchura del fiordo +[Resource islands] +Islas de recursos diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 65685d74a..bebd90675 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1782,3 +1782,39 @@ Kanalaren zabalera Lur-zubiaren zabalera [Lake size] Aintziraren tamaina +[Contested commons] +Lur komun lehiatuak +[Lattice] +Sareta +[Maze] +Labirintoa +[Fjord continent] +Fiordoen kontinentea +[Islet size] +Uharte txikiaren tamaina +[Home radius] +Hasierako eremuaren erradioa +[Cell size] +Gelaxkaren tamaina +[Room size] +Gelaren tamaina +[Corridor width] +Korridorearen zabalera +[Loopiness] +Begizta-maila +[Home island size] +Hasierako uhartearen tamaina +[Commons size] +Eremu komunaren tamaina +[Moat width] +Lubanarroaren zabalera +[Bridge count] +Zubi kopurua +[Continent size] +Kontinentearen tamaina +[Coast roughness] +Kostaldearen irregulartasuna +[Fjord width] +Fiordoaren zabalera +[Resource islands] +Baliabide-uharteak diff --git a/data/texts.fa.txt b/data/texts.fa.txt index c0021d92d..8ffd47448 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1770,3 +1770,39 @@ OpenGL در این بیلد موجود نیست. پهنای پل خشکی [Lake size] اندازهٔ دریاچه +[Contested commons] +سرزمین مشترک مورد مناقشه +[Lattice] +شبکه +[Maze] +هزارتو +[Fjord continent] +قارهٔ آبدره‌ای +[Islet size] +اندازهٔ جزیرهٔ کوچک +[Home radius] +شعاع پایگاه +[Cell size] +اندازهٔ سلول +[Room size] +اندازهٔ اتاق +[Corridor width] +پهنای راهرو +[Loopiness] +میزان حلقه‌دار بودن +[Home island size] +اندازهٔ جزیرهٔ آغازین +[Commons size] +اندازهٔ سرزمین مشترک +[Moat width] +پهنای خندق آبی +[Bridge count] +تعداد پل‌ها +[Continent size] +اندازهٔ قاره +[Coast roughness] +ناهمواری ساحل +[Fjord width] +پهنای آبدره +[Resource islands] +جزایر منابع diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 073828167..4143649b2 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1772,3 +1772,39 @@ Kanavan leveys Maasillan leveys [Lake size] Järven koko +[Contested commons] +Kiistelty yhteisalue +[Lattice] +Ruudukko +[Maze] +Sokkelo +[Fjord continent] +Vuonomainen manner +[Islet size] +Luodon koko +[Home radius] +Kotialueen säde +[Cell size] +Solun koko +[Room size] +Huoneen koko +[Corridor width] +Käytävän leveys +[Loopiness] +Silmukoiden määrä +[Home island size] +Kotisaaren koko +[Commons size] +Yhteisalueen koko +[Moat width] +Vallihaudan leveys +[Bridge count] +Siltojen määrä +[Continent size] +Mantereen koko +[Coast roughness] +Rannikon rikkonaisuus +[Fjord width] +Vuonon leveys +[Resource islands] +Luonnonvarasaaret diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 2c599e7e7..c56158645 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1782,3 +1782,39 @@ Largeur du chenal Largeur du pont terrestre [Lake size] Taille des lacs +[Contested commons] +Terres communes disputées +[Lattice] +Quadrillage +[Maze] +Labyrinthe +[Fjord continent] +Continent à fjords +[Islet size] +Taille de l'îlot +[Home radius] +Rayon de la zone de départ +[Cell size] +Taille des cellules +[Room size] +Taille des salles +[Corridor width] +Largeur des couloirs +[Loopiness] +Taux de boucles +[Home island size] +Taille de l'île de départ +[Commons size] +Taille de la zone commune +[Moat width] +Largeur des douves +[Bridge count] +Nombre de ponts +[Continent size] +Taille du continent +[Coast roughness] +Irrégularité des côtes +[Fjord width] +Largeur des fjords +[Resource islands] +Îles de ressources diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 8fcca2740..81931633f 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1842,3 +1842,39 @@ Cortex Πλάτος χερσαίας γέφυρας [Lake size] Μέγεθος λίμνης +[Contested commons] +Διεκδικούμενη κοινή γη +[Lattice] +Πλέγμα +[Maze] +Λαβύρινθος +[Fjord continent] +Ήπειρος με φιόρδ +[Islet size] +Μέγεθος νησίδας +[Home radius] +Ακτίνα αρχικής περιοχής +[Cell size] +Μέγεθος κελιού +[Room size] +Μέγεθος δωματίου +[Corridor width] +Πλάτος διαδρόμου +[Loopiness] +Βαθμός διασύνδεσης +[Home island size] +Μέγεθος αρχικού νησιού +[Commons size] +Μέγεθος κοινής περιοχής +[Moat width] +Πλάτος τάφρου +[Bridge count] +Αριθμός γεφυρών +[Continent size] +Μέγεθος ηπείρου +[Coast roughness] +Τραχύτητα ακτογραμμής +[Fjord width] +Πλάτος φιόρδ +[Resource islands] +Νησιά πόρων diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 6b32c5615..9f36e6172 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1772,3 +1772,39 @@ Csatorna szélessége Földhíd szélessége [Lake size] Tó mérete +[Contested commons] +Vitatott közös terület +[Lattice] +Rácsozat +[Maze] +Labirintus +[Fjord continent] +Fjordos kontinens +[Islet size] +Szigetecske mérete +[Home radius] +Kezdőterület sugara +[Cell size] +Cella mérete +[Room size] +Szoba mérete +[Corridor width] +Folyosó szélessége +[Loopiness] +Huroksűrűség +[Home island size] +Kezdősziget mérete +[Commons size] +Közös terület mérete +[Moat width] +Várárok szélessége +[Bridge count] +Hidak száma +[Continent size] +Kontinens mérete +[Coast roughness] +Partvonal tagoltsága +[Fjord width] +Fjord szélessége +[Resource islands] +Nyersanyagszigetek diff --git a/data/texts.id.txt b/data/texts.id.txt index df16711f0..cfd0f39da 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1768,3 +1768,39 @@ Lebar kanal Lebar jembatan darat [Lake size] Ukuran danau +[Contested commons] +Tanah bersama yang diperebutkan +[Lattice] +Kisi-kisi +[Maze] +Labirin +[Fjord continent] +Benua fjord +[Islet size] +Ukuran pulau kecil +[Home radius] +Radius markas +[Cell size] +Ukuran sel +[Room size] +Ukuran ruangan +[Corridor width] +Lebar koridor +[Loopiness] +Kepadatan jalur melingkar +[Home island size] +Ukuran pulau awal +[Commons size] +Ukuran tanah bersama +[Moat width] +Lebar parit air +[Bridge count] +Jumlah jembatan +[Continent size] +Ukuran benua +[Coast roughness] +Ketidakteraturan pantai +[Fjord width] +Lebar fjord +[Resource islands] +Pulau sumber daya diff --git a/data/texts.it.txt b/data/texts.it.txt index 686c87e0f..fc9a792b5 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1834,3 +1834,39 @@ Larghezza del canale Larghezza del ponte terrestre [Lake size] Dimensione del lago +[Contested commons] +Territori comuni contesi +[Lattice] +Reticolo +[Maze] +Labirinto +[Fjord continent] +Continente dei fiordi +[Islet size] +Dimensione dell'isolotto +[Home radius] +Raggio dell'area iniziale +[Cell size] +Dimensione delle celle +[Room size] +Dimensione delle stanze +[Corridor width] +Larghezza dei corridoi +[Loopiness] +Densità dei cicli +[Home island size] +Dimensione dell'isola iniziale +[Commons size] +Dimensione dell'area comune +[Moat width] +Larghezza del fossato +[Bridge count] +Numero di ponti +[Continent size] +Dimensione del continente +[Coast roughness] +Irregolarità della costa +[Fjord width] +Larghezza del fiordo +[Resource islands] +Isole di risorse diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 5658819b6..4a8db7a2d 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1768,3 +1768,39 @@ Globulation 2 のカーソルを表示します。 陸橋の幅 [Lake size] 湖の大きさ +[Contested commons] +争奪共有地 +[Lattice] +格子 +[Maze] +迷路 +[Fjord continent] +フィヨルド大陸 +[Islet size] +小島の大きさ +[Home radius] +拠点の半径 +[Cell size] +マスの大きさ +[Room size] +部屋の大きさ +[Corridor width] +通路の幅 +[Loopiness] +ループの多さ +[Home island size] +開始地点の島の大きさ +[Commons size] +共有地の大きさ +[Moat width] +堀の幅 +[Bridge count] +橋の数 +[Continent size] +大陸の大きさ +[Coast roughness] +海岸線の複雑さ +[Fjord width] +フィヨルドの幅 +[Resource islands] +資源島 diff --git a/data/texts.keys.txt b/data/texts.keys.txt index ad88e7155..ba31cb56f 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -883,3 +883,21 @@ [Channel width] [Land bridge width] [Lake size] +[Contested commons] +[Lattice] +[Maze] +[Fjord continent] +[Islet size] +[Home radius] +[Cell size] +[Room size] +[Corridor width] +[Loopiness] +[Home island size] +[Commons size] +[Moat width] +[Bridge count] +[Continent size] +[Coast roughness] +[Fjord width] +[Resource islands] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 5c2796baf..0d10a3b93 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1768,3 +1768,39 @@ Globulation 2 커서를 표시합니다. 육교 너비 [Lake size] 호수 크기 +[Contested commons] +쟁탈 공유지 +[Lattice] +격자 +[Maze] +미로 +[Fjord continent] +피오르 대륙 +[Islet size] +작은 섬 크기 +[Home radius] +기지 반경 +[Cell size] +칸 크기 +[Room size] +방 크기 +[Corridor width] +통로 너비 +[Loopiness] +순환로 밀도 +[Home island size] +시작 섬 크기 +[Commons size] +공유지 크기 +[Moat width] +해자 너비 +[Bridge count] +다리 수 +[Continent size] +대륙 크기 +[Coast roughness] +해안선 굴곡도 +[Fjord width] +피오르 너비 +[Resource islands] +자원 섬 diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 730b051f3..2959fe917 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1796,3 +1796,39 @@ Kanaalbreedte Breedte van de landbrug [Lake size] Meergrootte +[Contested commons] +Betwist gemeenschappelijk gebied +[Lattice] +Raster +[Maze] +Doolhof +[Fjord continent] +Fjordencontinent +[Islet size] +Grootte van het eilandje +[Home radius] +Straal van het startgebied +[Cell size] +Celgrootte +[Room size] +Kamergrootte +[Corridor width] +Gangbreedte +[Loopiness] +Aantal lussen +[Home island size] +Grootte van het starteiland +[Commons size] +Grootte van het gemeenschappelijke gebied +[Moat width] +Slotgrachtbreedte +[Bridge count] +Aantal bruggen +[Continent size] +Continentgrootte +[Coast roughness] +Grilligheid van de kust +[Fjord width] +Fjordbreedte +[Resource islands] +Grondstofeilanden diff --git a/data/texts.pl.txt b/data/texts.pl.txt index e5867139a..a88a98b7b 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1772,3 +1772,39 @@ Szerokość kanału Szerokość mostu lądowego [Lake size] Wielkość jeziora +[Contested commons] +Sporny teren wspólny +[Lattice] +Siatka +[Maze] +Labirynt +[Fjord continent] +Kontynent fiordowy +[Islet size] +Rozmiar wysepki +[Home radius] +Promień obszaru startowego +[Cell size] +Rozmiar komórki +[Room size] +Rozmiar pomieszczenia +[Corridor width] +Szerokość korytarza +[Loopiness] +Liczba pętli +[Home island size] +Rozmiar wyspy startowej +[Commons size] +Rozmiar terenu wspólnego +[Moat width] +Szerokość fosy +[Bridge count] +Liczba mostów +[Continent size] +Rozmiar kontynentu +[Coast roughness] +Urozmaicenie wybrzeża +[Fjord width] +Szerokość fiordu +[Resource islands] +Wyspy z surowcami diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 5fd8703fa..56f531e2b 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1778,3 +1778,39 @@ Largura do canal Largura da ponte terrestre [Lake size] Tamanho do lago +[Contested commons] +Terras comuns disputadas +[Lattice] +Grelha +[Maze] +Labirinto +[Fjord continent] +Continente de fiordes +[Islet size] +Tamanho do ilhéu +[Home radius] +Raio da área inicial +[Cell size] +Tamanho da célula +[Room size] +Tamanho da sala +[Corridor width] +Largura do corredor +[Loopiness] +Densidade de ciclos +[Home island size] +Tamanho da ilha inicial +[Commons size] +Tamanho da área comum +[Moat width] +Largura do fosso +[Bridge count] +Número de pontes +[Continent size] +Tamanho do continente +[Coast roughness] +Irregularidade da costa +[Fjord width] +Largura do fiorde +[Resource islands] +Ilhas de recursos diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 21f0c5f4e..b43125632 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1772,3 +1772,39 @@ Lățimea canalului Lățimea punții de uscat [Lake size] Mărimea lacului +[Contested commons] +Teritoriu comun disputat +[Lattice] +Grilă +[Maze] +Labirint +[Fjord continent] +Continent cu fiorduri +[Islet size] +Mărimea insuliței +[Home radius] +Raza bazei +[Cell size] +Mărimea celulei +[Room size] +Mărimea camerei +[Corridor width] +Lățimea coridorului +[Loopiness] +Densitatea buclelor +[Home island size] +Mărimea insulei de pornire +[Commons size] +Mărimea teritoriului comun +[Moat width] +Lățimea șanțului cu apă +[Bridge count] +Numărul de poduri +[Continent size] +Mărimea continentului +[Coast roughness] +Neregularitatea coastei +[Fjord width] +Lățimea fiordului +[Resource islands] +Insule cu resurse diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 19cbac446..699abc405 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1770,3 +1770,39 @@ OpenGL недоступен в этой сборке. Ширина перешейка [Lake size] Размер озера +[Contested commons] +Спорные общие земли +[Lattice] +Решётка +[Maze] +Лабиринт +[Fjord continent] +Материк с фьордами +[Islet size] +Размер островка +[Home radius] +Радиус базы +[Cell size] +Размер ячейки +[Room size] +Размер комнаты +[Corridor width] +Ширина коридора +[Loopiness] +Количество петель +[Home island size] +Размер стартового острова +[Commons size] +Размер общей земли +[Moat width] +Ширина рва +[Bridge count] +Количество мостов +[Continent size] +Размер материка +[Coast roughness] +Изрезанность побережья +[Fjord width] +Ширина фьорда +[Resource islands] +Острова с ресурсами diff --git a/data/texts.si.txt b/data/texts.si.txt index baa55b441..a68b19c87 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1772,3 +1772,39 @@ Samodejno prikaži pregled zemljevida v obliki torusa med premikanjem po zemljev Širina kopenskega mostu [Lake size] Velikost jezera +[Contested commons] +Sporno skupno ozemlje +[Lattice] +Mreža +[Maze] +Labirint +[Fjord continent] +Fjordska celina +[Islet size] +Velikost otočka +[Home radius] +Polmer baze +[Cell size] +Velikost celice +[Room size] +Velikost sobe +[Corridor width] +Širina hodnika +[Loopiness] +Gostota zank +[Home island size] +Velikost začetnega otoka +[Commons size] +Velikost skupnega ozemlja +[Moat width] +Širina vodnega jarka +[Bridge count] +Število mostov +[Continent size] +Velikost celine +[Coast roughness] +Razgibanost obale +[Fjord width] +Širina fjorda +[Resource islands] +Otoki z viri diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 22a960688..528b3cdfc 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1780,3 +1780,39 @@ Automaticky zobraziť prehľad mapy v tvare torusu pri pohybe po mape (OpenGL). Šírka pevninského mosta [Lake size] Veľkosť jazera +[Contested commons] +Sporné spoločné územie +[Lattice] +Mriežka +[Maze] +Bludisko +[Fjord continent] +Kontinent s fjordmi +[Islet size] +Veľkosť ostrovčeka +[Home radius] +Polomer základne +[Cell size] +Veľkosť bunky +[Room size] +Veľkosť miestnosti +[Corridor width] +Šírka chodby +[Loopiness] +Hustota slučiek +[Home island size] +Veľkosť štartovacieho ostrova +[Commons size] +Veľkosť spoločného územia +[Moat width] +Šírka vodnej priekopy +[Bridge count] +Počet mostov +[Continent size] +Veľkosť kontinentu +[Coast roughness] +Členitosť pobrežia +[Fjord width] +Šírka fjordu +[Resource islands] +Ostrovy so surovinami diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 23106af64..38110eea0 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1772,3 +1772,39 @@ OpenGL није доступан у овој верзији. Ширина копненог моста [Lake size] Величина језера +[Contested commons] +Спорно заједничко подручје +[Lattice] +Мрежа +[Maze] +Лавиринт +[Fjord continent] +Континент са фјордовима +[Islet size] +Величина острвцета +[Home radius] +Полупречник базе +[Cell size] +Величина ћелије +[Room size] +Величина собе +[Corridor width] +Ширина ходника +[Loopiness] +Учесталост петљи +[Home island size] +Величина почетног острва +[Commons size] +Величина заједничког подручја +[Moat width] +Ширина воденог рова +[Bridge count] +Број мостова +[Continent size] +Величина континента +[Coast roughness] +Разуђеност обале +[Fjord width] +Ширина фјорда +[Resource islands] +Острва са ресурсима diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 8a15e9678..25ff8cb10 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1782,3 +1782,39 @@ Kanalbredd Landbrons bredd [Lake size] Sjöstorlek +[Contested commons] +Omtvistad allmänning +[Lattice] +Rutnät +[Maze] +Labyrint +[Fjord continent] +Fjordkontinent +[Islet size] +Småöns storlek +[Home radius] +Basradie +[Cell size] +Cellstorlek +[Room size] +Rumsstorlek +[Corridor width] +Korridorbredd +[Loopiness] +Antal slingor +[Home island size] +Startöns storlek +[Commons size] +Allmänningens storlek +[Moat width] +Vallgravens bredd +[Bridge count] +Antal broar +[Continent size] +Kontinentstorlek +[Coast roughness] +Kustlinjens oregelbundenhet +[Fjord width] +Fjordbredd +[Resource islands] +Resursöar diff --git a/data/texts.tr.txt b/data/texts.tr.txt index c35b16ce6..812784e39 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1780,3 +1780,39 @@ Kanal genişliği Kara köprüsü genişliği [Lake size] Göl boyutu +[Contested commons] +Çekişmeli ortak alan +[Lattice] +Kafes +[Maze] +Labirent +[Fjord continent] +Fiyort kıtası +[Islet size] +Adacık boyutu +[Home radius] +Üs yarıçapı +[Cell size] +Hücre boyutu +[Room size] +Oda boyutu +[Corridor width] +Koridor genişliği +[Loopiness] +Döngü yoğunluğu +[Home island size] +Başlangıç adası boyutu +[Commons size] +Ortak alan boyutu +[Moat width] +Su hendeği genişliği +[Bridge count] +Köprü sayısı +[Continent size] +Kıta boyutu +[Coast roughness] +Kıyı girintiliği +[Fjord width] +Fiyort genişliği +[Resource islands] +Kaynak adaları diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 122a0c6c6..32461e4c8 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1768,3 +1768,39 @@ OpenGL недоступний у цій збірці. Ширина перешийка [Lake size] Розмір озера +[Contested commons] +Спірні спільні землі +[Lattice] +Ґратка +[Maze] +Лабіринт +[Fjord continent] +Материк із фіордами +[Islet size] +Розмір острівця +[Home radius] +Радіус бази +[Cell size] +Розмір комірки +[Room size] +Розмір кімнати +[Corridor width] +Ширина коридору +[Loopiness] +Кількість петель +[Home island size] +Розмір стартового острова +[Commons size] +Розмір спільної землі +[Moat width] +Ширина рову +[Bridge count] +Кількість мостів +[Continent size] +Розмір материка +[Coast roughness] +Порізаність узбережжя +[Fjord width] +Ширина фіорду +[Resource islands] +Острови з ресурсами diff --git a/data/texts.vi.txt b/data/texts.vi.txt index a8b257ddc..48fe16196 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1768,3 +1768,39 @@ Tự động hiển thị tổng quan bản đồ hình xuyến khi di chuyển Độ rộng cầu đất [Lake size] Kích thước hồ +[Contested commons] +Vùng đất chung tranh chấp +[Lattice] +Mạng lưới +[Maze] +Mê cung +[Fjord continent] +Lục địa vịnh hẹp +[Islet size] +Kích thước đảo nhỏ +[Home radius] +Bán kính căn cứ +[Cell size] +Kích thước ô +[Room size] +Kích thước phòng +[Corridor width] +Độ rộng hành lang +[Loopiness] +Mật độ vòng lặp +[Home island size] +Kích thước đảo khởi đầu +[Commons size] +Kích thước vùng đất chung +[Moat width] +Độ rộng hào nước +[Bridge count] +Số lượng cầu +[Continent size] +Kích thước lục địa +[Coast roughness] +Độ khúc khuỷu của bờ biển +[Fjord width] +Độ rộng vịnh hẹp +[Resource islands] +Đảo tài nguyên diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index fbbb43596..44d06c149 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1768,3 +1768,39 @@ OpenGL 在此版本中不可用。 陆桥宽度 [Lake size] 湖泊大小 +[Contested commons] +争夺公地 +[Lattice] +网格 +[Maze] +迷宫 +[Fjord continent] +峡湾大陆 +[Islet size] +小岛大小 +[Home radius] +基地半径 +[Cell size] +单元格大小 +[Room size] +房间大小 +[Corridor width] +通道宽度 +[Loopiness] +回环程度 +[Home island size] +起始岛屿大小 +[Commons size] +公地区域大小 +[Moat width] +护城河宽度 +[Bridge count] +桥梁数量 +[Continent size] +大陆大小 +[Coast roughness] +海岸线曲折度 +[Fjord width] +峡湾宽度 +[Resource islands] +资源岛屿 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 414321201..1b89bb199 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1844,3 +1844,39 @@ OpenGL 在此版本中不可用。 陸橋寬度 [Lake size] 湖泊大小 +[Contested commons] +爭奪公地 +[Lattice] +網格 +[Maze] +迷宮 +[Fjord continent] +峽灣大陸 +[Islet size] +小島大小 +[Home radius] +基地半徑 +[Cell size] +單元格大小 +[Room size] +房間大小 +[Corridor width] +通道寬度 +[Loopiness] +迴路密度 +[Home island size] +起始島嶼大小 +[Commons size] +公地區域大小 +[Moat width] +護城河寬度 +[Bridge count] +橋樑數量 +[Continent size] +大陸大小 +[Coast roughness] +海岸線曲折度 +[Fjord width] +峽灣寬度 +[Resource islands] +資源島嶼 diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md new file mode 100644 index 000000000..160eba1c0 --- /dev/null +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -0,0 +1,80 @@ +# Adding a map generator + +A generator is a registered module, not a new branch in a central dispatch switch. Its controls drive the editor, custom-game lobby, default initialization, range validation, mode history, stress runner and study catalog. + +## Module boundaries + +- **`core/`** owns requests, control metadata, registration, seed streams, validation and results. It does not implement terrain recipes. +- **`generators/`** contains one header/source pair per generator. The header defines its named options; the source owns its controls and ordered generation steps. Generator-specific terrain and resource routines remain local to that source. +- **`shared/`** contains height fields, instance-owned noise, terrain classification, region geometry, distance fields, resource filling and starting-position helpers. These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. +- **`compatibility/`** owns the historical descriptor codec and overloaded-field conversion. New generators do not add fields to that descriptor. `MapGenerator.h` is only a small compatibility facade for older callers. + +`Map` retains general terrain-editing operations, including `rebuildTerrain()` for bulk undermap edits. `Game` retains team, unit and building mutation APIs. Neither dispatches generation algorithms. + +## Register a module + +1. Add `ExampleGenerator.h/.cpp` under `src/map/generator/generators/`. Use a named options struct that reads its scalar values from the request by stable option ID. +2. Return a `GeneratorDefinition` from `exampleDefinition()`. Provide a stable string ID, unique nonnegative numeric ID, translation key, revision, editor-only availability, control list and callback. Numeric IDs are identifiers, never list positions; gaps are supported. `hasStartingColonies` defaults to true. Uniform alone disables normal starting-colony validation in the shipped catalog. +3. Add that definition to the single list in `GeneratorRegistry::builtins()` and its source to `src/SConscript`. Add the display name and control labels to the translation tables using the existing translation workflow. +4. Write the callback as `bool generate(Game&, GenerationContext&)`. Read the immutable request, build into the supplied fresh game, set `context.stage` before operations that may fail, and return false with a useful `context.detail` if the layout cannot be placed. + +Each `GeneratorControl` defines `id`, translation label, minimum, maximum, step, default, group, power-of-two display formatting and whether the value participates in terrain weighting. The all-zero-weight check reads this metadata, not generator-specific option names. IDs must be unique within a generator and cannot shadow shared controls. For irregular integer domains, supply a sorted, unique `allowedValues` list such as `{4, 8, 16}` with matching minimum and maximum. These are literal stored values; `powerOfTwo` instead formats stored exponents. Use `values()`, `indexOf()`, `valueAt()` and `displayValue()` in consumers, never selection-index arithmetic. Use `GeneratorControl::set()` for UI normalization. The service rejects malformed requests instead of silently changing their settings. + +Shared settings are width and height exponents, colony count, starting workers and background terrain. The request's `options` map contains only controls for its selected generator. Generator-specific structs give these values meaningful names such as `lake_size`, `channel_width` and `bridge_width`; there is no reuse of unrelated descriptor slots. + +Switch modes with `GenerationHistory::select()`. It remembers options per stable numeric ID and carries the shared controls between modes. Do not add UI-specific defaults or range tables. + +## Generation, randomness and failure + +Choose the generation sequence appropriate to the map's character. Modern height-field modes classify terrain, choose starting locations, add resources, then place colonies. Legacy modes place colonies before their final resource pass. Concrete Islands and Isles deliberately interleave region division, resources and starts. Shared helpers do not impose a universal pipeline. + +Use `context.stream("name")` for a per-attempt `std::mt19937` stream, or `context.bounded("name", count)` for a rejection-sampled bounded choice. Seed derivation is defined in `GenerationContext.cpp`: unsigned FNV-1a mixed with the root seed, followed by a fixed integer avalanche. Do not use `std::hash`, wall-clock reseeding, `rand()`, `srand()` or the gameplay RNG in generator algorithms. Noise tables and stamp caches belong to each height-map instance. + +General engine mutation functions still consume the synchronized gameplay RNG. `GenerationService` seeds and restores it with an RAII scope. This bridge is synchronous; this refactor does not make `Game` generation thread-safe. Independent processes can generate concurrently. + +The same seed, generator revision and settings reproduce a world within this implementation on the same platform. Historical seeds intentionally do not promise the same maps, and cross-platform floating-point identity is not a supported contract. Bump a generator's revision when its output changes deliberately after this framework lands. + +`GenerationService::generate()` returns generator ID, revision, seed, stage, error code and detail. It validates requests before world mutation and verifies dimensions, colony counts, starting swarms and worker counts afterward. The quantitative start proxy is an offline diagnostic, not an automatic terrain repair rule. + +A failed candidate may contain partial terrain or colonies. Discard it; never reuse it as another attempt or publish it. The lobby creates a fresh game for each of its five attempts, records derived seeds and launches the successful serialized preview. The editor similarly only opens a successfully generated fresh game. + +## Shapes, regions and constrained starts + +Use `shared/Geometry` for transformed radial shapes and label-grid stamping. Supply a named context stream; query `maximumRadius()` when budgeting space around the shape and account for transform stretch. Shape rasterization has explicit clipped/wrapped behavior and does not guarantee connectivity after other edits. + +Use `shared/Topology` for graph distances, sparse-label adjacency and passable-grid components. Choose cardinal or eight-neighbor adjacency and wrapping explicitly. These are geometry tools, not a substitute for the engine's unit pathfinding rules. Construct the appropriate passability mask for the invariant being checked. + +`Regions::splitUpPoints` keeps its existing local search by default. Whole-region weighted search is optional, bounded, and does not promise a global optimum. Weights travel with points during the final shuffle. For a single site, its spacing budget is half the shorter map dimension. Weights are positive integers up to 10,000. + +`placeSettlement` takes an explicit home mask and preferred building anchor. It checks the entire swarm footprint, places every requested worker, records the start and returns a diagnostic on failure. Create teams and bake the terrain first. Keep resource policy in the generator, before or after settlement as appropriate. Discard failed candidates even if a swarm has already been placed. + +Add optional pure `validateRequest` and `validateWorld` callbacks to the definition for relationships between settings and generator-specific invariants. Return an empty string on success or useful diagnostic detail on failure. Request validation is shared by the editor, lobby and service. Final validation runs after the common structural checks. Validate the finished terrain when later carving, resources or buildings can alter connectivity; never silently change the request to satisfy an invariant. + +[Framework upgrades for the new concepts](FRAMEWORK_UPGRADES.md) explains the motivating cases and the terrain, placement and resource bugs found during integration. + +## Compatibility and verification + +Old numeric IDs and descriptor bytes remain stable. The adapter resolves historical lake/channel/bridge sentinels and resource amounts; save files and replay files continue to contain full worlds. New options have no implicit legacy encoding. + +Build and run the contract checks: + +```sh +scons release=1 -j12 map-generator-study map-generator-defaults-test custom-setup-test +build/src/MapGeneratorDefaultsTest glob2-generator-contracts +build/src/CustomGameSetupHarness +``` + +The defaults test injects a test-only generator at ID 101, constructs its actual editor controls, edits its custom option, and generates a map. It also checks defaults, ranges, mode memory, codec roundtrips, legacy sentinels, interleaved repeatability, gameplay RNG restoration and structured failures. + +Run all registered playable defaults without maintaining another generator list: + +```sh +python3 tools/map_generator_study.py --count 1000 --start 20001 \ + --output artifacts/map-generator-refactor --label validation +python3 tools/plot_map_generator_refactor.py \ + artifacts/map-generator-refactor/validation.csv +``` + +For targeted settings, pass a JSON configuration list with `id`, `method` and a `params` object whose keys are stable control IDs. A sample uses registered defaults plus those overrides. Out-of-range values fail explicitly. `--binary` selects a separately built historical executable for comparison. + +Before accepting a generator or structural refactor, inspect fixed-seed previews and poor-performing examples, exercise range endpoints and crowded/rectangular maps, and investigate changes beyond the documented statistical thresholds. Historical PR #238 reports remain under `study/`; current refactor evidence is under `refactor/`. diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md new file mode 100644 index 000000000..0cc3d1c44 --- /dev/null +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -0,0 +1,40 @@ +# Framework upgrades for the new map concepts + +The final concepts from `/Users/bradley/glob2-mapgen` are incorporated as four independent modules: Contested Commons, Lattice, Maze and Fjord Continent. The source worktree remains unchanged. Each module owns its options and ordered recipe while reusing bounded radial geometry, graph topology, constrained settlement placement and clumped resource helpers. + +## Generalized capabilities + +| Requirement observed in the prototypes | Framework support | Responsibility retained by a future generator | +|---|---|---| +| Literal cell sizes and odd corridor widths | `GeneratorControl::allowedValues`, domain indexing, centralized normalization/display and catalog export | Named options, domains and tuned defaults | +| Rooms fitting cells, dimensions divisible by a cell size | Pure `validateRequest` callback, consumed by service, lobby and editor | Cross-control and dimension constraints; no silent settings rewrites | +| Jagged islands and stretched/rotated continents | `shared/Geometry`: invertible shape transform, seeded radial shape, conservative radius bound, label-grid rasterization | Coastline recipe, fjord paths, moat geometry and bridge endpoints | +| Joint dispersion of homes and shared objectives | Optional whole-region search in `Regions`, paired weight shuffling, convergence and evaluation budgets | Weight policy and checks on actual separation; best-response is not an equal-distance or global-optimum guarantee | +| Distances through a maze, connected peninsulas, neighboring resource zones | `shared/Topology`: graph distances, grid components with explicit wrapping/neighbor policy, sparse-label adjacency | Graph construction, corridor carving and resource-role assignment | +| Settlements restricted to a home island or room | `shared/Settlements`: whole-footprint mask, nearest legal anchor, exact worker count, per-colony diagnostics | Home mask, anchor choice and when to place starter resources | +| Generator-specific connectivity guarantees | Optional `validateWorld` callback after structural checks | Checking actual finished terrain or movement masks, with useful error detail | + +All helpers are ordinary functions or small value objects. There is no new generator superclass, pipeline dispatcher, terrain repair policy or serialization extension. Geometry and topology operate on dimensions and grids, independently of `Game`. Settlements use `Game` because buildings and units require its mutation APIs. + +## Bugs and safeguards + +- The point splitter previously shuffled points without weights. It now moves both together. Existing callers use uniform weights, so their sampled default outputs are unchanged. +- Whole-region dispersion is opt-in. It fails explicitly after the configured pass limit or 20 million distance evaluations rather than silently treating an unfinished search as convergence. Invalid weight arrays and insufficient region capacity are rejected. A single site uses half the shorter map dimension as its finite spacing budget. +- The prototypes multiply coastline amplitudes by as much as 1.4 but budget space using `radius * (1 + roughness)`. The new radial primitive bounds the amplitude sum and exposes its actual conservative maximum radius. Future moat/outlier placement must include the transform's stretch and both shapes' extents. +- The prototype settlement helper can succeed with fewer workers than requested. The shared helper fails with the requested/available count and the service retains its exact-count structural check. The entire swarm footprint must fit inside the home mask. +- The editor previously placed every Layout control at the same vertical position. Controls now occupy independent rows. Literal discrete values and exponent display use the same metadata in both UIs. +- Ellipse rasterization used 32-bit products that can overflow at supported large dimensions. Intermediate arithmetic is now 64-bit. Resource patch sizes are checked before modulo operations and engine assertions. +- Lattice and the prototype maze placed grass directly beside water. Those undermap combinations render as grass in the terrain lookup, so the apparent water pattern was not gameplay water. Both modules now budget shoreline width before calling `controlSand()` and expose dimensions for the finished terrain. +- Maze uses 20, 24 or 32-tile cells, larger home rooms and compact starter deposits. Closed graph edges carry a continuous one-tile stone seam on grass with sand and water on both sides, while connected edges remain open passages. +- Fjord deposits wheat and wood in compact patches on both banks of every fjord. Bank deposits are placed last and shared clump placement never overwrites another resource type. Single-colony maps skip the vacuous bank requirement. +- Lattice uses larger home clearings and puts wheat and wood on opposite rims. Its tiny repeating islets remain the dominant visual pattern without starving the colonies of construction space. + +The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. + +## Verification + +Synthetic test registrations cover irregular domains, multiple Layout controls, mode history, generation and request/world validation without adding production catalog entries. Helper checks cover transform roundtrips, radial bounds, wrapping, cardinal versus diagonal components, sparse region IDs, disconnected graphs, invalid graph inputs, weighted dispersion, home footprints, exact workers and gameplay RNG restoration. + +The final run covers 12,000 attempts (1,000 per playable generator, seeds 20001–21000). The eight PR #238 generators retain their controls and statistical behavior; only Shattered Coast retains its previously investigated failure-rate flag. Lattice generated 1,000/1,000 with 932 all-team proxy passes, Maze 1,000/1,000 with 999 passes, and Fjord Continent 1,000/1,000 with 995 passes. Contested Commons generated 999/1,000; its separated home-island economy intentionally scores lower on the generic start proxy. See [the study and charts](refactor/RESULTS.md) and [the module guide](ADDING_A_GENERATOR.md). + +The original 832-attempt edge suite remains recorded under `refactor/`. A further 320 attempts cover every new generator at control extremes, rectangular large maps, solo Fjord, 64×64 maps and crowded eight-colony layouts. Supported combinations repeat deterministically; impossible crowded cases fail cleanly during validation or settlement placement. Native SDL flows pass for all three control modes, along with save/load, replay and editor control checks. diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index efc39c023..680b19f3a 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -1,13 +1,94 @@ # Map generators -The map editor, custom-game lobby and study tools share generator control definitions, -including labels, ranges, steps and defaults. Uniform terrain is editor-only. -Normal lobby generation rolls a fresh random map; the preview snapshot is the map launched. +The generators now use a modular registry and per-attempt random state. See the [module and extension guide](ADDING_A_GENERATOR.md) and [refactor measurements and map previews](refactor/RESULTS.md). The tuning study below is the frozen PR #238 baseline. + +Random maps now start with generator-specific recipes that leave more grassy building room. The custom-game lobby from PR #237 and the map editor use the same names, available controls, ranges, steps, defaults, and per-method setting memory. Old Random is **Shattered Coast**; Old Islands is **Rugged Archipelago**. The modular catalog also includes **Contested Commons**, **Lattice**, **Maze**, and **Fjord Continent**. ## Development tools -Build the study executable with `scons release=1 map-generator-study`. -`build/src/MapGeneratorStudy --catalog` exports the current control definitions. -Use `python3 tools/map_generator_study.py --help` for seeded generation studies. -Study results and screenshots belong in ignored `artifacts/`, not in this directory. -Record investigation findings and validation results in the relevant pull request. +[Exact results and control reference](RESULTS.md) · [Raw validation results](study/validation.csv.gz) · [Source catalog snapshot](study/catalog.json) + +## One source of truth + +Each module under `src/map/generator/generators/` owns its control definitions, named options and generation sequence. The registry supplies those definitions to both UIs and the analysis tools. `GenerationRequest::setMethodDefaults()` applies the registered defaults; `GenerationHistory` remembers per-generator options while carrying shared settings between modes. The historical descriptor is isolated in `compatibility/`. + +Both `CustomGameScreen` and `NewMapScreen` render these definitions. The command-line generation stress tool samples the same valid ranges and steps for all 12 procedural modes. The lobby keeps its Terrain / Resources / Layout sections and scrolling; the editor uses the existing native Number widgets. The lobby's starting-worker rules control reads the same shared definition. A shared terrain-weight check rejects an all-zero terrain recipe in both screens. + +The study runner requests the compiled presets. The plotting tool requests `MapGeneratorStudy --catalog` and resolves names from the English translation table; the documentation and charts therefore do not maintain another copy of the current presets or names. Checked-in JSON/CSV is a historical measurement snapshot, not runtime configuration. Previous-setting cohorts remain explicit frozen configurations so later default changes cannot alter the baseline. + +All 33 language tables have the renamed generators and new labels. Existing translation keys, enum values and descriptor byte layout remain stable. Translation structure, placeholders and English-fallback regression checks pass; the new translations have not received native-speaker review. + +## What changed + +- Tune all eight procedural generators; Uniform retains its terrain selector. +- Add Crater Lakes lake size (10–40 in steps of 5), Concrete Islands channel width (5–8) and extra islands (0–6), and Isles island size (45–65 in steps of 5) and land bridge width (3–6). +- Terrain weights now allow 0–100, so the measured grass default of 75 is selectable. River width spans the explored 20–65 range in steps of 5; lake density spans 10–50 in steps of 5. Rugged Archipelago island size spans 50–70 and includes its default of 65. Existing smoothing, beach, fruit and repeat controls retain their established bounds. +- Remove the wheat, wood, stone and algae ratio widgets, whose serialized fields are not read by resource placement. Fruit remains configurable in the four modern height-map modes. Existing resource-placement algorithms are preserved. +- Restore the original legacy resource functions removed as unused code on newer master, then call them after placing starting bases/workers. Fix Rugged Archipelago base validation to use the current team instead of indexing team -1. +- Parameterize existing channel, bridge, island and crater constants without replacing the generation algorithms. +- Add Contested Commons, Lattice, Maze and Fjord Continent as registered modules. Their layout controls come from the same metadata as both UIs and the study catalog. +- Fix Lattice and Maze shoreline generation, enlarge their playable home areas, and use clumped resources. Maze uses coarse 20/24/32-tile cells and stone seams inside its water walls. Fjord guarantees clumped wheat and wood on both sides of every fjord. + +## Why these bounds and defaults + +The initial search and shortlists used 23,936 attempts, followed by 768 range-corner and 1,024 size/team checks. Configuration manifests and aggregate results are in [study/search-summary.csv](study/search-summary.csv). A separate original 16,000-attempt holdout supported the initial choices. This rebased implementation was then checked with a fresh **24,000-attempt comparison**: 1,000 fixed seeds for each of eight generators in three cohorts (new defaults, PR #237 lobby settings, and previous editor settings). + +The same seeds appear in multiple configurations; attempt counts are not counts of globally unique seeds. Selection balanced building space, starting resource access, generation reliability and recognizable terrain rather than maximizing grass alone. + +- Concrete widths 3–4 removed most or all water; widths 5–8 keep separate territories. Extra-island counts through 8 were explored; the UI stops at 6 to limit crowding. +- Isles sizes 35–65 and bridge widths 3–7 were explored. The selected ranges retain visible islands and narrow connecting bridges. The default size is 60, with the original bridge setting of 4. +- Crater size 25 and density 25 retain many round lakes with nearby resources. Large sparse lakes can leave resources farther from starts despite having plenty of grass. +- Rugged Archipelago size 70 approached merging islands in inspected samples. The default 65 increases interiors while retaining water separation. +- Shattered Coast keeps its rough patchwork coastlines with smoothing 3. Its improved space and resource access carry a small reliability tradeoff, shown in RESULTS.md. + +The conventional 0–100 terrain-weight domain is not a guarantee that every combination is usable. The measured recipes and new shape-control corners received the strongest coverage. + +## Visual review + +All eight final presets were inspected on fixed seeds 22001, 22002 and 22003, without replacing seeds. These tile-class views come from production maps; they show terrain, wheat, wood, stone and buildings. Algae, fruit and units are not separately colored. Wrapped edges can split a river or island across opposite sides of an image. + +![Same-seed map gallery](maps.png) + +[Swamp](generator-1.png) · [River](generator-2.png) · [Islands](generator-3.png) · [Crater Lakes](generator-4.png) · [Concrete Islands](generator-5.png) · [Isles](generator-6.png) · [Shattered Coast](generator-7.png) · [Rugged Archipelago](generator-8.png) + +The native lobby captures below exercise edited values, including five-unit increments and remembered per-mode settings. They show an update pending immediately after editing; automatic generation and snapshot launching are exercised separately by the same harness. + +| Concrete Islands | Isles | Crater Lakes | +|---|---|---| +| ![Channels](concrete-controls.png) | ![Island and bridge size](isles-controls.png) | ![Lake size](crater-controls.png) | + +| Rugged Archipelago | Shattered Coast | +|---|---| +| ![Rugged Archipelago](rugged-archipelago-controls.png) | ![Shattered Coast](shattered-coast-controls.png) | + +## Measurements and limitations + +Grass uses `Map::isGrass`; immediately free building tiles use `Map::isFreeForBuilding`. Clear 4×4 positions are valid top-left anchors, which overlap and must not be interpreted as independent buildings. Percentages exclude failed maps; every attempted seed and failure is retained in the raw data. + +The all-team start proxy requires each team to have at least 16 valid 4×4 anchors within 24 walking steps of its starting workers, reachable wheat within 24 steps, and wood within 32 steps. The flood follows the hard-space predicate with eight-neighbor movement and toroidal edges. This is a comparative diagnostic, not a complete pathfinding/economy simulation or a guarantee of fairness. + +Both prior-setting cohorts include the same legacy correctness fixes to isolate tuning. Without those fixes, the legacy modes lack their intended resources, and Rugged Archipelago can assert during base placement. The original study's main chart measured constructor settings, not the editor's usual timer-applied zero sand/desert settings; this PR records both relevant baselines explicitly. + +Placement failures still occur. Against the repaired PR #237 lobby baseline, River failures rise from 4 to 18 per 1,000 and Shattered Coast from 0 to 14, while their all-team start-proxy passes rise from 177 to 800 and 281 to 823. Against the previous editor baseline, River failures fall from 145 to 18. These are deliberate building-space/resource-access tradeoffs, not a claim of universally improved generation reliability. The earlier size/team checks expose small crowded maps: at 64×64/four teams, River generated 16/32 and Shattered Coast 17/32; at 128×128/eight teams, 21/32 and 22/32 respectively. These defaults target 128×128/four teams, not every dimension/roster combination. No full-match balance study was performed. + +Old saved descriptors retain their byte layout, but regenerated terrain intentionally changes. The previously unused Concrete Islands extra-island value zero now requests zero fixed neutral islands; a negative sentinel preserves the old random count for the frozen baseline. Legacy unused diameter 50 still selects original channel/bridge/crater constants. Mixed-version multiplayer generation was not tested. + +## Reproduction and checks + +From the repository root: + +```sh +scons release=1 -j12 map-generator-study map-generator-defaults-test custom-setup-test build/src/glob2 +mkdir -p artifacts/map-generator-validation/lobby +build/src/MapGeneratorDefaultsTest glob2-map-defaults-test artifacts/map-generator-validation +build/src/CustomGameSetupHarness +build/src/CustomGameSetupHarness artifacts/map-generator-validation/lobby +python3 data/check_translations.py --strict +python3 test/test_translations.py +python3 tools/map_generator_study.py --configs docs/map-generators/study/validation-configs.json --count 1000 --start 20001 --label validation --workers 8 +python3 tools/plot_map_generator_study.py artifacts/map-generator-validation/validation.csv +``` + +The plotting script requires NumPy and Matplotlib. Each sample runs in a fresh process; the dedicated executable overrides its clock to `1700000000 + seed` because the production generators reseed internally. It seeds Perlin noise and `srand`, and supplies the optional synchronized-RNG seed to `MapGenerator::generateMap` so PR #237’s `random_device` reseeding cannot override the study. Production UI calls omit that argument and retain random seeding. Before sampling, the runner checks three seed positions per configuration in independent repeated processes and aborts on any mismatch. Runner profiles are disposable and cleaned up automatically. UI harness profiles use explicit test names. + +The shared-control regression visits every selectable editor value, checks defaults and step normalization, compares lobby/editor method memory, and verifies serialization round trips. The native lobby harness exercises new dropdown/stepper controls, automatic preview debounce, snapshot ownership, generation recovery, teams, rules, saves and replays. The shared-control test is added to Linux CI alongside the existing lobby tests. Physical macOS mouse delivery is not covered by SDL event injection. diff --git a/docs/map-generators/refactor/RESULTS.md b/docs/map-generators/refactor/RESULTS.md new file mode 100644 index 000000000..1849ab847 --- /dev/null +++ b/docs/map-generators/refactor/RESULTS.md @@ -0,0 +1,76 @@ +# Modular generator validation + +1,000 seeds per playable generator (20001–21000), 128×128, four colonies and four workers. Failed attempts are retained; tile percentages exclude failures. The baseline is the frozen tuned cohort from PR #238. + +![Coverage](coverage.png) + +| Generator | Free tiles: #238 → refactor | Failures / 1,000 | All-team start proxy / 1,000 | Review flags | +|---|---:|---:|---:|---| +| Swamp | 44.64% → 44.65% | 0 → 1 | 952 → 940 | None | +| River | 40.74% → 40.72% | 18 → 20 | 800 → 780 | None | +| Islands | 33.60% → 33.60% | 0 → 0 | 985 → 986 | None | +| Crater lakes | 57.13% → 57.12% | 0 → 0 | 923 → 922 | None | +| Concrete islands | 37.62% → 37.53% | 8 → 5 | 981 → 982 | None | +| Isles | 23.95% → 23.93% | 0 → 0 | 1000 → 1000 | None | +| Shattered Coast | 36.42% → 36.57% | 14 → 26 | 823 → 799 | generation failures | +| Rugged Archipelago | 29.19% → 29.21% | 0 → 0 | 1000 → 1000 | None | +| Contested commons | — → 10.73% | — → 1 | — → 374 | New generator; no baseline | +| Lattice | — → 12.89% | — → 0 | — → 932 | New generator; no baseline | +| Maze | — → 34.49% | — → 0 | — → 999 | New generator; no baseline | +| Fjord continent | — → 23.03% | — → 0 | — → 995 | New generator; no baseline | + +Review thresholds: absolute mean free-area shift >2 percentage points; failure-rate increase >1 point; all-team proxy decrease >3 points. Flags require investigation, not automatic retuning. The start proxy measures nearby building anchors and reachable wheat/wood; it is not a fairness guarantee. + +[Investigation and visual assessment](REVIEW.md) · [Raw data](validation.csv.gz) · [Catalog](catalog.json) + +## Map previews + +Each row shows three fixed seeds and a weak successful start. Failed fixed seeds, if any, are labeled rather than replaced. + +### Swamp + +![Swamp](generator-1.png) + +### River + +![River](generator-2.png) + +### Islands + +![Islands](generator-3.png) + +### Crater lakes + +![Crater lakes](generator-4.png) + +### Concrete islands + +![Concrete islands](generator-5.png) + +### Isles + +![Isles](generator-6.png) + +### Shattered Coast + +![Shattered Coast](generator-7.png) + +### Rugged Archipelago + +![Rugged Archipelago](generator-8.png) + +### Contested commons + +![Contested commons](generator-9.png) + +### Lattice + +![Lattice](generator-10.png) + +### Maze + +![Maze](generator-11.png) + +### Fjord continent + +![Fjord continent](generator-12.png) diff --git a/docs/map-generators/refactor/REVIEW.md b/docs/map-generators/refactor/REVIEW.md new file mode 100644 index 000000000..80874fe65 --- /dev/null +++ b/docs/map-generators/refactor/REVIEW.md @@ -0,0 +1,54 @@ +# Refactor review + +The control ranges and defaults from PR #238 are unchanged. The primary comparison uses 1,000 attempts per playable generator, seeds 20001–21000. Every generator's mean immediately buildable area stayed within 0.15 percentage points of the baseline. All default-generation start-proxy changes stayed within the three-point review threshold. + +## Shattered Coast investigation + +The primary cohort produced 26 placement failures rather than the baseline's 14: a 1.2-percentage-point increase, above the one-point review threshold. The failure was inability to space all colonies on grass, not an exception, timeout or incomplete world reported as successful. The original flag remains visible in `RESULTS.md`. + +A separate 1,000-seed pilot (30001–31000) produced 33 failures. To investigate rather than retune around these seeds, the original PR #238 commit (`139e439f45f7cc4a616a968720549f9347b4a330`) was built in an isolated checkout. Both versions then ran 5,000 confirmation seeds, 30001–35000, with their compiled tuned presets: + +| Measure | PR #238 | Modular generators | Difference | +|---|---:|---:|---:| +| Failures | 99 / 5,000 (1.98%) | 122 / 5,000 (2.44%) | +0.46 percentage points | +| All-team start proxy | 4,050 / 5,000 (81.0%) | 4,000 / 5,000 (80.0%) | −1.0 point | +| Mean immediately buildable area | 36.41% | 36.31% | −0.10 points | + +The larger comparison did not reproduce the threshold crossing. In the first 1,000 confirmation seeds, the original implementation itself had 35 failures, compared with the refactor's 33, illustrating variation between finite seed cohorts. The algorithms and defaults were not changed to improve these results. + +A two-sided Fisher exact test for the 5,000-seed failure comparison gives p=0.134. The approximate 95% interval for the failure-rate difference is −0.12 to +1.04 percentage points. This is not proof of identical distributions or a strict non-inferiority bound below one point. It supports accepting this structural refactor alongside the unchanged controls, close terrain measurements, code inspection and visual review, while retaining failure-rate monitoring for future generator changes. + +The refactor intentionally changes seed-to-map outputs. This compares distributions, not matching historical map hashes. The baseline executable retains its historical fake-clock study harness; the new executable uses explicit seeds and no clock interposition. + +[Confirmation statistics](confirmation.json) · [Baseline confirmation rows](baseline-confirmation.csv.gz) · [Refactor confirmation rows](modular-confirmation.csv.gz) + +## Visual assessment + +All three fixed seeds (22001–22003) and one weak successful start per generator were inspected against the earlier gallery. No fixed preview seed needed replacement. + +| Generator | Preserved visual identity | +|---|---| +| Swamp | Broad, gently curved wetland channels and irregular connected grassy areas. | +| River | A dominant winding river, broad banks and occasional ponds. Existing straight bands at wrapping boundaries can still appear. | +| Islands | Rounded, separated landmasses with coastal resource belts and open interiors. | +| Crater Lakes | Connected grass punctured by round lakes, sometimes overlapping into larger pools. | +| Concrete Islands | Angular regions separated by narrow channels, with interior building space and larger resource patches. | +| Isles | Rounded islands joined by narrow land bridges, surrounded by more open water. | +| Shattered Coast | Fragmented, finely textured coastline and scattered inland building clearings. Weak seeds can remain predominantly water and fragmented grass. | +| Rugged Archipelago | Clearly separated islands with rough shores and chunky resource patches near the initial colonies. | + +The weak examples remain useful diagnostics. A successful map can still have poor local expansion or resource access; the service's structural checks deliberately do not enforce the offline start proxy or repair the terrain. + +## Edge settings and integration + +The edge suite covers 208 configurations with four seeds each: individual control endpoints, all-minimum/all-maximum combinations, small crowded maps, rectangular maps and 512×512 maps with twelve colonies. The initial run completed 832 attempts with 717 successes and 115 clean failures, with no crashes or timeouts. Every configuration also passed independent repeated-process checks. Impossible all-zero terrain recipes fail request validation; difficult placement combinations may fail after partial generation, and their candidates are discarded. + +The automated contracts exercise registered defaults/ranges/steps, both UIs' shared settings and mode memory, byte-codec roundtrips, legacy sentinels, a test-only nonconsecutive registration, immutable requests, interleaved map and full-state checksums, RNG restoration on success and failure, and structured errors. The custom-game harness exercises full save/load, real match launch, replay playback and native SDL interaction. + +Raw rows, configuration manifests and reproducibility checks are retained alongside this report. `source-hashes.json` records the final generator and study sources used for the published gallery. + +## Experimental-concept framework upgrades + +The subsequent geometry, topology, discrete-control, validation and constrained-settlement upgrades are documented in [the concept review](../FRAMEWORK_UPGRADES.md). No experimental generator was imported. A repeat of all 8,000 default attempts matched the preceding modular implementation's map hashes and quality outcomes exactly. The new optional helpers are exercised by synthetic test registrations, and existing generators keep local dispersion and their original generation sequence. + +The final edge rerun retained all 832 success/failure classifications (717 successes, 115 clean failures) and passed 624 repeated seed/configuration pairs. Fourteen hashes changed in small Concrete Islands or single-colony Isles configurations after the region-capacity and finite single-site-spacing fixes. [Framework regression counts](framework-regression.json). diff --git a/docs/map-generators/refactor/baseline-confirmation-configs.json b/docs/map-generators/refactor/baseline-confirmation-configs.json new file mode 100644 index 000000000..cb6c5a86c --- /dev/null +++ b/docs/map-generators/refactor/baseline-confirmation-configs.json @@ -0,0 +1,7 @@ +[ + { + "id": "shattered-holdout", + "method": 7, + "preset": true + } +] \ No newline at end of file diff --git a/docs/map-generators/refactor/baseline-confirmation-reproducibility.csv b/docs/map-generators/refactor/baseline-confirmation-reproducibility.csv new file mode 100644 index 000000000..53f7d321e --- /dev/null +++ b/docs/map-generators/refactor/baseline-confirmation-reproducibility.csv @@ -0,0 +1,7 @@ +config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status +shattered-holdout,7,30001,1,16384,5791,1634,3259,5700,5092,1889,7194,4520,4670,0.009705,2712395013894739790,112,19,17,4,277,237,105,108,ok +shattered-holdout,7,30001,1,16384,5791,1634,3259,5700,5092,1889,7194,4520,4670,0.010698,2712395013894739790,112,19,17,4,277,237,105,108,ok +shattered-holdout,7,32501,1,16384,6269,1498,3458,5159,5390,1808,7686,4104,4594,0.009830,545327954787896904,148,9,13,4,403,214,182,162,ok +shattered-holdout,7,32501,1,16384,6269,1498,3458,5159,5390,1808,7686,4104,4594,0.010109,545327954787896904,148,9,13,4,403,214,182,162,ok +shattered-holdout,7,35000,1,16384,7375,1070,3000,4939,6436,2763,8711,3559,4114,0.009394,17586944396754086295,252,8,12,4,465,186,208,126,ok +shattered-holdout,7,35000,1,16384,7375,1070,3000,4939,6436,2763,8711,3559,4114,0.009964,17586944396754086295,252,8,12,4,465,186,208,126,ok diff --git a/docs/map-generators/refactor/baseline-confirmation.csv.gz b/docs/map-generators/refactor/baseline-confirmation.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..a41b45bc97410964e959ac80ea654517ee9c8156 GIT binary patch literal 255191 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

)X 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 c#Iv!O$rf%KANG@Mtoz#s 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 6(!KMAo<)C>l*Hmd$- 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 literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/coverage.svg b/docs/map-generators/refactor/coverage.svg new file mode 100644 index 000000000..02ce3efba --- /dev/null +++ b/docs/map-generators/refactor/coverage.svg @@ -0,0 +1,2832 @@ + + + + + + + + 2026-09-09T22:47:23.526513 + image/svg+xml + + + Matplotlib v3.11.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/map-generators/refactor/edges-configs.json b/docs/map-generators/refactor/edges-configs.json new file mode 100644 index 000000000..ced8ce4ab --- /dev/null +++ b/docs/map-generators/refactor/edges-configs.json @@ -0,0 +1,1890 @@ +[ + { + "method": 1, + "preset": true, + "id": "edge-1-width-min", + "params": { + "width": 6 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-width-max", + "params": { + "width": 9 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-height-min", + "params": { + "height": 6 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-height-max", + "params": { + "height": 9 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-water-min", + "params": { + "water": 0 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-water-max", + "params": { + "water": 100 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-grass-min", + "params": { + "grass": 0 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-grass-max", + "params": { + "grass": 100 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-smoothing-min", + "params": { + "smoothing": 1 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-smoothing-max", + "params": { + "smoothing": 8 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-fruit-min", + "params": { + "fruit": 0 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-fruit-max", + "params": { + "fruit": 64 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-repeat-min", + "params": { + "repeat": 0 + } + }, + { + "method": 1, + "preset": true, + "id": "edge-1-repeat-max", + "params": { + "repeat": 5 + } + }, + { + "method": 1, + "preset": true, + "id": "size-1-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 1, + "preset": true, + "id": "size-1-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 1, + "preset": true, + "id": "size-1-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 1, + "preset": true, + "id": "size-1-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 1, + "preset": true, + "id": "size-1-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 1, + "preset": true, + "id": "size-1-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 1, + "preset": true, + "id": "corner-1-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "water": 0, + "grass": 0, + "smoothing": 1, + "fruit": 0, + "repeat": 0 + } + }, + { + "method": 1, + "preset": true, + "id": "corner-1-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "water": 100, + "grass": 100, + "smoothing": 8, + "fruit": 64, + "repeat": 5 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-width-min", + "params": { + "width": 6 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-width-max", + "params": { + "width": 9 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-height-min", + "params": { + "height": 6 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-height-max", + "params": { + "height": 9 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-water-min", + "params": { + "water": 0 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-water-max", + "params": { + "water": 100 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-sand-min", + "params": { + "sand": 0 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-sand-max", + "params": { + "sand": 100 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-grass-min", + "params": { + "grass": 0 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-grass-max", + "params": { + "grass": 100 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-desert-min", + "params": { + "desert": 0 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-desert-max", + "params": { + "desert": 100 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-smoothing-min", + "params": { + "smoothing": 1 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-smoothing-max", + "params": { + "smoothing": 8 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-river-width-min", + "params": { + "river-width": 20 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-river-width-max", + "params": { + "river-width": 65 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-fruit-min", + "params": { + "fruit": 0 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-fruit-max", + "params": { + "fruit": 64 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-repeat-min", + "params": { + "repeat": 0 + } + }, + { + "method": 2, + "preset": true, + "id": "edge-2-repeat-max", + "params": { + "repeat": 5 + } + }, + { + "method": 2, + "preset": true, + "id": "size-2-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 2, + "preset": true, + "id": "size-2-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 2, + "preset": true, + "id": "size-2-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 2, + "preset": true, + "id": "size-2-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 2, + "preset": true, + "id": "size-2-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 2, + "preset": true, + "id": "size-2-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 2, + "preset": true, + "id": "corner-2-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "water": 0, + "sand": 0, + "grass": 0, + "desert": 0, + "smoothing": 1, + "river-width": 20, + "fruit": 0, + "repeat": 0 + } + }, + { + "method": 2, + "preset": true, + "id": "corner-2-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "water": 100, + "sand": 100, + "grass": 100, + "desert": 100, + "smoothing": 8, + "river-width": 65, + "fruit": 64, + "repeat": 5 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-width-min", + "params": { + "width": 6 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-width-max", + "params": { + "width": 9 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-height-min", + "params": { + "height": 6 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-height-max", + "params": { + "height": 9 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-water-min", + "params": { + "water": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-water-max", + "params": { + "water": 100 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-sand-min", + "params": { + "sand": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-sand-max", + "params": { + "sand": 100 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-grass-min", + "params": { + "grass": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-grass-max", + "params": { + "grass": 100 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-desert-min", + "params": { + "desert": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-desert-max", + "params": { + "desert": 100 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-smoothing-min", + "params": { + "smoothing": 1 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-smoothing-max", + "params": { + "smoothing": 8 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-extra-islands-min", + "params": { + "extra-islands": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-extra-islands-max", + "params": { + "extra-islands": 8 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-fruit-min", + "params": { + "fruit": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-fruit-max", + "params": { + "fruit": 64 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-repeat-min", + "params": { + "repeat": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "edge-3-repeat-max", + "params": { + "repeat": 5 + } + }, + { + "method": 3, + "preset": true, + "id": "size-3-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 3, + "preset": true, + "id": "size-3-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 3, + "preset": true, + "id": "size-3-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 3, + "preset": true, + "id": "size-3-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 3, + "preset": true, + "id": "size-3-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 3, + "preset": true, + "id": "size-3-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 3, + "preset": true, + "id": "corner-3-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "water": 0, + "sand": 0, + "grass": 0, + "desert": 0, + "smoothing": 1, + "extra-islands": 0, + "fruit": 0, + "repeat": 0 + } + }, + { + "method": 3, + "preset": true, + "id": "corner-3-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "water": 100, + "sand": 100, + "grass": 100, + "desert": 100, + "smoothing": 8, + "extra-islands": 8, + "fruit": 64, + "repeat": 5 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-width-min", + "params": { + "width": 6 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-width-max", + "params": { + "width": 9 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-height-min", + "params": { + "height": 6 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-height-max", + "params": { + "height": 9 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-water-min", + "params": { + "water": 0 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-water-max", + "params": { + "water": 100 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-sand-min", + "params": { + "sand": 0 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-sand-max", + "params": { + "sand": 100 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-grass-min", + "params": { + "grass": 0 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-grass-max", + "params": { + "grass": 100 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-desert-min", + "params": { + "desert": 0 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-desert-max", + "params": { + "desert": 100 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-smoothing-min", + "params": { + "smoothing": 1 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-smoothing-max", + "params": { + "smoothing": 8 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-lake-density-min", + "params": { + "lake-density": 10 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-lake-density-max", + "params": { + "lake-density": 50 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-lake-size-min", + "params": { + "lake-size": 10 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-lake-size-max", + "params": { + "lake-size": 40 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-fruit-min", + "params": { + "fruit": 0 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-fruit-max", + "params": { + "fruit": 64 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-repeat-min", + "params": { + "repeat": 0 + } + }, + { + "method": 4, + "preset": true, + "id": "edge-4-repeat-max", + "params": { + "repeat": 5 + } + }, + { + "method": 4, + "preset": true, + "id": "size-4-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 4, + "preset": true, + "id": "size-4-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 4, + "preset": true, + "id": "size-4-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 4, + "preset": true, + "id": "size-4-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 4, + "preset": true, + "id": "size-4-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 4, + "preset": true, + "id": "size-4-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 4, + "preset": true, + "id": "corner-4-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "water": 0, + "sand": 0, + "grass": 0, + "desert": 0, + "smoothing": 1, + "lake-density": 10, + "lake-size": 10, + "fruit": 0, + "repeat": 0 + } + }, + { + "method": 4, + "preset": true, + "id": "corner-4-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "water": 100, + "sand": 100, + "grass": 100, + "desert": 100, + "smoothing": 8, + "lake-density": 50, + "lake-size": 40, + "fruit": 64, + "repeat": 5 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-width-min", + "params": { + "width": 6 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-width-max", + "params": { + "width": 9 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-height-min", + "params": { + "height": 6 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-height-max", + "params": { + "height": 9 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-channel-width-min", + "params": { + "channel-width": 5 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-channel-width-max", + "params": { + "channel-width": 8 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-extra-islands-min", + "params": { + "extra-islands": 0 + } + }, + { + "method": 5, + "preset": true, + "id": "edge-5-extra-islands-max", + "params": { + "extra-islands": 6 + } + }, + { + "method": 5, + "preset": true, + "id": "size-5-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 5, + "preset": true, + "id": "size-5-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 5, + "preset": true, + "id": "size-5-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 5, + "preset": true, + "id": "size-5-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 5, + "preset": true, + "id": "size-5-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 5, + "preset": true, + "id": "size-5-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 5, + "preset": true, + "id": "corner-5-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "channel-width": 5, + "extra-islands": 0 + } + }, + { + "method": 5, + "preset": true, + "id": "corner-5-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "channel-width": 8, + "extra-islands": 6 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-width-min", + "params": { + "width": 6 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-width-max", + "params": { + "width": 9 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-height-min", + "params": { + "height": 6 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-height-max", + "params": { + "height": 9 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-island-size-min", + "params": { + "island-size": 45 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-island-size-max", + "params": { + "island-size": 65 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-bridge-width-min", + "params": { + "bridge-width": 3 + } + }, + { + "method": 6, + "preset": true, + "id": "edge-6-bridge-width-max", + "params": { + "bridge-width": 6 + } + }, + { + "method": 6, + "preset": true, + "id": "size-6-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 6, + "preset": true, + "id": "size-6-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 6, + "preset": true, + "id": "size-6-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 6, + "preset": true, + "id": "size-6-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 6, + "preset": true, + "id": "size-6-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 6, + "preset": true, + "id": "size-6-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 6, + "preset": true, + "id": "corner-6-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "island-size": 45, + "bridge-width": 3 + } + }, + { + "method": 6, + "preset": true, + "id": "corner-6-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "island-size": 65, + "bridge-width": 6 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-width-min", + "params": { + "width": 6 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-width-max", + "params": { + "width": 9 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-height-min", + "params": { + "height": 6 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-height-max", + "params": { + "height": 9 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-water-min", + "params": { + "water": 0 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-water-max", + "params": { + "water": 100 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-sand-min", + "params": { + "sand": 0 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-sand-max", + "params": { + "sand": 100 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-grass-min", + "params": { + "grass": 0 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-grass-max", + "params": { + "grass": 100 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-smoothing-min", + "params": { + "smoothing": 1 + } + }, + { + "method": 7, + "preset": true, + "id": "edge-7-smoothing-max", + "params": { + "smoothing": 8 + } + }, + { + "method": 7, + "preset": true, + "id": "size-7-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 7, + "preset": true, + "id": "size-7-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 7, + "preset": true, + "id": "size-7-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 7, + "preset": true, + "id": "size-7-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 7, + "preset": true, + "id": "size-7-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 7, + "preset": true, + "id": "size-7-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 7, + "preset": true, + "id": "corner-7-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "water": 0, + "sand": 0, + "grass": 0, + "smoothing": 1 + } + }, + { + "method": 7, + "preset": true, + "id": "corner-7-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "water": 100, + "sand": 100, + "grass": 100, + "smoothing": 8 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-width-min", + "params": { + "width": 6 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-width-max", + "params": { + "width": 9 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-height-min", + "params": { + "height": 6 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-height-max", + "params": { + "height": 9 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-teams-min", + "params": { + "teams": 1 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-teams-max", + "params": { + "teams": 12 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-workers-min", + "params": { + "workers": 1 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-workers-max", + "params": { + "workers": 8 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-island-size-min", + "params": { + "island-size": 50 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-island-size-max", + "params": { + "island-size": 70 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-beach-size-min", + "params": { + "beach-size": 0 + } + }, + { + "method": 8, + "preset": true, + "id": "edge-8-beach-size-max", + "params": { + "beach-size": 4 + } + }, + { + "method": 8, + "preset": true, + "id": "size-8-6-6-4", + "params": { + "width": 6, + "height": 6, + "teams": 4 + } + }, + { + "method": 8, + "preset": true, + "id": "size-8-6-6-12", + "params": { + "width": 6, + "height": 6, + "teams": 12 + } + }, + { + "method": 8, + "preset": true, + "id": "size-8-7-7-8", + "params": { + "width": 7, + "height": 7, + "teams": 8 + } + }, + { + "method": 8, + "preset": true, + "id": "size-8-6-9-12", + "params": { + "width": 6, + "height": 9, + "teams": 12 + } + }, + { + "method": 8, + "preset": true, + "id": "size-8-9-6-12", + "params": { + "width": 9, + "height": 6, + "teams": 12 + } + }, + { + "method": 8, + "preset": true, + "id": "size-8-9-9-12", + "params": { + "width": 9, + "height": 9, + "teams": 12 + } + }, + { + "method": 8, + "preset": true, + "id": "corner-8-min", + "params": { + "width": 6, + "height": 6, + "teams": 1, + "workers": 1, + "island-size": 50, + "beach-size": 0 + } + }, + { + "method": 8, + "preset": true, + "id": "corner-8-max", + "params": { + "width": 9, + "height": 9, + "teams": 12, + "workers": 8, + "island-size": 70, + "beach-size": 4 + } + } +] \ No newline at end of file diff --git a/docs/map-generators/refactor/edges-reproducibility.csv b/docs/map-generators/refactor/edges-reproducibility.csv new file mode 100644 index 000000000..46ff37ed4 --- /dev/null +++ b/docs/map-generators/refactor/edges-reproducibility.csv @@ -0,0 +1,1249 @@ +config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status +edge-1-width-min,1,24001,1,8192,4506,192,2332,1162,3604,2622,4811,772,2609,0.000740,354655092834897733,787,8,15,4,264,396,150,496,ok +edge-1-width-min,1,24001,1,8192,4506,192,2332,1162,3604,2622,4811,772,2609,0.000750,354655092834897733,787,8,15,4,264,396,150,496,ok +edge-1-width-min,1,24003,1,8192,4613,193,2403,983,3624,2562,4869,684,2639,0.000747,7387189732466504953,304,11,10,4,326,428,135,493,ok +edge-1-width-min,1,24003,1,8192,4613,193,2403,983,3624,2562,4869,684,2639,0.000832,7387189732466504953,304,11,10,4,326,428,135,493,ok +edge-1-width-min,1,24004,1,8192,4620,158,2378,1036,3614,2727,4887,676,2629,0.000962,2283787201577684569,617,9,15,4,365,369,165,496,ok +edge-1-width-min,1,24004,1,8192,4620,158,2378,1036,3614,2727,4887,676,2629,0.000756,2283787201577684569,617,9,15,4,365,369,165,496,ok +edge-1-width-max,1,24001,1,65536,37323,1159,19630,7424,29612,23502,39237,4871,21428,0.005554,1510421835335318050,0,10,22,3,3212,3101,1305,3962,ok +edge-1-width-max,1,24001,1,65536,37323,1159,19630,7424,29612,23502,39237,4871,21428,0.005554,1510421835335318050,0,10,22,3,3212,3101,1305,3962,ok +edge-1-width-max,1,24003,1,65536,37237,1253,19644,7402,29543,23365,39129,4952,21455,0.008551,14655133220378056420,1113,13,12,4,3214,3093,1287,3970,ok +edge-1-width-max,1,24003,1,65536,37237,1253,19644,7402,29543,23365,39129,4952,21455,0.005900,14655133220378056420,1113,13,12,4,3214,3093,1287,3970,ok +edge-1-width-max,1,24004,1,65536,37291,1033,19678,7534,29538,23442,39224,4800,21512,0.005538,2696248690097268267,968,7,17,4,3572,2687,1398,3966,ok +edge-1-width-max,1,24004,1,65536,37291,1033,19678,7534,29538,23442,39224,4800,21512,0.008577,2696248690097268267,968,7,17,4,3572,2687,1398,3966,ok +edge-1-height-min,1,24001,1,8192,4611,153,2419,1009,3618,2677,4875,656,2661,0.000746,5993380641348072149,921,12,16,4,390,360,146,496,ok +edge-1-height-min,1,24001,1,8192,4611,153,2419,1009,3618,2677,4875,656,2661,0.000782,5993380641348072149,921,12,16,4,390,360,146,496,ok +edge-1-height-min,1,24003,1,8192,4757,102,2531,802,3628,2731,4960,502,2730,0.000738,1808543194305134926,750,14,12,4,430,394,219,495,ok +edge-1-height-min,1,24003,1,8192,4757,102,2531,802,3628,2731,4960,502,2730,0.000738,1808543194305134926,750,14,12,4,430,394,219,495,ok +edge-1-height-min,1,24004,1,8192,4651,130,2452,959,3631,2635,4906,609,2677,0.000739,1517223058165164336,596,14,13,4,440,297,191,495,ok +edge-1-height-min,1,24004,1,8192,4651,130,2452,959,3631,2635,4906,609,2677,0.000754,1517223058165164336,596,14,13,4,440,297,191,495,ok +edge-1-height-max,1,24001,1,65536,36887,1444,19417,7788,29444,23038,38891,5338,21307,0.125080,18349716135080740857,604,20,12,4,3366,2798,1183,3961,ok +edge-1-height-max,1,24001,1,65536,36887,1444,19417,7788,29444,23038,38891,5338,21307,0.005767,18349716135080740857,604,20,12,4,3366,2798,1183,3961,ok +edge-1-height-max,1,24003,1,65536,37321,1304,19671,7240,29663,23616,39187,4923,21426,0.128583,18016351330854298622,420,11,6,4,3160,3109,1291,3979,ok +edge-1-height-max,1,24003,1,65536,37321,1304,19671,7240,29663,23616,39187,4923,21426,0.023905,18016351330854298622,420,11,6,4,3160,3109,1291,3979,ok +edge-1-height-max,1,24004,1,65536,37162,1310,19428,7636,29562,23232,39119,5127,21290,0.127327,14147695598255769863,425,10,13,4,3343,2920,1244,3976,ok +edge-1-height-max,1,24004,1,65536,37162,1310,19428,7636,29562,23232,39119,5127,21290,0.010093,14147695598255769863,425,10,13,4,3343,2920,1244,3976,ok +edge-1-teams-min,1,24001,1,16384,9332,282,4946,1824,7377,5675,9806,1193,5385,0.001638,3691568818373152596,688,1,6,1,969,641,315,994,ok +edge-1-teams-min,1,24001,1,16384,9332,282,4946,1824,7377,5675,9806,1193,5385,0.001236,3691568818373152596,688,1,6,1,969,641,315,994,ok +edge-1-teams-min,1,24003,1,16384,9403,278,4989,1714,7363,5792,9842,1134,5408,0.001348,17111279724926110250,987,18,4,1,842,755,400,991,ok +edge-1-teams-min,1,24003,1,16384,9403,278,4989,1714,7363,5792,9842,1134,5408,0.001783,17111279724926110250,987,18,4,1,842,755,400,991,ok +edge-1-teams-min,1,24004,1,16384,9349,285,4978,1772,7375,5879,9811,1170,5403,0.002998,17259471453116953259,1217,5,4,1,593,1058,290,994,ok +edge-1-teams-min,1,24004,1,16384,9349,285,4978,1772,7375,5879,9811,1170,5403,0.001237,17259471453116953259,1217,5,4,1,593,1058,290,994,ok +edge-1-teams-max,1,24001,1,16384,9336,282,4946,1820,7176,5164,9807,1192,5385,0.007912,8377485932594510683,330,13,13,12,954,641,315,994,ok +edge-1-teams-max,1,24001,1,16384,9336,282,4946,1820,7176,5164,9807,1192,5385,0.002530,8377485932594510683,330,13,13,12,954,641,315,994,ok +edge-1-teams-max,1,24003,0,16384,0,0,16384,0,0,0,9842,1134,5408,0.001259,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-1-teams-max,1,24003,0,16384,0,0,16384,0,0,0,9842,1134,5408,0.002513,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-1-teams-max,1,24004,1,16384,9378,279,4953,1774,7185,5387,9840,1166,5378,0.004729,13667748426926015759,0,20,18,11,593,1057,290,994,ok +edge-1-teams-max,1,24004,1,16384,9378,279,4953,1774,7185,5387,9840,1166,5378,0.001920,13667748426926015759,0,20,18,11,593,1057,290,994,ok +edge-1-workers-min,1,24001,1,16384,9332,282,4946,1824,7344,5573,9806,1193,5385,0.001378,13890498528614818315,526,10,10,4,954,641,315,994,ok +edge-1-workers-min,1,24001,1,16384,9332,282,4946,1824,7344,5573,9806,1193,5385,0.001404,13890498528614818315,526,10,10,4,954,641,315,994,ok +edge-1-workers-min,1,24003,1,16384,9411,274,4987,1712,7341,5689,9850,1129,5405,0.005096,1743860520622184812,397,18,17,4,824,755,400,991,ok +edge-1-workers-min,1,24003,1,16384,9411,274,4987,1712,7341,5689,9850,1129,5405,0.001440,1743860520622184812,397,18,17,4,824,755,400,991,ok +edge-1-workers-min,1,24004,1,16384,9349,285,4978,1772,7328,5741,9811,1170,5403,0.001333,17965142136197182547,780,22,8,4,593,1057,290,994,ok +edge-1-workers-min,1,24004,1,16384,9349,285,4978,1772,7328,5741,9811,1170,5403,0.046888,17965142136197182547,780,22,8,4,593,1057,290,994,ok +edge-1-workers-max,1,24001,1,16384,9332,282,4946,1824,7316,5554,9806,1193,5385,0.001339,13890498528614818315,598,9,9,4,954,641,315,994,ok +edge-1-workers-max,1,24001,1,16384,9332,282,4946,1824,7316,5554,9806,1193,5385,0.001743,13890498528614818315,598,9,9,4,954,641,315,994,ok +edge-1-workers-max,1,24003,1,16384,9411,274,4987,1712,7313,5659,9850,1129,5405,0.001378,1743860520622184812,551,18,17,4,824,755,400,991,ok +edge-1-workers-max,1,24003,1,16384,9411,274,4987,1712,7313,5659,9850,1129,5405,0.001358,1743860520622184812,551,18,17,4,824,755,400,991,ok +edge-1-workers-max,1,24004,1,16384,9349,285,4978,1772,7300,5711,9811,1170,5403,0.004252,17965142136197182547,846,20,5,4,593,1057,290,994,ok +edge-1-workers-max,1,24004,1,16384,9349,285,4978,1772,7300,5711,9811,1170,5403,0.001410,17965142136197182547,846,20,5,4,593,1057,290,994,ok +edge-1-water-min,1,24001,0,16384,0,0,16384,0,0,0,16383,1,0,0.001091,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-1-water-min,1,24001,0,16384,0,0,16384,0,0,0,16383,1,0,0.001120,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-1-water-min,1,24003,0,16384,0,0,16384,0,0,0,16383,1,0,0.004801,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-1-water-min,1,24003,0,16384,0,0,16384,0,0,0,16383,1,0,0.001045,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-1-water-min,1,24004,0,16384,0,0,16384,0,0,0,16383,1,0,0.001035,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-1-water-min,1,24004,0,16384,0,0,16384,0,0,0,16383,1,0,0.001033,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-1-water-max,1,24001,1,16384,4979,323,8934,2148,3041,1720,5510,1397,9477,0.001381,14123798832588003375,148,15,7,4,689,842,313,1690,ok +edge-1-water-max,1,24001,1,16384,4979,323,8934,2148,3041,1720,5510,1397,9477,0.001366,14123798832588003375,148,15,7,4,689,842,313,1690,ok +edge-1-water-max,1,24003,1,16384,5107,339,9034,1904,3025,1892,5571,1291,9522,0.004533,6682157493061246418,129,9,7,4,662,955,375,1695,ok +edge-1-water-max,1,24003,1,16384,5107,339,9034,1904,3025,1892,5571,1291,9522,0.001416,6682157493061246418,129,9,7,4,662,955,375,1695,ok +edge-1-water-max,1,24004,1,16384,5182,281,9181,1740,3043,1977,5624,1150,9610,0.001383,10747540473145447402,39,21,14,4,1002,708,321,1695,ok +edge-1-water-max,1,24004,1,16384,5182,281,9181,1740,3043,1977,5624,1150,9610,0.001502,10747540473145447402,39,21,14,4,1002,708,321,1695,ok +edge-1-grass-min,1,24001,0,16384,0,0,16384,0,0,0,330,257,15797,0.001202,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-1-grass-min,1,24001,0,16384,0,0,16384,0,0,0,330,257,15797,0.003721,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-1-grass-min,1,24003,0,16384,0,0,16384,0,0,0,345,226,15813,0.002813,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-1-grass-min,1,24003,0,16384,0,0,16384,0,0,0,345,226,15813,0.001068,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-1-grass-min,1,24004,0,16384,0,0,16384,0,0,0,326,270,15788,0.001081,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-1-grass-min,1,24004,0,16384,0,0,16384,0,0,0,326,270,15788,0.003218,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-1-grass-max,1,24001,1,16384,11285,204,3407,1488,9885,8176,11680,947,3757,0.003325,3609620929673755109,1068,10,13,4,541,475,279,696,ok +edge-1-grass-max,1,24001,1,16384,11285,204,3407,1488,9885,8176,11680,947,3757,0.003975,3609620929673755109,1068,10,13,4,541,475,279,696,ok +edge-1-grass-max,1,24003,1,16384,11324,230,3384,1446,9942,8397,11687,952,3745,0.001341,4936522091075774384,971,23,11,4,558,514,213,698,ok +edge-1-grass-max,1,24003,1,16384,11324,230,3384,1446,9942,8397,11687,952,3745,0.003343,4936522091075774384,971,23,11,4,558,514,213,698,ok +edge-1-grass-max,1,24004,1,16384,11264,237,3389,1494,9918,8343,11660,983,3741,0.004670,16153151779798774698,1132,6,12,4,404,626,223,701,ok +edge-1-grass-max,1,24004,1,16384,11264,237,3389,1494,9918,8343,11660,983,3741,0.003137,16153151779798774698,1132,6,12,4,404,626,223,701,ok +edge-1-smoothing-min,1,24001,1,16384,8114,658,3825,3787,6783,3797,9138,2552,4694,0.003433,5021615561480563420,466,13,6,4,435,556,250,990,ok +edge-1-smoothing-min,1,24001,1,16384,8114,658,3825,3787,6783,3797,9138,2552,4694,0.001494,5021615561480563420,466,13,6,4,435,556,250,990,ok +edge-1-smoothing-min,1,24003,1,16384,8243,651,3920,3570,6875,3845,9180,2437,4767,0.002187,12728242639966732679,376,7,9,4,492,545,227,983,ok +edge-1-smoothing-min,1,24003,1,16384,8243,651,3920,3570,6875,3845,9180,2437,4767,0.004730,12728242639966732679,376,7,9,4,492,545,227,983,ok +edge-1-smoothing-min,1,24004,1,16384,8229,645,3964,3546,6848,3913,9169,2420,4795,0.001407,7515091598127115981,416,9,3,4,543,511,240,994,ok +edge-1-smoothing-min,1,24004,1,16384,8229,645,3964,3546,6848,3913,9169,2420,4795,0.001414,7515091598127115981,416,9,3,4,543,511,240,994,ok +edge-1-smoothing-max,1,24001,1,16384,9465,233,5012,1674,7381,5845,9902,1069,5413,0.001902,7108776383409922387,728,16,15,4,666,949,364,991,ok +edge-1-smoothing-max,1,24001,1,16384,9465,233,5012,1674,7381,5845,9902,1069,5413,0.001357,7108776383409922387,728,16,15,4,666,949,364,991,ok +edge-1-smoothing-max,1,24003,1,16384,9470,260,5040,1614,7295,5826,9880,1066,5438,0.005065,6057965752441029688,795,14,14,4,702,967,398,988,ok +edge-1-smoothing-max,1,24003,1,16384,9470,260,5040,1614,7295,5826,9880,1066,5438,0.002917,6057965752441029688,795,14,14,4,702,967,398,988,ok +edge-1-smoothing-max,1,24004,1,16384,9350,307,4949,1778,7329,5697,9808,1196,5380,0.003134,12055984761683483090,519,12,11,4,827,781,307,993,ok +edge-1-smoothing-max,1,24004,1,16384,9350,307,4949,1778,7329,5697,9808,1196,5380,0.001379,12055984761683483090,519,12,11,4,827,781,307,993,ok +edge-1-fruit-min,1,24001,1,16384,9332,282,4946,1824,7342,5640,9806,1193,5385,0.032317,11947252659821411454,579,10,10,4,954,641,315,994,ok +edge-1-fruit-min,1,24001,1,16384,9332,282,4946,1824,7342,5640,9806,1193,5385,0.001444,11947252659821411454,579,10,10,4,954,641,315,994,ok +edge-1-fruit-min,1,24003,1,16384,9411,274,4987,1712,7352,5779,9850,1129,5405,0.001347,5813234162792613140,556,18,17,4,824,755,400,991,ok +edge-1-fruit-min,1,24003,1,16384,9411,274,4987,1712,7352,5779,9850,1129,5405,0.001906,5813234162792613140,556,18,17,4,824,755,400,991,ok +edge-1-fruit-min,1,24004,1,16384,9349,285,4978,1772,7329,5810,9811,1170,5403,0.001419,5815179870271813462,888,20,6,4,593,1057,290,994,ok +edge-1-fruit-min,1,24004,1,16384,9349,285,4978,1772,7329,5810,9811,1170,5403,0.001336,5815179870271813462,888,20,6,4,593,1057,290,994,ok +edge-1-fruit-max,1,24001,1,16384,9332,282,4946,1824,7044,4165,9806,1193,5385,0.001389,6867792694369419104,327,10,10,4,954,641,315,994,ok +edge-1-fruit-max,1,24001,1,16384,9332,282,4946,1824,7044,4165,9806,1193,5385,0.005230,6867792694369419104,327,10,10,4,954,641,315,994,ok +edge-1-fruit-max,1,24003,1,16384,9411,274,4987,1712,7017,4404,9850,1129,5405,0.001407,4327981281064200431,328,18,17,4,824,755,400,991,ok +edge-1-fruit-max,1,24003,1,16384,9411,274,4987,1712,7017,4404,9850,1129,5405,0.002306,4327981281064200431,328,18,17,4,824,755,400,991,ok +edge-1-fruit-max,1,24004,1,16384,9349,285,4978,1772,7026,4313,9811,1170,5403,0.001422,4390929319407353961,651,20,6,4,593,1057,290,994,ok +edge-1-fruit-max,1,24004,1,16384,9349,285,4978,1772,7026,4313,9811,1170,5403,0.001380,4390929319407353961,651,20,6,4,593,1057,290,994,ok +edge-1-repeat-min,1,24001,1,16384,9332,282,4946,1824,7332,5567,9806,1193,5385,0.001382,13890498528614818315,575,10,10,4,954,641,315,994,ok +edge-1-repeat-min,1,24001,1,16384,9332,282,4946,1824,7332,5567,9806,1193,5385,0.002214,13890498528614818315,575,10,10,4,954,641,315,994,ok +edge-1-repeat-min,1,24003,1,16384,9411,274,4987,1712,7329,5680,9850,1129,5405,0.001691,1743860520622184812,551,18,17,4,824,755,400,991,ok +edge-1-repeat-min,1,24003,1,16384,9411,274,4987,1712,7329,5680,9850,1129,5405,0.009181,1743860520622184812,551,18,17,4,824,755,400,991,ok +edge-1-repeat-min,1,24004,1,16384,9349,285,4978,1772,7316,5732,9811,1170,5403,0.002014,17965142136197182547,851,20,6,4,593,1057,290,994,ok +edge-1-repeat-min,1,24004,1,16384,9349,285,4978,1772,7316,5732,9811,1170,5403,0.004363,17965142136197182547,851,20,6,4,593,1057,290,994,ok +edge-1-repeat-max,1,24001,1,16384,8192,728,4008,3456,6496,2060,9124,2424,4836,0.000693,3221758993762238880,184,2,10,4,384,544,32,960,ok +edge-1-repeat-max,1,24001,1,16384,8192,728,4008,3456,6496,2060,9124,2424,4836,0.000712,3221758993762238880,184,2,10,4,384,544,32,960,ok +edge-1-repeat-max,1,24003,1,16384,7556,660,3232,4936,5732,836,8872,3096,4416,0.001259,15318678605301309947,108,6,4,4,540,416,156,960,ok +edge-1-repeat-max,1,24003,1,16384,7556,660,3232,4936,5732,836,8872,3096,4416,0.000595,15318678605301309947,108,6,4,4,540,416,156,960,ok +edge-1-repeat-max,1,24004,1,16384,8180,728,3756,3720,6464,1564,9172,2556,4656,0.001808,2270769969064918328,138,5,6,4,244,500,124,960,ok +edge-1-repeat-max,1,24004,1,16384,8180,728,3756,3720,6464,1564,9172,2556,4656,0.000576,2270769969064918328,138,5,6,4,244,500,124,960,ok +size-1-6-6-4,1,24001,1,4096,2318,73,1233,472,1764,1089,2443,308,1345,0.000442,17079717480519420910,319,11,6,4,104,269,85,247,ok +size-1-6-6-4,1,24001,1,4096,2318,73,1233,472,1764,1089,2443,308,1345,0.001205,17079717480519420910,319,11,6,4,104,269,85,247,ok +size-1-6-6-4,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000303,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-1-6-6-4,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000738,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-1-6-6-4,1,24004,1,4096,2220,136,1120,620,1746,1089,2383,446,1267,0.001094,11627436278644698582,310,7,18,4,121,194,66,247,ok +size-1-6-6-4,1,24004,1,4096,2220,136,1120,620,1746,1089,2383,446,1267,0.000431,11627436278644698582,310,7,18,4,121,194,66,247,ok +size-1-6-6-12,1,24001,0,4096,0,0,4096,0,0,0,2443,308,1345,0.000325,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-1-6-6-12,1,24001,0,4096,0,0,4096,0,0,0,2443,308,1345,0.000323,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-1-6-6-12,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000967,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-1-6-6-12,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000304,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-1-6-6-12,1,24004,0,4096,0,0,4096,0,0,0,2382,447,1267,0.000394,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-1-6-6-12,1,24004,0,4096,0,0,4096,0,0,0,2382,447,1267,0.001097,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-1-7-7-8,1,24001,1,16384,9332,282,4946,1824,7252,5357,9806,1193,5385,0.001508,2571324380836695905,490,13,13,8,954,641,315,994,ok +size-1-7-7-8,1,24001,1,16384,9332,282,4946,1824,7252,5357,9806,1193,5385,0.004267,2571324380836695905,490,13,13,8,954,641,315,994,ok +size-1-7-7-8,1,24003,1,16384,9415,274,4987,1708,7303,5583,9851,1128,5405,0.009835,9280150380393299210,0,18,17,7,789,743,397,991,ok +size-1-7-7-8,1,24003,1,16384,9415,274,4987,1708,7303,5583,9851,1128,5405,0.002735,9280150380393299210,0,18,17,7,789,743,397,991,ok +size-1-7-7-8,1,24004,1,16384,9353,285,4978,1768,7240,5547,9812,1169,5403,0.001501,11396205303651550806,814,20,18,8,593,1057,290,994,ok +size-1-7-7-8,1,24004,1,16384,9353,285,4978,1768,7240,5547,9812,1169,5403,0.002575,11396205303651550806,814,20,18,8,593,1057,290,994,ok +size-1-6-9-12,1,24001,1,32768,18450,768,9592,3958,14638,11066,19484,2747,10537,0.003206,205956527766588996,524,10,15,12,1260,1711,586,1990,ok +size-1-6-9-12,1,24001,1,32768,18450,768,9592,3958,14638,11066,19484,2747,10537,0.048581,205956527766588996,524,10,15,12,1260,1711,586,1990,ok +size-1-6-9-12,1,24003,1,32768,18547,735,9666,3820,14623,11114,19544,2644,10580,0.008060,4488402287655512689,213,12,19,12,1536,1454,677,1988,ok +size-1-6-9-12,1,24003,1,32768,18547,735,9666,3820,14623,11114,19544,2644,10580,0.007410,4488402287655512689,213,12,19,12,1536,1454,677,1988,ok +size-1-6-9-12,1,24004,1,32768,18464,725,9613,3966,14601,11185,19483,2707,10578,0.005707,9377437999984426408,268,11,17,12,1628,1403,567,1990,ok +size-1-6-9-12,1,24004,1,32768,18464,725,9613,3966,14601,11185,19483,2707,10578,0.020534,9377437999984426408,268,11,17,12,1628,1403,567,1990,ok +size-1-9-6-12,1,24001,1,32768,18428,650,9648,4042,14596,10861,19461,2670,10637,0.003411,16480885287169114817,632,13,21,12,1586,1355,637,1981,ok +size-1-9-6-12,1,24001,1,32768,18428,650,9648,4042,14596,10861,19461,2670,10637,0.031473,16480885287169114817,632,13,21,12,1586,1355,637,1981,ok +size-1-9-6-12,1,24003,1,32768,18407,682,9623,4056,14488,10953,19462,2708,10598,0.003339,5197832662345982046,549,17,19,12,1585,1429,650,1980,ok +size-1-9-6-12,1,24003,1,32768,18407,682,9623,4056,14488,10953,19462,2708,10598,0.004480,5197832662345982046,549,17,19,12,1585,1429,650,1980,ok +size-1-9-6-12,1,24004,1,32768,18651,551,9813,3753,14547,11197,19611,2428,10729,0.007475,15292689523055164251,490,13,19,12,1644,1494,707,1978,ok +size-1-9-6-12,1,24004,1,32768,18651,551,9813,3753,14547,11197,19611,2428,10729,0.003726,15292689523055164251,490,13,19,12,1644,1494,707,1978,ok +size-1-9-9-12,1,24001,1,262144,149149,5198,78183,29614,118418,94574,156739,20005,85400,0.140929,16415405346331145548,0,100000,16,11,12901,12409,5160,15840,ok +size-1-9-9-12,1,24001,1,262144,149149,5198,78183,29614,118418,94574,156739,20005,85400,0.207999,16415405346331145548,0,100000,16,11,12901,12409,5160,15840,ok +size-1-9-9-12,1,24003,1,262144,148916,5302,78338,29588,118280,94638,156537,20096,85511,0.302889,17473792559893687585,378,19,16,12,12411,12609,5360,15855,ok +size-1-9-9-12,1,24003,1,262144,148916,5302,78338,29588,118280,94638,156537,20096,85511,0.298061,17473792559893687585,378,19,16,12,12411,12609,5360,15855,ok +size-1-9-9-12,1,24004,1,262144,149399,4538,78867,29340,118261,94428,156953,19209,85982,0.187686,14278055095835393210,637,16,16,12,12710,12805,5373,15920,ok +size-1-9-9-12,1,24004,1,262144,149399,4538,78867,29340,118261,94428,156953,19209,85982,0.226154,14278055095835393210,637,16,16,12,12710,12805,5373,15920,ok +corner-1-min,1,24001,0,0,0,0,0,0,0,0,0,0,0,0.041574,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-1-min,1,24001,0,0,0,0,0,0,0,0,0,0,0,0.000003,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-1-min,1,24003,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-1-min,1,24003,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-1-min,1,24004,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-1-min,1,24004,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-1-max,1,24001,1,262144,113996,5220,113136,29792,56340,17376,121524,20084,120536,0.128402,11153872659230279882,281,1,2,12,25640,13892,7568,21696,ok +corner-1-max,1,24001,1,262144,113996,5220,113136,29792,56340,17376,121524,20084,120536,0.095332,11153872659230279882,281,1,2,12,25640,13892,7568,21696,ok +corner-1-max,1,24003,1,262144,111552,5024,110128,35440,56996,16952,120524,22744,118876,0.064198,13304278174034654577,181,15,1,12,18940,17776,8092,21504,ok +corner-1-max,1,24003,1,262144,111552,5024,110128,35440,56996,16952,120524,22744,118876,0.082056,13304278174034654577,181,15,1,12,18940,17776,8092,21504,ok +corner-1-max,1,24004,1,262144,112668,6588,110440,32448,55832,17184,120676,22812,118656,0.074396,765892347145801071,106,1,1,12,23484,14736,7608,21664,ok +corner-1-max,1,24004,1,262144,112668,6588,110440,32448,55832,17184,120676,22812,118656,0.075969,765892347145801071,106,1,1,12,23484,14736,7608,21664,ok +edge-2-width-min,2,24001,1,8192,4472,313,2617,790,3197,2353,4705,705,2782,0.001032,12503860844371180222,0,22,7,3,359,564,254,472,ok +edge-2-width-min,2,24001,1,8192,4472,313,2617,790,3197,2353,4705,705,2782,0.001542,12503860844371180222,0,22,7,3,359,564,254,472,ok +edge-2-width-min,2,24003,1,8192,4383,285,2580,944,3163,2287,4669,753,2770,0.001019,2289046138649135511,650,13,13,4,527,425,174,482,ok +edge-2-width-min,2,24003,1,8192,4383,285,2580,944,3163,2287,4669,753,2770,0.002871,2289046138649135511,650,13,13,4,527,425,174,482,ok +edge-2-width-min,2,24004,1,8192,4460,248,2581,903,3245,2383,4719,696,2777,0.003043,12008136029917599670,469,5,16,4,420,508,193,478,ok +edge-2-width-min,2,24004,1,8192,4460,248,2581,903,3245,2383,4719,696,2777,0.001045,12008136029917599670,469,5,16,4,420,508,193,478,ok +edge-2-width-max,2,24001,1,65536,37588,1941,22369,3638,26676,23371,38639,3757,23140,0.179237,898222588874111289,1231,23,24,4,3715,4034,3058,3843,ok +edge-2-width-max,2,24001,1,65536,37588,1941,22369,3638,26676,23371,38639,3757,23140,0.155979,898222588874111289,1231,23,24,4,3715,4034,3058,3843,ok +edge-2-width-max,2,24003,1,65536,37493,1887,22178,3978,26774,23275,38591,3874,23071,0.133704,7657302466715763635,1260,20,26,4,4216,3547,2853,3813,ok +edge-2-width-max,2,24003,1,65536,37493,1887,22178,3978,26774,23275,38591,3874,23071,0.085862,7657302466715763635,1260,20,26,4,4216,3547,2853,3813,ok +edge-2-width-max,2,24004,1,65536,37637,2411,22604,2884,26389,23282,38567,3843,23126,0.059894,384484046444111817,1346,28,28,1,4031,3714,3402,3783,ok +edge-2-width-max,2,24004,1,65536,37637,2411,22604,2884,26389,23282,38567,3843,23126,0.021655,384484046444111817,1346,28,28,1,4031,3714,3402,3783,ok +edge-2-height-min,2,24001,1,8192,4520,253,2725,694,3273,2398,4740,596,2856,0.002338,21500989661319052,662,15,15,4,328,495,323,472,ok +edge-2-height-min,2,24001,1,8192,4520,253,2725,694,3273,2398,4740,596,2856,0.002602,21500989661319052,662,15,15,4,328,495,323,472,ok +edge-2-height-min,2,24003,1,8192,4618,216,2688,670,3365,2477,4821,548,2823,0.002526,9714228658532047050,0,100000,14,2,490,332,335,459,ok +edge-2-height-min,2,24003,1,8192,4618,216,2688,670,3365,2477,4821,548,2823,0.002060,9714228658532047050,0,100000,14,2,490,332,335,459,ok +edge-2-height-min,2,24004,1,8192,4599,226,2717,650,3215,2238,4796,548,2848,0.001803,3068368816682946139,719,18,7,4,422,535,318,473,ok +edge-2-height-min,2,24004,1,8192,4599,226,2717,650,3215,2238,4796,548,2848,0.002870,3068368816682946139,719,18,7,4,422,535,318,473,ok +edge-2-height-max,2,24001,1,65536,37439,1851,22285,3961,26656,22969,38603,3823,23110,0.025513,3088623821463467421,1053,23,40,3,4024,3721,2938,3837,ok +edge-2-height-max,2,24001,1,65536,37439,1851,22285,3961,26656,22969,38603,3823,23110,0.044465,3088623821463467421,1053,23,40,3,4024,3721,2938,3837,ok +edge-2-height-max,2,24003,1,65536,37331,1747,22148,4310,26658,22598,38577,3899,23060,0.050557,14885947647247422686,1256,29,34,0,3959,3797,2816,3609,ok +edge-2-height-max,2,24003,1,65536,37331,1747,22148,4310,26658,22598,38577,3899,23060,0.106717,14885947647247422686,1256,29,34,0,3959,3797,2816,3609,ok +edge-2-height-max,2,24004,1,65536,37334,2033,22487,3682,26353,22681,38493,3866,23177,0.039131,6600342002624190149,0,21,100000,1,3880,3854,3150,3764,ok +edge-2-height-max,2,24004,1,65536,37334,2033,22487,3682,26353,22681,38493,3866,23177,0.245534,6600342002624190149,0,21,100000,1,3880,3854,3150,3764,ok +edge-2-teams-min,2,24001,1,16384,8946,923,5518,997,6276,4247,9294,1417,5673,0.001784,18091492188118644277,902,1,9,1,1247,899,485,959,ok +edge-2-teams-min,2,24001,1,16384,8946,923,5518,997,6276,4247,9294,1417,5673,0.002718,18091492188118644277,902,1,9,1,1247,899,485,959,ok +edge-2-teams-min,2,24003,1,16384,9552,607,5280,945,7338,5547,9860,1077,5447,0.004448,9744020794824514254,1742,11,13,1,828,883,465,960,ok +edge-2-teams-min,2,24003,1,16384,9552,607,5280,945,7338,5547,9860,1077,5447,0.001560,9744020794824514254,1742,11,13,1,828,883,465,960,ok +edge-2-teams-min,2,24004,1,16384,9018,622,5520,1224,6595,5307,9397,1230,5757,0.001751,11258575627840650753,1106,9,10,1,954,740,700,964,ok +edge-2-teams-min,2,24004,1,16384,9018,622,5520,1224,6595,5307,9397,1230,5757,0.002096,11258575627840650753,1106,9,10,1,954,740,700,964,ok +edge-2-teams-max,2,24001,1,16384,8953,919,5515,997,6176,3965,9301,1413,5670,0.002302,18386146786474578188,0,100000,17,11,1202,869,447,959,ok +edge-2-teams-max,2,24001,1,16384,8953,919,5515,997,6176,3965,9301,1413,5670,0.002343,18386146786474578188,0,100000,17,11,1202,869,447,959,ok +edge-2-teams-max,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.004491,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-2-teams-max,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.004771,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-2-teams-max,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.003091,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-teams-max,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.001614,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-workers-min,2,24001,1,16384,8949,920,5518,997,6268,4190,9297,1414,5673,0.004927,5660897849027081340,320,15,20,4,1210,899,485,959,ok +edge-2-workers-min,2,24001,1,16384,8949,920,5518,997,6268,4190,9297,1414,5673,0.001813,5660897849027081340,320,15,20,4,1210,899,485,959,ok +edge-2-workers-min,2,24003,1,16384,9552,607,5280,945,7362,5545,9860,1077,5447,0.001713,6519285490690037608,602,11,13,4,800,839,465,960,ok +edge-2-workers-min,2,24003,1,16384,9552,607,5280,945,7362,5545,9860,1077,5447,0.023920,6519285490690037608,602,11,13,4,800,839,465,960,ok +edge-2-workers-min,2,24004,1,16384,9030,610,5520,1224,6562,5234,9409,1218,5757,0.001741,14833552175046425086,388,9,10,4,954,737,700,964,ok +edge-2-workers-min,2,24004,1,16384,9030,610,5520,1224,6562,5234,9409,1218,5757,0.001745,14833552175046425086,388,9,10,4,954,737,700,964,ok +edge-2-workers-max,2,24001,1,16384,8949,920,5518,997,6240,4189,9297,1414,5673,0.001947,5660897849027081340,418,15,17,4,1210,899,485,959,ok +edge-2-workers-max,2,24001,1,16384,8949,920,5518,997,6240,4189,9297,1414,5673,0.008564,5660897849027081340,418,15,17,4,1210,899,485,959,ok +edge-2-workers-max,2,24003,1,16384,9552,607,5280,945,7334,5521,9860,1077,5447,0.001743,6519285490690037608,737,10,12,4,800,839,465,960,ok +edge-2-workers-max,2,24003,1,16384,9552,607,5280,945,7334,5521,9860,1077,5447,0.002654,6519285490690037608,737,10,12,4,800,839,465,960,ok +edge-2-workers-max,2,24004,1,16384,9030,610,5520,1224,6534,5207,9409,1218,5757,0.001885,14833552175046425086,488,8,9,4,954,737,700,964,ok +edge-2-workers-max,2,24004,1,16384,9030,610,5520,1224,6534,5207,9409,1218,5757,0.001807,14833552175046425086,488,8,9,4,954,737,700,964,ok +edge-2-water-min,2,24001,1,16384,14609,1028,0,747,14514,13149,14991,1393,0,0.001878,15818806745139821749,1659,100000,100000,0,0,0,6,0,ok +edge-2-water-min,2,24001,1,16384,14609,1028,0,747,14514,13149,14991,1393,0,0.002063,15818806745139821749,1659,100000,100000,0,0,0,6,0,ok +edge-2-water-min,2,24003,1,16384,14653,1155,0,576,14533,13340,14948,1436,0,0.001564,17836359041140578458,1565,100000,100000,0,0,0,15,0,ok +edge-2-water-min,2,24003,1,16384,14653,1155,0,576,14533,13340,14948,1436,0,0.004168,17836359041140578458,1565,100000,100000,0,0,0,15,0,ok +edge-2-water-min,2,24004,1,16384,14196,1348,0,840,14102,12533,14624,1760,0,0.001831,7469588657728267115,1789,100000,100000,0,0,0,0,0,ok +edge-2-water-min,2,24004,1,16384,14196,1348,0,840,14102,12533,14624,1760,0,0.003183,7469588657728267115,1789,100000,100000,0,0,0,0,0,ok +edge-2-water-max,2,24001,1,16384,6173,831,7375,2005,3535,1864,6760,1833,7791,0.002096,17006424890009917153,331,8,14,4,991,928,626,1495,ok +edge-2-water-max,2,24001,1,16384,6173,831,7375,2005,3535,1864,6760,1833,7791,0.001947,17006424890009917153,331,8,14,4,991,928,626,1495,ok +edge-2-water-max,2,24003,1,16384,6199,590,7573,2022,3133,1707,6764,1598,8022,0.001729,1703270134573847676,0,34,6,3,854,1395,724,1490,ok +edge-2-water-max,2,24003,1,16384,6199,590,7573,2022,3133,1707,6764,1598,8022,0.001654,1703270134573847676,0,34,6,3,854,1395,724,1490,ok +edge-2-water-max,2,24004,1,16384,6125,557,8336,1366,3463,1818,6540,1236,8608,0.003470,5844940350594474362,214,14,8,4,619,1167,781,1487,ok +edge-2-water-max,2,24004,1,16384,6125,557,8336,1366,3463,1818,6540,1236,8608,0.001874,5844940350594474362,214,14,8,4,619,1167,781,1487,ok +edge-2-sand-min,2,24001,1,16384,9027,884,5523,950,7164,5282,9351,1353,5680,0.003961,1287794769456463877,918,17,20,4,764,391,614,987,ok +edge-2-sand-min,2,24001,1,16384,9027,884,5523,950,7164,5282,9351,1353,5680,0.004739,1287794769456463877,918,17,20,4,764,391,614,987,ok +edge-2-sand-min,2,24003,1,16384,8866,778,5504,1236,7309,5475,9282,1391,5711,0.001672,15593615391680384401,912,7,13,4,453,396,610,981,ok +edge-2-sand-min,2,24003,1,16384,8866,778,5504,1236,7309,5475,9282,1391,5711,0.001632,15593615391680384401,912,7,13,4,453,396,610,981,ok +edge-2-sand-min,2,24004,1,16384,9022,739,5413,1210,6553,5232,9402,1340,5642,0.001837,1511840691034133524,485,8,9,4,1089,837,454,988,ok +edge-2-sand-min,2,24004,1,16384,9022,739,5413,1210,6553,5232,9402,1340,5642,0.001761,1511840691034133524,485,8,9,4,1089,837,454,988,ok +edge-2-sand-max,2,24001,1,16384,5334,6360,3120,1570,3649,1915,5953,7143,3288,0.001899,13415170006420454930,293,6,10,4,497,478,620,546,ok +edge-2-sand-max,2,24001,1,16384,5334,6360,3120,1570,3649,1915,5953,7143,3288,0.001834,13415170006420454930,293,6,10,4,497,478,620,546,ok +edge-2-sand-max,2,24003,1,16384,5162,6659,3141,1422,4293,2611,5742,7368,3274,0.005413,7498629016206827803,344,100000,100000,0,0,0,776,540,ok +edge-2-sand-max,2,24003,1,16384,5162,6659,3141,1422,4293,2611,5742,7368,3274,0.001899,7498629016206827803,344,100000,100000,0,0,0,776,540,ok +edge-2-sand-max,2,24004,1,16384,5283,6530,3055,1516,3582,1899,5807,7289,3288,0.001881,8936974655475243293,278,13,14,4,429,879,298,534,ok +edge-2-sand-max,2,24004,1,16384,5283,6530,3055,1516,3582,1899,5807,7289,3288,0.001776,8936974655475243293,278,13,14,4,429,879,298,534,ok +edge-2-grass-min,2,24001,0,16384,0,0,16384,0,0,0,602,1671,14111,0.001525,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-2-grass-min,2,24001,0,16384,0,0,16384,0,0,0,602,1671,14111,0.001764,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-2-grass-min,2,24003,1,16384,329,975,13785,1295,21,0,639,1647,14098,0.002534,17907722784531174403,0,100000,100000,0,0,0,228,2336,ok +edge-2-grass-min,2,24003,1,16384,329,975,13785,1295,21,0,639,1647,14098,0.002342,17907722784531174403,0,100000,100000,0,0,0,228,2336,ok +edge-2-grass-min,2,24004,0,16384,0,0,16384,0,0,0,594,2055,13735,0.001598,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-grass-min,2,24004,0,16384,0,0,16384,0,0,0,594,2055,13735,0.091162,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-grass-max,2,24001,1,16384,10045,658,4676,1005,7993,6246,10397,1152,4835,0.003787,15747200618786921993,1153,19,20,4,842,474,642,801,ok +edge-2-grass-max,2,24001,1,16384,10045,658,4676,1005,7993,6246,10397,1152,4835,0.001965,15747200618786921993,1153,19,20,4,842,474,642,801,ok +edge-2-grass-max,2,24003,1,16384,10419,541,4704,720,8382,6634,10649,898,4837,0.013078,4750185470642720124,771,15,13,4,698,693,548,806,ok +edge-2-grass-max,2,24003,1,16384,10419,541,4704,720,8382,6634,10649,898,4837,0.001639,4750185470642720124,771,15,13,4,698,693,548,806,ok +edge-2-grass-max,2,24004,1,16384,10329,362,4495,1198,8179,7021,10683,958,4743,0.001822,2892416573280956093,1021,11,20,4,1004,585,472,785,ok +edge-2-grass-max,2,24004,1,16384,10329,362,4495,1198,8179,7021,10683,958,4743,0.001850,2892416573280956093,1021,11,20,4,1004,585,472,785,ok +edge-2-desert-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.002526,5660897849027081340,418,15,17,4,1210,899,485,959,ok +edge-2-desert-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.003701,5660897849027081340,418,15,17,4,1210,899,485,959,ok +edge-2-desert-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.001695,6519285490690037608,723,11,13,4,800,839,465,960,ok +edge-2-desert-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.001638,6519285490690037608,723,11,13,4,800,839,465,960,ok +edge-2-desert-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.001811,14833552175046425086,492,9,10,4,954,737,700,964,ok +edge-2-desert-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.001832,14833552175046425086,492,9,10,4,954,737,700,964,ok +edge-2-desert-max,2,24001,1,16384,3921,7965,3006,1492,2507,982,4500,8716,3168,0.003974,5497211519881356870,86,11,39,3,765,267,289,536,ok +edge-2-desert-max,2,24001,1,16384,3921,7965,3006,1492,2507,982,4500,8716,3168,0.002214,5497211519881356870,86,11,39,3,765,267,289,536,ok +edge-2-desert-max,2,24003,1,16384,4122,7759,3085,1418,2558,1182,4698,8471,3215,0.001557,3480572482632351213,202,42,42,2,504,566,390,540,ok +edge-2-desert-max,2,24003,1,16384,4122,7759,3085,1418,2558,1182,4698,8471,3215,0.004231,3480572482632351213,202,42,42,2,504,566,390,540,ok +edge-2-desert-max,2,24004,1,16384,4737,7281,2882,1484,3435,1841,5247,8026,3111,0.001872,1099917036119030571,197,2,12,4,577,412,210,534,ok +edge-2-desert-max,2,24004,1,16384,4737,7281,2882,1484,3435,1841,5247,8026,3111,0.002180,1099917036119030571,197,2,12,4,577,412,210,534,ok +edge-2-smoothing-min,2,24001,1,16384,9048,666,5536,1134,6861,4387,9468,1220,5696,0.001960,2613473709282920172,630,12,12,4,728,696,667,958,ok +edge-2-smoothing-min,2,24001,1,16384,9048,666,5536,1134,6861,4387,9468,1220,5696,0.001791,2613473709282920172,630,12,12,4,728,696,667,958,ok +edge-2-smoothing-min,2,24003,1,16384,9245,449,5296,1394,6634,3921,9726,1134,5524,0.001587,66553570091746125,511,4,4,4,841,975,690,958,ok +edge-2-smoothing-min,2,24003,1,16384,9245,449,5296,1394,6634,3921,9726,1134,5524,0.001617,66553570091746125,511,4,4,4,841,975,690,958,ok +edge-2-smoothing-min,2,24004,1,16384,9166,422,5510,1286,6625,5123,9579,1056,5749,0.028921,14894153078089829296,804,8,9,4,1029,723,700,962,ok +edge-2-smoothing-min,2,24004,1,16384,9166,422,5510,1286,6625,5123,9579,1056,5749,0.004390,14894153078089829296,804,8,9,4,1029,723,700,962,ok +edge-2-smoothing-max,2,24001,1,16384,9387,615,5534,848,6629,5251,9657,1036,5691,0.052779,16332416003315470351,0,23,100000,3,933,1036,689,959,ok +edge-2-smoothing-max,2,24001,1,16384,9387,615,5534,848,6629,5251,9657,1036,5691,0.001801,16332416003315470351,0,23,100000,3,933,1036,689,959,ok +edge-2-smoothing-max,2,24003,1,16384,9051,873,5496,964,6949,5555,9361,1352,5671,0.001729,10575936385512013596,434,13,14,4,849,777,378,954,ok +edge-2-smoothing-max,2,24003,1,16384,9051,873,5496,964,6949,5555,9361,1352,5671,0.001665,10575936385512013596,434,13,14,4,849,777,378,954,ok +edge-2-smoothing-max,2,24004,0,16384,0,0,16384,0,0,0,9489,1140,5755,0.001620,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-smoothing-max,2,24004,0,16384,0,0,16384,0,0,0,9489,1140,5755,0.097096,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-river-width-min,2,24001,1,16384,9047,976,4395,1966,7052,4809,9643,1957,4784,0.001543,995488406803712075,0,7,100000,3,672,470,755,961,ok +edge-2-river-width-min,2,24001,1,16384,9047,976,4395,1966,7052,4809,9643,1957,4784,0.001637,995488406803712075,0,7,100000,3,672,470,755,961,ok +edge-2-river-width-min,2,24003,1,16384,8950,938,4380,2116,6847,4524,9574,1993,4817,0.001492,14309378745742228429,597,9,16,4,548,762,697,961,ok +edge-2-river-width-min,2,24003,1,16384,8950,938,4380,2116,6847,4524,9574,1993,4817,0.001798,14309378745742228429,597,9,16,4,548,762,697,961,ok +edge-2-river-width-min,2,24004,1,16384,8871,743,4938,1832,7021,4914,9428,1653,5303,0.004875,17138623470901157076,407,13,8,4,761,643,346,965,ok +edge-2-river-width-min,2,24004,1,16384,8871,743,4938,1832,7021,4914,9428,1653,5303,0.001867,17138623470901157076,407,13,8,4,761,643,346,965,ok +edge-2-river-width-max,2,24001,1,16384,9270,595,5649,870,6418,5302,9554,1026,5804,0.002624,13040961475971353362,1330,19,38,3,969,967,815,951,ok +edge-2-river-width-max,2,24001,1,16384,9270,595,5649,870,6418,5302,9554,1026,5804,0.004005,13040961475971353362,1330,19,38,3,969,967,815,951,ok +edge-2-river-width-max,2,24003,1,16384,9304,653,5671,756,6441,5446,9555,1026,5803,0.002171,4763919030790921477,496,28,28,1,881,1058,819,961,ok +edge-2-river-width-max,2,24003,1,16384,9304,653,5671,756,6441,5446,9555,1026,5803,0.002442,4763919030790921477,496,28,28,1,881,1058,819,961,ok +edge-2-river-width-max,2,24004,0,16384,0,0,16384,0,0,0,9501,1120,5763,0.002452,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-river-width-max,2,24004,0,16384,0,0,16384,0,0,0,9501,1120,5763,0.003508,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-fruit-min,2,24001,1,16384,9605,382,5629,768,7199,5630,9817,765,5802,0.001847,7176297995739189086,667,16,12,4,815,758,753,997,ok +edge-2-fruit-min,2,24001,1,16384,9605,382,5629,768,7199,5630,9817,765,5802,0.001914,7176297995739189086,667,16,12,4,815,758,753,997,ok +edge-2-fruit-min,2,24003,0,16384,0,0,16384,0,0,0,9815,858,5711,0.001389,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-2-fruit-min,2,24003,0,16384,0,0,16384,0,0,0,9815,858,5711,0.002189,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-2-fruit-min,2,24004,0,16384,0,0,16384,0,0,0,9918,513,5953,0.029737,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-fruit-min,2,24004,0,16384,0,0,16384,0,0,0,9918,513,5953,0.002514,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-2-fruit-max,2,24001,1,16384,5156,5803,3755,1670,3094,1029,5835,6636,3913,0.002021,3788484734914861314,80,24,38,3,803,411,467,651,ok +edge-2-fruit-max,2,24001,1,16384,5156,5803,3755,1670,3094,1029,5835,6636,3913,0.002181,3788484734914861314,80,24,38,3,803,411,467,651,ok +edge-2-fruit-max,2,24003,1,16384,5437,5607,3794,1546,3664,1417,6079,6379,3926,0.001584,5260591269051681174,299,20,30,4,489,416,507,654,ok +edge-2-fruit-max,2,24003,1,16384,5437,5607,3794,1546,3664,1417,6079,6379,3926,0.004311,5260591269051681174,299,20,30,4,489,416,507,654,ok +edge-2-fruit-max,2,24004,1,16384,5728,5261,3557,1838,3774,1339,6412,6180,3792,0.001967,210623947230701153,141,26,28,3,781,486,299,651,ok +edge-2-fruit-max,2,24004,1,16384,5728,5261,3557,1838,3774,1339,6412,6180,3792,0.002174,210623947230701153,141,26,28,3,781,486,299,651,ok +edge-2-repeat-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.001920,5660897849027081340,418,15,17,4,1210,899,485,959,ok +edge-2-repeat-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.001904,5660897849027081340,418,15,17,4,1210,899,485,959,ok +edge-2-repeat-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.003039,6519285490690037608,723,11,13,4,800,839,465,960,ok +edge-2-repeat-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.013182,6519285490690037608,723,11,13,4,800,839,465,960,ok +edge-2-repeat-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.004103,14833552175046425086,492,9,10,4,954,737,700,964,ok +edge-2-repeat-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.001836,14833552175046425086,492,9,10,4,954,737,700,964,ok +edge-2-repeat-max,2,24001,1,16384,7104,480,3680,5120,4860,1120,8608,3008,4768,0.000539,15024589055358181125,70,1,2,4,448,828,416,800,ok +edge-2-repeat-max,2,24001,1,16384,7104,480,3680,5120,4860,1120,8608,3008,4768,0.000517,15024589055358181125,70,1,2,4,448,828,416,800,ok +edge-2-repeat-max,2,24003,1,16384,7104,416,3552,5312,4904,600,8640,3040,4704,0.000494,5590689445227312896,30,2,2,4,596,600,512,928,ok +edge-2-repeat-max,2,24003,1,16384,7104,416,3552,5312,4904,600,8640,3040,4704,0.001229,5590689445227312896,30,2,2,4,596,600,512,928,ok +edge-2-repeat-max,2,24004,1,16384,7520,576,3616,4672,4596,608,8768,2912,4704,0.000511,17324586385127991603,37,1,1,4,576,1308,352,896,ok +edge-2-repeat-max,2,24004,1,16384,7520,576,3616,4672,4596,608,8768,2912,4704,0.001039,17324586385127991603,37,1,1,4,576,1308,352,896,ok +size-2-6-6-4,2,24001,1,4096,2214,141,1319,422,1504,786,2340,350,1406,0.000527,12702816318780975272,171,5,14,4,396,110,114,238,ok +size-2-6-6-4,2,24001,1,4096,2214,141,1319,422,1504,786,2340,350,1406,0.000517,12702816318780975272,171,5,14,4,396,110,114,238,ok +size-2-6-6-4,2,24003,1,4096,2208,132,1288,468,1559,927,2344,366,1386,0.001279,3938647859703722544,247,8,8,4,183,251,115,234,ok +size-2-6-6-4,2,24003,1,4096,2208,132,1288,468,1559,927,2344,366,1386,0.000519,3938647859703722544,247,8,8,4,183,251,115,234,ok +size-2-6-6-4,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.001112,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-2-6-6-4,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.000359,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-2-6-6-12,2,24001,0,4096,0,0,4096,0,0,0,2340,350,1406,0.000469,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-2-6-6-12,2,24001,0,4096,0,0,4096,0,0,0,2340,350,1406,0.000448,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-2-6-6-12,2,24003,0,4096,0,0,4096,0,0,0,2344,366,1386,0.000368,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-2-6-6-12,2,24003,0,4096,0,0,4096,0,0,0,2344,366,1386,0.000380,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-2-6-6-12,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.001593,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-2-6-6-12,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.001113,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-2-7-7-8,2,24001,1,16384,8949,920,5518,997,6208,4023,9297,1414,5673,0.003702,16617434980050527102,0,100000,17,7,1213,897,452,959,ok +size-2-7-7-8,2,24001,1,16384,8949,920,5518,997,6208,4023,9297,1414,5673,0.005853,16617434980050527102,0,100000,17,7,1213,897,452,959,ok +size-2-7-7-8,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.001526,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +size-2-7-7-8,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.026580,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +size-2-7-7-8,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.001863,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +size-2-7-7-8,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.006247,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +size-2-6-9-12,2,24001,1,32768,17769,892,10374,3733,13014,9371,18891,2748,11129,0.070280,11686570519299650906,628,25,24,11,1846,1898,752,1930,ok +size-2-6-9-12,2,24001,1,32768,17769,892,10374,3733,13014,9371,18891,2748,11129,0.050174,11686570519299650906,628,25,24,11,1846,1898,752,1930,ok +size-2-6-9-12,2,24003,1,32768,17580,1137,9993,4058,13138,9747,18693,3161,10914,0.033997,3187380565725862322,511,22,18,12,1913,1783,485,1913,ok +size-2-6-9-12,2,24003,1,32768,17580,1137,9993,4058,13138,9747,18693,3161,10914,0.068471,3187380565725862322,511,22,18,12,1913,1783,485,1913,ok +size-2-6-9-12,2,24004,1,32768,17767,1012,10224,3765,13087,9517,18840,2887,11041,0.197296,4914701710707839179,760,20,20,12,1895,1868,661,1807,ok +size-2-6-9-12,2,24004,1,32768,17767,1012,10224,3765,13087,9517,18840,2887,11041,0.022514,4914701710707839179,760,20,20,12,1895,1868,661,1807,ok +size-2-9-6-12,2,24001,1,32768,18175,731,10642,3220,12980,9656,19142,2339,11287,0.044046,14530006048841308419,250,14,16,12,1925,1919,1087,1800,ok +size-2-9-6-12,2,24001,1,32768,18175,731,10642,3220,12980,9656,19142,2339,11287,0.021462,14530006048841308419,250,14,16,12,1925,1919,1087,1800,ok +size-2-9-6-12,2,24003,1,32768,17666,1085,10025,3992,13142,9649,18745,3081,10942,0.084821,15235139514388628623,596,15,12,12,1830,1875,567,1779,ok +size-2-9-6-12,2,24003,1,32768,17666,1085,10025,3992,13142,9649,18745,3081,10942,0.076352,15235139514388628623,596,15,12,12,1830,1875,567,1779,ok +size-2-9-6-12,2,24004,1,32768,17915,956,10413,3484,13062,9824,18905,2695,11168,0.076457,17904064177986095803,611,15,18,12,1879,1960,765,1866,ok +size-2-9-6-12,2,24004,1,32768,17915,956,10413,3484,13062,9824,18905,2695,11168,0.070326,17904064177986095803,611,15,18,12,1879,1960,765,1866,ok +size-2-9-9-12,2,24001,1,262144,149100,12955,91905,8184,109726,85886,152601,16958,92585,0.848434,9572728202868954291,0,100000,100000,7,14454,13999,10668,15420,ok +size-2-9-9-12,2,24001,1,262144,149100,12955,91905,8184,109726,85886,152601,16958,92585,0.701838,9572728202868954291,0,100000,100000,7,14454,13999,10668,15420,ok +size-2-9-9-12,2,24003,1,262144,147869,13206,89841,11228,103729,78628,152140,18741,91263,0.598385,10176986978518237311,522,14,13,12,16701,16929,10248,15436,ok +size-2-9-9-12,2,24003,1,262144,147869,13206,89841,11228,103729,78628,152140,18741,91263,0.744190,10176986978518237311,522,14,13,12,16701,16929,10248,15436,ok +size-2-9-9-12,2,24004,1,262144,150768,12248,91836,7292,106434,95707,153413,15855,92876,0.844746,6462093021749933095,1443,51,52,2,14398,15267,14415,15377,ok +size-2-9-9-12,2,24004,1,262144,150768,12248,91836,7292,106434,95707,153413,15855,92876,0.854000,6462093021749933095,1443,51,52,2,14398,15267,14415,15377,ok +corner-2-min,2,24001,0,0,0,0,0,0,0,0,0,0,0,0.000009,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-2-min,2,24001,0,0,0,0,0,0,0,0,0,0,0,0.000011,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-2-min,2,24003,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-2-min,2,24003,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-2-min,2,24004,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-2-min,2,24004,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-2-max,2,24001,1,262144,47952,136696,52192,25304,17240,224,56348,149348,56448,0.011546,11845766406100531494,0,1,1,0,6924,11520,4992,9280,ok +corner-2-max,2,24001,1,262144,47952,136696,52192,25304,17240,224,56348,149348,56448,0.076648,11845766406100531494,0,1,1,0,6924,11520,4992,9280,ok +corner-2-max,2,24003,1,262144,48208,137304,51392,25240,15364,96,56604,149924,55616,0.100834,6995744940389614058,0,2,1,0,7984,10620,5600,8992,ok +corner-2-max,2,24003,1,262144,48208,137304,51392,25240,15364,96,56604,149924,55616,0.070179,6995744940389614058,0,2,1,0,7984,10620,5600,8992,ok +corner-2-max,2,24004,1,262144,48112,137336,51520,25176,15408,224,56476,149924,55744,0.245977,6866782697663556605,5,1,2,0,10300,9036,4480,8896,ok +corner-2-max,2,24004,1,262144,48112,137336,51520,25176,15408,224,56476,149924,55744,0.033469,6866782697663556605,5,1,2,0,10300,9036,4480,8896,ok +edge-3-width-min,3,24001,1,8192,3779,316,2579,1518,2694,1583,4175,1072,2945,0.002238,5528040763038411418,317,9,17,4,425,411,150,546,ok +edge-3-width-min,3,24001,1,8192,3779,316,2579,1518,2694,1583,4175,1072,2945,0.000858,5528040763038411418,317,9,17,4,425,411,150,546,ok +edge-3-width-min,3,24003,1,8192,3777,357,2576,1482,2693,1601,4156,1096,2940,0.000876,16057670206668850732,83,8,19,4,474,325,189,546,ok +edge-3-width-min,3,24003,1,8192,3777,357,2576,1482,2693,1601,4156,1096,2940,0.000895,16057670206668850732,83,8,19,4,474,325,189,546,ok +edge-3-width-min,3,24004,1,8192,3781,319,2604,1488,2707,1578,4170,1060,2962,0.001447,10194287565770016619,221,12,11,4,433,399,151,545,ok +edge-3-width-min,3,24004,1,8192,3781,319,2604,1488,2707,1578,4170,1060,2962,0.002381,10194287565770016619,221,12,11,4,433,399,151,545,ok +edge-3-width-max,3,24001,1,65536,34383,1840,24878,4435,22401,19171,35580,4055,25901,0.144345,1121796287204890133,1175,26,27,3,4614,4122,3144,4371,ok +edge-3-width-max,3,24001,1,65536,34383,1840,24878,4435,22401,19171,35580,4055,25901,0.077997,1121796287204890133,1175,26,27,3,4614,4122,3144,4371,ok +edge-3-width-max,3,24003,1,65536,34350,1826,24873,4487,22446,19094,35550,4065,25921,0.006762,15420516851104356162,1205,30,31,3,4581,4180,3048,4376,ok +edge-3-width-max,3,24003,1,65536,34350,1826,24873,4487,22446,19094,35550,4065,25921,0.006519,15420516851104356162,1205,30,31,3,4581,4180,3048,4376,ok +edge-3-width-max,3,24004,1,65536,34467,1809,24982,4278,22472,19480,35608,3946,25982,0.078431,9783622262180087610,1119,37,38,2,4933,3799,3157,4337,ok +edge-3-width-max,3,24004,1,65536,34467,1809,24982,4278,22472,19480,35608,3946,25982,0.015036,9783622262180087610,1119,37,38,2,4933,3799,3157,4337,ok +edge-3-height-min,3,24001,1,8192,3781,330,2595,1486,2692,1543,4171,1069,2952,0.002316,8296798997807980933,211,15,8,4,378,459,155,545,ok +edge-3-height-min,3,24001,1,8192,3781,330,2595,1486,2692,1543,4171,1069,2952,0.000926,8296798997807980933,211,15,8,4,378,459,155,545,ok +edge-3-height-min,3,24003,1,8192,3777,340,2615,1460,2674,1588,4159,1068,2965,0.002542,3287260318131999003,201,18,10,4,361,496,146,543,ok +edge-3-height-min,3,24003,1,8192,3777,340,2615,1460,2674,1588,4159,1068,2965,0.002197,3287260318131999003,201,18,10,4,361,496,146,543,ok +edge-3-height-min,3,24004,1,8192,3747,304,2585,1556,2674,1470,4161,1077,2954,0.000893,8306510028419146177,130,11,8,4,362,437,182,545,ok +edge-3-height-min,3,24004,1,8192,3747,304,2585,1556,2674,1470,4161,1077,2954,0.003821,8306510028419146177,130,11,8,4,362,437,182,545,ok +edge-3-height-max,3,24001,1,65536,34189,2110,24709,4528,22420,19210,35400,4370,25766,0.007522,14451902555196200460,1362,31,49,1,3801,4953,2913,4377,ok +edge-3-height-max,3,24001,1,65536,34189,2110,24709,4528,22420,19210,35400,4370,25766,0.011496,14451902555196200460,1362,31,49,1,3801,4953,2913,4377,ok +edge-3-height-max,3,24003,1,65536,34397,1997,24946,4196,22518,19529,35501,4093,25942,0.009812,4567928969128129527,1060,46,20,3,4720,3986,3075,4345,ok +edge-3-height-max,3,24003,1,65536,34397,1997,24946,4196,22518,19529,35501,4093,25942,0.007646,4567928969128129527,1060,46,20,3,4720,3986,3075,4345,ok +edge-3-height-max,3,24004,1,65536,34403,1989,24918,4226,22434,19318,35539,4099,25898,0.005880,16325195449582695868,1130,52,29,3,4891,3848,3136,4354,ok +edge-3-height-max,3,24004,1,65536,34403,1989,24918,4226,22434,19318,35539,4099,25898,0.006299,16325195449582695868,1130,52,29,3,4891,3848,3136,4354,ok +edge-3-teams-min,3,24001,1,16384,8637,466,6301,980,5592,4748,8908,955,6521,0.001594,15467076962016452729,1415,14,39,0,1146,1053,810,1094,ok +edge-3-teams-min,3,24001,1,16384,8637,466,6301,980,5592,4748,8908,955,6521,0.001584,15467076962016452729,1415,14,39,0,1146,1053,810,1094,ok +edge-3-teams-min,3,24003,1,16384,8642,446,6268,1028,5596,4769,8922,959,6503,0.001588,16213537115684893476,1725,21,37,0,1149,1050,811,1081,ok +edge-3-teams-min,3,24003,1,16384,8642,446,6268,1028,5596,4769,8922,959,6503,0.003589,16213537115684893476,1725,21,37,0,1149,1050,811,1081,ok +edge-3-teams-min,3,24004,1,16384,8636,448,6262,1038,5595,4741,8919,966,6499,0.001615,15389565672406902104,1193,31,8,0,1167,1017,817,1085,ok +edge-3-teams-min,3,24004,1,16384,8636,448,6262,1038,5595,4741,8919,966,6499,0.001595,15389565672406902104,1193,31,8,0,1167,1017,817,1085,ok +edge-3-teams-max,3,24001,1,16384,7305,663,4863,3553,5349,2758,8215,2433,5736,0.001878,15526420115868331245,54,9,13,12,676,784,246,1093,ok +edge-3-teams-max,3,24001,1,16384,7305,663,4863,3553,5349,2758,8215,2433,5736,0.001959,15526420115868331245,54,9,13,12,676,784,246,1093,ok +edge-3-teams-max,3,24003,1,16384,7240,720,4857,3567,5297,2654,8166,2495,5723,0.001934,9678267356762982999,113,8,13,12,674,767,246,1087,ok +edge-3-teams-max,3,24003,1,16384,7240,720,4857,3567,5297,2654,8166,2495,5723,0.001852,9678267356762982999,113,8,13,12,674,767,246,1087,ok +edge-3-teams-max,3,24004,1,16384,7189,643,4847,3705,5236,2467,8154,2487,5743,0.001940,17741115356312039064,62,10,11,12,644,728,318,1079,ok +edge-3-teams-max,3,24004,1,16384,7189,643,4847,3705,5236,2467,8154,2487,5743,0.026656,17741115356312039064,62,10,11,12,644,728,318,1079,ok +edge-3-workers-min,3,24001,1,16384,7907,530,5612,2335,5485,3698,8532,1694,6158,0.001681,12754827918508373822,536,13,13,4,1123,743,471,1084,ok +edge-3-workers-min,3,24001,1,16384,7907,530,5612,2335,5485,3698,8532,1694,6158,0.001573,12754827918508373822,536,13,13,4,1123,743,471,1084,ok +edge-3-workers-min,3,24003,1,16384,8072,494,5698,2120,5526,3950,8630,1551,6203,0.001615,13384879564688048696,682,20,16,4,1080,957,422,1089,ok +edge-3-workers-min,3,24003,1,16384,8072,494,5698,2120,5526,3950,8630,1551,6203,0.004522,13384879564688048696,682,20,16,4,1080,957,422,1089,ok +edge-3-workers-min,3,24004,1,16384,8056,534,5672,2122,5550,4015,8607,1593,6184,0.001584,7198112912547603041,629,13,19,4,1071,918,427,1084,ok +edge-3-workers-min,3,24004,1,16384,8056,534,5672,2122,5550,4015,8607,1593,6184,0.003620,7198112912547603041,629,13,19,4,1071,918,427,1084,ok +edge-3-workers-max,3,24001,1,16384,7907,530,5612,2335,5457,3658,8532,1694,6158,0.001616,12754827918508373822,526,12,12,4,1123,743,471,1084,ok +edge-3-workers-max,3,24001,1,16384,7907,530,5612,2335,5457,3658,8532,1694,6158,0.001508,12754827918508373822,526,12,12,4,1123,743,471,1084,ok +edge-3-workers-max,3,24003,1,16384,8072,494,5698,2120,5498,3919,8630,1551,6203,0.002459,13384879564688048696,786,20,16,4,1080,957,422,1089,ok +edge-3-workers-max,3,24003,1,16384,8072,494,5698,2120,5498,3919,8630,1551,6203,0.001559,13384879564688048696,786,20,16,4,1080,957,422,1089,ok +edge-3-workers-max,3,24004,1,16384,8056,534,5672,2122,5522,3987,8607,1593,6184,0.004305,7198112912547603041,778,12,18,4,1071,918,427,1084,ok +edge-3-workers-max,3,24004,1,16384,8056,534,5672,2122,5522,3987,8607,1593,6184,0.002560,7198112912547603041,778,12,18,4,1071,918,427,1084,ok +edge-3-water-min,3,24001,1,16384,14576,1038,0,770,14469,12851,14986,1398,0,0.004253,7011207854252150283,1735,100000,100000,0,0,0,2,0,ok +edge-3-water-min,3,24001,1,16384,14576,1038,0,770,14469,12851,14986,1398,0,0.001609,7011207854252150283,1735,100000,100000,0,0,0,2,0,ok +edge-3-water-min,3,24003,1,16384,14496,966,0,922,14387,12555,14989,1395,0,0.001492,17813000635001715956,1796,100000,100000,0,0,0,4,0,ok +edge-3-water-min,3,24003,1,16384,14496,966,0,922,14387,12555,14989,1395,0,0.001513,17813000635001715956,1796,100000,100000,0,0,0,4,0,ok +edge-3-water-min,3,24004,1,16384,14600,1066,0,718,14503,12983,14985,1399,0,0.003047,12600054127630809182,1888,100000,100000,0,0,0,3,0,ok +edge-3-water-min,3,24004,1,16384,14600,1066,0,718,14503,12983,14985,1399,0,0.005787,12600054127630809182,1888,100000,100000,0,0,0,3,0,ok +edge-3-water-max,3,24001,1,16384,5858,399,8156,1971,3175,1730,6382,1380,8622,0.003365,3028610027892560003,246,8,12,4,1157,891,527,1485,ok +edge-3-water-max,3,24001,1,16384,5858,399,8156,1971,3175,1730,6382,1380,8622,0.001580,3028610027892560003,246,8,12,4,1157,891,527,1485,ok +edge-3-water-max,3,24003,1,16384,5943,404,8209,1828,3199,1924,6425,1315,8644,0.004482,16231263052623910756,392,16,7,4,1151,1013,477,1496,ok +edge-3-water-max,3,24003,1,16384,5943,404,8209,1828,3199,1924,6425,1315,8644,0.005360,16231263052623910756,392,16,7,4,1151,1013,477,1496,ok +edge-3-water-max,3,24004,1,16384,5945,436,8183,1820,3262,2071,6413,1344,8627,0.016115,4402223064569854256,276,11,16,4,1265,865,461,1481,ok +edge-3-water-max,3,24004,1,16384,5945,436,8183,1820,3262,2071,6413,1344,8627,0.002601,4402223064569854256,276,11,16,4,1265,865,461,1481,ok +edge-3-sand-min,3,24001,1,16384,7860,728,5467,2329,5608,3830,8480,1889,6015,0.001659,2946918700507912519,378,6,14,4,1061,716,378,1114,ok +edge-3-sand-min,3,24001,1,16384,7860,728,5467,2329,5608,3830,8480,1889,6015,0.001558,2946918700507912519,378,6,14,4,1061,716,378,1114,ok +edge-3-sand-min,3,24003,1,16384,7979,708,5569,2128,5629,4034,8540,1769,6075,0.006882,17593408615499011249,766,20,16,4,1039,901,311,1114,ok +edge-3-sand-min,3,24003,1,16384,7979,708,5569,2128,5629,4034,8540,1769,6075,0.001581,17593408615499011249,766,20,16,4,1039,901,311,1114,ok +edge-3-sand-min,3,24004,1,16384,7988,730,5526,2140,5682,4139,8542,1798,6044,0.002828,7014644291778475253,896,13,20,4,1042,848,314,1110,ok +edge-3-sand-min,3,24004,1,16384,7988,730,5526,2140,5682,4139,8542,1798,6044,0.004365,7014644291778475253,896,13,20,4,1042,848,314,1110,ok +edge-3-sand-max,3,24001,1,16384,4786,6211,3229,2158,3165,1827,5253,7287,3844,0.001445,2135500165035206320,243,6,11,4,652,555,324,636,ok +edge-3-sand-max,3,24001,1,16384,4786,6211,3229,2158,3165,1827,5253,7287,3844,0.002860,2135500165035206320,243,6,11,4,652,555,324,636,ok +edge-3-sand-max,3,24003,1,16384,4838,6285,3261,2000,3158,1939,5262,7282,3840,0.005026,1391947087025844220,315,14,15,4,668,594,309,640,ok +edge-3-sand-max,3,24003,1,16384,4838,6285,3261,2000,3158,1939,5262,7282,3840,0.001508,1391947087025844220,315,14,15,4,668,594,309,640,ok +edge-3-sand-max,3,24004,1,16384,4864,6272,3266,1982,3232,2123,5273,7262,3849,0.001480,14692715695912506646,246,13,18,4,765,468,316,632,ok +edge-3-sand-max,3,24004,1,16384,4864,6272,3266,1982,3232,2123,5273,7262,3849,0.001542,14692715695912506646,246,13,18,4,765,468,316,632,ok +edge-3-grass-min,3,24001,0,16384,0,0,16384,0,0,0,8,1847,14529,0.001211,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-3-grass-min,3,24001,0,16384,0,0,16384,0,0,0,8,1847,14529,0.003670,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-3-grass-min,3,24003,0,16384,0,0,16384,0,0,0,10,1844,14530,0.003749,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-3-grass-min,3,24003,0,16384,0,0,16384,0,0,0,10,1844,14530,0.001201,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-3-grass-min,3,24004,0,16384,0,0,16384,0,0,0,5,1849,14530,0.001299,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-3-grass-min,3,24004,0,16384,0,0,16384,0,0,0,5,1849,14530,0.001169,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-3-grass-max,3,24001,1,16384,8963,512,4485,2424,7148,5252,9604,1721,5059,0.001550,3006635018881676965,822,14,14,4,764,623,331,926,ok +edge-3-grass-max,3,24001,1,16384,8963,512,4485,2424,7148,5252,9604,1721,5059,0.004568,3006635018881676965,822,14,14,4,764,623,331,926,ok +edge-3-grass-max,3,24003,1,16384,9100,481,4575,2228,7153,5436,9683,1592,5109,0.001615,16155656616633487546,658,15,17,4,829,701,313,920,ok +edge-3-grass-max,3,24003,1,16384,9100,481,4575,2228,7153,5436,9683,1592,5109,0.001576,16155656616633487546,658,15,17,4,829,701,313,920,ok +edge-3-grass-max,3,24004,1,16384,9109,509,4548,2218,7201,5579,9679,1616,5089,0.002959,3721709725734800813,1081,14,22,4,874,637,306,914,ok +edge-3-grass-max,3,24004,1,16384,9109,509,4548,2218,7201,5579,9679,1616,5089,0.001577,3721709725734800813,1081,14,22,4,874,637,306,914,ok +edge-3-desert-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.039450,12754827918508373822,533,13,13,4,1123,743,471,1084,ok +edge-3-desert-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.002335,12754827918508373822,533,13,13,4,1123,743,471,1084,ok +edge-3-desert-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.004556,13384879564688048696,793,20,16,4,1080,957,422,1089,ok +edge-3-desert-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.001608,13384879564688048696,793,20,16,4,1080,957,422,1089,ok +edge-3-desert-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001627,7198112912547603041,779,13,19,4,1071,918,427,1084,ok +edge-3-desert-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.002638,7198112912547603041,779,13,19,4,1071,918,427,1084,ok +edge-3-desert-max,3,24001,1,16384,3516,7008,2560,3300,2544,330,4563,8656,3165,0.001557,17078476210265677871,18,14,17,4,363,322,191,625,ok +edge-3-desert-max,3,24001,1,16384,3516,7008,2560,3300,2544,330,4563,8656,3165,0.004225,17078476210265677871,18,14,17,4,363,322,191,625,ok +edge-3-desert-max,3,24003,1,16384,3663,6983,2619,3119,2738,292,4645,8543,3196,0.005704,6493544119451687889,26,23,22,4,351,337,140,620,ok +edge-3-desert-max,3,24003,1,16384,3663,6983,2619,3119,2738,292,4645,8543,3196,0.001433,6493544119451687889,26,23,22,4,351,337,140,620,ok +edge-3-desert-max,3,24004,1,16384,3665,6985,2566,3168,2709,382,4659,8566,3159,0.001549,18152073624814054768,5,10,14,3,463,247,144,620,ok +edge-3-desert-max,3,24004,1,16384,3665,6985,2566,3168,2709,382,4659,8566,3159,0.001518,18152073624814054768,5,10,14,3,463,247,144,620,ok +edge-3-smoothing-min,3,24001,1,16384,7907,542,5579,2356,5471,3674,8540,1717,6127,0.001556,2522226670394524074,547,13,10,4,1241,666,427,1087,ok +edge-3-smoothing-min,3,24001,1,16384,7907,542,5579,2356,5471,3674,8540,1717,6127,0.002024,2522226670394524074,547,13,10,4,1241,666,427,1087,ok +edge-3-smoothing-min,3,24003,1,16384,7989,518,5671,2206,5482,3837,8579,1616,6189,0.002766,5300193696107674525,752,13,13,4,1018,979,407,1093,ok +edge-3-smoothing-min,3,24003,1,16384,7989,518,5671,2206,5482,3837,8579,1616,6189,0.001552,5300193696107674525,752,13,13,4,1018,979,407,1093,ok +edge-3-smoothing-min,3,24004,1,16384,8063,522,5637,2162,5529,3912,8632,1600,6152,0.001673,17631944061623576957,765,12,19,4,1050,947,435,1080,ok +edge-3-smoothing-min,3,24004,1,16384,8063,522,5637,2162,5529,3912,8632,1600,6152,0.001889,17631944061623576957,765,12,19,4,1050,947,435,1080,ok +edge-3-smoothing-max,3,24001,1,16384,7920,539,5610,2315,5478,3686,8550,1692,6142,0.003520,11141673957128682515,426,8,11,4,1187,698,460,1093,ok +edge-3-smoothing-max,3,24001,1,16384,7920,539,5610,2315,5478,3686,8550,1692,6142,0.001992,11141673957128682515,426,8,11,4,1187,698,460,1093,ok +edge-3-smoothing-max,3,24003,1,16384,8048,501,5697,2138,5478,3868,8620,1567,6197,0.005673,2630897386024857052,604,20,12,4,1030,1024,418,1086,ok +edge-3-smoothing-max,3,24003,1,16384,8048,501,5697,2138,5478,3868,8620,1567,6197,0.118373,2630897386024857052,604,20,12,4,1030,1024,418,1086,ok +edge-3-smoothing-max,3,24004,1,16384,8082,487,5727,2088,5517,3957,8638,1528,6218,0.001557,9305954735862488224,768,12,23,4,1178,854,438,1085,ok +edge-3-smoothing-max,3,24004,1,16384,8082,487,5727,2088,5517,3957,8638,1528,6218,0.001572,9305954735862488224,768,12,23,4,1178,854,438,1085,ok +edge-3-extra-islands-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.002429,12754827918508373822,533,13,13,4,1123,743,471,1084,ok +edge-3-extra-islands-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.003526,12754827918508373822,533,13,13,4,1123,743,471,1084,ok +edge-3-extra-islands-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.001586,13384879564688048696,793,20,16,4,1080,957,422,1089,ok +edge-3-extra-islands-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.003008,13384879564688048696,793,20,16,4,1080,957,422,1089,ok +edge-3-extra-islands-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001709,7198112912547603041,779,13,19,4,1071,918,427,1084,ok +edge-3-extra-islands-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.005363,7198112912547603041,779,13,19,4,1071,918,427,1084,ok +edge-3-extra-islands-max,3,24001,1,16384,7264,700,4863,3557,5466,3020,8176,2472,5736,0.003253,6464814558566477888,194,9,10,4,678,784,246,1093,ok +edge-3-extra-islands-max,3,24001,1,16384,7264,700,4863,3557,5466,3020,8176,2472,5736,0.001793,6464814558566477888,194,9,10,4,678,784,246,1093,ok +edge-3-extra-islands-max,3,24003,1,16384,7219,739,4857,3569,5436,3003,8146,2515,5723,0.002916,18057301383644286829,176,8,8,4,674,767,246,1087,ok +edge-3-extra-islands-max,3,24003,1,16384,7219,739,4857,3569,5436,3003,8146,2515,5723,0.006614,18057301383644286829,176,8,8,4,674,767,246,1087,ok +edge-3-extra-islands-max,3,24004,1,16384,7171,649,4849,3715,5373,2707,8141,2498,5745,0.072596,4649899318218875381,197,9,11,4,644,728,318,1079,ok +edge-3-extra-islands-max,3,24004,1,16384,7171,649,4849,3715,5373,2707,8141,2498,5745,0.001553,4649899318218875381,197,9,11,4,644,728,318,1079,ok +edge-3-fruit-min,3,24001,1,16384,8311,132,5829,2112,5771,4432,8826,1186,6372,0.001649,9074598716795909270,685,14,14,4,1184,779,497,1124,ok +edge-3-fruit-min,3,24001,1,16384,8311,132,5829,2112,5771,4432,8826,1186,6372,0.001563,9074598716795909270,685,14,14,4,1184,779,497,1124,ok +edge-3-fruit-min,3,24003,1,16384,8430,96,5910,1948,5768,4519,8906,1069,6409,0.001703,4550702485884274400,797,20,18,4,1109,995,478,1123,ok +edge-3-fruit-min,3,24003,1,16384,8430,96,5910,1948,5768,4519,8906,1069,6409,0.001558,4550702485884274400,797,20,18,4,1109,995,478,1123,ok +edge-3-fruit-min,3,24004,1,16384,8398,119,5887,1980,5773,4508,8881,1108,6395,0.004665,4367030019395138836,808,16,20,4,1120,961,464,1124,ok +edge-3-fruit-min,3,24004,1,16384,8398,119,5887,1980,5773,4508,8881,1108,6395,0.002655,4367030019395138836,808,16,20,4,1120,961,464,1124,ok +edge-3-fruit-max,3,24001,1,16384,4751,5143,3386,3104,3225,666,5713,6692,3979,0.002006,11118651149639723521,14,14,9,3,491,464,254,756,ok +edge-3-fruit-max,3,24001,1,16384,4751,5143,3386,3104,3225,666,5713,6692,3979,0.001616,11118651149639723521,14,14,9,3,491,464,254,756,ok +edge-3-fruit-max,3,24003,1,16384,4864,5139,3472,2909,3284,651,5755,6590,4039,0.001795,13841240366447628489,46,21,8,4,560,454,214,748,ok +edge-3-fruit-max,3,24003,1,16384,4864,5139,3472,2909,3284,651,5755,6590,4039,0.001680,13841240366447628489,46,21,8,4,560,454,214,748,ok +edge-3-fruit-max,3,24004,1,16384,4856,5144,3430,2954,3254,695,5764,6617,4003,0.002682,11238189415334656871,32,12,21,4,662,381,196,754,ok +edge-3-fruit-max,3,24004,1,16384,4856,5144,3430,2954,3254,695,5764,6617,4003,0.001622,11238189415334656871,32,12,21,4,662,381,196,754,ok +edge-3-repeat-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.001564,12754827918508373822,533,13,13,4,1123,743,471,1084,ok +edge-3-repeat-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.001939,12754827918508373822,533,13,13,4,1123,743,471,1084,ok +edge-3-repeat-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.001558,13384879564688048696,793,20,16,4,1080,957,422,1089,ok +edge-3-repeat-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.002084,13384879564688048696,793,20,16,4,1080,957,422,1089,ok +edge-3-repeat-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001561,7198112912547603041,779,13,19,4,1071,918,427,1084,ok +edge-3-repeat-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001615,7198112912547603041,779,13,19,4,1071,918,427,1084,ok +edge-3-repeat-max,3,24001,1,16384,6180,672,3644,5888,4608,748,7716,3584,5084,0.000586,11485152635733424568,4,2,4,0,428,488,224,896,ok +edge-3-repeat-max,3,24001,1,16384,6180,672,3644,5888,4608,748,7716,3584,5084,0.000562,11485152635733424568,4,2,4,0,428,488,224,896,ok +edge-3-repeat-max,3,24003,1,16384,6112,640,3616,6016,4344,416,7680,3616,5088,0.000556,9445496948617613408,6,2,9,0,384,480,384,896,ok +edge-3-repeat-max,3,24003,1,16384,6112,640,3616,6016,4344,416,7680,3616,5088,0.000548,9445496948617613408,6,2,9,0,384,480,384,896,ok +edge-3-repeat-max,3,24004,1,16384,6072,632,3784,5896,4624,752,7644,3548,5192,0.000558,4872015995867199970,6,2,6,0,508,448,188,896,ok +edge-3-repeat-max,3,24004,1,16384,6072,632,3784,5896,4624,752,7644,3548,5192,0.001155,4872015995867199970,6,2,6,0,508,448,188,896,ok +size-3-6-6-4,3,24001,1,4096,1757,196,1139,1004,1298,550,2010,695,1391,0.001480,2408001620924930647,101,7,11,4,179,135,50,269,ok +size-3-6-6-4,3,24001,1,4096,1757,196,1139,1004,1298,550,2010,695,1391,0.000528,2408001620924930647,101,7,11,4,179,135,50,269,ok +size-3-6-6-4,3,24003,1,4096,1717,259,1072,1048,1263,584,1982,781,1333,0.001305,7780550642838424467,0,10,8,3,123,193,44,247,ok +size-3-6-6-4,3,24003,1,4096,1717,259,1072,1048,1263,584,1982,781,1333,0.001225,7780550642838424467,0,10,8,3,123,193,44,247,ok +size-3-6-6-4,3,24004,1,4096,1723,172,1120,1081,1271,466,2003,708,1385,0.000506,14041330535239152130,60,7,9,4,170,141,54,269,ok +size-3-6-6-4,3,24004,1,4096,1723,172,1120,1081,1271,466,2003,708,1385,0.000519,14041330535239152130,60,7,9,4,170,141,54,269,ok +size-3-6-6-12,3,24001,1,4096,1399,269,673,1755,922,57,1823,1154,1119,0.000666,1489856931770057666,6,8,7,8,75,99,55,135,ok +size-3-6-6-12,3,24001,1,4096,1399,269,673,1755,922,57,1823,1154,1119,0.001186,1489856931770057666,6,8,7,8,75,99,55,135,ok +size-3-6-6-12,3,24003,1,4096,1420,300,797,1579,940,87,1845,1106,1145,0.000701,2284419457830081742,13,9,7,11,96,86,49,208,ok +size-3-6-6-12,3,24003,1,4096,1420,300,797,1579,940,87,1845,1106,1145,0.000903,2284419457830081742,13,9,7,11,96,86,49,208,ok +size-3-6-6-12,3,24004,1,4096,1420,259,712,1705,932,78,1843,1120,1133,0.000672,13781994263373817762,6,13,6,10,99,88,41,179,ok +size-3-6-6-12,3,24004,1,4096,1420,259,712,1705,932,78,1843,1120,1133,0.000719,13781994263373817762,6,13,6,10,99,88,41,179,ok +size-3-7-7-8,3,24001,1,16384,7632,605,5192,2955,5447,3362,8401,2077,5906,0.001719,3134242000182279350,211,13,12,8,872,830,315,1079,ok +size-3-7-7-8,3,24001,1,16384,7632,605,5192,2955,5447,3362,8401,2077,5906,0.001729,3134242000182279350,211,13,12,8,872,830,315,1079,ok +size-3-7-7-8,3,24003,1,16384,7523,582,5165,3114,5407,3207,8346,2133,5905,0.007214,9477542308708889540,259,12,14,8,744,826,364,1091,ok +size-3-7-7-8,3,24003,1,16384,7523,582,5165,3114,5407,3207,8346,2133,5905,0.001786,9477542308708889540,259,12,14,8,744,826,364,1091,ok +size-3-7-7-8,3,24004,1,16384,7533,615,5147,3089,5407,3156,8335,2154,5895,0.001971,3885494216138766292,80,10,11,8,917,683,349,1087,ok +size-3-7-7-8,3,24004,1,16384,7533,615,5147,3089,5407,3156,8335,2154,5895,0.004681,3885494216138766292,80,10,11,8,917,683,349,1087,ok +size-3-6-9-12,3,24001,1,32768,15532,1255,10797,5184,10922,7265,16862,3843,12063,0.004440,4292325463971593553,333,25,26,11,1788,1831,738,2168,ok +size-3-6-9-12,3,24001,1,32768,15532,1255,10797,5184,10922,7265,16862,3843,12063,0.003479,4292325463971593553,333,25,26,11,1788,1831,738,2168,ok +size-3-6-9-12,3,24003,1,32768,15617,1211,10890,5050,10904,7336,16944,3726,12098,0.003421,9077211435807253382,268,24,22,12,1739,1869,842,2177,ok +size-3-6-9-12,3,24003,1,32768,15617,1211,10890,5050,10904,7336,16944,3726,12098,0.003402,9077211435807253382,268,24,22,12,1739,1869,842,2177,ok +size-3-6-9-12,3,24004,1,32768,15671,1114,10899,5084,10929,7385,16998,3649,12121,0.066922,16684913468144428077,441,16,17,12,1733,1961,784,2184,ok +size-3-6-9-12,3,24004,1,32768,15671,1114,10899,5084,10929,7385,16998,3649,12121,0.003441,16684913468144428077,441,16,17,12,1733,1961,784,2184,ok +size-3-9-6-12,3,24001,1,32768,15587,1146,10912,5123,10901,7225,16933,3701,12134,0.004738,11860156535094621637,13,100000,22,10,1751,1899,778,2183,ok +size-3-9-6-12,3,24001,1,32768,15587,1146,10912,5123,10901,7225,16933,3701,12134,0.005198,11860156535094621637,13,100000,22,10,1751,1899,778,2183,ok +size-3-9-6-12,3,24003,1,32768,15659,1207,10862,5040,10978,7434,16960,3723,12085,0.003789,14734992354316944878,45,20,100000,11,1833,1809,783,2170,ok +size-3-9-6-12,3,24003,1,32768,15659,1207,10862,5040,10978,7434,16960,3723,12085,0.105734,14734992354316944878,45,20,100000,11,1833,1809,783,2170,ok +size-3-9-6-12,3,24004,1,32768,15662,1185,10905,5016,10944,7338,16951,3688,12129,0.003726,13574052253604112191,89,14,18,12,2107,1564,787,2185,ok +size-3-9-6-12,3,24004,1,32768,15662,1185,10905,5016,10944,7338,16951,3688,12129,0.005941,13574052253604112191,89,14,18,12,2107,1564,787,2185,ok +size-3-9-9-12,3,24001,1,262144,137934,7746,100142,16322,90050,78820,142345,15896,103903,0.324298,17791796161650287991,1279,29,45,6,18801,16092,12744,17386,ok +size-3-9-9-12,3,24001,1,262144,137934,7746,100142,16322,90050,78820,142345,15896,103903,0.313554,17791796161650287991,1279,29,45,6,18801,16092,12744,17386,ok +size-3-9-9-12,3,24003,1,262144,138248,7774,100139,15983,90246,79624,142476,15759,103909,0.255989,2802800432745166429,1189,32,48,7,17575,17347,12834,17477,ok +size-3-9-9-12,3,24003,1,262144,138248,7774,100139,15983,90246,79624,142476,15759,103909,0.058551,2802800432745166429,1189,32,48,7,17575,17347,12834,17477,ok +size-3-9-9-12,3,24004,1,262144,138018,7475,100529,16122,89932,78537,142386,15522,104236,0.339608,2986413288923889907,1170,51,42,3,17800,17223,12811,17390,ok +size-3-9-9-12,3,24004,1,262144,138018,7475,100529,16122,89932,78537,142386,15522,104236,0.262262,2986413288923889907,1170,51,42,3,17800,17223,12811,17390,ok +corner-3-min,3,24001,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-3-min,3,24001,0,0,0,0,0,0,0,0,0,0,0,0.000022,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-3-min,3,24003,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-3-min,3,24003,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-3-min,3,24004,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-3-min,3,24004,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-3-max,3,24001,1,262144,47052,132844,49888,32360,16268,32,56752,148976,56416,0.015632,3882014515311933110,1,12,4,0,7968,10304,4704,9280,ok +corner-3-max,3,24001,1,262144,47052,132844,49888,32360,16268,32,56752,148976,56416,0.014599,3882014515311933110,1,12,4,0,7968,10304,4704,9280,ok +corner-3-max,3,24003,1,262144,47068,133132,50016,31928,16156,64,56808,149048,56288,0.038924,12350383196907311314,2,5,5,0,9088,9280,4608,9248,ok +corner-3-max,3,24003,1,262144,47068,133132,50016,31928,16156,64,56808,149048,56288,0.124088,12350383196907311314,2,5,5,0,9088,9280,4608,9248,ok +corner-3-max,3,24004,1,262144,46656,133312,49920,32256,15256,224,56448,149408,56288,0.070951,9589820212205955278,1,1,3,0,8032,10240,4480,9280,ok +corner-3-max,3,24004,1,262144,46656,133312,49920,32256,15256,224,56448,149408,56288,0.020159,9589820212205955278,1,1,3,0,8032,10240,4480,9280,ok +edge-4-width-min,4,24001,1,8192,5378,285,1593,936,4631,3576,5644,753,1795,0.000853,4541659758168081294,799,11,13,4,244,275,125,317,ok +edge-4-width-min,4,24001,1,8192,5378,285,1593,936,4631,3576,5644,753,1795,0.001823,4541659758168081294,799,11,13,4,244,275,125,317,ok +edge-4-width-min,4,24003,1,8192,5318,297,1581,996,4642,3494,5606,794,1792,0.001666,18160520391721934799,724,9,7,4,213,250,115,316,ok +edge-4-width-min,4,24003,1,8192,5318,297,1581,996,4642,3494,5606,794,1792,0.002143,18160520391721934799,724,9,7,4,213,250,115,316,ok +edge-4-width-min,4,24004,1,8192,5390,268,1600,934,4668,3631,5667,734,1791,0.000812,11804285690529431025,295,13,20,4,229,298,91,317,ok +edge-4-width-min,4,24004,1,8192,5390,268,1600,934,4668,3631,5667,734,1791,0.000794,11804285690529431025,295,13,20,4,229,298,91,317,ok +edge-4-width-max,4,24001,1,65536,42919,2255,12891,7471,37697,30747,45080,5982,14474,0.006483,12734081638166914099,1354,14,20,4,2126,2061,923,2537,ok +edge-4-width-max,4,24001,1,65536,42919,2255,12891,7471,37697,30747,45080,5982,14474,0.009254,12734081638166914099,1354,14,20,4,2126,2061,923,2537,ok +edge-4-width-max,4,24003,1,65536,42855,2285,12828,7568,37743,30584,45044,6061,14431,0.012220,2107868326165549141,712,18,15,4,2034,2130,845,2535,ok +edge-4-width-max,4,24003,1,65536,42855,2285,12828,7568,37743,30584,45044,6061,14431,0.006295,2107868326165549141,712,18,15,4,2034,2130,845,2535,ok +edge-4-width-max,4,24004,1,65536,42909,2313,12782,7532,37796,30922,45068,6076,14392,0.052076,6059020898415111911,735,15,25,4,2219,1977,832,2529,ok +edge-4-width-max,4,24004,1,65536,42909,2313,12782,7532,37796,30922,45068,6076,14392,0.006980,6059020898415111911,735,15,25,4,2219,1977,832,2529,ok +edge-4-height-min,4,24001,1,8192,5325,259,1583,1025,4598,3411,5632,770,1790,0.000814,9093900865957847469,922,26,10,3,215,302,106,316,ok +edge-4-height-min,4,24001,1,8192,5325,259,1583,1025,4598,3411,5632,770,1790,0.000810,9093900865957847469,922,26,10,3,215,302,106,316,ok +edge-4-height-min,4,24003,1,8192,5308,279,1579,1026,4629,3519,5616,791,1785,0.000836,6046727951330402993,839,16,16,4,250,230,97,315,ok +edge-4-height-min,4,24003,1,8192,5308,279,1579,1026,4629,3519,5616,791,1785,0.000940,6046727951330402993,839,16,16,4,250,230,97,315,ok +edge-4-height-min,4,24004,1,8192,5338,274,1604,976,4630,3453,5630,760,1802,0.000862,2739917840607138342,756,8,13,4,261,257,99,318,ok +edge-4-height-min,4,24004,1,8192,5338,274,1604,976,4630,3453,5630,760,1802,0.000870,2739917840607138342,756,8,13,4,261,257,99,318,ok +edge-4-height-max,4,24001,1,65536,42907,2242,12858,7529,37753,30832,45078,5994,14464,0.008825,8351122231019620984,862,19,19,4,2162,2004,885,2551,ok +edge-4-height-max,4,24001,1,65536,42907,2242,12858,7529,37753,30832,45078,5994,14464,0.005740,8351122231019620984,862,19,19,4,2162,2004,885,2551,ok +edge-4-height-max,4,24003,1,65536,43086,2213,13071,7166,37684,31105,45179,5796,14561,0.037019,16747919569644837282,1025,20,27,4,2261,2132,910,2550,ok +edge-4-height-max,4,24003,1,65536,43086,2213,13071,7166,37684,31105,45179,5796,14561,0.005916,16747919569644837282,1025,20,27,4,2261,2132,910,2550,ok +edge-4-height-max,4,24004,1,65536,42840,2269,12711,7716,37781,30509,45075,6112,14349,0.047404,4070000572400454117,678,13,16,4,2075,2000,890,2549,ok +edge-4-height-max,4,24004,1,65536,42840,2269,12711,7716,37781,30509,45075,6112,14349,0.011417,4070000572400454117,678,13,16,4,2075,2000,890,2549,ok +edge-4-teams-min,4,24001,1,16384,10687,563,3186,1948,9392,7468,11261,1533,3590,0.001680,14607272382324076510,2187,15,23,1,535,506,213,632,ok +edge-4-teams-min,4,24001,1,16384,10687,563,3186,1948,9392,7468,11261,1533,3590,0.001362,14607272382324076510,2187,15,23,1,535,506,213,632,ok +edge-4-teams-min,4,24003,1,16384,10682,556,3153,1993,9432,7515,11261,1550,3573,0.002074,3459117622114281977,1171,8,12,1,488,514,213,632,ok +edge-4-teams-min,4,24003,1,16384,10682,556,3153,1993,9432,7515,11261,1550,3573,0.001369,3459117622114281977,1171,8,12,1,488,514,213,632,ok +edge-4-teams-min,4,24004,1,16384,10625,598,3118,2043,9432,7583,11208,1620,3556,0.002790,5151524803494005197,1311,9,8,1,453,500,200,635,ok +edge-4-teams-min,4,24004,1,16384,10625,598,3118,2043,9432,7583,11208,1620,3556,0.001979,5151524803494005197,1311,9,8,1,453,500,200,635,ok +edge-4-teams-max,4,24001,1,16384,10712,562,3158,1952,9201,6972,11288,1534,3562,0.026853,2362421247749281498,439,25,23,11,534,503,213,632,ok +edge-4-teams-max,4,24001,1,16384,10712,562,3158,1952,9201,6972,11288,1534,3562,0.001812,2362421247749281498,439,25,23,11,534,503,213,632,ok +edge-4-teams-max,4,24003,1,16384,10682,556,3153,1993,9238,7035,11261,1550,3573,0.001896,1927621497452940970,743,19,22,12,483,493,213,632,ok +edge-4-teams-max,4,24003,1,16384,10682,556,3153,1993,9238,7035,11261,1550,3573,0.001906,1927621497452940970,743,19,22,12,483,493,213,632,ok +edge-4-teams-max,4,24004,1,16384,10645,583,3113,2043,9235,7102,11228,1605,3551,0.003720,3573620643882315263,663,16,21,12,450,500,200,635,ok +edge-4-teams-max,4,24004,1,16384,10645,583,3113,2043,9235,7102,11228,1605,3551,0.001902,3573620643882315263,663,16,21,12,450,500,200,635,ok +edge-4-workers-min,4,24001,1,16384,10712,562,3158,1952,9370,7365,11288,1534,3562,0.001459,8254136538010680180,518,18,23,4,534,506,213,632,ok +edge-4-workers-min,4,24001,1,16384,10712,562,3158,1952,9370,7365,11288,1534,3562,0.001987,8254136538010680180,518,18,23,4,534,506,213,632,ok +edge-4-workers-min,4,24003,1,16384,10682,556,3153,1993,9395,7405,11261,1550,3573,0.006068,1062923027973093604,729,8,12,4,485,506,213,632,ok +edge-4-workers-min,4,24003,1,16384,10682,556,3153,1993,9395,7405,11261,1550,3573,0.001524,1062923027973093604,729,8,12,4,485,506,213,632,ok +edge-4-workers-min,4,24004,1,16384,10625,598,3118,2043,9384,7450,11208,1620,3556,0.001524,3707328080328529486,802,9,8,4,453,500,200,635,ok +edge-4-workers-min,4,24004,1,16384,10625,598,3118,2043,9384,7450,11208,1620,3556,0.002563,3707328080328529486,802,9,8,4,453,500,200,635,ok +edge-4-workers-max,4,24001,1,16384,10712,562,3158,1952,9342,7335,11288,1534,3562,0.001546,8254136538010680180,539,15,23,4,534,506,213,632,ok +edge-4-workers-max,4,24001,1,16384,10712,562,3158,1952,9342,7335,11288,1534,3562,0.001747,8254136538010680180,539,15,23,4,534,506,213,632,ok +edge-4-workers-max,4,24003,1,16384,10682,556,3153,1993,9367,7397,11261,1550,3573,0.001507,1062923027973093604,802,8,11,4,485,506,213,632,ok +edge-4-workers-max,4,24003,1,16384,10682,556,3153,1993,9367,7397,11261,1550,3573,0.001528,1062923027973093604,802,8,11,4,485,506,213,632,ok +edge-4-workers-max,4,24004,1,16384,10625,598,3118,2043,9356,7417,11208,1620,3556,0.001507,3707328080328529486,865,9,7,4,453,500,200,635,ok +edge-4-workers-max,4,24004,1,16384,10625,598,3118,2043,9356,7417,11208,1620,3556,0.084143,3707328080328529486,865,9,7,4,453,500,200,635,ok +edge-4-water-min,4,24001,1,16384,14642,1102,0,640,14534,13094,14979,1405,0,0.003265,4794024178719344639,2103,100000,100000,0,0,0,3,0,ok +edge-4-water-min,4,24001,1,16384,14642,1102,0,640,14534,13094,14979,1405,0,0.001436,4794024178719344639,2103,100000,100000,0,0,0,3,0,ok +edge-4-water-min,4,24003,1,16384,14668,1110,0,606,14564,13209,14987,1397,0,0.001432,18136764116766803282,1599,100000,100000,0,0,0,2,0,ok +edge-4-water-min,4,24003,1,16384,14668,1110,0,606,14564,13209,14987,1397,0,0.003431,18136764116766803282,1599,100000,100000,0,0,0,2,0,ok +edge-4-water-min,4,24004,1,16384,14707,1145,0,532,14613,13471,14984,1400,0,0.001409,307839220994827965,1851,100000,100000,0,0,0,0,0,ok +edge-4-water-min,4,24004,1,16384,14707,1145,0,532,14613,13471,14984,1400,0,0.001458,307839220994827965,1851,100000,100000,0,0,0,0,0,ok +edge-4-water-max,4,24001,1,16384,5964,477,8269,1674,3289,1952,6416,1313,8655,0.001498,1543614798758202213,0,12,100000,2,924,1113,546,1497,ok +edge-4-water-max,4,24001,1,16384,5964,477,8269,1674,3289,1952,6416,1313,8655,0.002464,1543614798758202213,0,12,100000,2,924,1113,546,1497,ok +edge-4-water-max,4,24003,1,16384,5814,429,8089,2052,3230,1718,6366,1452,8566,0.001482,11021664634660526930,124,12,9,4,1066,961,460,1496,ok +edge-4-water-max,4,24003,1,16384,5814,429,8089,2052,3230,1718,6366,1452,8566,0.001655,11021664634660526930,124,12,9,4,1066,961,460,1496,ok +edge-4-water-max,4,24004,1,16384,5751,469,8106,2058,3298,1980,6308,1497,8579,0.001494,16619325114269491904,82,10,11,4,851,1114,390,1498,ok +edge-4-water-max,4,24004,1,16384,5751,469,8106,2058,3298,1980,6308,1497,8579,0.004484,16619325114269491904,82,10,11,4,851,1114,390,1498,ok +edge-4-sand-min,4,24001,1,16384,10683,786,2951,1964,9623,7596,11268,1765,3351,0.001491,13410321720515600371,560,16,24,4,383,421,155,654,ok +edge-4-sand-min,4,24001,1,16384,10683,786,2951,1964,9623,7596,11268,1765,3351,0.001459,13410321720515600371,560,16,24,4,383,421,155,654,ok +edge-4-sand-min,4,24003,1,16384,10643,822,2932,1987,9641,7650,11226,1814,3344,0.002153,14477966123237028695,826,10,14,4,355,395,151,651,ok +edge-4-sand-min,4,24003,1,16384,10643,822,2932,1987,9641,7650,11226,1814,3344,0.003203,14477966123237028695,826,10,14,4,355,395,151,651,ok +edge-4-sand-min,4,24004,1,16384,10579,886,2912,2007,9602,7685,11160,1891,3333,0.001476,9589238647282499469,1053,9,14,4,332,414,131,648,ok +edge-4-sand-min,4,24004,1,16384,10579,886,2912,2007,9602,7685,11160,1891,3333,0.001496,9589238647282499469,1053,9,14,4,332,414,131,648,ok +edge-4-sand-max,4,24001,1,16384,5577,7541,1678,1588,4836,3299,6038,8343,2003,0.001450,14189173806338207264,459,13,15,4,223,295,128,333,ok +edge-4-sand-max,4,24001,1,16384,5577,7541,1678,1588,4836,3299,6038,8343,2003,0.004784,14189173806338207264,459,13,15,4,223,295,128,333,ok +edge-4-sand-max,4,24003,1,16384,5524,7472,1682,1706,4825,3192,6047,8332,2005,0.001617,15696502119339532185,213,18,18,4,254,208,136,332,ok +edge-4-sand-max,4,24003,1,16384,5524,7472,1682,1706,4825,3192,6047,8332,2005,0.001451,15696502119339532185,213,18,18,4,254,208,136,332,ok +edge-4-sand-max,4,24004,1,16384,5512,7459,1674,1739,4854,3370,6038,8341,2005,0.043162,4849518523568563639,232,14,10,4,155,288,107,331,ok +edge-4-sand-max,4,24004,1,16384,5512,7459,1674,1739,4854,3370,6038,8341,2005,0.003715,4849518523568563639,232,14,10,4,155,288,107,331,ok +edge-4-grass-min,4,24001,0,16384,0,0,16384,0,0,0,21,3564,12799,0.001299,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-4-grass-min,4,24001,0,16384,0,0,16384,0,0,0,21,3564,12799,0.001272,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-4-grass-min,4,24003,0,16384,0,0,16384,0,0,0,19,3576,12789,0.001155,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-4-grass-min,4,24003,0,16384,0,0,16384,0,0,0,19,3576,12789,0.001153,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-4-grass-min,4,24004,0,16384,0,0,16384,0,0,0,16,3576,12792,0.001179,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-4-grass-min,4,24004,0,16384,0,0,16384,0,0,0,16,3576,12792,0.001216,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-4-grass-max,4,24001,1,16384,11660,473,2497,1754,10666,8734,12174,1347,2863,0.003041,3081479129659193038,697,17,25,4,365,360,164,514,ok +edge-4-grass-max,4,24001,1,16384,11660,473,2497,1754,10666,8734,12174,1347,2863,0.001578,3081479129659193038,697,17,25,4,365,360,164,514,ok +edge-4-grass-max,4,24003,1,16384,11613,484,2450,1837,10639,8706,12157,1401,2826,0.001499,16220650475573237379,958,10,13,4,331,369,169,516,ok +edge-4-grass-max,4,24003,1,16384,11613,484,2450,1837,10639,8706,12157,1401,2826,0.001618,16220650475573237379,958,10,13,4,331,369,169,516,ok +edge-4-grass-max,4,24004,1,16384,11573,500,2445,1866,10635,8780,12120,1430,2834,0.001487,1097820657693125625,1055,11,23,4,310,369,167,516,ok +edge-4-grass-max,4,24004,1,16384,11573,500,2445,1866,10635,8780,12120,1430,2834,0.001531,1097820657693125625,1055,11,23,4,310,369,167,516,ok +edge-4-desert-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.001414,8254136538010680180,518,15,23,4,534,506,213,632,ok +edge-4-desert-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.001970,8254136538010680180,518,15,23,4,534,506,213,632,ok +edge-4-desert-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.004261,1062923027973093604,770,8,12,4,485,506,213,632,ok +edge-4-desert-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.001904,1062923027973093604,770,8,12,4,485,506,213,632,ok +edge-4-desert-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.097196,3707328080328529486,841,9,8,4,453,500,200,635,ok +edge-4-desert-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.003716,3707328080328529486,841,9,8,4,453,500,200,635,ok +edge-4-desert-max,4,24001,1,16384,4943,7917,1383,2141,4408,1932,5719,8988,1677,0.001510,4426490326196461153,331,10,18,4,153,184,103,325,ok +edge-4-desert-max,4,24001,1,16384,4943,7917,1383,2141,4408,1932,5719,8988,1677,0.001509,4426490326196461153,331,10,18,4,153,184,103,325,ok +edge-4-desert-max,4,24003,1,16384,4853,7837,1383,2311,4328,1578,5705,8995,1684,0.002314,12785947869004050330,244,12,11,4,192,140,98,324,ok +edge-4-desert-max,4,24003,1,16384,4853,7837,1383,2311,4328,1578,5705,8995,1684,0.001495,12785947869004050330,244,12,11,4,192,140,98,324,ok +edge-4-desert-max,4,24004,1,16384,4797,7851,1376,2360,4276,1502,5665,9038,1681,0.001514,10830302627865370351,152,21,19,4,152,183,89,328,ok +edge-4-desert-max,4,24004,1,16384,4797,7851,1376,2360,4276,1502,5665,9038,1681,0.001478,10830302627865370351,152,21,19,4,152,183,89,328,ok +edge-4-smoothing-min,4,24001,1,16384,10618,497,3184,2085,9255,6983,11262,1530,3592,0.001955,11493225337334270162,919,15,23,4,512,523,227,634,ok +edge-4-smoothing-min,4,24001,1,16384,10618,497,3184,2085,9255,6983,11262,1530,3592,0.001499,11493225337334270162,919,15,23,4,512,523,227,634,ok +edge-4-smoothing-min,4,24003,1,16384,10620,499,3141,2124,9304,7010,11274,1552,3558,0.002631,5034150749963636557,482,22,20,4,432,556,215,636,ok +edge-4-smoothing-min,4,24003,1,16384,10620,499,3141,2124,9304,7010,11274,1552,3558,0.003138,5034150749963636557,482,22,20,4,432,556,215,636,ok +edge-4-smoothing-min,4,24004,1,16384,10500,571,3056,2257,9308,7006,11171,1697,3516,0.001606,17443006140792250911,1011,8,6,4,399,499,200,636,ok +edge-4-smoothing-min,4,24004,1,16384,10500,571,3056,2257,9308,7006,11171,1697,3516,0.001544,17443006140792250911,1011,8,6,4,399,499,200,636,ok +edge-4-smoothing-max,4,24001,1,16384,10658,510,3189,2027,9305,7218,11272,1519,3593,0.004813,3760489223540803542,686,24,22,4,465,565,228,633,ok +edge-4-smoothing-max,4,24001,1,16384,10658,510,3189,2027,9305,7218,11272,1519,3593,0.001484,3760489223540803542,686,24,22,4,465,565,228,633,ok +edge-4-smoothing-max,4,24003,1,16384,10730,584,3174,1896,9391,7381,11264,1530,3590,0.001580,3114500145959872434,1162,29,17,3,562,437,233,635,ok +edge-4-smoothing-max,4,24003,1,16384,10730,584,3174,1896,9391,7381,11264,1530,3590,0.001513,3114500145959872434,1162,29,17,3,562,437,233,635,ok +edge-4-smoothing-max,4,24004,1,16384,10660,560,3113,2051,9406,7473,11243,1583,3558,0.001501,731412055027956474,901,12,19,4,425,523,214,634,ok +edge-4-smoothing-max,4,24004,1,16384,10660,560,3113,2051,9406,7473,11243,1583,3558,0.001974,731412055027956474,901,12,19,4,425,523,214,634,ok +edge-4-lake-density-min,4,24001,1,16384,11044,514,3530,1296,9357,7548,11450,1157,3777,0.001494,8629925748382678687,1000,21,19,4,567,588,434,637,ok +edge-4-lake-density-min,4,24001,1,16384,11044,514,3530,1296,9357,7548,11450,1157,3777,0.004653,8629925748382678687,1000,21,19,4,567,588,434,637,ok +edge-4-lake-density-min,4,24003,1,16384,10877,496,3411,1600,9299,7467,11375,1289,3720,0.005198,3261879646515407256,660,21,17,4,626,572,293,636,ok +edge-4-lake-density-min,4,24003,1,16384,10877,496,3411,1600,9299,7467,11375,1289,3720,0.001453,3261879646515407256,660,21,17,4,626,572,293,636,ok +edge-4-lake-density-min,4,24004,1,16384,11026,576,3522,1260,9382,7756,11412,1202,3770,0.001473,3513928367786325251,523,13,19,4,430,730,391,636,ok +edge-4-lake-density-min,4,24004,1,16384,11026,576,3522,1260,9382,7756,11412,1202,3770,0.001471,3513928367786325251,523,13,19,4,430,730,391,636,ok +edge-4-lake-density-max,4,24001,1,16384,10285,657,2844,2598,9289,6716,11046,1956,3382,0.001576,5093769824615020375,892,13,10,4,372,357,171,635,ok +edge-4-lake-density-max,4,24001,1,16384,10285,657,2844,2598,9289,6716,11046,1956,3382,0.003514,5093769824615020375,892,13,10,4,372,357,171,635,ok +edge-4-lake-density-max,4,24003,1,16384,10355,584,2887,2558,9294,6652,11127,1857,3400,0.001545,3048521784300642060,829,15,18,4,411,381,175,636,ok +edge-4-lake-density-max,4,24003,1,16384,10355,584,2887,2558,9294,6652,11127,1857,3400,0.001550,3048521784300642060,829,15,18,4,411,381,175,636,ok +edge-4-lake-density-max,4,24004,1,16384,10269,623,2900,2592,9238,6626,11042,1916,3426,0.001521,14214230773756149775,722,9,5,4,387,373,177,636,ok +edge-4-lake-density-max,4,24004,1,16384,10269,623,2900,2592,9238,6626,11042,1916,3426,0.001538,14214230773756149775,722,9,5,4,387,373,177,636,ok +edge-4-lake-size-min,4,24001,1,16384,10382,783,2942,2277,8925,6631,11058,1919,3407,0.003082,356576884332847302,667,22,16,4,569,465,318,637,ok +edge-4-lake-size-min,4,24001,1,16384,10382,783,2942,2277,8925,6631,11058,1919,3407,0.112929,356576884332847302,667,22,16,4,569,465,318,637,ok +edge-4-lake-size-min,4,24003,1,16384,10342,787,2872,2383,8839,6746,11043,1978,3363,0.001512,11976132177559827711,26,18,16,4,485,599,326,637,ok +edge-4-lake-size-min,4,24003,1,16384,10342,787,2872,2383,8839,6746,11043,1978,3363,0.001443,11976132177559827711,26,18,16,4,485,599,326,637,ok +edge-4-lake-size-min,4,24004,1,16384,10480,728,3036,2140,9019,6817,11109,1796,3479,0.001476,12510093574884878762,882,9,13,4,465,548,350,637,ok +edge-4-lake-size-min,4,24004,1,16384,10480,728,3036,2140,9019,6817,11109,1796,3479,0.001486,12510093574884878762,882,9,13,4,465,548,350,637,ok +edge-4-lake-size-max,4,24001,1,16384,10768,580,3202,1834,9397,7630,11297,1494,3593,0.002013,4917776454980176967,664,21,24,4,482,540,248,630,ok +edge-4-lake-size-max,4,24001,1,16384,10768,580,3202,1834,9397,7630,11297,1494,3593,0.002851,4917776454980176967,664,21,24,4,482,540,248,630,ok +edge-4-lake-size-max,4,24003,1,16384,10749,589,3192,1854,9406,7498,11269,1515,3600,0.001652,192118136470395207,956,11,14,4,530,493,226,635,ok +edge-4-lake-size-max,4,24003,1,16384,10749,589,3192,1854,9406,7498,11269,1515,3600,0.002128,192118136470395207,956,11,14,4,530,493,226,635,ok +edge-4-lake-size-max,4,24004,1,16384,10712,612,3188,1872,9343,7454,11246,1549,3589,0.001618,13579775533962361372,805,13,9,4,518,469,276,633,ok +edge-4-lake-size-max,4,24004,1,16384,10712,612,3188,1872,9343,7454,11246,1549,3589,0.008064,13579775533962361372,805,13,9,4,518,469,276,633,ok +edge-4-fruit-min,4,24001,1,16384,11265,46,3343,1730,9862,8410,11718,910,3756,0.073504,13727028702911961066,705,20,23,4,552,540,231,654,ok +edge-4-fruit-min,4,24001,1,16384,11265,46,3343,1730,9862,8410,11718,910,3756,0.003111,13727028702911961066,705,20,23,4,552,540,231,654,ok +edge-4-fruit-min,4,24003,1,16384,11225,59,3291,1809,9865,8278,11699,965,3720,0.001501,13210986106375009244,726,16,13,4,503,564,213,659,ok +edge-4-fruit-min,4,24003,1,16384,11225,59,3291,1809,9865,8278,11699,965,3720,0.001605,13210986106375009244,726,16,13,4,503,564,213,659,ok +edge-4-fruit-min,4,24004,1,16384,11164,81,3271,1868,9856,8281,11650,1020,3714,0.001488,7241652660036938659,1018,19,14,4,488,532,208,660,ok +edge-4-fruit-min,4,24004,1,16384,11164,81,3271,1868,9856,8281,11650,1020,3714,0.004024,7241652660036938659,1018,19,14,4,488,532,208,660,ok +edge-4-fruit-max,4,24001,1,16384,6325,5984,1820,2255,5345,2188,7114,7113,2157,0.003653,4362957826146166621,302,5,16,4,217,258,129,407,ok +edge-4-fruit-max,4,24001,1,16384,6325,5984,1820,2255,5345,2188,7114,7113,2157,0.001529,4362957826146166621,302,5,16,4,217,258,129,407,ok +edge-4-fruit-max,4,24003,1,16384,6281,5932,1833,2338,5340,2156,7119,7100,2165,0.001547,9401746125361811691,230,10,11,4,244,223,116,407,ok +edge-4-fruit-max,4,24003,1,16384,6281,5932,1833,2338,5340,2156,7119,7100,2165,0.001494,9401746125361811691,230,10,11,4,244,223,116,407,ok +edge-4-fruit-max,4,24004,1,16384,6203,5948,1785,2448,5250,1947,7076,7171,2137,0.001585,16759074768503135645,271,13,7,4,229,254,117,406,ok +edge-4-fruit-max,4,24004,1,16384,6203,5948,1785,2448,5250,1947,7076,7171,2137,0.001581,16759074768503135645,271,13,7,4,229,254,117,406,ok +edge-4-repeat-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.004474,8254136538010680180,518,15,23,4,534,506,213,632,ok +edge-4-repeat-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.001495,8254136538010680180,518,15,23,4,534,506,213,632,ok +edge-4-repeat-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.001511,1062923027973093604,770,8,12,4,485,506,213,632,ok +edge-4-repeat-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.001594,1062923027973093604,770,8,12,4,485,506,213,632,ok +edge-4-repeat-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.001533,3707328080328529486,841,9,8,4,453,500,200,635,ok +edge-4-repeat-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.001629,3707328080328529486,841,9,8,4,453,500,200,635,ok +edge-4-repeat-max,4,24001,1,16384,9272,792,2096,4224,8236,2900,10584,2904,2896,0.000678,2300712310789260919,392,4,6,4,64,360,56,576,ok +edge-4-repeat-max,4,24001,1,16384,9272,792,2096,4224,8236,2900,10584,2904,2896,0.000747,2300712310789260919,392,4,6,4,64,360,56,576,ok +edge-4-repeat-max,4,24003,1,16384,9308,584,2404,4088,7556,1824,10736,2568,3080,0.001151,15676489074034912807,294,4,1,4,220,664,244,576,ok +edge-4-repeat-max,4,24003,1,16384,9308,584,2404,4088,7556,1824,10736,2568,3080,0.001113,15676489074034912807,294,4,1,4,220,664,244,576,ok +edge-4-repeat-max,4,24004,1,16384,9476,608,2396,3904,7632,1984,10788,2528,3068,0.000609,10650943619486687672,347,7,3,4,256,352,224,576,ok +edge-4-repeat-max,4,24004,1,16384,9476,608,2396,3904,7632,1984,10788,2528,3068,0.000593,10650943619486687672,347,7,3,4,256,352,224,576,ok +size-4-6-6-4,4,24001,1,4096,2645,130,789,532,2240,1571,2807,394,895,0.000685,6522132994922700061,886,16,17,4,117,137,44,158,ok +size-4-6-6-4,4,24001,1,4096,2645,130,789,532,2240,1571,2807,394,895,0.000466,6522132994922700061,886,16,17,4,117,137,44,158,ok +size-4-6-6-4,4,24003,1,4096,2613,131,766,586,2247,1495,2792,423,881,0.000501,3893475650893073251,590,9,15,4,101,113,54,158,ok +size-4-6-6-4,4,24003,1,4096,2613,131,766,586,2247,1495,2792,423,881,0.000491,3893475650893073251,590,9,15,4,101,113,54,158,ok +size-4-6-6-4,4,24004,1,4096,2688,140,810,458,2271,1659,2820,369,907,0.001783,16598518496421687426,657,19,19,4,115,158,52,158,ok +size-4-6-6-4,4,24004,1,4096,2688,140,810,458,2271,1659,2820,369,907,0.001887,16598518496421687426,657,19,19,4,115,158,52,158,ok +size-4-6-6-12,4,24001,0,4096,0,0,4096,0,0,0,2807,394,895,0.000366,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-4-6-6-12,4,24001,0,4096,0,0,4096,0,0,0,2807,394,895,0.000962,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-4-6-6-12,4,24003,0,4096,0,0,4096,0,0,0,2790,424,882,0.000424,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-4-6-6-12,4,24003,0,4096,0,0,4096,0,0,0,2790,424,882,0.000505,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-4-6-6-12,4,24004,0,4096,0,0,4096,0,0,0,2820,369,907,0.001127,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-4-6-6-12,4,24004,0,4096,0,0,4096,0,0,0,2820,369,907,0.000386,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-4-7-7-8,4,24001,1,16384,10712,562,3158,1952,9281,7171,11288,1534,3562,0.001894,15583763586484531551,518,25,23,7,534,503,213,632,ok +size-4-7-7-8,4,24001,1,16384,10712,562,3158,1952,9281,7171,11288,1534,3562,0.001633,15583763586484531551,518,25,23,7,534,503,213,632,ok +size-4-7-7-8,4,24003,1,16384,10682,556,3153,1993,9317,7244,11261,1550,3573,0.002103,5482014022574351707,770,11,12,8,485,492,213,632,ok +size-4-7-7-8,4,24003,1,16384,10682,556,3153,1993,9317,7244,11261,1550,3573,0.001760,5482014022574351707,770,11,12,8,485,492,213,632,ok +size-4-7-7-8,4,24004,1,16384,10631,597,3113,2043,9301,7281,11214,1619,3551,0.004585,7813010578140552413,841,16,21,8,450,500,200,635,ok +size-4-7-7-8,4,24004,1,16384,10631,597,3113,2043,9301,7281,11214,1619,3551,0.001704,7813010578140552413,841,16,21,8,450,500,200,635,ok +size-4-6-9-12,4,24001,1,32768,21547,1105,6540,3576,18628,14887,22595,2886,7287,0.003564,14873471884697784680,685,37,38,10,1164,1045,453,1264,ok +size-4-6-9-12,4,24001,1,32768,21547,1105,6540,3576,18628,14887,22595,2886,7287,0.004295,14873471884697784680,685,37,38,10,1164,1045,453,1264,ok +size-4-6-9-12,4,24003,1,32768,21447,1109,6422,3790,18715,15021,22559,2997,7212,0.079111,3903544181060677136,412,19,21,12,973,1049,463,1272,ok +size-4-6-9-12,4,24003,1,32768,21447,1109,6422,3790,18715,15021,22559,2997,7212,0.005647,3903544181060677136,412,19,21,12,973,1049,463,1272,ok +size-4-6-9-12,4,24004,1,32768,21571,1103,6525,3569,18716,14860,22606,2883,7279,0.007159,4155787913330145562,674,19,20,12,1010,1108,479,1269,ok +size-4-6-9-12,4,24004,1,32768,21571,1103,6525,3569,18716,14860,22606,2883,7279,0.010276,4155787913330145562,674,19,20,12,1010,1108,479,1269,ok +size-4-9-6-12,4,24001,1,32768,21136,1097,6235,4300,18564,14126,22413,3241,7114,0.003605,16525546010019320907,998,22,19,12,836,1048,423,1271,ok +size-4-9-6-12,4,24001,1,32768,21136,1097,6235,4300,18564,14126,22413,3241,7114,0.003836,16525546010019320907,998,22,19,12,836,1048,423,1271,ok +size-4-9-6-12,4,24003,1,32768,21401,1185,6291,3891,18748,14854,22511,3133,7124,0.006538,4891460588746945516,641,24,16,12,924,1070,401,1274,ok +size-4-9-6-12,4,24003,1,32768,21401,1185,6291,3891,18748,14854,22511,3133,7124,0.003958,4891460588746945516,641,24,16,12,924,1070,401,1274,ok +size-4-9-6-12,4,24004,1,32768,21514,1155,6484,3615,18699,14990,22539,2962,7267,0.004790,7109692120240274476,831,25,27,11,1109,973,465,1270,ok +size-4-9-6-12,4,24004,1,32768,21514,1155,6484,3615,18699,14990,22539,2962,7267,0.006008,7109692120240274476,831,25,27,11,1109,973,465,1270,ok +size-4-9-9-12,4,24001,1,262144,171342,9373,50976,30453,151343,124193,180016,24580,57548,0.386439,9883531783368080857,689,29,32,11,8197,8064,3479,10146,ok +size-4-9-9-12,4,24001,1,262144,171342,9373,50976,30453,151343,124193,180016,24580,57548,0.163084,9883531783368080857,689,29,32,11,8197,8064,3479,10146,ok +size-4-9-9-12,4,24003,1,262144,171683,9069,51518,29874,151181,123641,180299,23974,57871,0.143692,14463795164623604611,889,22,24,12,8277,8389,3576,10148,ok +size-4-9-9-12,4,24003,1,262144,171683,9069,51518,29874,151181,123641,180299,23974,57871,0.176869,14463795164623604611,889,22,24,12,8277,8389,3576,10148,ok +size-4-9-9-12,4,24004,1,262144,171322,9023,51209,30590,151167,122582,180171,24281,57692,0.331625,3652722481845561522,827,33,41,11,7695,8693,3491,10158,ok +size-4-9-9-12,4,24004,1,262144,171322,9023,51209,30590,151167,122582,180171,24281,57692,0.138683,3652722481845561522,827,33,41,11,7695,8693,3491,10158,ok +corner-4-min,4,24001,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-4-min,4,24001,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-4-min,4,24003,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-4-min,4,24003,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-4-min,4,24004,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-4-min,4,24004,0,0,0,0,0,0,0,0,0,0,0,0.000012,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-4-max,4,24001,1,262144,35892,120480,48348,57424,15088,0,56940,149176,56028,0.011404,11431102598582502079,0,7,7,0,7080,6856,3104,9376,ok +corner-4-max,4,24001,1,262144,35892,120480,48348,57424,15088,0,56940,149176,56028,0.011668,11431102598582502079,0,7,7,0,7080,6856,3104,9376,ok +corner-4-max,4,24003,1,262144,32812,117332,48224,63776,12072,0,56680,149304,56160,0.131737,10343313809265980641,0,4,1,0,7376,6496,2580,9376,ok +corner-4-max,4,24003,1,262144,32812,117332,48224,63776,12072,0,56680,149304,56160,0.043587,10343313809265980641,0,4,1,0,7376,6496,2580,9376,ok +corner-4-max,4,24004,1,262144,33936,116304,47616,64288,13388,0,57040,148656,56448,0.084651,3610559417604788182,0,3,1,0,6304,7008,3304,9376,ok +corner-4-max,4,24004,1,262144,33936,116304,47616,64288,13388,0,57040,148656,56448,0.012865,3610559417604788182,0,3,1,0,6304,7008,3304,9376,ok +edge-5-width-min,5,24001,1,8192,4244,61,1954,1933,2178,957,4679,1028,2485,0.006794,17171761383611781295,156,2,9,4,1042,914,24,2,ok +edge-5-width-min,5,24001,1,8192,4244,61,1954,1933,2178,957,4679,1028,2485,0.057199,17171761383611781295,156,2,9,4,1042,914,24,2,ok +edge-5-width-min,5,24003,1,8192,4171,60,2022,1939,2113,847,4590,1022,2580,0.005190,5703813900008587670,131,1,8,4,1091,854,24,2,ok +edge-5-width-min,5,24003,1,8192,4171,60,2022,1939,2113,847,4590,1022,2580,0.052252,5703813900008587670,131,1,8,4,1091,854,24,2,ok +edge-5-width-min,5,24004,1,8192,4664,46,1654,1828,2348,1038,5089,960,2143,0.005554,7841045921695058678,162,1,7,4,1213,987,24,2,ok +edge-5-width-min,5,24004,1,8192,4664,46,1654,1828,2348,1038,5089,960,2143,0.005486,7841045921695058678,162,1,7,4,1213,987,24,2,ok +edge-5-width-max,5,24001,1,65536,53666,136,5327,6407,39819,34855,55233,3337,6966,0.356904,6551436702106436975,353,4,36,3,8300,5437,24,2,ok +edge-5-width-max,5,24001,1,65536,53666,136,5327,6407,39819,34855,55233,3337,6966,0.484098,6551436702106436975,353,4,36,3,8300,5437,24,2,ok +edge-5-width-max,5,24003,1,65536,53379,173,5199,6785,37943,32469,55071,3562,6903,0.502737,18123336872848219798,91,2,100000,2,10113,5210,24,0,ok +edge-5-width-max,5,24003,1,65536,53379,173,5199,6785,37943,32469,55071,3562,6903,0.457281,18123336872848219798,91,2,100000,2,10113,5210,24,0,ok +edge-5-width-max,5,24004,1,65536,55344,134,4349,5709,41200,36288,56767,2985,5784,0.572890,14238173093679142971,1070,1,58,3,8876,5152,24,0,ok +edge-5-width-max,5,24004,1,65536,55344,134,4349,5709,41200,36288,56767,2985,5784,0.527236,14238173093679142971,1070,1,58,3,8876,5152,24,0,ok +edge-5-height-min,5,24001,1,8192,4042,55,2024,2071,2128,830,4510,1091,2591,0.020844,18181004065509020172,118,3,5,4,907,897,24,3,ok +edge-5-height-min,5,24001,1,8192,4042,55,2024,2071,2128,830,4510,1091,2591,0.008257,18181004065509020172,118,3,5,4,907,897,24,3,ok +edge-5-height-min,5,24003,1,8192,4498,44,1731,1919,2250,982,4934,1004,2254,0.015101,13641719035445064182,141,1,11,4,1180,955,24,1,ok +edge-5-height-min,5,24003,1,8192,4498,44,1731,1919,2250,982,4934,1004,2254,0.005411,13641719035445064182,141,1,11,4,1180,955,24,1,ok +edge-5-height-min,5,24004,1,8192,4375,60,1728,2029,2222,1038,4850,1073,2269,0.005258,8915583425988203371,229,5,7,4,1047,990,24,2,ok +edge-5-height-min,5,24004,1,8192,4375,60,1728,2029,2222,1038,4850,1073,2269,0.046863,8915583425988203371,229,5,7,4,1047,990,24,2,ok +edge-5-height-max,5,24001,1,65536,56580,112,3915,4929,43156,38625,57790,2576,5170,0.467537,2606329050636720493,968,2,73,2,8597,4717,24,0,ok +edge-5-height-max,5,24001,1,65536,56580,112,3915,4929,43156,38625,57790,2576,5170,0.410093,2606329050636720493,968,2,73,2,8597,4717,24,0,ok +edge-5-height-max,5,24003,1,65536,53244,113,5319,6860,37061,31890,54942,3543,7051,0.557101,7521527562398943057,59,3,100000,2,10876,5194,24,1,ok +edge-5-height-max,5,24003,1,65536,53244,113,5319,6860,37061,31890,54942,3543,7051,0.522673,7521527562398943057,59,3,100000,2,10876,5194,24,1,ok +edge-5-height-max,5,24004,1,65536,52480,139,5628,7289,38311,32618,54266,3787,7483,0.563887,13289230739172764216,236,2,55,2,8893,5160,24,1,ok +edge-5-height-max,5,24004,1,65536,52480,139,5628,7289,38311,32618,54266,3787,7483,0.681050,13289230739172764216,236,2,55,2,8893,5160,24,1,ok +edge-5-teams-min,5,24001,1,16384,12077,72,1889,2346,7754,6362,12655,1246,2483,0.010668,12139880955903287514,512,3,27,1,3273,1018,6,0,ok +edge-5-teams-min,5,24001,1,16384,12077,72,1889,2346,7754,6362,12655,1246,2483,0.049256,12139880955903287514,512,3,27,1,3273,1018,6,0,ok +edge-5-teams-min,5,24003,1,16384,12083,75,1872,2354,7369,5967,12657,1253,2474,0.021280,3350966425812963857,1184,1,9,1,3590,1089,6,0,ok +edge-5-teams-min,5,24003,1,16384,12083,75,1872,2354,7369,5967,12657,1253,2474,0.010580,3350966425812963857,1184,1,9,1,3590,1089,6,0,ok +edge-5-teams-min,5,24004,1,16384,11895,53,1968,2468,7299,5781,12504,1287,2593,0.073430,5773595217240884218,1063,1,46,0,3409,1149,6,0,ok +edge-5-teams-min,5,24004,1,16384,11895,53,1968,2468,7299,5781,12504,1287,2593,0.016160,5773595217240884218,1063,1,46,0,3409,1149,6,0,ok +edge-5-teams-max,5,24001,1,16384,8600,136,3685,3963,4271,1573,9495,2117,4772,0.041424,1242877349242468839,73,3,10,12,1974,2037,72,0,ok +edge-5-teams-max,5,24001,1,16384,8600,136,3685,3963,4271,1573,9495,2117,4772,0.109248,1242877349242468839,73,3,10,12,1974,2037,72,0,ok +edge-5-teams-max,5,24003,1,16384,8690,119,3701,3874,4226,1439,9593,2054,4737,0.029636,2931377641924482398,65,3,14,12,2119,2024,72,0,ok +edge-5-teams-max,5,24003,1,16384,8690,119,3701,3874,4226,1439,9593,2054,4737,0.039162,2931377641924482398,65,3,14,12,2119,2024,72,0,ok +edge-5-teams-max,5,24004,1,16384,8749,104,3693,3838,4280,1477,9623,2017,4744,0.094048,15092112594054926975,77,4,8,12,2019,2126,72,0,ok +edge-5-teams-max,5,24004,1,16384,8749,104,3693,3838,4280,1477,9623,2017,4744,0.041540,15092112594054926975,77,4,8,12,2019,2126,72,0,ok +edge-5-workers-min,5,24001,1,16384,11254,76,2321,2733,6638,4665,11914,1441,3029,0.098683,7123999391715415104,0,6,100000,3,2607,1911,24,0,ok +edge-5-workers-min,5,24001,1,16384,11254,76,2321,2733,6638,4665,11914,1441,3029,0.073933,7123999391715415104,0,6,100000,3,2607,1911,24,0,ok +edge-5-workers-min,5,24003,1,16384,10432,89,2681,3182,5881,3814,11191,1680,3513,0.064071,1175273401662215666,0,6,100000,3,2573,1877,24,0,ok +edge-5-workers-min,5,24003,1,16384,10432,89,2681,3182,5881,3814,11191,1680,3513,0.108449,1175273401662215666,0,6,100000,3,2573,1877,24,0,ok +edge-5-workers-min,5,24004,1,16384,10593,71,2710,3010,6258,4069,11298,1572,3514,0.034747,5190250561350529432,462,5,21,4,2432,1799,24,0,ok +edge-5-workers-min,5,24004,1,16384,10593,71,2710,3010,6258,4069,11298,1572,3514,0.084410,5190250561350529432,462,5,21,4,2432,1799,24,0,ok +edge-5-workers-max,5,24001,1,16384,11254,76,2321,2733,6578,4579,11914,1441,3029,0.087516,14655486661103581760,524,1,13,4,2596,1954,24,0,ok +edge-5-workers-max,5,24001,1,16384,11254,76,2321,2733,6578,4579,11914,1441,3029,0.085124,14655486661103581760,524,1,13,4,2596,1954,24,0,ok +edge-5-workers-max,5,24003,1,16384,10432,89,2681,3182,5869,3832,11191,1680,3513,0.050959,5061961116353793522,382,1,12,4,2521,1913,24,0,ok +edge-5-workers-max,5,24003,1,16384,10432,89,2681,3182,5869,3832,11191,1680,3513,0.093912,5061961116353793522,382,1,12,4,2521,1913,24,0,ok +edge-5-workers-max,5,24004,1,16384,10593,71,2710,3010,6062,3952,11298,1572,3514,0.013060,12822396717584548248,371,2,21,4,2511,1888,24,0,ok +edge-5-workers-max,5,24004,1,16384,10593,71,2710,3010,6062,3952,11298,1572,3514,0.011030,12822396717584548248,371,2,21,4,2511,1888,24,0,ok +edge-5-channel-width-min,5,24001,1,16384,11254,76,2321,2733,6532,4472,11914,1441,3029,0.059151,16198348791202468928,520,2,23,4,2649,1963,24,0,ok +edge-5-channel-width-min,5,24001,1,16384,11254,76,2321,2733,6532,4472,11914,1441,3029,0.021339,16198348791202468928,520,2,23,4,2649,1963,24,0,ok +edge-5-channel-width-min,5,24003,1,16384,10432,89,2681,3182,5810,3616,11191,1680,3513,0.081161,9817715955724006130,371,1,27,4,2597,1912,24,0,ok +edge-5-channel-width-min,5,24003,1,16384,10432,89,2681,3182,5810,3616,11191,1680,3513,0.050435,9817715955724006130,371,1,27,4,2597,1912,24,0,ok +edge-5-channel-width-min,5,24004,1,16384,10593,71,2710,3010,6117,3988,11298,1572,3514,0.076508,13565529459509754520,502,3,15,4,2428,1932,24,0,ok +edge-5-channel-width-min,5,24004,1,16384,10593,71,2710,3010,6117,3988,11298,1572,3514,0.089174,13565529459509754520,502,3,15,4,2428,1932,24,0,ok +edge-5-channel-width-max,5,24001,1,16384,8097,60,6048,2179,4518,2917,8615,1147,6622,0.087184,10995638509345458802,575,2,9,4,1891,1578,24,3759,ok +edge-5-channel-width-max,5,24001,1,16384,8097,60,6048,2179,4518,2917,8615,1147,6622,0.100170,10995638509345458802,575,2,9,4,1891,1578,24,3759,ok +edge-5-channel-width-max,5,24003,1,16384,6852,57,7040,2435,3653,1935,7428,1272,7684,0.087515,12403104663351606330,351,2,12,4,1692,1393,24,4431,ok +edge-5-channel-width-max,5,24003,1,16384,6852,57,7040,2435,3653,1935,7428,1272,7684,0.113171,12403104663351606330,351,2,12,4,1692,1393,24,4431,ok +edge-5-channel-width-max,5,24004,1,16384,7228,56,6833,2267,3822,2008,7770,1190,7424,0.034384,15586038024190125750,236,3,8,4,1771,1519,24,4311,ok +edge-5-channel-width-max,5,24004,1,16384,7228,56,6833,2267,3822,2008,7770,1190,7424,0.034391,15586038024190125750,236,3,8,4,1771,1519,24,4311,ok +edge-5-extra-islands-min,5,24001,1,16384,12315,35,1850,2184,7554,5742,12847,1127,2410,0.054103,15319057177535303165,755,2,22,4,2497,2160,24,0,ok +edge-5-extra-islands-min,5,24001,1,16384,12315,35,1850,2184,7554,5742,12847,1127,2410,0.010895,15319057177535303165,755,2,22,4,2497,2160,24,0,ok +edge-5-extra-islands-min,5,24003,1,16384,12276,73,1811,2224,7898,6001,12813,1184,2387,0.070369,816497216597935629,849,3,14,4,2289,1985,24,1,ok +edge-5-extra-islands-min,5,24003,1,16384,12276,73,1811,2224,7898,6001,12813,1184,2387,0.019834,816497216597935629,849,3,14,4,2289,1985,24,1,ok +edge-5-extra-islands-min,5,24004,1,16384,12419,42,1821,2102,8050,6148,12931,1093,2360,0.014440,4024635329982450899,838,2,34,3,2248,2017,24,1,ok +edge-5-extra-islands-min,5,24004,1,16384,12419,42,1821,2102,8050,6148,12931,1093,2360,0.124215,4024635329982450899,838,2,34,3,2248,2017,24,1,ok +edge-5-extra-islands-max,5,24001,1,16384,9382,109,3265,3628,5115,3089,10240,1920,4224,0.013927,3677509818522375674,27,2,21,4,2411,1739,24,0,ok +edge-5-extra-islands-max,5,24001,1,16384,9382,109,3265,3628,5115,3089,10240,1920,4224,0.084618,3677509818522375674,27,2,21,4,2411,1739,24,0,ok +edge-5-extra-islands-max,5,24003,1,16384,9036,114,3424,3810,4740,2576,9935,2019,4430,0.108538,4650947493334634350,363,2,16,4,2521,1653,24,0,ok +edge-5-extra-islands-max,5,24003,1,16384,9036,114,3424,3810,4740,2576,9935,2019,4430,0.015736,4650947493334634350,363,2,16,4,2521,1653,24,0,ok +edge-5-extra-islands-max,5,24004,1,16384,10012,76,2948,3348,5484,3047,10796,1748,3840,0.016219,1822594680024405239,582,3,10,4,2800,1601,24,1,ok +edge-5-extra-islands-max,5,24004,1,16384,10012,76,2948,3348,5484,3047,10796,1748,3840,0.166408,1822594680024405239,582,3,10,4,2800,1601,24,1,ok +size-5-6-6-4,5,24001,1,4096,1501,47,1265,1283,677,89,1783,687,1626,0.002810,4651726352976272702,8,3,5,2,360,355,24,0,ok +size-5-6-6-4,5,24001,1,4096,1501,47,1265,1283,677,89,1783,687,1626,0.003919,4651726352976272702,8,3,5,2,360,355,24,0,ok +size-5-6-6-4,5,24003,1,4096,1734,42,1139,1181,783,90,2004,632,1460,0.004965,4106057933311672777,8,3,4,3,439,403,24,0,ok +size-5-6-6-4,5,24003,1,4096,1734,42,1139,1181,783,90,2004,632,1460,0.002865,4106057933311672777,8,3,4,3,439,403,24,0,ok +size-5-6-6-4,5,24004,1,4096,1638,38,1189,1231,736,152,1917,652,1527,0.004491,7623062973509903136,0,3,4,3,394,392,24,3,ok +size-5-6-6-4,5,24004,1,4096,1638,38,1189,1231,736,152,1917,652,1527,0.002673,7623062973509903136,0,3,4,3,394,392,24,3,ok +size-5-6-6-12,5,24001,0,4096,923,43,1709,1421,876,280,1220,753,2123,0.006725,6375674900583504980,4096,100000,100000,0,24,17,6,0,failed +size-5-6-6-12,5,24001,0,4096,923,43,1709,1421,876,280,1220,753,2123,0.005554,6375674900583504980,4096,100000,100000,0,24,17,6,0,failed +size-5-6-6-12,5,24003,0,4096,950,28,1717,1401,908,311,1243,727,2126,0.002221,10650788504315581361,4096,100000,100000,0,22,12,6,2,failed +size-5-6-6-12,5,24003,0,4096,950,28,1717,1401,908,311,1243,727,2126,0.002094,10650788504315581361,4096,100000,100000,0,22,12,6,2,failed +size-5-6-6-12,5,24004,0,4096,857,38,1825,1376,817,253,1139,723,2234,0.002856,11680217874443233530,4096,100000,100000,0,19,15,6,2,failed +size-5-6-6-12,5,24004,0,4096,857,38,1825,1376,817,253,1139,723,2234,0.002932,11680217874443233530,4096,100000,100000,0,19,15,6,2,failed +size-5-7-7-8,5,24001,1,16384,9267,106,3419,3592,4717,2317,10108,1897,4379,0.184038,801385284587197134,201,4,13,8,2197,2139,48,0,ok +size-5-7-7-8,5,24001,1,16384,9267,106,3419,3592,4717,2317,10108,1897,4379,0.082424,801385284587197134,201,4,13,8,2197,2139,48,0,ok +size-5-7-7-8,5,24003,1,16384,9302,120,3370,3592,4733,2215,10135,1910,4339,0.066871,8610794216540169989,201,5,9,8,2298,2054,48,0,ok +size-5-7-7-8,5,24003,1,16384,9302,120,3370,3592,4733,2215,10135,1910,4339,0.107539,8610794216540169989,201,5,9,8,2298,2054,48,0,ok +size-5-7-7-8,5,24004,1,16384,9418,97,3303,3566,4837,2461,10236,1876,4272,0.014448,12042202373557920104,208,4,14,8,2262,2099,48,1,ok +size-5-7-7-8,5,24004,1,16384,9418,97,3303,3566,4837,2461,10236,1876,4272,0.042163,12042202373557920104,208,4,14,8,2262,2099,48,1,ok +size-5-6-9-12,5,24001,1,32768,20327,179,5644,6618,11064,6319,21893,3480,7395,0.249690,5418256031852975315,290,4,26,12,4634,4311,72,0,ok +size-5-6-9-12,5,24001,1,32768,20327,179,5644,6618,11064,6319,21893,3480,7395,0.296518,5418256031852975315,290,4,26,12,4634,4311,72,0,ok +size-5-6-9-12,5,24003,1,32768,20829,136,5353,6450,11173,6552,22386,3356,7026,0.282039,16836414780086224395,330,2,16,12,4935,4400,72,0,ok +size-5-6-9-12,5,24003,1,32768,20829,136,5353,6450,11173,6552,22386,3356,7026,0.273700,16836414780086224395,330,2,16,12,4935,4400,72,0,ok +size-5-6-9-12,5,24004,1,32768,19996,173,5816,6783,11077,6452,21615,3564,7589,0.288426,11284631810791223717,159,4,15,12,4414,4181,72,0,ok +size-5-6-9-12,5,24004,1,32768,19996,173,5816,6783,11077,6452,21615,3564,7589,0.351615,11284631810791223717,159,4,15,12,4414,4181,72,0,ok +size-5-9-6-12,5,24001,1,32768,19833,207,5904,6824,10503,6101,21463,3618,7687,0.444717,9914795222840391452,333,2,16,12,4713,4299,72,4,ok +size-5-9-6-12,5,24001,1,32768,19833,207,5904,6824,10503,6101,21463,3618,7687,0.277273,9914795222840391452,333,2,16,12,4713,4299,72,4,ok +size-5-9-6-12,5,24003,1,32768,20018,129,5759,6862,10908,6336,21649,3563,7556,0.264060,10626352557760732287,323,5,18,12,4566,4223,72,1,ok +size-5-9-6-12,5,24003,1,32768,20018,129,5759,6862,10908,6336,21649,3563,7556,0.191762,10626352557760732287,323,5,18,12,4566,4223,72,1,ok +size-5-9-6-12,5,24004,1,32768,20170,153,5939,6506,10916,6357,21714,3400,7654,0.241783,2930393585886118444,323,5,23,12,4609,4321,72,0,ok +size-5-9-6-12,5,24004,1,32768,20170,153,5939,6506,10916,6357,21714,3400,7654,0.456094,2930393585886118444,323,5,23,12,4609,4321,72,0,ok +size-5-9-9-12,5,24001,1,262144,222605,484,17401,21654,179792,162618,227887,11308,22949,4.060011,18016697878824307604,687,3,92,9,25058,17437,72,0,ok +size-5-9-9-12,5,24001,1,262144,222605,484,17401,21654,179792,162618,227887,11308,22949,4.333187,18016697878824307604,687,3,92,9,25058,17437,72,0,ok +size-5-9-9-12,5,24003,1,262144,222341,511,17745,21547,177847,159953,227618,11284,23242,4.594076,13204712083959918866,0,5,100000,7,27034,17139,72,0,ok +size-5-9-9-12,5,24003,1,262144,222341,511,17745,21547,177847,159953,227618,11284,23242,4.365123,13204712083959918866,0,5,100000,7,27034,17139,72,0,ok +size-5-9-9-12,5,24004,1,262144,225930,351,16056,19807,181496,165172,230811,10240,21093,4.264293,13394875853363115778,0,3,100000,6,26369,17741,72,0,ok +size-5-9-9-12,5,24004,1,262144,225930,351,16056,19807,181496,165172,230811,10240,21093,4.021998,13394875853363115778,0,3,100000,6,26369,17741,72,0,ok +corner-5-min,5,24001,0,4096,0,0,4096,0,0,0,0,0,4096,0.001281,15070008172932738451,4096,100000,100000,0,0,0,0,2596,failed +corner-5-min,5,24001,0,4096,0,0,4096,0,0,0,0,0,4096,0.001310,15070008172932738451,4096,100000,100000,0,0,0,0,2596,failed +corner-5-min,5,24003,0,4096,0,0,4096,0,0,0,0,0,4096,0.001297,3473231813497914905,4096,100000,100000,0,0,0,0,2649,failed +corner-5-min,5,24003,0,4096,0,0,4096,0,0,0,0,0,4096,0.003182,3473231813497914905,4096,100000,100000,0,0,0,0,2649,failed +corner-5-min,5,24004,0,4096,0,0,4096,0,0,0,0,0,4096,0.001413,18126423395565846657,4096,100000,100000,0,0,0,0,2901,failed +corner-5-min,5,24004,0,4096,0,0,4096,0,0,0,0,0,4096,0.002548,18126423395565846657,4096,100000,100000,0,0,0,0,2901,failed +corner-5-max,5,24001,1,262144,189760,411,51837,20136,146952,131029,194706,10476,56962,3.919425,8267887055777218505,144,2,100000,7,26242,16193,72,31719,ok +corner-5-max,5,24001,1,262144,189760,411,51837,20136,146952,131029,194706,10476,56962,4.016038,8267887055777218505,144,2,100000,7,26242,16193,72,31719,ok +corner-5-max,5,24003,1,262144,186756,427,52992,21969,143202,126250,192167,11412,58565,4.240843,15587302819288139138,280,2,67,7,26102,17074,72,32031,ok +corner-5-max,5,24003,1,262144,186756,427,52992,21969,143202,126250,192167,11412,58565,3.981434,15587302819288139138,280,2,67,7,26102,17074,72,32031,ok +corner-5-max,5,24004,1,262144,189203,418,52102,20421,143164,126522,194185,10622,57337,4.158956,8924188191017556042,819,1,63,5,29311,16345,72,32098,ok +corner-5-max,5,24004,1,262144,189203,418,52102,20421,143164,126522,194185,10622,57337,3.665281,8924188191017556042,819,1,63,5,29311,16345,72,32098,ok +edge-6-width-min,6,24001,1,8192,3876,322,2089,1905,2086,901,4414,1293,2485,0.007337,8301481773998981155,215,4,10,4,841,845,24,74,ok +edge-6-width-min,6,24001,1,8192,3876,322,2089,1905,2086,901,4414,1293,2485,0.004646,8301481773998981155,215,4,10,4,841,845,24,74,ok +edge-6-width-min,6,24003,1,8192,3400,330,2761,1701,1809,562,3857,1200,3135,0.007951,11260754162497162439,149,1,5,4,747,740,24,46,ok +edge-6-width-min,6,24003,1,8192,3400,330,2761,1701,1809,562,3857,1200,3135,0.010023,11260754162497162439,149,1,5,4,747,740,24,46,ok +edge-6-width-min,6,24004,1,8192,3312,314,2753,1813,1773,546,3807,1228,3157,0.032714,11622027674796834941,131,3,4,4,712,723,24,98,ok +edge-6-width-min,6,24004,1,8192,3312,314,2753,1813,1773,546,3807,1228,3157,0.006903,11622027674796834941,131,3,4,4,712,723,24,98,ok +edge-6-width-max,6,24001,1,65536,26058,1471,31659,6348,18362,14960,27762,4673,33101,0.292315,15831918896906361328,1090,2,25,4,3896,3696,24,94,ok +edge-6-width-max,6,24001,1,65536,26058,1471,31659,6348,18362,14960,27762,4673,33101,0.263758,15831918896906361328,1090,2,25,4,3896,3696,24,94,ok +edge-6-width-max,6,24003,1,65536,26796,1685,30000,7055,19392,15893,28653,5230,31653,0.205091,7572749383616265500,1172,4,28,4,3748,3552,24,100,ok +edge-6-width-max,6,24003,1,65536,26796,1685,30000,7055,19392,15893,28653,5230,31653,0.236993,7572749383616265500,1172,4,28,4,3748,3552,24,100,ok +edge-6-width-max,6,24004,1,65536,24443,1364,33803,5926,17244,13924,25994,4355,35187,0.280962,4840076621543531009,948,2,29,4,3733,3362,24,87,ok +edge-6-width-max,6,24004,1,65536,24443,1364,33803,5926,17244,13924,25994,4355,35187,0.253937,4840076621543531009,948,2,29,4,3733,3362,24,87,ok +edge-6-height-min,6,24001,1,8192,3675,369,2214,1934,1957,754,4183,1347,2662,0.004422,602148175110294740,177,2,9,4,784,830,24,56,ok +edge-6-height-min,6,24001,1,8192,3675,369,2214,1934,1957,754,4183,1347,2662,0.005902,602148175110294740,177,2,9,4,784,830,24,56,ok +edge-6-height-min,6,24003,1,8192,3424,434,2560,1774,1825,598,3916,1341,2935,0.034891,10724234975970144101,108,3,8,4,728,767,24,73,ok +edge-6-height-min,6,24003,1,8192,3424,434,2560,1774,1825,598,3916,1341,2935,0.004659,10724234975970144101,108,3,8,4,728,767,24,73,ok +edge-6-height-min,6,24004,1,8192,3836,443,2048,1865,2042,783,4319,1392,2481,0.005227,16560501378303616241,192,4,9,4,817,873,24,75,ok +edge-6-height-min,6,24004,1,8192,3836,443,2048,1865,2042,783,4319,1392,2481,0.004520,16560501378303616241,192,4,9,4,817,873,24,75,ok +edge-6-height-max,6,24001,1,65536,27059,1123,31121,6233,19455,15821,28676,4270,32590,0.213385,14239867738790271377,1169,1,39,3,3902,3598,24,97,ok +edge-6-height-max,6,24001,1,65536,27059,1123,31121,6233,19455,15821,28676,4270,32590,0.273713,14239867738790271377,1169,1,39,3,3902,3598,24,97,ok +edge-6-height-max,6,24003,1,65536,24646,936,34597,5357,17463,14225,26029,3639,35868,0.302143,5829103401409267935,1005,4,26,4,3723,3356,24,99,ok +edge-6-height-max,6,24003,1,65536,24646,936,34597,5357,17463,14225,26029,3639,35868,0.256553,5829103401409267935,1005,4,26,4,3723,3356,24,99,ok +edge-6-height-max,6,24004,1,65536,24685,1027,34316,5508,17292,13948,26121,3825,35590,0.402356,14424515157103611352,947,1,43,3,3703,3586,24,100,ok +edge-6-height-max,6,24004,1,65536,24685,1027,34316,5508,17292,13948,26121,3825,35590,0.277400,14424515157103611352,947,1,43,3,3703,3586,24,100,ok +edge-6-teams-min,6,24001,0,16384,1670,146,13956,612,1670,1353,1819,455,14110,0.002701,13724714730050812290,16384,100000,100000,0,0,0,0,0,failed +edge-6-teams-min,6,24001,0,16384,1670,146,13956,612,1670,1353,1819,455,14110,0.002511,13724714730050812290,16384,100000,100000,0,0,0,0,0,failed +edge-6-teams-min,6,24003,0,16384,1602,129,14015,638,1602,1292,1770,450,14164,0.015378,5555895874860407129,16384,100000,100000,0,0,0,0,0,failed +edge-6-teams-min,6,24003,0,16384,1602,129,14015,638,1602,1292,1770,450,14164,0.002591,5555895874860407129,16384,100000,100000,0,0,0,0,0,failed +edge-6-teams-min,6,24004,0,16384,1645,141,13942,656,1645,1327,1811,473,14100,0.002517,16883743711992608893,16384,100000,100000,0,0,0,0,0,failed +edge-6-teams-min,6,24004,0,16384,1645,141,13942,656,1645,1327,1811,473,14100,0.004241,16883743711992608893,16384,100000,100000,0,0,0,0,0,failed +edge-6-teams-max,6,24001,1,16384,10616,701,1141,3926,6177,1756,11936,2656,1792,0.042114,1078194504538917648,104,3,11,12,2066,2061,72,92,ok +edge-6-teams-max,6,24001,1,16384,10616,701,1141,3926,6177,1756,11936,2656,1792,0.053325,1078194504538917648,104,3,11,12,2066,2061,72,92,ok +edge-6-teams-max,6,24003,1,16384,10158,791,1312,4123,6263,1692,11603,2828,1953,0.174375,10246265899692024718,100,5,7,12,1737,1846,72,115,ok +edge-6-teams-max,6,24003,1,16384,10158,791,1312,4123,6263,1692,11603,2828,1953,0.049400,10246265899692024718,100,5,7,12,1737,1846,72,115,ok +edge-6-teams-max,6,24004,1,16384,10483,668,1386,3847,6530,1837,11815,2564,2005,0.083996,9955619800240440577,71,6,7,12,1827,1814,72,164,ok +edge-6-teams-max,6,24004,1,16384,10483,668,1386,3847,6530,1837,11815,2564,2005,0.069513,9955619800240440577,71,6,7,12,1827,1814,72,164,ok +edge-6-workers-min,6,24001,1,16384,6997,592,6141,2654,4047,2304,7700,1921,6763,0.074498,10478685938506820208,505,4,14,4,1454,1404,24,99,ok +edge-6-workers-min,6,24001,1,16384,6997,592,6141,2654,4047,2304,7700,1921,6763,0.008573,10478685938506820208,505,4,14,4,1454,1404,24,99,ok +edge-6-workers-min,6,24003,1,16384,6893,597,5962,2932,3919,2164,7668,2070,6646,0.008415,16638262727558778513,320,4,10,4,1459,1423,24,92,ok +edge-6-workers-min,6,24003,1,16384,6893,597,5962,2932,3919,2164,7668,2070,6646,0.050374,16638262727558778513,320,4,10,4,1459,1423,24,92,ok +edge-6-workers-min,6,24004,1,16384,7089,539,5668,3088,4002,2187,7906,2092,6386,0.012881,8225055719630622565,403,5,15,4,1497,1498,24,89,ok +edge-6-workers-min,6,24004,1,16384,7089,539,5668,3088,4002,2187,7906,2092,6386,0.022610,8225055719630622565,403,5,15,4,1497,1498,24,89,ok +edge-6-workers-max,6,24001,1,16384,6997,592,6141,2654,4018,2266,7700,1921,6763,0.013143,17181742508508505200,514,2,5,4,1438,1421,24,99,ok +edge-6-workers-max,6,24001,1,16384,6997,592,6141,2654,4018,2266,7700,1921,6763,0.009956,17181742508508505200,514,2,5,4,1438,1421,24,99,ok +edge-6-workers-max,6,24003,1,16384,6893,597,5962,2932,3872,2135,7668,2070,6646,0.032319,3330212283408972177,454,1,8,4,1497,1404,24,92,ok +edge-6-workers-max,6,24003,1,16384,6893,597,5962,2932,3872,2135,7668,2070,6646,0.040489,3330212283408972177,454,1,8,4,1497,1404,24,92,ok +edge-6-workers-max,6,24004,1,16384,7089,539,5668,3088,4081,2174,7906,2092,6386,0.029831,1518336612106003301,395,2,6,4,1466,1422,24,89,ok +edge-6-workers-max,6,24004,1,16384,7089,539,5668,3088,4081,2174,7906,2092,6386,0.008412,1518336612106003301,395,2,6,4,1466,1422,24,89,ok +edge-6-island-size-min,6,24001,1,16384,4428,448,8951,2557,2435,1019,5105,1729,9550,0.048474,2337629969544153032,241,2,9,4,920,969,24,98,ok +edge-6-island-size-min,6,24001,1,16384,4428,448,8951,2557,2435,1019,5105,1729,9550,0.008013,2337629969544153032,241,2,9,4,920,969,24,98,ok +edge-6-island-size-min,6,24003,1,16384,4286,482,9017,2599,2278,895,4948,1799,9637,0.007839,9941356866946445158,185,5,7,4,963,941,24,87,ok +edge-6-island-size-min,6,24003,1,16384,4286,482,9017,2599,2278,895,4948,1799,9637,0.012050,9941356866946445158,185,5,7,4,963,941,24,87,ok +edge-6-island-size-min,6,24004,1,16384,4449,588,8518,2829,2441,937,5182,2019,9183,0.051960,16934352449055104569,213,4,9,4,950,954,24,81,ok +edge-6-island-size-min,6,24004,1,16384,4449,588,8518,2829,2441,937,5182,2019,9183,0.009031,16934352449055104569,213,4,9,4,950,954,24,81,ok +edge-6-island-size-max,6,24001,1,16384,7474,535,5436,2939,4230,2384,8253,2022,6109,0.014569,9523367320184550060,434,1,10,4,1601,1539,24,95,ok +edge-6-island-size-max,6,24001,1,16384,7474,535,5436,2939,4230,2384,8253,2022,6109,0.009447,9523367320184550060,434,1,10,4,1601,1539,24,95,ok +edge-6-island-size-max,6,24003,1,16384,7411,563,5413,2997,4178,2414,8193,2064,6127,0.054111,15266498219304234197,500,2,17,4,1586,1543,24,95,ok +edge-6-island-size-max,6,24003,1,16384,7411,563,5413,2997,4178,2414,8193,2064,6127,0.009252,15266498219304234197,500,2,17,4,1586,1543,24,95,ok +edge-6-island-size-max,6,24004,1,16384,7668,574,5058,3084,4472,2525,8461,2132,5791,0.060344,17668093791446278362,437,3,9,4,1587,1505,24,100,ok +edge-6-island-size-max,6,24004,1,16384,7668,574,5058,3084,4472,2525,8461,2132,5791,0.041982,17668093791446278362,437,3,9,4,1587,1505,24,100,ok +edge-6-bridge-width-min,6,24001,1,16384,6752,614,6275,2743,3775,2233,7478,1990,6916,0.017442,5556296913352800080,484,1,6,4,1459,1414,24,100,ok +edge-6-bridge-width-min,6,24001,1,16384,6752,614,6275,2743,3775,2233,7478,1990,6916,0.009452,5556296913352800080,484,1,6,4,1459,1414,24,100,ok +edge-6-bridge-width-min,6,24003,1,16384,6628,601,6157,2998,3657,1990,7398,2115,6871,0.027132,102958655870675986,437,3,13,4,1459,1408,24,100,ok +edge-6-bridge-width-min,6,24003,1,16384,6628,601,6157,2998,3657,1990,7398,2115,6871,0.056861,102958655870675986,437,3,13,4,1459,1408,24,100,ok +edge-6-bridge-width-min,6,24004,1,16384,6815,555,5878,3136,3834,2191,7625,2143,6616,0.008786,399138138934018572,493,2,13,4,1496,1381,24,100,ok +edge-6-bridge-width-min,6,24004,1,16384,6815,555,5878,3136,3834,2191,7625,2143,6616,0.068053,399138138934018572,493,2,13,4,1496,1381,24,100,ok +edge-6-bridge-width-max,6,24001,1,16384,7510,555,5769,2550,4283,2364,8182,1834,6368,0.140124,11436370948444010525,495,2,11,4,1621,1502,24,100,ok +edge-6-bridge-width-max,6,24001,1,16384,7510,555,5769,2550,4283,2364,8182,1834,6368,0.068692,11436370948444010525,495,2,11,4,1621,1502,24,100,ok +edge-6-bridge-width-max,6,24003,1,16384,7484,541,5558,2801,4338,2282,8225,1946,6213,0.010345,16058708852202796820,494,5,6,4,1503,1539,24,98,ok +edge-6-bridge-width-max,6,24003,1,16384,7484,541,5558,2801,4338,2282,8225,1946,6213,0.010996,16058708852202796820,494,5,6,4,1503,1539,24,98,ok +edge-6-bridge-width-max,6,24004,1,16384,7710,540,5172,2962,4523,2493,8502,2029,5853,0.018877,6983343505154156422,500,1,15,4,1591,1492,24,100,ok +edge-6-bridge-width-max,6,24004,1,16384,7710,540,5172,2962,4523,2493,8502,2029,5853,0.010890,6983343505154156422,500,1,15,4,1591,1492,24,100,ok +size-6-6-6-4,6,24001,1,4096,2272,237,328,1259,1210,213,2623,871,602,0.003098,4297036404364719968,69,1,5,4,472,486,24,14,ok +size-6-6-6-4,6,24001,1,4096,2272,237,328,1259,1210,213,2623,871,602,0.002529,4297036404364719968,69,1,5,4,472,486,24,14,ok +size-6-6-6-4,6,24003,1,4096,2234,228,437,1197,1145,307,2555,838,703,0.019897,4448878305702187952,72,2,3,4,473,512,24,51,ok +size-6-6-6-4,6,24003,1,4096,2234,228,437,1197,1145,307,2555,838,703,0.002531,4448878305702187952,72,2,3,4,473,512,24,51,ok +size-6-6-6-4,6,24004,1,4096,2221,216,409,1250,1156,213,2565,846,685,0.002576,2339653472317238558,54,3,3,4,501,460,24,45,ok +size-6-6-6-4,6,24004,1,4096,2221,216,409,1250,1156,213,2565,846,685,0.002497,2339653472317238558,54,3,3,4,501,460,24,45,ok +size-6-6-6-12,6,24001,0,4096,3852,21,0,223,3852,3283,3982,112,2,0.004930,9275156586024609708,4096,100000,100000,0,0,0,0,0,failed +size-6-6-6-12,6,24001,0,4096,3852,21,0,223,3852,3283,3982,112,2,0.002065,9275156586024609708,4096,100000,100000,0,0,0,0,0,failed +size-6-6-6-12,6,24003,0,4096,3820,37,0,239,3820,3275,3949,140,7,0.001936,18090506768242481536,4096,100000,100000,0,0,0,0,0,failed +size-6-6-6-12,6,24003,0,4096,3820,37,0,239,3820,3275,3949,140,7,0.002883,18090506768242481536,4096,100000,100000,0,0,0,0,0,failed +size-6-6-6-12,6,24004,0,4096,3418,91,52,535,3418,2601,3641,344,111,0.001839,17445749914128481966,4096,100000,100000,0,0,0,0,18,failed +size-6-6-6-12,6,24004,0,4096,3418,91,52,535,3418,2601,3641,344,111,0.003876,17445749914128481966,4096,100000,100000,0,0,0,0,18,failed +size-6-7-7-8,6,24001,1,16384,7102,813,3831,4638,4157,1047,8432,3140,4812,0.059656,1544098799779975629,124,3,7,8,1382,1355,48,120,ok +size-6-7-7-8,6,24001,1,16384,7102,813,3831,4638,4157,1047,8432,3140,4812,0.020510,1544098799779975629,124,3,7,8,1382,1355,48,120,ok +size-6-7-7-8,6,24003,1,16384,8184,754,3349,4097,4605,1365,9376,2796,4212,0.161835,5206711405975171718,142,4,11,8,1661,1710,48,131,ok +size-6-7-7-8,6,24003,1,16384,8184,754,3349,4097,4605,1365,9376,2796,4212,0.057456,5206711405975171718,142,4,11,8,1661,1710,48,131,ok +size-6-7-7-8,6,24004,1,16384,8523,758,3139,3964,4863,1805,9647,2761,3976,0.127345,9930504172083004857,179,3,8,8,1724,1728,48,140,ok +size-6-7-7-8,6,24004,1,16384,8523,758,3139,3964,4863,1805,9647,2761,3976,0.056197,9930504172083004857,179,3,8,8,1724,1728,48,140,ok +size-6-6-9-12,6,24001,1,32768,15663,1333,7412,8360,9034,3817,17998,5537,9233,0.295250,9744454154937155772,270,3,12,12,3164,3153,72,213,ok +size-6-6-9-12,6,24001,1,32768,15663,1333,7412,8360,9034,3817,17998,5537,9233,0.248852,9744454154937155772,270,3,12,12,3164,3153,72,213,ok +size-6-6-9-12,6,24003,1,32768,15764,1139,7675,8190,9091,3567,18075,5275,9418,0.195161,3945711515277836980,260,4,14,12,3241,3120,72,172,ok +size-6-6-9-12,6,24003,1,32768,15764,1139,7675,8190,9091,3567,18075,5275,9418,0.186376,3945711515277836980,260,4,14,12,3241,3120,72,172,ok +size-6-6-9-12,6,24004,1,32768,15416,1151,8118,8083,8872,3617,17643,5219,9906,0.222475,1130691957958131145,242,5,12,12,3152,3080,72,222,ok +size-6-6-9-12,6,24004,1,32768,15416,1151,8118,8083,8872,3617,17643,5219,9906,0.159700,1130691957958131145,242,5,12,12,3152,3080,72,222,ok +size-6-9-6-12,6,24001,1,32768,14684,2078,7268,8738,8231,3564,17085,6468,9215,0.197666,11857700093439170408,274,3,18,12,3150,2991,72,248,ok +size-6-9-6-12,6,24001,1,32768,14684,2078,7268,8738,8231,3564,17085,6468,9215,0.158530,11857700093439170408,274,3,18,12,3150,2991,72,248,ok +size-6-9-6-12,6,24003,1,32768,15472,1813,7527,7956,8974,3896,17641,5818,9309,0.238045,2610189826550231322,285,4,10,12,3140,3046,72,185,ok +size-6-9-6-12,6,24003,1,32768,15472,1813,7527,7956,8974,3896,17641,5818,9309,0.296042,2610189826550231322,285,4,10,12,3140,3046,72,185,ok +size-6-9-6-12,6,24004,1,32768,14973,1858,7737,8200,8473,3515,17249,5972,9547,0.238810,8162734438950355997,223,4,15,12,3090,3098,72,244,ok +size-6-9-6-12,6,24004,1,32768,14973,1858,7737,8200,8473,3515,17249,5972,9547,0.298055,8162734438950355997,223,4,15,12,3090,3098,72,244,ok +size-6-9-9-12,6,24001,1,262144,94799,7136,122592,37617,71106,54946,104894,26009,131241,3.791361,14985575880983416012,1018,2,49,9,12308,11073,72,273,ok +size-6-9-9-12,6,24001,1,262144,94799,7136,122592,37617,71106,54946,104894,26009,131241,3.800057,14985575880983416012,1018,2,49,9,12308,11073,72,273,ok +size-6-9-9-12,6,24003,1,262144,93848,6430,127677,34189,70435,54384,103023,23608,135513,3.426301,14775211458720681956,819,3,48,6,12070,11031,72,281,ok +size-6-9-9-12,6,24003,1,262144,93848,6430,127677,34189,70435,54384,103023,23608,135513,3.702816,14775211458720681956,819,3,48,6,12070,11031,72,281,ok +size-6-9-9-12,6,24004,1,262144,96866,6257,124848,34173,72649,57187,106027,23423,132694,3.626516,10644597901623392893,890,3,51,7,12315,11590,72,288,ok +size-6-9-9-12,6,24004,1,262144,96866,6257,124848,34173,72649,57187,106027,23423,132694,3.707816,10644597901623392893,890,3,51,7,12315,11590,72,288,ok +corner-6-min,6,24001,0,4096,364,37,3413,282,364,225,431,179,3486,0.000680,13032831175469049256,4096,100000,100000,0,0,0,0,0,failed +corner-6-min,6,24001,0,4096,364,37,3413,282,364,225,431,179,3486,0.000708,13032831175469049256,4096,100000,100000,0,0,0,0,0,failed +corner-6-min,6,24003,0,4096,346,67,3381,302,346,205,418,219,3459,0.001828,4538073216810535782,4096,100000,100000,0,0,0,0,0,failed +corner-6-min,6,24003,0,4096,346,67,3381,302,346,205,418,219,3459,0.000677,4538073216810535782,4096,100000,100000,0,0,0,0,0,failed +corner-6-min,6,24004,0,4096,321,48,3399,328,321,186,402,213,3481,0.001331,6504701441893939564,4096,100000,100000,0,0,0,0,0,failed +corner-6-min,6,24004,0,4096,321,48,3399,328,321,186,402,213,3481,0.001437,6504701441893939564,4096,100000,100000,0,0,0,0,0,failed +corner-6-max,6,24001,1,262144,120167,6188,102948,32841,93768,70201,129117,22679,110348,3.929968,6449499192887501291,885,1,40,10,13739,12300,72,298,ok +corner-6-max,6,24001,1,262144,120167,6188,102948,32841,93768,70201,129117,22679,110348,4.096833,6449499192887501291,885,1,40,10,13739,12300,72,298,ok +corner-6-max,6,24003,1,262144,123597,6580,96143,35824,96452,69703,133362,24589,104193,3.753294,12680308190373788541,942,3,48,6,13940,12845,72,297,ok +corner-6-max,6,24003,1,262144,123597,6580,96143,35824,96452,69703,133362,24589,104193,3.665227,12680308190373788541,942,3,48,6,13940,12845,72,297,ok +corner-6-max,6,24004,1,262144,119881,6052,103104,33107,91750,68214,128814,22657,110673,3.485251,3158686122395171908,895,1,50,9,14656,13115,72,294,ok +corner-6-max,6,24004,1,262144,119881,6052,103104,33107,91750,68214,128814,22657,110673,3.755580,3158686122395171908,895,1,50,9,14656,13115,72,294,ok +edge-7-width-min,7,24001,0,8192,0,0,8192,0,0,0,3891,2244,2057,0.019886,7749714202859875114,8192,100000,100000,0,0,0,0,0,failed +edge-7-width-min,7,24001,0,8192,0,0,8192,0,0,0,3891,2244,2057,0.058049,7749714202859875114,8192,100000,100000,0,0,0,0,0,failed +edge-7-width-min,7,24003,0,8192,0,0,8192,0,0,0,4320,2078,1794,0.097895,4139461070367048631,8192,100000,100000,0,0,0,0,0,failed +edge-7-width-min,7,24003,0,8192,0,0,8192,0,0,0,4320,2078,1794,0.042093,4139461070367048631,8192,100000,100000,0,0,0,0,0,failed +edge-7-width-min,7,24004,1,8192,4013,811,710,2658,3119,849,4936,2127,1129,0.053420,1932614858527718601,161,14,15,4,388,169,257,114,ok +edge-7-width-min,7,24004,1,8192,4013,811,710,2658,3119,849,4936,2127,1129,0.091390,1932614858527718601,161,14,15,4,388,169,257,114,ok +edge-7-width-max,7,24001,1,65536,29290,4778,12068,19400,28288,14750,34528,14549,16459,0.184120,4289464862370130736,295,11,18,4,461,191,270,116,ok +edge-7-width-max,7,24001,1,65536,29290,4778,12068,19400,28288,14750,34528,14549,16459,0.260332,4289464862370130736,295,11,18,4,461,191,270,116,ok +edge-7-width-max,7,24003,1,65536,28922,4642,14159,17813,27868,14568,33718,13584,18234,0.357274,5039114077258876817,580,8,22,4,490,192,292,184,ok +edge-7-width-max,7,24003,1,65536,28922,4642,14159,17813,27868,14568,33718,13584,18234,0.213452,5039114077258876817,580,8,22,4,490,192,292,184,ok +edge-7-width-max,7,24004,1,65536,29612,7023,5639,23262,28592,10392,37684,18630,9222,0.386971,2919346984229201714,500,6,14,4,521,229,190,87,ok +edge-7-width-max,7,24004,1,65536,29612,7023,5639,23262,28592,10392,37684,18630,9222,0.247708,2919346984229201714,500,6,14,4,521,229,190,87,ok +edge-7-height-min,7,24001,1,8192,3401,590,1612,2589,2654,733,4093,1904,2195,0.073513,9639025586956667039,130,16,23,4,306,123,238,175,ok +edge-7-height-min,7,24001,1,8192,3401,590,1612,2589,2654,733,4093,1904,2195,0.085679,9639025586956667039,130,16,23,4,306,123,238,175,ok +edge-7-height-min,7,24003,0,8192,0,0,8192,0,0,0,4278,1925,1989,0.080120,16889309551585618671,8192,100000,100000,0,0,0,0,0,failed +edge-7-height-min,7,24003,0,8192,0,0,8192,0,0,0,4278,1925,1989,0.021210,16889309551585618671,8192,100000,100000,0,0,0,0,0,failed +edge-7-height-min,7,24004,0,8192,0,0,8192,0,0,0,4861,2534,797,0.068787,11722665298193346431,8192,100000,100000,0,0,0,0,0,failed +edge-7-height-min,7,24004,0,8192,0,0,8192,0,0,0,4861,2534,797,0.179168,11722665298193346431,8192,100000,100000,0,0,0,0,0,failed +edge-7-height-max,7,24001,1,65536,28770,5154,11868,19744,27745,13904,34182,15115,16239,0.190713,8462703386345682414,439,8,14,4,468,185,292,144,ok +edge-7-height-max,7,24001,1,65536,28770,5154,11868,19744,27745,13904,34182,15115,16239,0.200248,8462703386345682414,439,8,14,4,468,185,292,144,ok +edge-7-height-max,7,24003,1,65536,28274,5226,13150,18886,27294,13827,33367,14755,17414,0.196687,17965852414514832769,389,10,17,4,466,174,260,108,ok +edge-7-height-max,7,24003,1,65536,28274,5226,13150,18886,27294,13827,33367,14755,17414,0.242180,17965852414514832769,389,10,17,4,466,174,260,108,ok +edge-7-height-max,7,24004,1,65536,30239,6899,5731,22667,29214,11368,37957,18238,9341,0.278769,16426413119338499249,417,9,12,4,485,240,220,121,ok +edge-7-height-max,7,24004,1,65536,30239,6899,5731,22667,29214,11368,37957,18238,9341,0.199088,16426413119338499249,417,9,12,4,485,240,220,121,ok +edge-7-teams-min,7,24001,1,16384,6727,1513,2961,5183,6504,3026,8186,4144,4054,0.024204,2996695497079447710,565,5,11,1,117,37,49,34,ok +edge-7-teams-min,7,24001,1,16384,6727,1513,2961,5183,6504,3026,8186,4144,4054,0.030498,2996695497079447710,565,5,11,1,117,37,49,34,ok +edge-7-teams-min,7,24003,1,16384,6971,1448,2991,4974,6747,3047,8377,3959,4048,0.195630,10850214427827422687,778,7,19,1,113,49,42,33,ok +edge-7-teams-min,7,24003,1,16384,6971,1448,2991,4974,6747,3047,8377,3959,4048,0.054465,10850214427827422687,778,7,19,1,113,49,42,33,ok +edge-7-teams-min,7,24004,1,16384,7338,1720,1599,5727,7084,2650,9315,4587,2482,0.068931,16120643585322759056,723,5,9,1,140,45,49,22,ok +edge-7-teams-min,7,24004,1,16384,7338,1720,1599,5727,7084,2650,9315,4587,2482,0.126231,16120643585322759056,723,5,9,1,140,45,49,22,ok +edge-7-teams-max,7,24001,0,16384,0,0,16384,0,0,0,7774,4313,4297,0.066800,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-teams-max,7,24001,0,16384,0,0,16384,0,0,0,7774,4313,4297,0.205120,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-teams-max,7,24003,0,16384,0,0,16384,0,0,0,8268,4021,4095,0.093587,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-7-teams-max,7,24003,0,16384,0,0,16384,0,0,0,8268,4021,4095,0.055442,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-7-teams-max,7,24004,0,16384,0,0,16384,0,0,0,9186,4638,2560,0.127962,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-7-teams-max,7,24004,0,16384,0,0,16384,0,0,0,9186,4638,2560,0.027212,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-7-workers-min,7,24001,1,16384,6469,1542,3046,5327,5538,1915,7958,4248,4178,0.077322,4204626813648185965,173,8,23,4,428,218,217,69,ok +edge-7-workers-min,7,24001,1,16384,6469,1542,3046,5327,5538,1915,7958,4248,4178,0.144408,4204626813648185965,173,8,23,4,428,218,217,69,ok +edge-7-workers-min,7,24003,1,16384,7197,1387,2877,4923,6359,2453,8599,3869,3916,0.107967,3252641365549671066,107,11,35,3,361,173,236,151,ok +edge-7-workers-min,7,24003,1,16384,7197,1387,2877,4923,6359,2453,8599,3869,3916,0.082422,3252641365549671066,107,11,35,3,361,173,236,151,ok +edge-7-workers-min,7,24004,1,16384,7434,1672,1633,5645,6587,2213,9362,4500,2522,0.115490,5596711191859025707,204,8,13,4,372,164,243,35,ok +edge-7-workers-min,7,24004,1,16384,7434,1672,1633,5645,6587,2213,9362,4500,2522,0.076133,5596711191859025707,204,8,13,4,372,164,243,35,ok +edge-7-workers-max,7,24001,1,16384,6469,1542,3046,5327,5512,1892,7958,4248,4178,0.167392,14634870893842776685,217,8,22,4,428,218,215,69,ok +edge-7-workers-max,7,24001,1,16384,6469,1542,3046,5327,5512,1892,7958,4248,4178,0.161514,14634870893842776685,217,8,22,4,428,218,215,69,ok +edge-7-workers-max,7,24003,1,16384,7197,1387,2877,4923,6331,2430,8599,3869,3916,0.215144,3252641365549671066,108,11,35,3,361,173,236,151,ok +edge-7-workers-max,7,24003,1,16384,7197,1387,2877,4923,6331,2430,8599,3869,3916,0.132042,3252641365549671066,108,11,35,3,361,173,236,151,ok +edge-7-workers-max,7,24004,1,16384,7434,1672,1633,5645,6559,2180,9362,4500,2522,0.103474,5596711191859025707,210,6,13,4,372,164,243,35,ok +edge-7-workers-max,7,24004,1,16384,7434,1672,1633,5645,6559,2180,9362,4500,2522,0.086251,5596711191859025707,210,6,13,4,372,164,243,35,ok +edge-7-water-min,7,24001,0,16384,0,0,16384,0,0,0,15616,768,0,0.018816,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-water-min,7,24001,0,16384,0,0,16384,0,0,0,15616,768,0,0.071860,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-water-min,7,24003,0,16384,0,0,16384,0,0,0,13971,2413,0,0.035242,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-7-water-min,7,24003,0,16384,0,0,16384,0,0,0,13971,2413,0,0.019439,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-7-water-min,7,24004,0,16384,0,0,16384,0,0,0,16133,251,0,0.033833,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-7-water-min,7,24004,0,16384,0,0,16384,0,0,0,16133,251,0,0.021039,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-7-water-max,7,24001,1,16384,1705,1166,8863,4650,1280,125,2512,3494,10378,0.076079,9060107067306753658,9,100000,45,0,121,162,62,179,ok +edge-7-water-max,7,24001,1,16384,1705,1166,8863,4650,1280,125,2512,3494,10378,0.050275,9060107067306753658,9,100000,45,0,121,162,62,179,ok +edge-7-water-max,7,24003,1,16384,3110,1863,5477,5934,2703,480,4520,4873,6991,0.125294,14641659834926888497,40,56,113,1,160,100,67,139,ok +edge-7-water-max,7,24003,1,16384,3110,1863,5477,5934,2703,480,4520,4873,6991,0.054056,14641659834926888497,40,56,113,1,160,100,67,139,ok +edge-7-water-max,7,24004,1,16384,2783,1286,7166,5149,2217,327,3893,3883,8608,0.116582,8546910261617826056,40,100000,52,2,216,156,114,169,ok +edge-7-water-max,7,24004,1,16384,2783,1286,7166,5149,2217,327,3893,3883,8608,0.128315,8546910261617826056,40,100000,52,2,216,156,114,169,ok +edge-7-sand-min,7,24001,1,16384,6422,1350,3486,5126,5590,2220,7742,3944,4698,0.092266,18420736929713685340,170,15,52,2,366,173,213,117,ok +edge-7-sand-min,7,24001,1,16384,6422,1350,3486,5126,5590,2220,7742,3944,4698,0.074208,18420736929713685340,170,15,52,2,366,173,213,117,ok +edge-7-sand-min,7,24003,1,16384,4962,1320,5353,4749,4222,1531,6062,3691,6631,0.086618,12794643825776072709,85,15,59,3,377,160,123,177,ok +edge-7-sand-min,7,24003,1,16384,4962,1320,5353,4749,4222,1531,6062,3691,6631,0.135348,12794643825776072709,85,15,59,3,377,160,123,177,ok +edge-7-sand-min,7,24004,1,16384,7898,1043,3285,4158,6941,3288,9047,3130,4207,0.033000,12672208926715367398,365,9,17,4,437,221,219,134,ok +edge-7-sand-min,7,24004,1,16384,7898,1043,3285,4158,6941,3288,9047,3130,4207,0.132046,12672208926715367398,365,9,17,4,437,221,219,134,ok +edge-7-sand-max,7,24001,1,16384,3344,6561,1353,5126,2836,538,4784,9291,2309,0.135224,14567139433359851325,83,13,31,4,294,55,79,101,ok +edge-7-sand-max,7,24001,1,16384,3344,6561,1353,5126,2836,538,4784,9291,2309,0.025340,14567139433359851325,83,13,31,4,294,55,79,101,ok +edge-7-sand-max,7,24003,1,16384,2834,8036,925,4589,2349,458,4059,10514,1811,0.086162,14334491603640591819,49,24,32,4,222,145,38,71,ok +edge-7-sand-max,7,24003,1,16384,2834,8036,925,4589,2349,458,4059,10514,1811,0.031681,14334491603640591819,49,24,32,4,222,145,38,71,ok +edge-7-sand-max,7,24004,1,16384,2575,7158,1073,5578,2141,322,4061,10187,2136,0.065618,10342097500297702324,44,34,31,3,141,105,108,57,ok +edge-7-sand-max,7,24004,1,16384,2575,7158,1073,5578,2141,322,4061,10187,2136,0.157132,10342097500297702324,44,34,31,3,141,105,108,57,ok +edge-7-grass-min,7,24001,0,16384,0,0,16384,0,0,0,0,1167,15217,0.062430,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-grass-min,7,24001,0,16384,0,0,16384,0,0,0,0,1167,15217,0.046082,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-grass-min,7,24003,0,16384,0,0,16384,0,0,0,0,523,15861,0.022064,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-7-grass-min,7,24003,0,16384,0,0,16384,0,0,0,0,523,15861,0.152995,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed +edge-7-grass-min,7,24004,0,16384,0,0,16384,0,0,0,0,456,15928,0.081969,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-7-grass-min,7,24004,0,16384,0,0,16384,0,0,0,0,456,15928,0.047870,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +edge-7-grass-max,7,24001,1,16384,11093,783,1312,3196,10095,6485,12063,2385,1936,0.074527,9127560645823346963,453,5,36,3,555,176,187,103,ok +edge-7-grass-max,7,24001,1,16384,11093,783,1312,3196,10095,6485,12063,2385,1936,0.158525,9127560645823346963,453,5,36,3,555,176,187,103,ok +edge-7-grass-max,7,24003,1,16384,9270,1150,1771,4193,8327,4639,10471,3254,2659,0.034744,4133842926619578305,433,14,17,4,417,192,254,101,ok +edge-7-grass-max,7,24003,1,16384,9270,1150,1771,4193,8327,4639,10471,3254,2659,0.233804,4133842926619578305,433,14,17,4,417,192,254,101,ok +edge-7-grass-max,7,24004,1,16384,9250,1166,1820,4148,8334,4209,10515,3253,2616,0.022217,14889756881492068813,457,7,18,4,387,186,263,140,ok +edge-7-grass-max,7,24004,1,16384,9250,1166,1820,4148,8334,4209,10515,3253,2616,0.114765,14889756881492068813,457,7,18,4,387,186,263,140,ok +edge-7-smoothing-min,7,24001,1,16384,4637,3391,855,7501,3864,537,6907,7336,2141,0.011141,7854435468174684219,72,6,7,4,312,163,218,50,ok +edge-7-smoothing-min,7,24001,1,16384,4637,3391,855,7501,3864,537,6907,7336,2141,0.006329,7854435468174684219,72,6,7,4,312,163,218,50,ok +edge-7-smoothing-min,7,24003,1,16384,5166,3168,982,7068,4426,830,7368,6894,2122,0.010020,14522476709359291924,85,15,16,4,291,163,206,56,ok +edge-7-smoothing-min,7,24003,1,16384,5166,3168,982,7068,4426,830,7368,6894,2122,0.007431,14522476709359291924,85,15,16,4,291,163,206,56,ok +edge-7-smoothing-min,7,24004,1,16384,5275,2855,1246,7008,4542,941,7435,6489,2460,0.006783,4167283095269788562,131,8,16,4,299,203,151,66,ok +edge-7-smoothing-min,7,24004,1,16384,5275,2855,1246,7008,4542,941,7435,6489,2460,0.006278,4167283095269788562,131,8,16,4,299,203,151,66,ok +edge-7-smoothing-max,7,24001,0,16384,0,0,16384,0,0,0,8780,2386,5218,3.301861,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-smoothing-max,7,24001,0,16384,0,0,16384,0,0,0,8780,2386,5218,3.377591,253967105468988286,16384,100000,100000,0,0,0,0,0,failed +edge-7-smoothing-max,7,24003,1,16384,7956,814,4334,3280,7025,4090,8815,2463,5106,3.445955,2945018518815158476,232,8,17,4,496,206,149,158,ok +edge-7-smoothing-max,7,24003,1,16384,7956,814,4334,3280,7025,4090,8815,2463,5106,3.478460,2945018518815158476,232,8,17,4,496,206,149,158,ok +edge-7-smoothing-max,7,24004,1,16384,8319,709,4140,3216,7333,4189,9162,2322,4900,3.135704,926688446117414204,268,10,26,4,493,184,229,136,ok +edge-7-smoothing-max,7,24004,1,16384,8319,709,4140,3216,7333,4189,9162,2322,4900,3.287979,926688446117414204,268,10,26,4,493,184,229,136,ok +size-7-6-6-4,7,24001,1,4096,1581,375,621,1519,985,139,1962,1138,996,0.019082,4942677217394702267,41,9,13,4,294,103,119,76,ok +size-7-6-6-4,7,24001,1,4096,1581,375,621,1519,985,139,1962,1138,996,0.114679,4942677217394702267,41,9,13,4,294,103,119,76,ok +size-7-6-6-4,7,24003,1,4096,1926,295,692,1183,1224,247,2244,892,960,0.148391,9993748889653469060,92,10,12,4,307,166,149,70,ok +size-7-6-6-4,7,24003,1,4096,1926,295,692,1183,1224,247,2244,892,960,0.055659,9993748889653469060,92,10,12,4,307,166,149,70,ok +size-7-6-6-4,7,24004,0,4096,0,0,4096,0,0,0,2460,1206,430,0.033299,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-7-6-6-4,7,24004,0,4096,0,0,4096,0,0,0,2460,1206,430,0.113059,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-7-6-6-12,7,24001,0,4096,0,0,4096,0,0,0,1868,1165,1063,0.058378,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-7-6-6-12,7,24001,0,4096,0,0,4096,0,0,0,1868,1165,1063,0.025972,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed +size-7-6-6-12,7,24003,0,4096,0,0,4096,0,0,0,2200,899,997,0.094144,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-7-6-6-12,7,24003,0,4096,0,0,4096,0,0,0,2200,899,997,0.017872,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed +size-7-6-6-12,7,24004,0,4096,0,0,4096,0,0,0,2487,1180,429,0.021959,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-7-6-6-12,7,24004,0,4096,0,0,4096,0,0,0,2487,1180,429,0.022632,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed +size-7-7-7-8,7,24001,1,16384,6567,1517,3009,5291,5025,1371,8049,4203,4132,0.129933,1760404624663908432,132,10,12,8,746,343,293,161,ok +size-7-7-7-8,7,24001,1,16384,6567,1517,3009,5291,5025,1371,8049,4203,4132,0.046047,1760404624663908432,132,10,12,8,746,343,293,161,ok +size-7-7-7-8,7,24003,1,16384,7095,1390,2948,4951,5554,1797,8500,3890,3994,0.093135,3541327904259981104,71,14,13,8,681,330,370,199,ok +size-7-7-7-8,7,24003,1,16384,7095,1390,2948,4951,5554,1797,8500,3890,3994,0.072566,3541327904259981104,71,14,13,8,681,330,370,199,ok +size-7-7-7-8,7,24004,0,16384,0,0,16384,0,0,0,9150,4668,2566,0.240752,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +size-7-7-7-8,7,24004,0,16384,0,0,16384,0,0,0,9150,4668,2566,0.119946,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed +size-7-6-9-12,7,24001,0,32768,0,0,32768,0,0,0,16046,7940,8782,0.267628,17663834589658576963,32768,100000,100000,0,0,0,0,0,failed +size-7-6-9-12,7,24001,0,32768,0,0,32768,0,0,0,16046,7940,8782,0.119421,17663834589658576963,32768,100000,100000,0,0,0,0,0,failed +size-7-6-9-12,7,24003,0,32768,0,0,32768,0,0,0,16123,8205,8440,0.121072,2126817964852287323,32768,100000,100000,0,0,0,0,0,failed +size-7-6-9-12,7,24003,0,32768,0,0,32768,0,0,0,16123,8205,8440,0.143997,2126817964852287323,32768,100000,100000,0,0,0,0,0,failed +size-7-6-9-12,7,24004,0,32768,0,0,32768,0,0,0,17991,9354,5423,0.113465,14399021283580285130,32768,100000,100000,0,0,0,0,0,failed +size-7-6-9-12,7,24004,0,32768,0,0,32768,0,0,0,17991,9354,5423,0.037693,14399021283580285130,32768,100000,100000,0,0,0,0,0,failed +size-7-9-6-12,7,24001,1,32768,13001,2473,7384,9910,10314,3257,15703,7469,9596,0.123001,697173060607085614,115,12,38,11,1346,573,528,378,ok +size-7-9-6-12,7,24001,1,32768,13001,2473,7384,9910,10314,3257,15703,7469,9596,0.052905,697173060607085614,115,12,38,11,1346,573,528,378,ok +size-7-9-6-12,7,24003,0,32768,0,0,32768,0,0,0,17583,7586,7599,0.109105,4324331741806674723,32768,100000,100000,0,0,0,0,0,failed +size-7-9-6-12,7,24003,0,32768,0,0,32768,0,0,0,17583,7586,7599,0.116770,4324331741806674723,32768,100000,100000,0,0,0,0,0,failed +size-7-9-6-12,7,24004,0,32768,0,0,32768,0,0,0,18658,9739,4371,0.271079,1060199958884461770,32768,100000,100000,0,0,0,0,0,failed +size-7-9-6-12,7,24004,0,32768,0,0,32768,0,0,0,18658,9739,4371,0.115559,1060199958884461770,32768,100000,100000,0,0,0,0,0,failed +size-7-9-9-12,7,24001,1,262144,115973,20461,48225,77485,112961,59742,136825,59539,65780,0.894000,16861156850021574435,549,11,22,12,1421,690,661,363,ok +size-7-9-9-12,7,24001,1,262144,115973,20461,48225,77485,112961,59742,136825,59539,65780,0.865847,16861156850021574435,549,11,22,12,1421,690,661,363,ok +size-7-9-9-12,7,24003,1,262144,123593,20709,43844,73998,120485,63773,144618,57984,59542,0.975009,17276785293891729727,666,10,22,12,1580,609,679,387,ok +size-7-9-9-12,7,24003,1,262144,123593,20709,43844,73998,120485,63773,144618,57984,59542,0.991754,17276785293891729727,666,10,22,12,1580,609,679,387,ok +size-7-9-9-12,7,24004,1,262144,118599,28700,23874,90971,115508,44908,149999,74099,38046,1.230714,10847581075193611586,467,11,14,12,1679,549,623,311,ok +size-7-9-9-12,7,24004,1,262144,118599,28700,23874,90971,115508,44908,149999,74099,38046,1.097365,10847581075193611586,467,11,14,12,1679,549,623,311,ok +corner-7-min,7,24001,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-7-min,7,24001,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-7-min,7,24003,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-7-min,7,24003,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-7-min,7,24004,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-7-min,7,24004,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed +corner-7-max,7,24001,1,262144,61736,80069,58973,61366,58624,23551,76619,111541,73984,5.101618,3928696166872537512,199,7,13,12,1663,553,608,395,ok +corner-7-max,7,24001,1,262144,61736,80069,58973,61366,58624,23551,76619,111541,73984,5.273087,3928696166872537512,199,7,13,12,1663,553,608,395,ok +corner-7-max,7,24003,1,262144,62664,78735,60194,60551,59721,24893,77566,109731,74847,5.953248,15068249694491358920,230,11,22,12,1501,473,681,502,ok +corner-7-max,7,24003,1,262144,62664,78735,60194,60551,59721,24893,77566,109731,74847,5.364685,15068249694491358920,230,11,22,12,1501,473,681,502,ok +corner-7-max,7,24004,1,262144,64821,77421,60608,59294,61712,26156,79518,107701,74925,6.030400,16857853472906737771,258,11,16,12,1485,616,720,391,ok +corner-7-max,7,24004,1,262144,64821,77421,60608,59294,61712,26156,79518,107701,74925,3.885962,16857853472906737771,258,11,16,12,1485,616,720,391,ok +edge-8-width-min,8,24001,1,8192,4167,475,2012,1538,3068,1679,4511,1256,2425,0.023390,11168460705556684115,478,3,4,4,487,528,4,27,ok +edge-8-width-min,8,24001,1,8192,4167,475,2012,1538,3068,1679,4511,1256,2425,0.006128,11168460705556684115,478,3,4,4,487,528,4,27,ok +edge-8-width-min,8,24003,1,8192,3715,486,2424,1567,2725,1432,4027,1281,2884,0.006104,17785469579485744303,374,3,4,4,403,503,4,15,ok +edge-8-width-min,8,24003,1,8192,3715,486,2424,1567,2725,1432,4027,1281,2884,0.007637,17785469579485744303,374,3,4,4,403,503,4,15,ok +edge-8-width-min,8,24004,1,8192,4036,590,1864,1702,2875,1576,4400,1447,2345,0.007919,13688052440775195898,361,3,3,4,523,554,4,31,ok +edge-8-width-min,8,24004,1,8192,4036,590,1864,1702,2875,1576,4400,1447,2345,0.005369,13688052440775195898,361,3,3,4,523,554,4,31,ok +edge-8-width-max,8,24001,1,65536,47703,1431,11807,4595,33063,28267,48732,3756,13048,0.221746,16732010231130978669,1331,13,14,4,5516,9040,4,239,ok +edge-8-width-max,8,24001,1,65536,47703,1431,11807,4595,33063,28267,48732,3756,13048,0.343654,16732010231130978669,1331,13,14,4,5516,9040,4,239,ok +edge-8-width-max,8,24003,1,65536,45385,1406,14360,4385,30988,26301,46356,3628,15552,0.213345,13337121346526928145,1369,13,14,4,5896,8417,4,277,ok +edge-8-width-max,8,24003,1,65536,45385,1406,14360,4385,30988,26301,46356,3628,15552,0.362500,13337121346526928145,1369,13,14,4,5896,8417,4,277,ok +edge-8-width-max,8,24004,1,65536,45740,1633,12992,5171,30749,25697,46855,4240,14441,0.371658,1230238689060249801,1369,13,14,4,5328,9579,4,543,ok +edge-8-width-max,8,24004,1,65536,45740,1633,12992,5171,30749,25697,46855,4240,14441,0.254556,1230238689060249801,1369,13,14,4,5328,9579,4,543,ok +edge-8-height-min,8,24001,1,8192,3811,488,2340,1553,2816,1535,4157,1273,2762,0.023963,8092515821314941492,398,2,4,4,507,404,4,16,ok +edge-8-height-min,8,24001,1,8192,3811,488,2340,1553,2816,1535,4157,1273,2762,0.004608,8092515821314941492,398,2,4,4,507,404,4,16,ok +edge-8-height-min,8,24003,1,8192,3415,446,3016,1315,2377,1261,3684,1106,3402,0.011287,4553775760903998981,295,3,4,4,496,458,4,28,ok +edge-8-height-min,8,24003,1,8192,3415,446,3016,1315,2377,1261,3684,1106,3402,0.005565,4553775760903998981,295,3,4,4,496,458,4,28,ok +edge-8-height-min,8,24004,1,8192,3069,454,3261,1408,1950,869,3353,1165,3674,0.026750,10086129522298792280,182,3,4,4,489,546,4,21,ok +edge-8-height-min,8,24004,1,8192,3069,454,3261,1408,1950,869,3353,1165,3674,0.004667,10086129522298792280,182,3,4,4,489,546,4,21,ok +edge-8-height-max,8,24001,1,65536,47458,1401,12177,4500,32066,27256,48448,3669,13419,0.417064,16737113895491891884,1340,13,14,4,7213,8095,4,0,ok +edge-8-height-max,8,24001,1,65536,47458,1401,12177,4500,32066,27256,48448,3669,13419,0.284789,16737113895491891884,1340,13,14,4,7213,8095,4,0,ok +edge-8-height-max,8,24003,1,65536,46049,1751,12205,5531,30842,25849,47241,4537,13758,0.282010,12613659818067807716,1369,13,14,4,6451,8672,4,0,ok +edge-8-height-max,8,24003,1,65536,46049,1751,12205,5531,30842,25849,47241,4537,13758,0.249848,12613659818067807716,1369,13,14,4,6451,8672,4,0,ok +edge-8-height-max,8,24004,1,65536,42637,1729,15649,5521,29066,24337,43811,4528,17197,0.352061,6552194030669331597,1331,13,14,4,5362,8125,4,26,ok +edge-8-height-max,8,24004,1,65536,42637,1729,15649,5521,29066,24337,43811,4528,17197,0.324213,6552194030669331597,1331,13,14,4,5362,8125,4,26,ok +edge-8-teams-min,8,24001,1,16384,6966,459,7568,1391,4976,3907,7229,1157,7998,0.031341,8731846698091770699,1205,9,10,1,622,1347,1,121,ok +edge-8-teams-min,8,24001,1,16384,6966,459,7568,1391,4976,3907,7229,1157,7998,0.043410,8731846698091770699,1205,9,10,1,622,1347,1,121,ok +edge-8-teams-min,8,24003,1,16384,7483,477,6934,1490,5469,4389,7766,1231,7387,0.064573,8603429038783025515,1247,10,10,1,585,1408,1,117,ok +edge-8-teams-min,8,24003,1,16384,7483,477,6934,1490,5469,4389,7766,1231,7387,0.011875,8603429038783025515,1247,10,10,1,585,1408,1,117,ok +edge-8-teams-min,8,24004,1,16384,7065,414,7575,1330,4576,3586,7331,1076,7977,0.057349,1187664599781887709,1140,10,10,1,949,1519,1,121,ok +edge-8-teams-min,8,24004,1,16384,7065,414,7575,1330,4576,3586,7331,1076,7977,0.013850,1187664599781887709,1140,10,10,1,949,1519,1,121,ok +edge-8-teams-max,8,24001,1,16384,7758,1189,3871,3566,5789,2581,8526,3005,4853,0.008123,16900393535412682397,128,3,4,12,827,890,12,24,ok +edge-8-teams-max,8,24001,1,16384,7758,1189,3871,3566,5789,2581,8526,3005,4853,0.015489,16900393535412682397,128,3,4,12,827,890,12,24,ok +edge-8-teams-max,8,24003,1,16384,7419,1075,4516,3374,5537,2474,8143,2770,5471,0.008760,2282274312235013453,178,3,4,12,848,783,11,55,ok +edge-8-teams-max,8,24003,1,16384,7419,1075,4516,3374,5537,2474,8143,2770,5471,0.007175,2282274312235013453,178,3,4,12,848,783,11,55,ok +edge-8-teams-max,8,24004,1,16384,6957,1345,4019,4063,5295,2190,7772,3397,5215,0.009615,2586129154889094525,141,3,4,12,677,734,11,82,ok +edge-8-teams-max,8,24004,1,16384,6957,1345,4019,4063,5295,2190,7772,3397,5215,0.009106,2586129154889094525,141,3,4,12,677,734,11,82,ok +edge-8-workers-min,8,24001,1,16384,6350,789,6670,2575,4506,2615,6844,2083,7457,0.005736,6515502212627969163,440,5,6,4,765,1007,4,99,ok +edge-8-workers-min,8,24001,1,16384,6350,789,6670,2575,4506,2615,6844,2083,7457,0.004918,6515502212627969163,440,5,6,4,765,1007,4,99,ok +edge-8-workers-min,8,24003,1,16384,6052,820,6891,2621,4350,2440,6573,2131,7680,0.006827,5808590115991954283,460,5,6,4,882,748,4,98,ok +edge-8-workers-min,8,24003,1,16384,6052,820,6891,2621,4350,2440,6573,2131,7680,0.087149,5808590115991954283,460,5,6,4,882,748,4,98,ok +edge-8-workers-min,8,24004,1,16384,6036,808,7012,2528,4257,2396,6508,2077,7799,0.008937,8755176265363253992,513,5,6,4,880,827,4,100,ok +edge-8-workers-min,8,24004,1,16384,6036,808,7012,2528,4257,2396,6508,2077,7799,0.006001,8755176265363253992,513,5,6,4,880,827,4,100,ok +edge-8-workers-max,8,24001,1,16384,6350,789,6670,2575,4478,2575,6844,2083,7457,0.081936,6515502212627969163,430,5,5,4,765,1007,4,99,ok +edge-8-workers-max,8,24001,1,16384,6350,789,6670,2575,4478,2575,6844,2083,7457,0.005097,6515502212627969163,430,5,5,4,765,1007,4,99,ok +edge-8-workers-max,8,24003,1,16384,6052,820,6891,2621,4322,2400,6573,2131,7680,0.006804,5808590115991954283,450,5,5,4,882,748,4,98,ok +edge-8-workers-max,8,24003,1,16384,6052,820,6891,2621,4322,2400,6573,2131,7680,0.005200,5808590115991954283,450,5,5,4,882,748,4,98,ok +edge-8-workers-max,8,24004,1,16384,6036,808,7012,2528,4229,2356,6508,2077,7799,0.005224,8755176265363253992,503,5,5,4,880,827,4,100,ok +edge-8-workers-max,8,24004,1,16384,6036,808,7012,2528,4229,2356,6508,2077,7799,0.115159,8755176265363253992,503,5,5,4,880,827,4,100,ok +edge-8-island-size-min,8,24001,1,16384,4135,553,9707,1989,2991,1552,4505,1545,10334,0.004854,10172128676173860437,187,3,5,4,531,529,4,99,ok +edge-8-island-size-min,8,24001,1,16384,4135,553,9707,1989,2991,1552,4505,1545,10334,0.005815,10172128676173860437,187,3,5,4,531,529,4,99,ok +edge-8-island-size-min,8,24003,1,16384,3812,620,9763,2189,2801,1368,4225,1713,10446,0.080019,11538144604986265008,268,4,4,4,470,457,4,99,ok +edge-8-island-size-min,8,24003,1,16384,3812,620,9763,2189,2801,1368,4225,1713,10446,0.014929,11538144604986265008,268,4,4,4,470,457,4,99,ok +edge-8-island-size-min,8,24004,1,16384,3718,673,9965,2028,2673,1287,4105,1687,10592,0.004762,10959576368931806186,263,4,5,4,530,431,4,100,ok +edge-8-island-size-min,8,24004,1,16384,3718,673,9965,2028,2673,1287,4105,1687,10592,0.007813,10959576368931806186,263,4,5,4,530,431,4,100,ok +edge-8-island-size-max,8,24001,1,16384,7747,832,4999,2806,5396,3247,8301,2242,5841,0.005476,16066042146348431062,517,5,6,4,960,1307,4,91,ok +edge-8-island-size-max,8,24001,1,16384,7747,832,4999,2806,5396,3247,8301,2242,5841,0.025421,16066042146348431062,517,5,6,4,960,1307,4,91,ok +edge-8-island-size-max,8,24003,1,16384,7465,930,5151,2838,5018,2963,8001,2350,6033,0.007803,2882025045471101485,553,5,6,4,1281,1082,4,82,ok +edge-8-island-size-max,8,24003,1,16384,7465,930,5151,2838,5018,2963,8001,2350,6033,0.104742,2882025045471101485,553,5,6,4,1281,1082,4,82,ok +edge-8-island-size-max,8,24004,1,16384,7386,874,5304,2820,5009,2938,7917,2293,6174,0.010600,17272371466343954211,600,5,6,4,975,1318,4,98,ok +edge-8-island-size-max,8,24004,1,16384,7386,874,5304,2820,5009,2938,7917,2293,6174,0.005044,17272371466343954211,600,5,6,4,975,1318,4,98,ok +edge-8-beach-size-min,8,24001,1,16384,6357,475,7288,2264,4501,2605,6857,1610,7917,0.008022,642294031756670187,437,5,6,4,765,1007,4,100,ok +edge-8-beach-size-min,8,24001,1,16384,6357,475,7288,2264,4501,2605,6857,1610,7917,0.004196,642294031756670187,437,5,6,4,765,1007,4,100,ok +edge-8-beach-size-min,8,24003,1,16384,6057,497,7512,2318,4343,2430,6590,1662,8132,0.004405,11476863113224605731,457,5,6,4,882,748,4,100,ok +edge-8-beach-size-min,8,24003,1,16384,6057,497,7512,2318,4343,2430,6590,1662,8132,0.007247,11476863113224605731,457,5,6,4,882,748,4,100,ok +edge-8-beach-size-min,8,24004,1,16384,6040,503,7650,2191,4249,2385,6519,1602,8263,0.004310,18276758955520330632,510,5,6,4,880,827,4,100,ok +edge-8-beach-size-min,8,24004,1,16384,6040,503,7650,2191,4249,2385,6519,1602,8263,0.007436,18276758955520330632,510,5,6,4,880,827,4,100,ok +edge-8-beach-size-max,8,24001,1,16384,6336,2170,4829,3049,4480,2597,6815,3753,5816,0.030607,5803748850750772787,437,5,6,4,765,1007,4,78,ok +edge-8-beach-size-max,8,24001,1,16384,6336,2170,4829,3049,4480,2597,6815,3753,5816,0.017551,5803748850750772787,437,5,6,4,765,1007,4,78,ok +edge-8-beach-size-max,8,24003,1,16384,6038,2257,4995,3094,4324,2425,6537,3859,5988,0.007158,195699143552790923,456,5,6,4,882,748,4,80,ok +edge-8-beach-size-max,8,24003,1,16384,6038,2257,4995,3094,4324,2425,6537,3859,5988,0.065655,195699143552790923,456,5,6,4,882,748,4,80,ok +edge-8-beach-size-max,8,24004,1,16384,6030,2141,5127,3086,4239,2384,6491,3732,6161,0.008905,6657545027504875160,510,5,6,4,880,827,4,100,ok +edge-8-beach-size-max,8,24004,1,16384,6030,2141,5127,3086,4239,2384,6491,3732,6161,0.009795,6657545027504875160,510,5,6,4,880,827,4,100,ok +size-8-6-6-4,8,24001,1,4096,1582,388,942,1184,1087,323,1834,996,1266,0.001110,6499355411517282126,64,2,3,4,240,172,3,22,ok +size-8-6-6-4,8,24001,1,4096,1582,388,942,1184,1087,323,1834,996,1266,0.001630,6499355411517282126,64,2,3,4,240,172,3,22,ok +size-8-6-6-4,8,24003,1,4096,1790,418,668,1220,1299,473,2050,1045,1001,0.001221,6987438381204511612,180,2,3,4,202,205,4,16,ok +size-8-6-6-4,8,24003,1,4096,1790,418,668,1220,1299,473,2050,1045,1001,0.001387,6987438381204511612,180,2,3,4,202,205,4,16,ok +size-8-6-6-4,8,24004,1,4096,1864,409,525,1298,1300,425,2138,1078,880,0.000860,5781171558839341672,143,2,3,4,244,236,4,25,ok +size-8-6-6-4,8,24004,1,4096,1864,409,525,1298,1300,425,2138,1078,880,0.000860,5781171558839341672,143,2,3,4,244,236,4,25,ok +size-8-6-6-12,8,24001,1,4096,3031,247,82,736,2000,711,3266,629,201,0.006894,8654506366848622993,277,2,3,12,470,311,10,0,ok +size-8-6-6-12,8,24001,1,4096,3031,247,82,736,2000,711,3266,629,201,0.005010,8654506366848622993,277,2,3,12,470,311,10,0,ok +size-8-6-6-12,8,24003,1,4096,3093,295,55,653,2051,688,3304,633,159,0.004199,18078120065523699720,258,2,3,12,481,311,10,2,ok +size-8-6-6-12,8,24003,1,4096,3093,295,55,653,2051,688,3304,633,159,0.004389,18078120065523699720,258,2,3,12,481,311,10,2,ok +size-8-6-6-12,8,24004,1,4096,3090,205,128,673,2020,659,3315,545,236,0.006114,17738325349339576213,294,2,2,12,493,327,10,5,ok +size-8-6-6-12,8,24004,1,4096,3090,205,128,673,2020,659,3315,545,236,0.004699,17738325349339576213,294,2,2,12,493,327,10,5,ok +size-8-7-7-8,8,24001,1,16384,5702,964,6678,3040,4074,1777,6299,2490,7595,0.044214,7948674464223908679,93,3,4,8,666,794,8,63,ok +size-8-7-7-8,8,24001,1,16384,5702,964,6678,3040,4074,1777,6299,2490,7595,0.055451,7948674464223908679,93,3,4,8,666,794,8,63,ok +size-8-7-7-8,8,24003,1,16384,6457,895,6096,2936,4665,2157,7057,2371,6956,0.009191,5605007882511156480,198,3,4,8,720,904,8,41,ok +size-8-7-7-8,8,24003,1,16384,6457,895,6096,2936,4665,2157,7057,2371,6956,0.011074,5605007882511156480,198,3,4,8,720,904,8,41,ok +size-8-7-7-8,8,24004,1,16384,6208,976,6116,3084,4443,2087,6811,2533,7040,0.007935,2557215965569984820,233,3,4,8,715,882,8,54,ok +size-8-7-7-8,8,24004,1,16384,6208,976,6116,3084,4443,2087,6811,2533,7040,0.020009,2557215965569984820,233,3,4,8,715,882,8,54,ok +size-8-6-9-12,8,24001,1,32768,29759,414,1357,1238,17594,13078,30100,1040,1628,0.031754,1146600372999166395,725,6,7,12,5196,6718,11,0,ok +size-8-6-9-12,8,24001,1,32768,29759,414,1357,1238,17594,13078,30100,1040,1628,0.134063,1146600372999166395,725,6,7,12,5196,6718,11,0,ok +size-8-6-9-12,8,24003,1,32768,29903,541,832,1492,18105,13671,30308,1293,1167,0.106047,439073163953902621,796,6,7,12,5184,6362,12,0,ok +size-8-6-9-12,8,24003,1,32768,29903,541,832,1492,18105,13671,30308,1293,1167,0.108150,439073163953902621,796,6,7,12,5184,6362,12,0,ok +size-8-6-9-12,8,24004,1,32768,29513,552,980,1723,17263,12406,29983,1433,1352,0.114159,1615816978887300187,732,6,7,12,5073,6925,12,0,ok +size-8-6-9-12,8,24004,1,32768,29513,552,980,1723,17263,12406,29983,1433,1352,0.015979,1615816978887300187,732,6,7,12,5073,6925,12,0,ok +size-8-9-6-12,8,24001,1,32768,30314,572,212,1670,18051,13234,30807,1433,528,0.083175,10246488510024421728,664,6,7,12,4881,7130,12,11,ok +size-8-9-6-12,8,24001,1,32768,30314,572,212,1670,18051,13234,30807,1433,528,0.036305,10246488510024421728,664,6,7,12,4881,7130,12,11,ok +size-8-9-6-12,8,24003,1,32768,28972,708,932,2156,16986,12213,29547,1811,1410,0.103459,10334973683976933424,591,6,7,12,4854,6880,12,2,ok +size-8-9-6-12,8,24003,1,32768,28972,708,932,2156,16986,12213,29547,1811,1410,0.105073,10334973683976933424,591,6,7,12,4854,6880,12,2,ok +size-8-9-6-12,8,24004,1,32768,29356,664,716,2032,17411,12858,29870,1708,1190,0.118923,592925104582868716,586,6,7,12,4350,7343,12,5,ok +size-8-9-6-12,8,24004,1,32768,29356,664,716,2032,17411,12858,29870,1708,1190,0.043719,592925104582868716,586,6,7,12,4350,7343,12,5,ok +size-8-9-9-12,8,24001,1,262144,122523,5699,115974,17948,85439,70549,126210,14732,121202,1.380596,639902297847682829,1216,12,13,12,17030,19802,12,1756,ok +size-8-9-9-12,8,24001,1,262144,122523,5699,115974,17948,85439,70549,126210,14732,121202,1.213039,639902297847682829,1216,12,13,12,17030,19802,12,1756,ok +size-8-9-9-12,8,24003,1,262144,116913,5448,122562,17221,81595,66803,120458,14107,127579,1.400221,13366311633393428937,1245,12,13,12,14847,20219,12,1505,ok +size-8-9-9-12,8,24003,1,262144,116913,5448,122562,17221,81595,66803,120458,14107,127579,1.301910,13366311633393428937,1245,12,13,12,14847,20219,12,1505,ok +size-8-9-9-12,8,24004,1,262144,119369,5846,118969,17960,82617,68027,123048,14883,124213,1.291885,876562435972935345,1245,12,13,12,14249,22251,12,2026,ok +size-8-9-9-12,8,24004,1,262144,119369,5846,118969,17960,82617,68027,123048,14883,124213,1.182049,876562435972935345,1245,12,13,12,14249,22251,12,2026,ok +corner-8-min,8,24001,1,4096,754,88,2858,396,509,230,836,287,2973,0.001091,2094329235498495192,230,3,4,1,156,71,1,25,ok +corner-8-min,8,24001,1,4096,754,88,2858,396,509,230,836,287,2973,0.001493,2094329235498495192,230,3,4,1,156,71,1,25,ok +corner-8-min,8,24003,1,4096,738,89,2841,428,522,226,833,305,2958,0.000964,11439071887512958536,226,3,5,1,114,84,1,25,ok +corner-8-min,8,24003,1,4096,738,89,2841,428,522,226,833,305,2958,0.000970,11439071887512958536,226,3,5,1,114,84,1,25,ok +corner-8-min,8,24004,1,4096,823,81,2754,438,677,347,920,301,2875,0.000989,17713212695282631944,341,3,4,1,49,79,1,25,ok +corner-8-min,8,24004,1,4096,823,81,2754,438,677,347,920,301,2875,0.000961,17713212695282631944,341,3,4,1,49,79,1,25,ok +corner-8-max,8,24001,1,262144,137991,15548,88089,20516,93443,77856,141558,26250,94336,1.640130,6975546691930020269,1236,12,12,12,18311,25937,12,1370,ok +corner-8-max,8,24001,1,262144,137991,15548,88089,20516,93443,77856,141558,26250,94336,1.640624,6975546691930020269,1236,12,12,12,18311,25937,12,1370,ok +corner-8-max,8,24003,1,262144,131834,14505,96383,19422,88851,73699,135281,24557,102306,1.549813,13500037929696646449,1236,12,12,12,16689,25994,12,1224,ok +corner-8-max,8,24003,1,262144,131834,14505,96383,19422,88851,73699,135281,24557,102306,1.594883,13500037929696646449,1236,12,12,12,16689,25994,12,1224,ok +corner-8-max,8,24004,1,262144,134906,15168,91778,20292,90927,75562,138482,25724,97938,1.523318,8163465322631622208,1228,12,12,12,17357,26322,12,1936,ok +corner-8-max,8,24004,1,262144,134906,15168,91778,20292,90927,75562,138482,25724,97938,1.487342,8163465322631622208,1228,12,12,12,17357,26322,12,1936,ok diff --git a/docs/map-generators/refactor/edges.csv.gz b/docs/map-generators/refactor/edges.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..ea3756b0dea8ae89e082025dd887105266ee0e72 GIT binary patch literal 40284 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

M)i=&Ll{*C+!ealWGjmv2|| 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 gxwlE0Z3e}ml;oA< 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
5qBt1U)?w}|lQhsq;) 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 literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-10.png b/docs/map-generators/refactor/generator-10.png new file mode 100644 index 0000000000000000000000000000000000000000..e359bbac791bcca5430252c4d72f68ff74431b7b GIT binary patch literal 42471 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 literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-12.png b/docs/map-generators/refactor/generator-12.png new file mode 100644 index 0000000000000000000000000000000000000000..66d5bba328e0c3268f3cec9f58d5c2ba55c4041f GIT binary patch literal 35401 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-2.png b/docs/map-generators/refactor/generator-2.png new file mode 100644 index 0000000000000000000000000000000000000000..af6835fde57948a8674c0079c329c7276ef1dd9b GIT binary patch literal 38733 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 literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-3.png b/docs/map-generators/refactor/generator-3.png new file mode 100644 index 0000000000000000000000000000000000000000..d2ccaf29661a1a8197fde48a551c23414fcf5f34 GIT binary patch literal 44637 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 literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-4.png b/docs/map-generators/refactor/generator-4.png new file mode 100644 index 0000000000000000000000000000000000000000..aa4e6f131c8e23be6afbf95c76bea1f6509994a3 GIT binary patch literal 42806 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

P-WH(z9Ib5)S`3}>5rNJGiCJBi>Y zYuKX|?6{=|u=>uYfyc!^RaOYaz5mpsA_{I|!Jhl{qf4SrnukY7^~94dfM}FumMjdA zxuOTIT#k7B z{cqU~>=c0Yy~9R_oWJ#;6ueg6vc_3jDeTV|`*!Y=m=OqoahD6VJB(k?Fa%?uypNwW zeIZRmgSSc(`ZNO5eTA&dxvV+T{6~6u zFGV`NZe1m@zifGU|F4|j`omj;scsha^&1l~!(TmMbBVY7P#TPa>0zDE6mjY&F~B~1hz&E#?gb|fj71vlRR1}f=0XG1 zvt?HPR~=`U>iJ*rMyHPPCbC!%1P`$^x^-N`P~3{pC?;TfqIKmrS2YuDcsc{DHt~-= zgdT@nIHWAKlsKQ6$T_)tQ6PINjaOXN8X(afu;rvRq?65+%$x*SXjHlv1Z{sBEuTtxE@4?bISF74OsPjUD= zV(cUW_3H-?1Xl-p#qXd3Pw))K_DRSl+lT?=(-ekdkHSg_3X`uM8t>kEvnT^9ok~-F z@P>>?@;fEDT>b9fifuHEJkQYt9`W&{E%{kcU6AgYfR6I9Z~k(M);XNtOeFK1IWyHy z*lx=+bA6(!x&M#;76mA_%Zh(6$H;yvT%hSP#%i|Q4x~6R4*axtuP%}-MIm)v=~1Zx zSb7?{cLI0Cy?kM@Zm)2QfaovoT{WEjJl(*!ekeRBE>K~iO*=UMr8Wwm5%sck#XKAL z{p8Rk@IpAbjg_kw5<;2M$#JJQ)N?#~%tu|nA|DK`M3T3^8L5riO5R5Ea`~qNLKFW~ z!}8X8^jf;SyH@NB``#UmXv%RyR54cl7b$yE=hVZ!qZ?QnQ#5f0<*iKvc=F`;)Wra4 z7><_WRuzr4p-%#4>=5It{~O73!|)uNY>Gqc?o{5jLtm|lJBfeL!A>eq@%XeD`w?1# zPr#LYTS0%ALpx4B5fv2IJh$o(qL1Q2CQ|3;dAtKyDvcn(PfgSAW} zV~+ERo-azdPFwe`9#?XkBY}eHn(S)Vv=nXx0wD78;xo%;3`r9IA`G$jGZv;PBR_p8 zpWvERHW9u~3Q~6uO?K(@ionErmG1FW$)4B!ts>cKR(bt4Z`-uTRUYb#&o=lQ2 zBG4(8IQ;ceO3)(o&soAlSd$0>8i!a{!RV#)s%b5vwj>UlR~cNqXoE`i$sdJ~O=bM; z$O9$aCM6r3LcZ`8BfoAuVI{}!akCD%JH>raugpn6*H7u6pr@R!@^_%X7Qbbnc*Y^{cs@aF;@U*Js4Q#q#*W_5;mj>Q8`$WtL?VlWZ#9Sj7+&RfP zy)e^$Ctc*%SMbuT%HA0~Amh^Fke=%j``w53{1g@UmCWz<3k4oMt6`RoT3SoZjZziS zE;in^n6I$~s+VHHx{TAtT}!PmPi?x~-Y=qWkBp}JSiFlJ(2_^cZiS`7s4>W=vT!L| z&;AP-y(r5~S6%O@ameARiYz!dsftSV$nUQELP899LXdfN+)NVc^OeBCMgK-<0TgHc zv@e%>L1C0+R*Xu~@<0tnJ}^zae^=3jk-bEY$n3d0mgtsm-ujEpSUhd~q55)j#Q$M_ zu-0*UrQW;(xs8kF#y=(v7C``gkUL$|Z!z5ZGxzT(7$V?7_+bMTgNwsClYhCsjMdAU zcvfPyqG~3GQ!vR!S?e*S&9W(c47NQp8n0dLOflbvDk3cwO^lO;XF=f+i=%ODO{g_cc z4d)D#(*CjRXt0=7Kfl+8K!36HzYwpmr&!gSYkKIFpFDyD6)UAP9Ou2+ ze(dI`5!%Iuo^MN59H!-O1q+~H>YUkYaoL8N;T|s4@0ji{UB_mi3!&OUvef32nKD%V zJnbw;$$0dzh|6YsrBG+uiFL%IO;2I!`I1Oy(*>>*ByPsI1^K1Yi#)?hWAe%+e_Xx!7}&j=zBQ07Noh{wZSq<4C<1om;@dwNhAo6)x5Jh0v)y8FK4-`$LlVEIvI-qtbqXYXl0JD ziPb|Xnc@P*!tI(>pky0nd?1&T8yZ@xcB410}KJwSU&Y5e(XwavdBDTg~ znWrraN`x1!_L+@jk0y+*81INI`Z-3XH|luTuqNiSg`=yj6~pRH@@qsS>GZctHM)?S z!`RC^ok&U+Ys|cfNp-Dfn9Q+T%oA<^T;w7O3plC*l{{3F&f$$^!1i#D5)R3y*my$v zsV%TnB!}mYLQQ;k zrDU}VgzaWxGjj-KhH6AdDw9FNU^2~lEl9+wud)>FMMC&g>1nAkX-}bk-w^6~5vp9P zys0eeIA{~VjaOTOzN;e))G+|U%D@;wYwRv0T}pr#95$Q=vw<@GzBpr;l^Zy`eY<(W zAOjZYm^?7b?ym>v4qf&PK&Ahs9rZE3LLYBf|69V4G>&6(kG++y24#U!&$|-LK#9T5 zHLGrSK@aP3^+sq%#oA>v+Mj(pHe945U8yX-En>*iRhJ*3%7nXWdapeR&G&X;qAfM| zE2wVCAuHk9aJ)Po?SOYNlFIz}(tmY3_esC())Uc}Vy~|Mm%vBn^1TAMI-*Ntm6}V) z)k*W$BX5)RJ(Z_yrY^7AUUZfO#j0yE)~vrD3Q$gux}N!bCm5Agy+`03y*^GrV#HkG%A z*~?3BrmU=!IiKYaOK^O6TDb#cT;Mv&y(TUnrB$PXWa^~1{7$_!p(yX(G`#{^YEF%i(GwrDlZ%^gP?04XF#i*i=Xb znY*8|v}!u3R(2xeuw5I2i{qmW!Tb6)Ew?a&rkO@UYWnu)<%~-0cMUU5>O4$4-H`@> z$0a>n4uDnoKwpV$a-ef4-d9_$Swy`S=(JAFcEG=C#BaZjgOa; ziIl+9tJ6U>TK)$e06esriWM5yb=WC*S%X{E1SY$;Abo>ML^OzZiq=m?)G@Y|1M+kwm1lW*L(F^t>r?r9-ldM0@^+ zHj-?&&e0CQ+`%@TJT`_a70TYy89cLRP2^arsr2aCFIjJAhbbYbc8+uQJ0kfXzb}*O zHH`E!O$&V%>#Bk**S_Kn`o?CnO_PGu_;T)IcSPZXAwxAl+mrW>dJJYt>k&9{ z&t`dWYnU7v%hMf~{uDaAcoyO{J&sBQbU(J5z!^h9O8~Hb9O9jqo@QdSy`$8Jyp1cX zo@xA1VRpxQWFDZ29xCz(aInp%l*inKE-!t~Ah(v_4cbG`rBv>!1Y*@?RgxAym@YNt zk`AVvY;p!$7lKprGoBst%83m78FpQpWj3K`HjFCw)3AWj_WY2a< z^u3v)t8xVsa>O-b%{+r}pzL&LrAOut>;Kkj6lxqjmiS1(FdsG*!pUu!MusIkhtw)I zW-msx^f5GM>B9~sbrABFt}T)J5bIOb+Fpjfhu8?Wq=r`-Ao!OTSSKF4g9Lk>6M#+R zJIx5RI{)m^tW;*BJ}s%NlxoBU_Gw#Pnv`m|#}*(#x5S?q&AE%UHM`{pJC=A%q5I33 zPhi5QUS`Xc*WH2OOq1}(bGh0z6Tk5otNuMK{_XI~@aDmihN!p8=T~tC)}XDjSN%sq z&`ulv6mU%YKl<>+RSg2>9SX+p?$A(=6rKlT*ugZiI%iRUV^;eb7z?NVzgrmVFWMB@ zYrz^9`}T}7pmKR&VjuA>+euz@!YZK>tgjWH==X%=&D3%%M{44Hyta(cyHv7z1TXA) z(a547px1$(7XWUNNWi9zb~h@r{7*Fbj}LGfwt+a4{*T`k4#Q{Vtqm?y5yT(Enk3bZ zKyKWQ7GP3<6#I4njMo75Q+nR2PkhBHtdoH((eGSm=XbJZNKUFMR;;eBzp_PA@&sW{ z+kkb2=z*>~v`%5Zmbr}|4uYC|9!5)DclMONEtA|}-hXh)t*wJFbQL*;&YQTJ zZeC8^e5R0GX+e|hD=Hd(o3s@Ls;UFcPW_#D#cpNJ^M8~CZrNvkZUT|kxu)SNmHDb> z36#`lSA){wp!Q!@S9IMAyx1%Yy=6&NTn=uz-#gYoS41TrPjnCtbJx;(gFX+#DR-!^ z{9UpqJumRmiFoDk*Q!=rFV%Y%Cft{oR~sl3Ez}@sRyI&G(z$LUbhU9kK(MeGOd}KzsEF5 zu2uK#o00(Mys~aD{UTESx@(-aIzsP(>DS4+Des=o*EQr)h*bcsX_ZbpHxz^6@h3+PTg`A*RTrfFJLhVH<{#%!7$jn45N}Nw-$6GmJGn>Y}sTzFcsGH6Hk?z0BC?HpnW{(n@$~-STk4t z2q}L^y&#HeSddt4&8*-abO=AF2%tfGn@cE1D@=cML|D$2zc-yVqQ^M)~_U9SHJ^-%7ZYk=^=KY9_JUvcXYs&_R-|3DCOF4o`3ovHvB}qkPTpj`OT0 zL6(UxPH7d+s>^}Ga284GQrYVv|V$Dw!|{5OQ;6Xar0da)KhL;58YYpkSI{1s#5`Uhw6j9aC_N%<*|=u(xF z)?*)zCed7A@1La)#DW(~64zJxJs`-yQ6u~&~txjS~RvdS?m zP3&QVtG_w;(&tUSCi{f043vkR3q(f^6B{KvqJap^|EOsEU)!9P5^!;ac1!b+%8XO$ z=ZsJeXyga=u>OB5P2hjT^Nr=`63sZSfJagN7}jMIeZGLq7xL=|o9XIa+yk@5b4k|# zpN&yLX`FMSlWcX9Bgu3VYFWcOzfCCLtljum(K=NTo&^jt1C68+t;QeOP8}R&3%NBc zR=c>tAfg@}9{m5v6&}S7QR*Y@$;M|&htfqdPXa!`7EktO+AC$2%6G;u!Q*b zaaoTFXQjf=Ic<=PdyILu&8dp4c zSCas9G>|K-M>Fh_9Uh7HM7g{NAffjZnQ-JTOeA6uEcfjPuf2(Jade)0ILK)kitZ;K zM!Bm7i>*iwPeBaOVGw2C+yPwbOr{N}b;Ast99^6oLF|{mlG2+M!Fmk*;RCz*U(Nd+~<@*TMwxudiP`r@s_8%e7Ch29^oLuHK6Q8=Pu z@k#4!JuPe(`ru%u_v|+*+F`^rHQ6U^voJ9P)mPA5RZb91VtnHmu90`OTgTK~#xtT7)3dx%k`7pP7R+?w{;%xEzicJ>uG&vQo20zvrR+M?DUt1 zWRM23(M9!&mF6m&TibnnK%pk$&2)ie3-ZSb(ti6u)J{Zgw^@~hTm+l0um)>(fjDT= z_im4;TVP+e&l93yftuk2KiXV@%mJY9Ht6)%UG?u#NzuCI$9=vkmpjhkt3%D&?bB(; zIcVRTsuZ|>Gnt_LdEuQhona)>4w!~GA?059kmY|lnFLeGx3nauIGFKw5uniXbPoLL zkKphX{@4RMH(u$!E*rn1Qapam0Yq`tK>Fj-J!3V_Vg4PQG85F5#zzi7QUg(+nymTa;YcPkEWCMyL->EYk$=#Ert zZaw~--7)?h5QZ7p;_A8r3Lm?J#vjT$M#$(+wYBXJV<7*d2YL;&21i(7Xl19EDZu-0?Yravwdc(m+iyL*MP2i321N z1Ep~h6{M3ki=Q%<IHGA<~*ySWZvMG;te7>dm z4$95G*l^G}rywNfaQT^No=BS7;fsstBjBP}fK@vAUfor^zS~KD5LfJxE8rNc{}8tI zJicV~^x6~Eoy7&jcGj50qSe-e@2*m=j`?rSLOY6Ic~#bxKf)P2QB}bcs%xjDEU9iD zGg3N!m~(Il74R_;AoXepNfe+)=PE!y^3+`6)3lIe{#xtUYCXAer9X7hAu@ldh7np- zHiyEDE))BFxCc*ky*#vR5q>^HtWpK%5ZMyx#Chu7m|T`DZZ=nB^>I$YLCeUAhu|-S zM*9VO>1RIqPU1R=k{CAI5}$oSKaRo%o6m&^Zln_t^EPKbYGy@cPlO0Hxw5I4$UCMp zC=^NDy5H5-;Rg^h5v=ztE6-|}fbN)lXUVPTrs_~~di9DG7tM)Y0qZ;-PaP~>Zn(xl zYu@RWg?|7^>Wa1E@1v@RXKXHKz~^sS#}}*P+Tw^ks&s4JJR|9!`YOWdp9c7-?_0Hx z=1SuvoUG2*dF69S*%p}%$M|mdRrv&%TGJYDj&}d6rlgAxc*X%xT|?*j;p5N>*m8Rj zBoFNK0UBvti0rSB6fo{yjCo&O*Q??mC6WEeq*%~+pOjc(F@qi{U5zBI0#d`5 z)S%pP#!2qM97%XE0v=7SbdY}8c=phJIG$dIk@8|C-!c}LYpv;nbeMT#zgYmQui`&y ztx+SX@J;Xb6xoDqY9)y;>0RbsP?#<7>4~)Y@Bq~sXk#k7*T&iplnQ7?CH(2~mZ4ll zv6~74YCyd+_N7yxX<`AdiC`JFuW-oNK4rRuQEh3juB#%mT)hUpE;yF!c2QCij2cpv z#73Um9u~p?v5McuY5!T)!)zHjr64Dl_pj#VE|@x&{P95^%tES|AMp*_^I+y?`7xYQ zz1HC~BF-Fv;AMwTOxNk&d{WeSJkbg0yi7#N?4nYMNG0UNBnqjb%hy2&T9&^ly^mC$_i^GYKwQ`Mxe^2ygN7;>i^or1)o zMkz($9p#sRbu`76&HK9tpo}hF-`gxR)Xdr*TJ(|Ejo`7J@>$qK!_((wAMyP*MJ7H8 z?EX2Y{kii*zTc+gD|BdSd%nRy@R)7SYg74~&%WPLiLs()zNsBx?m^U!I!v}@KEF4P6xM(6hyP>q_g78Il$sCz+``uG)` zRM#}1ua{dcRek-ZdZMgqYBIQbVyIprB3U=L?OBkCm?)|(JSKE&an_@i?JoVI2b=On zS$z{`ZPAfO`}2O#?!lv3<~QINroaS_eYtzUm$5ex$ol#K$7gsr%Xp@Bys<`Ylrd$e zcgZJg{8z2@5SO`BmVfF07%9d84Qxac%X+YN=Ck4yaJbzIc(KQ_vwaUN5LO;vEyf)jEGuRSSEdRU9THKZl{3b0jj?dY&(IXi@J^qlc| zBzoI-95pgk`~H%q|9$7R5T|P09hNsQ%kKrq3r9P!9CHT+rGcFcv$L%*rE}ZQ`IPp* zf35I0H5<~w=g?uY?1q{eUHSU`8C{It+`pq==3D!}Sz*a~s|QRz2Ie>s{mkW2mRr&a z_z|_pfCB~A{-35)uFE6tbiA4)PuoIN<<9i^)NKZ^QqubAe!W+(HM@9iN+Bq!pR27a zJo$4&=%Sk=kA>wk`)G0P@pYEr+0)n{LJxmb9tOy`ssWMQ!0`#D^bYoXs`kZLy^sUf zFIDyL`>44rhftlu<458FE#?T00nDUTqQ~nCGEv|`e%j^&CncuCPe%-AVu0gML#b$% zd*WKt+tXFfO^Q^Ys#{l_s(V=rsIJ!kv}8UzFjCm@FHZ-{N3HH)r}m(a<@@_v?5gPo zOS!j@qh)uq-)AFY&SD8RFH>7$Zk$zTc+Bcr120O(P$y_BZ*Gvfk z*Yg!%xaA%W%vSV(NiWOAG^F#VLpv~TqwKf|SmaNhrf#xyfS2hk+1{z2Vw&kZP>PfM=ckOI1eRfxRbR_ty6%qDegDt z-JPv}5_odyzzes{lI0Bb?5E)~%RWnILH|_W^(WGISmx3fq>;5DgE|^>F~zkhzgMzc z*`tS&-7?l+b^=N0<>gB>uZqa`z~FCA2fX$NJlAr@$Tk?^BU}Bzq!||uTqr;JT%g?h z!SW#5y{W@C34Eg)R)A!17=7+wZ;oa34cs?-PqWqkl#11ZWp3U(b>}Ql>+i@ad?>n{ zM^F5Hbu#AbHSZpQei%9w_=O_u-7(uzV-Gi)FMH0fviwOteAI|bOc(OxR(ZdlBHINC zNXEZr0*CV{pjvF29C4rJTQL<@6Y|W__Qbh;kqh+?zKag*IorfDoL?x*|J2CcJI=;4 zCPF~P0yp{GuxN>+yDT?W>WW!vqpEl3nqH+i>seSTPY9e9M=ch9Z%N8`JWu4{k zy~1(OJviaXT#gM01)9P>2ODAs(B&ZL4F<5k)`*^=B&$5`j*&wxH~|Bgcwi$$$&=hais@*E!bzI%2M# zrn+!=cmKV>a(3S(=KQGz>~S~yipzBq{)hv{)^0%mou2cj9PwLDzz<*vly)G>D;-}G-7BkZk$$T!tki&+Jt)C3F{4H8Rbol8eO zWH(&fJz#K%Ou20-=j?eyqvQI9=tz#{Qu1dA)vNaRwDLa_?`?h88ArktMEAToLh0PA zw;GNS+~%Rqfh~(KmRR1jcj|OxCyEX_fSZcm@;Z2wuP27=3>t!Fly;J?v_1N+*v*ug z#wBFs35l7j5fm^$9{U~3H1TPNCcV+~DaBNoAf~}!qFKQssB)xlbke(6Cv|D`yW6(a zb_}>{$wU?W+oGbMHW~w~!2c+uBXt`%z@0mYScwMa+=w4^`vCWi_DPlmHvO2V5EipJ zd60s6xD334>fPk7k)-$O!1_*Oy$kM~x-jTgRGruX=$J)?>y7S%x3oW(;5wJq|c#GvI97O$^48vl&e6O!XQnb8THkZJEb>?d# zU7J1!Y`rO;=em4ji!4(yE#I#V*sdt?4(e%a5;Rj12Kf$BY1jnO-=1rMr2oN}r9A%A zp)NIAhG#?hA*3Pk^>+wws>)foxE!!meXhkeJ^x&~`>#73brk*YOJkMw#VMElzFeBG z`sr5v>`ixF4+*N&JHIIjvrVp&$voR=4# zudsx%2afmywICPsd@dobn;W~(4?N4eEZ^8F6)H^^T7D)C4Lxy?WZx#9ngLga{@^;s zh5W3~;n)55Rg336M1fd2seyS$k<+>EvyN!C5SLD6HpM{TxAcV?$&nGSNxihsKgQ9+ z5brn!_28c?bx4IOpBWUW6`C+gUcY)-S(Fb3B?umihMsK({zJQ3K5we@2-Q7x>!lMn zeGDRuoJIQE%Po@Dub`j3@U1M|%y{_;RsdBNi*wp=joe6f&u3|Z=S3BidnKE7MY`m3 ziN!giFB)3p7*fSH?n0mQWAxiPAr8rZjOS#$j3r!S*_L<%U!Fp5sZ!4?G_~3&OI_b* zfd>Qyvcbemy8D%eeu6IiU!9KQKaFk=e!A_pT<^YT$m`G`0IlHPXivqgz`vPgdH$_B zz28unx_^nyXl^sB=@aAR&?$r1)3HixLYU^?>-Rg4of{7WAG&iL7|H#ZEBGlXpb6FP{^~LulV+iHkX>lh=)) zL%F$3ZL|wNa}905f8nDc^Yn#l-M^=EDF`rA&Q0$|TFi7t?oS#g^in5N<)sFS1a&*~ zpcP_+0vMB(5i+`f2X%z>nw6^feU7em9_%+NE(squ;96vW8V57}^dU4sm+{LtmK!?J zvnOfg?lI1{$33nH2k89s`}LeKRD-57h~3|fOFOOIg}o?8A~_t$2k>8tS2+He$9{eT zvj4X`XLV&AzPRzo)I8!l28DhXJU+kC2G!?2wswy5aq+MWXIrxW#cl)Ky^((sj=qAF z?*qfjfAof3^b}uW`RvBviYAHk58~Z?A|K^wCmt>L zt%QH5)GXE>{|u!Fr_X(5edPW19Eg&XHT=HB{FbMc{QL0h;C4FDR<=Tw6{Y&;Z#%;t z-mrN4Q(FJR4bLm3zRblSq10YN&aa7KT+7PSV?}-N)#=6`M4{#4a)UfA5B}AwRZR=) z#j=uYUhpeAQmSw(c>|{Cu_EXPN1H104i@!8F`}h^fFTLI0lyspJD8vtD+7z?MhNRF zMr43R?mLu5xf0&Ovrb|>2)zm06+JdGq+v|eV=!SBDcygDfzoI-xQaG8k$hsJTpSn1 zKlC(g?d5f`XE|pRS1(V0@*QEBn0#j1g)jz-QMhH-p1Ci9?gUPnM0Pn_@-nwE_4MF3 z5Qel1GfWsv&pHLE_esCk<=g?z8b1nme@!YnNQo&c=TI6xQ52hi+42?oV)xw2-9pqg z@}GxMh?^_cJ&7_yO>-cN2<^;c*UeYzDG#DyBBFCm3EtN)EZk1S?6Ta?emp?k(W6bC zmHM~1J;9VYFEQXNSEM7Ga=bV;hq+5Q5I|Yp`TEC3f$-O(p^rYe9}$$}wD3HufU`0? zg&!`Q4;Y_ELnFuiAFIC)#=RcZjE0JyVc-fOuM`Y2;IP>n39p}NHrH-OEG5_L)8ywh zn{oG2Afs6JUW&fI&7%Rn<8Poa?qhhaAKXL!A+?!&o_AH*P**s*$w%CTEhu*_p&o)o z;-#nK{;jFLC=9)|2Raow_okk+e$Yj(OkKiz&pa$jdAxt--kK&;L#lWVe5B9E&OxVO z-PNdT7EcU653>XNv!}gvuYl*-uEloCgbsGKMP4f9w&#`Gv!t-7u$%b;q68Cy8;$5N zQ8CF`NN;%g&LXgfE*9rSusq}WN{($hdEGMM=*VOZ{9^dpx1P(_F7$hHOR>4Z#dWLQ za1MlK_d87s?lxS8R40r0o`YhY6m^|_eoSOl+^f5#oDBm&c(4R z-5&;yP)B~xU^S_$wARd|d$Y}K-W*sXJ+Xt7btAor8IKskM^D!?bN9-nl7Giedc%d1 zO{=$>w@q5 z-B0@#BQ6;9{1TU}KLqiTt3mSmEb#jzQ~vH^IVj$7rQCF^VuZetXET96ml(ee3@avH zC!sXOvpxUSlJe~1J+=i}-9;&%X37IcE}4cF`u+XxofMR6%>1YOngTBpOp^7-LQktq ze{Y!(!=ivuovTGj_oqm~r_OCgHjTVIgonQDMP`JFh!^&1umDC#C;KQ*zw)@@<#SdR zf#jRFt5Ta4@H*mx%W0{1K1mI7@tL6Sch9WrH;nXjN7<-%hrDxrw9yp${}gxj|4i?F z94B4mq{FP4+!oHJxoat{)Wjh}#o=BAEhNp!6nOign$_cxt( zQ_*Z3CPZySjb<07tnB*g{Br#V*Y~H-58ofYH=obr{d&LNpZDi!%9Z&sO`3T3Lj$m! z-M_olVlTZTv78wZhNFpLH9JP67WU4ax_%sEahskp+{4^>E{mPWTknZN4bZv@i_9&| znm(>g6qwW=)h0nA9s%oYQNIJMvSdL2g1-rmVfoS9M+{MQ)y31U@W$h$7yV)@id(=Q-XF%# z;G-!%J2;(b;1?K8sZ})M{x}(Mp3`cl%!V?yoVDd`})j9tb(F;+An)E3#|p8 z$NYrDHcRWXUqt?;lYNL1XY76>C@d*TfMQf_pfocUPgG+h1O`TfiP?y@-<2tF)tZ8y zg2;oWWPd~9*JZn7A%WW4=svjwLycV@qK@g*u@9!1bc0={?SE3s@FZ2*|JV5tIX=Nd&zn<$2S(lpssx0GoIF46d&4@(j! zB%)i4q*LDKdR;^H`4i#dkyMB=FNP(@oVQ{efEE*DiKpWw_&WlC7MD1C1}4wQmAU#P znq_^JKUk)yk5+9Uy|@L`J;JrQssN=u!+#6(w3b=g+un)Xk2l0h2+{#&5hPEkP=<|< zSE5Il8CL=0+H{wUl^J(Js;#mGKh5+tEj|Ew-PAH?Gj!6L?y>Ru%GobdCBdKI?^$zx z7foVyZOf`Q0L!1MN_S3xLp(rJckP#D`;GkHZ$zaS2Wz>IhQBPIi$|4PUYxPquQhvXX?^@9OnOOUD(J=nDvK>t| zkQb(O`ODvIXQV!x=cxJDN_#oGr!0Uys2^)`T}7mMqHW;J{VH63LVIwS z^u0SQvg|OtcsiwK0I0Y}N{ng-TDV?$9PNZ{IDld}wwNwhW8BUW^kBJ6)g4?X=S9%7 zkP;!5y}(Ol=_&8VcIjsv@|mrqx$P5Yn6Bw5aGQsb9pjjumN)(yRKsG8e6y;;#E>;u zn8>}1i%_kd3+K6x<=0J9deL7pBxuNE2+S7pfNEyVQHz6U8%Fza35^ztX>H%MzuL1Z zUC<;W2$6}^b)J1f6gY|(X*;)TifBiy*@WxB*`N4}c^cB@Lmnnz9}ihd9faki$?uS) zF!tqFl6|rA4$`t7K3m6uZll>a9WJxb45VmNcG2fR#&7hh?5G3Yw7!PXikjflj{JlO z@`n@}X)w{-^ra4nC}8W{-~PU@Y+NFLLu!Uvj&BEh=0f(#JLu^{j!W#Q==gfw3&YR; z0rVP(4=b`1^%Zx5JJ=b(8_x&`lvO?B2neu@$ME8-Lae*9tBUq&_3N}tMQ}2E z^7$AG%kVN94aX7UeMV)BgCy$S8c^wc#TI4Qt{f29(6zsVPNUaxU-$S4VOk>m))HU|~gAE|n$b})M*X|{YP-^fYtAG zp*NWKK}OTrNt{&6{7}kMIn|otcsug0Eu9JglVJYly(%*d$ak_#EQ~8bL9J9boPdRL5=ee>H(vuk8+WMiZN zRc5gwE(@!xBRIie|-ydi}%nWpob zqLkH9 zys{p88q_uB#ofYiUS|Lbhs#~vjmjEOIJ<+1E{$#FIUB*j1}+YyAVoFfz7<#_rVFfS zEAtI|HEFSid3^2H_KG}4ZgR=?ey0qSB4H9Q%G>A2Y|}*MmsZLd;cy9s0Plm%9Yo=p ztu0La(I3hWCG%Kj;0AKyqAu7&-sEvZIYv70k*oj@IE?X>AQe3{mx5ClX8wJZpW9T} z< zik8agU*~TqyU?KxhmGc~-BVYKQi7s?eX@Y%wP1U%g*hI$WBP^P2i{bUoGnRs*Xpvd z&aNW!({`|6c0%?np=YDOQ}wYdrykKfg{}N>kGGYca?YiFi%6s#-#*=2N;>^HW2Gm& zk*Ob77QWaXxUvXI5_y;K84V^S)zu5}B2`z_pWaYSKp%tSlgQl|^^Izmv>mgxmsNbq zaN6T+c}Ube!M*g{s(0N#hXqv+^DzRFBtOniVnxNGu#@W03Y{$Hw}QO6Z3b-GihW%BOj?@$~s51c#0?Zl=30{w_5(*OVf literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-5.png b/docs/map-generators/refactor/generator-5.png new file mode 100644 index 0000000000000000000000000000000000000000..2fab88732c5768f4a37a60992d22632dae1aee9c GIT binary patch literal 44413 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

6@_U1!yGNF)Z#vmIu6;VT-N)pQXMXjRX@jr7 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'&W!D(NtbZ`t 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 literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-6.png b/docs/map-generators/refactor/generator-6.png new file mode 100644 index 0000000000000000000000000000000000000000..5a4b562a271daa1cbee565f7d8dfc256e05da711 GIT binary patch literal 44818 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

80^QQVHm#&zY!I_mESe=u8%U4Xm0(~7wMT#s*2 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 Jc}wC?{{ntxVk-au literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/generator-9.png b/docs/map-generators/refactor/generator-9.png new file mode 100644 index 0000000000000000000000000000000000000000..4c52c1cb346cc603e8f10959898e5ae20bae8e46 GIT binary patch literal 38828 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 R(`@Mf=pgjrMc3~?`5)(fMEL*! literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/modular-confirmation-configs.json b/docs/map-generators/refactor/modular-confirmation-configs.json new file mode 100644 index 000000000..cb6c5a86c --- /dev/null +++ b/docs/map-generators/refactor/modular-confirmation-configs.json @@ -0,0 +1,7 @@ +[ + { + "id": "shattered-holdout", + "method": 7, + "preset": true + } +] \ No newline at end of file diff --git a/docs/map-generators/refactor/modular-confirmation-reproducibility.csv b/docs/map-generators/refactor/modular-confirmation-reproducibility.csv new file mode 100644 index 000000000..3717d6068 --- /dev/null +++ b/docs/map-generators/refactor/modular-confirmation-reproducibility.csv @@ -0,0 +1,7 @@ +config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status +shattered-holdout,7,30001,1,16384,7046,1453,2169,5716,6188,1967,8810,4315,3259,0.018893,4967080206951358053,163,9,13,4,387,233,158,31,ok +shattered-holdout,7,30001,1,16384,7046,1453,2169,5716,6188,1967,8810,4315,3259,0.018362,4967080206951358053,163,9,13,4,387,233,158,31,ok +shattered-holdout,7,32501,1,16384,6100,1500,3549,5235,5315,1836,7507,4130,4747,0.017830,18383014318267758100,168,7,23,4,404,135,166,144,ok +shattered-holdout,7,32501,1,16384,6100,1500,3549,5235,5315,1836,7507,4130,4747,0.018262,18383014318267758100,168,7,23,4,404,135,166,144,ok +shattered-holdout,7,35000,1,16384,6471,1599,3166,5148,5639,1999,7894,4197,4293,0.018125,10314852731698056258,253,4,17,4,416,175,161,82,ok +shattered-holdout,7,35000,1,16384,6471,1599,3166,5148,5639,1999,7894,4197,4293,0.018156,10314852731698056258,253,4,17,4,416,175,161,82,ok diff --git a/docs/map-generators/refactor/modular-confirmation.csv.gz b/docs/map-generators/refactor/modular-confirmation.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..22a88a88b28dd96692ace652ef41460678efa73e GIT binary patch literal 255104 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�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%0Dc_x`fW8qfC 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 Cx)LOSTJWB=Zzwl8d@ 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 @^N+(` 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 Zv$+Sa|GdV;L42Z*^8Y!x8@~Xc3IK#IgpU9K literal 0 HcmV?d00001 diff --git a/docs/map-generators/refactor/previews.json b/docs/map-generators/refactor/previews.json new file mode 100644 index 000000000..7e5da04b3 --- /dev/null +++ b/docs/map-generators/refactor/previews.json @@ -0,0 +1,338 @@ +[ + { + "method": 1, + "seed": 22001, + "success": true, + "hash": "14920531807079716896", + "role": "fixed" + }, + { + "method": 1, + "seed": 22002, + "success": true, + "hash": "11206795816039919690", + "role": "fixed" + }, + { + "method": 1, + "seed": 22003, + "success": true, + "hash": "9177038106079592176", + "role": "fixed" + }, + { + "method": 1, + "seed": 20027, + "success": true, + "hash": "544194403467468821", + "role": "weak-start" + }, + { + "method": 2, + "seed": 22001, + "success": true, + "hash": "11437096384349004182", + "role": "fixed" + }, + { + "method": 2, + "seed": 22002, + "success": true, + "hash": "14770049955031301025", + "role": "fixed" + }, + { + "method": 2, + "seed": 22003, + "success": true, + "hash": "6504539841165983025", + "role": "fixed" + }, + { + "method": 2, + "seed": 20874, + "success": true, + "hash": "1561439438145097576", + "role": "weak-start" + }, + { + "method": 3, + "seed": 22001, + "success": true, + "hash": "9860058263762090266", + "role": "fixed" + }, + { + "method": 3, + "seed": 22002, + "success": true, + "hash": "5210807649688818574", + "role": "fixed" + }, + { + "method": 3, + "seed": 22003, + "success": true, + "hash": "10803015415216018498", + "role": "fixed" + }, + { + "method": 3, + "seed": 20665, + "success": true, + "hash": "9069473070465845841", + "role": "weak-start" + }, + { + "method": 4, + "seed": 22001, + "success": true, + "hash": "5044719679931046782", + "role": "fixed" + }, + { + "method": 4, + "seed": 22002, + "success": true, + "hash": "16710061729624559854", + "role": "fixed" + }, + { + "method": 4, + "seed": 22003, + "success": true, + "hash": "5470594655607351316", + "role": "fixed" + }, + { + "method": 4, + "seed": 20992, + "success": true, + "hash": "13598708499441320366", + "role": "weak-start" + }, + { + "method": 5, + "seed": 22001, + "success": true, + "hash": "4081282500915162465", + "role": "fixed" + }, + { + "method": 5, + "seed": 22002, + "success": true, + "hash": "518787808480994771", + "role": "fixed" + }, + { + "method": 5, + "seed": 22003, + "success": true, + "hash": "10208999434179203736", + "role": "fixed" + }, + { + "method": 5, + "seed": 20382, + "success": true, + "hash": "3929662265599224957", + "role": "weak-start" + }, + { + "method": 6, + "seed": 22001, + "success": true, + "hash": "15492766370562744084", + "role": "fixed" + }, + { + "method": 6, + "seed": 22002, + "success": true, + "hash": "16842018058235473864", + "role": "fixed" + }, + { + "method": 6, + "seed": 22003, + "success": true, + "hash": "13407323231470023606", + "role": "fixed" + }, + { + "method": 6, + "seed": 20989, + "success": true, + "hash": "14400468275467415138", + "role": "weak-start" + }, + { + "method": 7, + "seed": 22001, + "success": true, + "hash": "11574187082192437596", + "role": "fixed" + }, + { + "method": 7, + "seed": 22002, + "success": true, + "hash": "4034283354063186255", + "role": "fixed" + }, + { + "method": 7, + "seed": 22003, + "success": true, + "hash": "6154660420075554243", + "role": "fixed" + }, + { + "method": 7, + "seed": 20666, + "success": true, + "hash": "5979258390125815431", + "role": "weak-start" + }, + { + "method": 8, + "seed": 22001, + "success": true, + "hash": "5116402118393623094", + "role": "fixed" + }, + { + "method": 8, + "seed": 22002, + "success": true, + "hash": "11962301441114388923", + "role": "fixed" + }, + { + "method": 8, + "seed": 22003, + "success": true, + "hash": "6485435245488238917", + "role": "fixed" + }, + { + "method": 8, + "seed": 20412, + "success": true, + "hash": "4564946222045695100", + "role": "weak-start" + }, + { + "method": 9, + "seed": 22001, + "success": true, + "hash": "17350664697783344402", + "role": "fixed" + }, + { + "method": 9, + "seed": 22002, + "success": true, + "hash": "4057095887845812880", + "role": "fixed" + }, + { + "method": 9, + "seed": 22003, + "success": true, + "hash": "5606765741091488537", + "role": "fixed" + }, + { + "method": 9, + "seed": 20335, + "success": true, + "hash": "1400871654614562697", + "role": "weak-start" + }, + { + "method": 10, + "seed": 22001, + "success": true, + "hash": "8489149481487705140", + "role": "fixed" + }, + { + "method": 10, + "seed": 22002, + "success": true, + "hash": "14038765396739683628", + "role": "fixed" + }, + { + "method": 10, + "seed": 22003, + "success": true, + "hash": "13729186607969906376", + "role": "fixed" + }, + { + "method": 10, + "seed": 20204, + "success": true, + "hash": "6476948285032075565", + "role": "weak-start" + }, + { + "method": 11, + "seed": 22001, + "success": true, + "hash": "16784330220506045212", + "role": "fixed" + }, + { + "method": 11, + "seed": 22002, + "success": true, + "hash": "8998178716352640516", + "role": "fixed" + }, + { + "method": 11, + "seed": 22003, + "success": true, + "hash": "2020498579335314712", + "role": "fixed" + }, + { + "method": 11, + "seed": 20166, + "success": true, + "hash": "8525398947293004777", + "role": "weak-start" + }, + { + "method": 12, + "seed": 22001, + "success": true, + "hash": "10008600412084261952", + "role": "fixed" + }, + { + "method": 12, + "seed": 22002, + "success": true, + "hash": "9047696792498758786", + "role": "fixed" + }, + { + "method": 12, + "seed": 22003, + "success": true, + "hash": "13791541075664039917", + "role": "fixed" + }, + { + "method": 12, + "seed": 20058, + "success": true, + "hash": "1776682512706564171", + "role": "weak-start" + } +] diff --git a/docs/map-generators/refactor/source-hashes.json b/docs/map-generators/refactor/source-hashes.json new file mode 100644 index 000000000..cbe2fece0 --- /dev/null +++ b/docs/map-generators/refactor/source-hashes.json @@ -0,0 +1,67 @@ +{ + "src/map/MapTerrain.cpp": "6b0770b830e5793ee54ff99c53d5fcde9bfd981b9120f774e665b55884c811ba", + "src/map/generator/MapGenerationDescriptor.h": "c756e3ca6e4b9c034e64b87c16d079998ec61367bc9bb78a2c5c06c561920e79", + "src/map/generator/MapGenerator.h": "bbefa9dad74ce3f27a1df059110b52812ba14f4b46777a6def9c789f1909590d", + "src/map/generator/compatibility/LegacyGenerationDescriptor.cpp": "ced08706e0d340bca8b9f0e8fb22105d52a2491a093f75771e30eef98e89233b", + "src/map/generator/compatibility/LegacyGenerationDescriptor.h": "03d1b6d003dc844b607932eb3982cbfd9e36d7661a5339e269431a1ad7f07461", + "src/map/generator/core/GenerationContext.cpp": "240ae5475f5987fc5c4cddb9e34d9b7077bf3a1be098e7caa22896478d399172", + "src/map/generator/core/GenerationContext.h": "a237976bdfa93a0cf2ea28024be91f3e6cc7da61d2f970e4efa9415da71c3971", + "src/map/generator/core/GenerationRequest.h": "79e63cd7cef6db6d25e23fd9b8145f8f45c70d93f8f3167cc9e9961d7f8fb828", + "src/map/generator/core/GenerationResult.h": "a4b758f990b3f0ee6d499d59475dab169540f458f9c999e175f93dfa9c621b4f", + "src/map/generator/core/GenerationService.cpp": "53115fba252ec8f23ece99e3082d7784168b68ee0ae0ac2af35a0666646438ee", + "src/map/generator/core/GenerationService.h": "7564d5fbf3fd4ab126dd512974719a8cc1cecc59beb48d309cd32e4935a89db9", + "src/map/generator/core/GenerationValidation.cpp": "f4b50b8db615bc2515bdfdde53cf568d7eea7a9e16c0cdac4351514c89110677", + "src/map/generator/core/GenerationValidation.h": "f47be67ed359b2cba0fdf38db1947eae8a4b19e2f2776eeb6b9d62dd51a8abf1", + "src/map/generator/core/GeneratorControls.cpp": "66d5c3d1de47cdd22f1ff6d23e4cd1663122c3346b135e671c031ffdde22e278", + "src/map/generator/core/GeneratorControls.h": "7bc42d9c89d13b019c53b3069b3e65d50d87e87c2f27b1b9fefb337be9084b9c", + "src/map/generator/core/GeneratorDefinition.h": "c2b152b0aa882142433e2069ff78f8080a37e3753c7bb70d14cac255f5c10821", + "src/map/generator/core/GeneratorRegistry.cpp": "d9fbba6f6b3adffbf7683f1c8068d73535725f9f60b2b9c955c1c08bbd393fe7", + "src/map/generator/core/GeneratorRegistry.h": "69e230056fe1a83fcdd48ac2b0f8812e96c96318d2eb3348c8b4f098833d12a9", + "src/map/generator/generators/ConcreteIslandsGenerator.cpp": "cf0eb0e6e6f91453f1a0d3a4c000f14fb82c59945c3612f43f83479358e78f4b", + "src/map/generator/generators/ConcreteIslandsGenerator.h": "4bbfd34137c86a2e910357bf2c9282bbafc47a1886b1b577b3ea9f1ecde3d140", + "src/map/generator/generators/ContestedCommonsGenerator.cpp": "491d5c65ad3f04c743885d8d9a7486ac16f17e868da8d9f309053dd5e3cd2349", + "src/map/generator/generators/ContestedCommonsGenerator.h": "2d88d55f091434feaec60f00fd12673b12f56fe9e5182fdf291bbe3cb08f44ac", + "src/map/generator/generators/CraterLakesGenerator.cpp": "09afba075c4d47e1c5f7330747da0c99040e73abf08a23fb6030fb33666c3286", + "src/map/generator/generators/CraterLakesGenerator.h": "87114c131ffad00bcdbb51231b8b859c49fbfea0670a0a65046d61d120fbd107", + "src/map/generator/generators/FjordContinentGenerator.cpp": "a27ffc9c275de0f7a410a924ebbff5bf3a89be1e54c4e1eb0452bd17d80adbb1", + "src/map/generator/generators/FjordContinentGenerator.h": "dc148bf0b0fd2254cba15209e911c64b081ad4f7518e9fe26f36263f81fad04d", + "src/map/generator/generators/IslandsGenerator.cpp": "50ed4f5bbf296bb20509260ab686ad5f2d0b62d041b1b63395c56098fc2b04e0", + "src/map/generator/generators/IslandsGenerator.h": "cbdbe99acbc597e5395071453009983b34566eb6c650d750281a677fc9ab1978", + "src/map/generator/generators/IslesGenerator.cpp": "74942466f6b3484307d8124df092777847b09d76727d07fe63287e814afe119f", + "src/map/generator/generators/IslesGenerator.h": "866eac88f549aee03e98683d8ee95f04e0ce3df942439d51496aceb9b3fec41c", + "src/map/generator/generators/LatticeGenerator.cpp": "fd1ca0199e2c7f787bad4d021c53e107b7e57475c54dc589923746aa4d74b0c0", + "src/map/generator/generators/LatticeGenerator.h": "f03dbcd8cae6b0e35511fdb14d406c642be9fa368a30be0ab04650ec8ead4264", + "src/map/generator/generators/MazeGenerator.cpp": "c17495886a1fb9f5b7f3f2569240651b5a03fe8720a8a19fd639a82dc1baacdc", + "src/map/generator/generators/MazeGenerator.h": "a5ca3e25bcafca300a4db299adce23c6f461bebd23b5b5d7b73df87435f26131", + "src/map/generator/generators/RiverGenerator.cpp": "c854c1d9790054887193233d2eb09ddd714539f08d7179b6df9379827e1e6f18", + "src/map/generator/generators/RiverGenerator.h": "b25e24ad205686297cef1a2b574b6cf79c6148644cfe616c62c47ee027b6f799", + "src/map/generator/generators/RuggedArchipelagoGenerator.cpp": "74ed1dbb1dc4a89e1798d4d83291a4579340de582c84bf592d97f011159758f1", + "src/map/generator/generators/RuggedArchipelagoGenerator.h": "f52f6adae0acf722dcd23095cddb6d71a063657ceeff1db0a581b7dd2cf0b938", + "src/map/generator/generators/ShatteredCoastGenerator.cpp": "70749910fe77046135b288e6f967e3aabac8113b0e919db1ef7e0b3acfd65298", + "src/map/generator/generators/ShatteredCoastGenerator.h": "3eb86756b16a00540333e06603f7274334cf88d41d8ab308f27e9a7e129a7674", + "src/map/generator/generators/SwampGenerator.cpp": "e96de2707223e768686bf3a12027a2e0ca2be62924c4edb4bcc753e0cff6e675", + "src/map/generator/generators/SwampGenerator.h": "08b0dbc4530a30c67544d33520e842314950cca1f5df883344164509aa66d1c1", + "src/map/generator/generators/UniformGenerator.cpp": "22afc16de74969ae8b6cdae8f1532877a89b1c41889a5a2c1c3c5377f8a50646", + "src/map/generator/generators/UniformGenerator.h": "b14505f0fa6d04c54fcac6708efc4ce57fb9818e1ab03675144aaec97cf14859", + "src/map/generator/shared/Distances.cpp": "5c72f66a1a3b89cee1e19f7ceeffd8e9a3915fad2a3ac102cc4c72859889ec26", + "src/map/generator/shared/Distances.h": "65ba65217e7eb1b033afff228bd42c434c2b48b8cde3679e2f12030063cfb97a", + "src/map/generator/shared/Geometry.cpp": "41646048406e3ded77f33d38a80887071dd23767fa295692d5fb135ae2574f17", + "src/map/generator/shared/Geometry.h": "0a82e862a5f1fc14112372489be59b313a61d1e7ff2e24522de895fc7c2f114f", + "src/map/generator/shared/HeightMap.cpp": "bc09a2efe2b3cee3dcbc309ba9f959b61baca2a673ad14c50a5964cdfb91a226", + "src/map/generator/shared/HeightMap.h": "00372062bb44ae9ceabc1f7e53d01fd127fdf14ca4a4b08785d5e9dedc8451eb", + "src/map/generator/shared/Noise.cpp": "350f26d01eb0a53c426e769c723c78a9958ba0438d58a2bb1bb9d16e00bbc96d", + "src/map/generator/shared/Noise.h": "c17ae67a3c1e6a6bcdfb7475f789a7428bc1604fb9945e88f58334b3925505ab", + "src/map/generator/shared/Regions.cpp": "57f831a48491a5e062f83bb873d28f565de84cb63f417531919b16c849c39aad", + "src/map/generator/shared/Regions.h": "4c9bda081f36b864f8fced230355613d09c0a31baadf9b77a8b95c5377d7e187", + "src/map/generator/shared/Resources.cpp": "1ffc40e3a5bd4ef21bf247f4c76076f2126d60f04242384a06186af02cff2295", + "src/map/generator/shared/Resources.h": "81570481031dcf16e2a0ba27623575370915ceaeae5491336b9c52d7b82c0b30", + "src/map/generator/shared/Settlements.cpp": "89f3f184d259a5e610bcb7d19fea4291863f0c7b843f0514e4a56eee216d6728", + "src/map/generator/shared/Settlements.h": "303ffa9af486ecdf9d01dd281eebbdbad6ac54bd70d33d1e2efdfb01df361344", + "src/map/generator/shared/StartingPositions.cpp": "91ab2f58dcebf539d58376b2c993fa587e1cce38bae54ec3b321bedbf25dc035", + "src/map/generator/shared/StartingPositions.h": "7479f38a45ae46ca835d98fc36959be75bb800eaeeedac45aeac7dc4c238ef4e", + "src/map/generator/shared/Terrain.cpp": "20d8140845138298a57143a57db4281b61bcb534b46bfde7ad454637768e4bcf", + "src/map/generator/shared/Terrain.h": "c93fc9b9308f5b752790a21d9b29cf11a2e3282591ec07256a2e12ac5ff98e9c", + "src/map/generator/shared/Topology.cpp": "ab743eed980e9625ba6399d40e19160b44055dd08ae67f155461236886321398", + "src/map/generator/shared/Topology.h": "877e2f6567b86eaf087cbf34bf0247454a8d41b0d6e0687de377b5af16d8eaa2", + "test/MapGeneratorStudy.cpp": "c13c6288082194f3603943895258c315ac87740d48a849208c24114a1f77be74" +} diff --git a/docs/map-generators/refactor/summary.json b/docs/map-generators/refactor/summary.json new file mode 100644 index 000000000..2dd837175 --- /dev/null +++ b/docs/map-generators/refactor/summary.json @@ -0,0 +1,364 @@ +[ + { + "method": 1, + "id": "swamp", + "revision": 1, + "name": "Swamp", + "attempts": 1000, + "successes": 999, + "all_teams_proxy": 940, + "grass_tiles": 56.582614108248876, + "grass_tiles_p5": 55.7366943359375, + "grass_tiles_p95": 57.3980712890625, + "free": 44.65428563328954, + "free_p5": 44.329833984375, + "free_p95": 44.9462890625, + "fit4": 34.17453708591404, + "fit4_p5": 33.026123046875, + "fit4_p95": 35.321044921875, + "water_tiles": 29.685373850412912, + "water_tiles_p5": 28.863525390625, + "water_tiles_p95": 30.4571533203125, + "sand_tiles": 1.9446204016516517, + "sand_tiles_p5": 1.4520263671875, + "sand_tiles_p95": 2.4658203125, + "shore": 11.787391639686561, + "shore_p5": 10.484619140625, + "shore_p95": 13.162231445312496, + "review_flags": [], + "free_delta_pp": 0.016456043445792545, + "failure_delta_pp": 0.10000000000000009, + "proxy_delta_pp": -1.2000000000000028 + }, + { + "method": 2, + "id": "river", + "revision": 1, + "name": "River", + "attempts": 1000, + "successes": 980, + "all_teams_proxy": 780, + "grass_tiles": 55.91467409717794, + "grass_tiles_p5": 54.339599609375, + "grass_tiles_p95": 57.31353759765625, + "free": 40.72438765545281, + "free_p5": 38.5302734375, + "free_p95": 43.61236572265625, + "fit4": 30.5191725127551, + "fit4_p5": 27.374267578125, + "fit4_p95": 33.14910888671875, + "water_tiles": 33.51710728236607, + "water_tiles_p5": 32.56195068359375, + "water_tiles_p95": 34.35089111328125, + "sand_tiles": 3.6775986029177297, + "sand_tiles_p5": 2.67913818359375, + "sand_tiles_p95": 5.13336181640625, + "shore": 6.890620017538265, + "shore_p5": 5.55999755859375, + "shore_p95": 8.3013916015625, + "review_flags": [], + "free_delta_pp": -0.015043939426902853, + "failure_delta_pp": 0.20000000000000018, + "proxy_delta_pp": -2.0 + }, + { + "method": 3, + "id": "islands", + "revision": 1, + "name": "Islands", + "attempts": 1000, + "successes": 1000, + "all_teams_proxy": 986, + "grass_tiles": 48.9701416015625, + "grass_tiles_p5": 48.52294921875, + "grass_tiles_p95": 49.38385009765625, + "free": 33.59825439453125, + "free_p5": 33.41064453125, + "free_p95": 33.79547119140625, + "fit4": 23.830609130859376, + "fit4_p5": 22.97943115234375, + "fit4_p95": 24.658203125, + "water_tiles": 34.558172607421874, + "water_tiles_p5": 34.11865234375, + "water_tiles_p95": 34.94293212890625, + "sand_tiles": 3.235235595703125, + "sand_tiles_p5": 2.911376953125, + "sand_tiles_p95": 3.5952758789062496, + "shore": 13.2364501953125, + "shore_p5": 12.51220703125, + "shore_p95": 14.00177001953125, + "review_flags": [], + "free_delta_pp": -0.003216552734372158, + "failure_delta_pp": 0.0, + "proxy_delta_pp": 0.09999999999999432 + }, + { + "method": 4, + "id": "crater-lakes", + "revision": 1, + "name": "Crater lakes", + "attempts": 1000, + "successes": 1000, + "all_teams_proxy": 922, + "grass_tiles": 65.40447998046875, + "grass_tiles_p5": 64.5074462890625, + "grass_tiles_p95": 66.29730224609375, + "free": 57.11531982421875, + "free_p5": 56.817626953125, + "free_p95": 57.391357421875, + "fit4": 45.11155395507812, + "fit4_p5": 43.07159423828125, + "fit4_p95": 47.100830078125, + "water_tiles": 19.604144287109374, + "water_tiles_p5": 18.914794921875, + "water_tiles_p95": 20.39825439453125, + "sand_tiles": 3.39337158203125, + "sand_tiles_p5": 3.14300537109375, + "sand_tiles_p95": 3.6563110351562496, + "shore": 11.598004150390626, + "shore_p5": 10.0872802734375, + "shore_p95": 13.03802490234375, + "review_flags": [], + "free_delta_pp": -0.011444091796875, + "failure_delta_pp": 0.0, + "proxy_delta_pp": -0.09999999999999432 + }, + { + "method": 5, + "id": "concrete-islands", + "revision": 1, + "name": "Concrete islands", + "attempts": 1000, + "successes": 995, + "all_teams_proxy": 982, + "grass_tiles": 65.49762361612751, + "grass_tiles_p5": 62.3760986328125, + "grass_tiles_p95": 68.861083984375, + "free": 37.533885246545225, + "free_p5": 34.896240234375, + "free_p95": 40.875244140625, + "fit4": 24.51387798366834, + "fit4_p5": 21.8310546875, + "fit4_p95": 27.8106689453125, + "water_tiles": 15.638556552292714, + "water_tiles_p5": 13.9794921875, + "water_tiles_p95": 17.3785400390625, + "sand_tiles": 0.4861649556375628, + "sand_tiles_p5": 0.32958984375, + "sand_tiles_p95": 0.6591796875, + "shore": 18.377654875942213, + "shore_p5": 16.7156982421875, + "shore_p95": 19.793701171875, + "review_flags": [], + "free_delta_pp": -0.08270912955734389, + "failure_delta_pp": -0.30000000000000027, + "proxy_delta_pp": 0.10000000000000853 + }, + { + "method": 6, + "id": "isles", + "revision": 1, + "name": "Isles", + "attempts": 1000, + "successes": 1000, + "all_teams_proxy": 1000, + "grass_tiles": 42.17257080078125, + "grass_tiles_p5": 40.765380859375, + "grass_tiles_p95": 43.603515625, + "free": 23.9344482421875, + "free_p5": 22.75360107421875, + "free_p95": 25.115966796875, + "fit4": 13.21666259765625, + "fit4_p5": 12.23724365234375, + "fit4_p95": 14.27032470703125, + "water_tiles": 36.59349365234375, + "water_tiles_p5": 34.515380859375, + "water_tiles_p95": 38.60504150390625, + "sand_tiles": 3.458172607421875, + "sand_tiles_p5": 3.00872802734375, + "sand_tiles_p95": 3.936767578125, + "shore": 17.775762939453124, + "shore_p5": 16.67449951171875, + "shore_p95": 18.85406494140625, + "review_flags": [], + "free_delta_pp": -0.013067626953123579, + "failure_delta_pp": 0.0, + "proxy_delta_pp": 0.0 + }, + { + "method": 7, + "id": "shattered-coast", + "revision": 1, + "name": "Shattered Coast", + "attempts": 1000, + "successes": 974, + "all_teams_proxy": 799, + "grass_tiles": 41.89434952314874, + "grass_tiles_p5": 33.8702392578125, + "grass_tiles_p95": 48.626708984375, + "free": 36.569658784650926, + "free_p5": 28.9202880859375, + "free_p95": 42.94097900390624, + "fit4": 14.542403015512706, + "fit4_p5": 9.37286376953125, + "fit4_p95": 19.759521484374996, + "water_tiles": 18.59351438173768, + "water_tiles_p5": 12.16888427734375, + "water_tiles_p95": 23.250732421874996, + "sand_tiles": 8.555192575317633, + "sand_tiles_p5": 6.9720458984375, + "sand_tiles_p95": 10.441284179687498, + "shore": 30.956943519795946, + "shore_p5": 27.41851806640625, + "shore_p95": 35.2581787109375, + "review_flags": [ + "generation failures" + ], + "free_delta_pp": 0.14510318991842297, + "failure_delta_pp": 1.200000000000001, + "proxy_delta_pp": -2.3999999999999915 + }, + { + "method": 8, + "id": "rugged-archipelago", + "revision": 1, + "name": "Rugged Archipelago", + "attempts": 1000, + "successes": 1000, + "all_teams_proxy": 1000, + "grass_tiles": 39.84324340820312, + "grass_tiles_p5": 35.577392578125, + "grass_tiles_p95": 44.3914794921875, + "free": 29.211114501953126, + "free_p5": 25.3765869140625, + "free_p95": 33.19122314453125, + "fit4": 17.377459716796874, + "fit4_p5": 14.342041015625, + "fit4_p95": 20.52764892578125, + "water_tiles": 38.933770751953126, + "water_tiles_p5": 33.11737060546875, + "water_tiles_p95": 44.2279052734375, + "sand_tiles": 5.015771484375, + "sand_tiles_p5": 4.498291015625, + "sand_tiles_p95": 5.615234375, + "shore": 16.20721435546875, + "shore_p5": 15.03265380859375, + "shore_p95": 17.4560546875, + "review_flags": [], + "free_delta_pp": 0.023834228515625, + "failure_delta_pp": 0.0, + "proxy_delta_pp": 0.0 + }, + { + "method": 9, + "id": "contested-commons", + "revision": 1, + "name": "Contested commons", + "attempts": 1000, + "successes": 999, + "all_teams_proxy": 374, + "grass_tiles": 25.01883597464652, + "grass_tiles_p5": 24.76806640625, + "grass_tiles_p95": 25.2685546875, + "free": 10.73317580275588, + "free_p5": 9.447021484375, + "free_p95": 12.1588134765625, + "fit4": 3.7920794329485736, + "fit4_p5": 2.734375, + "fit4_p95": 4.970703124999998, + "water_tiles": 64.691357617383, + "water_tiles_p5": 64.2388916015625, + "water_tiles_p95": 65.17333984375, + "sand_tiles": 1.2643625070382882, + "sand_tiles_p5": 1.0986328125, + "sand_tiles_p95": 1.434326171875, + "shore": 9.025443900932181, + "shore_p5": 8.6541748046875, + "shore_p95": 9.3994140625, + "review_flags": [] + }, + { + "method": 10, + "id": "lattice", + "revision": 1, + "name": "Lattice", + "attempts": 1000, + "successes": 1000, + "all_teams_proxy": 932, + "grass_tiles": 18.939208984375, + "grass_tiles_p5": 18.939208984375, + "grass_tiles_p95": 18.939208984375, + "free": 12.8915771484375, + "free_p5": 12.628173828125, + "free_p95": 13.18359375, + "fit4": 1.806304931640625, + "fit4_p5": 1.300048828125, + "fit4_p95": 2.349853515625, + "water_tiles": 37.158203125, + "water_tiles_p5": 37.158203125, + "water_tiles_p95": 37.158203125, + "sand_tiles": 7.672119140625, + "sand_tiles_p5": 7.672119140625, + "sand_tiles_p95": 7.672119140625, + "shore": 36.23046875, + "shore_p5": 36.23046875, + "shore_p95": 36.23046875, + "review_flags": [] + }, + { + "method": 11, + "id": "maze", + "revision": 1, + "name": "Maze", + "attempts": 1000, + "successes": 1000, + "all_teams_proxy": 999, + "grass_tiles": 41.42435302734375, + "grass_tiles_p5": 41.009521484375, + "grass_tiles_p95": 41.85791015625, + "free": 34.49039916992187, + "free_p5": 33.94775390625, + "free_p95": 35.064697265625, + "fit4": 10.533880615234375, + "fit4_p5": 9.53369140625, + "fit4_p95": 11.492919921875, + "water_tiles": 25.06688232421875, + "water_tiles_p5": 24.786376953125, + "water_tiles_p95": 25.32379150390625, + "sand_tiles": 7.98978271484375, + "sand_tiles_p5": 7.135009765625, + "sand_tiles_p95": 8.81988525390625, + "shore": 25.51898193359375, + "shore_p5": 24.9505615234375, + "shore_p95": 26.0986328125, + "review_flags": [] + }, + { + "method": 12, + "id": "fjord-continent", + "revision": 1, + "name": "Fjord continent", + "attempts": 1000, + "successes": 1000, + "all_teams_proxy": 995, + "grass_tiles": 24.8265869140625, + "grass_tiles_p5": 24.1815185546875, + "grass_tiles_p95": 25.47607421875, + "free": 23.02662353515625, + "free_p5": 22.38739013671875, + "free_p95": 23.69415283203125, + "fit4": 13.698980712890625, + "fit4_p5": 12.774658203125, + "fit4_p95": 14.6307373046875, + "water_tiles": 64.62662963867187, + "water_tiles_p5": 64.105224609375, + "water_tiles_p95": 65.17333984375, + "sand_tiles": 0.057891845703125, + "sand_tiles_p5": 0.030517578125, + "sand_tiles_p95": 0.09765625, + "shore": 10.4888916015625, + "shore_p5": 9.8876953125, + "shore_p95": 11.102294921875, + "review_flags": [] + } +] diff --git a/docs/map-generators/refactor/validation-configs.json b/docs/map-generators/refactor/validation-configs.json new file mode 100644 index 000000000..e828475be --- /dev/null +++ b/docs/map-generators/refactor/validation-configs.json @@ -0,0 +1,42 @@ +[ + { + "id": "modular-1", + "method": 1, + "preset": true + }, + { + "id": "modular-2", + "method": 2, + "preset": true + }, + { + "id": "modular-3", + "method": 3, + "preset": true + }, + { + "id": "modular-4", + "method": 4, + "preset": true + }, + { + "id": "modular-5", + "method": 5, + "preset": true + }, + { + "id": "modular-6", + "method": 6, + "preset": true + }, + { + "id": "modular-7", + "method": 7, + "preset": true + }, + { + "id": "modular-8", + "method": 8, + "preset": true + } +] \ No newline at end of file diff --git a/docs/map-generators/refactor/validation-reproducibility.csv b/docs/map-generators/refactor/validation-reproducibility.csv new file mode 100644 index 000000000..af73c3037 --- /dev/null +++ b/docs/map-generators/refactor/validation-reproducibility.csv @@ -0,0 +1,49 @@ +config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status +modular-1,1,20001,1,16384,9252,324,4868,1940,7319,5558,9755,1293,5336,0.002564,10661502771248767044,944,13,13,4,859,682,287,993,ok +modular-1,1,20001,1,16384,9252,324,4868,1940,7319,5558,9755,1293,5336,0.001404,10661502771248767044,944,13,13,4,859,682,287,993,ok +modular-1,1,20501,1,16384,9323,303,4876,1882,7315,5686,9807,1243,5334,0.001610,17241070552458762342,752,9,9,4,828,713,358,991,ok +modular-1,1,20501,1,16384,9323,303,4876,1882,7315,5686,9807,1243,5334,0.001764,17241070552458762342,752,9,9,4,828,713,358,991,ok +modular-1,1,21000,1,16384,9341,309,4966,1768,7285,5467,9805,1192,5387,0.001610,10655558613566185882,414,11,11,4,681,875,390,994,ok +modular-1,1,21000,1,16384,9341,309,4966,1768,7285,5467,9805,1192,5387,0.002883,10655558613566185882,414,11,11,4,681,875,390,994,ok +modular-2,2,20001,0,16384,0,0,16384,0,0,0,9558,1052,5774,0.001761,2606947197152000752,16384,100000,100000,0,0,0,0,0,failed +modular-2,2,20001,0,16384,0,0,16384,0,0,0,9558,1052,5774,0.004111,2606947197152000752,16384,100000,100000,0,0,0,0,0,failed +modular-2,2,20501,1,16384,8944,716,5586,1138,6567,4750,9313,1280,5791,0.003159,10780039130517181389,629,18,13,4,688,855,727,965,ok +modular-2,2,20501,1,16384,8944,716,5586,1138,6567,4750,9313,1280,5791,0.001913,10780039130517181389,629,18,13,4,688,855,727,965,ok +modular-2,2,21000,1,16384,9139,470,5461,1314,6668,5122,9544,1120,5720,0.001810,10550789423753792819,647,16,20,4,1045,693,633,965,ok +modular-2,2,21000,1,16384,9139,470,5461,1314,6668,5122,9544,1120,5720,0.001801,10550789423753792819,647,16,20,4,1045,693,633,965,ok +modular-3,3,20001,1,16384,8014,509,5659,2202,5511,3886,8590,1608,6186,0.004006,11247383660930392137,307,13,16,4,881,1073,447,1088,ok +modular-3,3,20001,1,16384,8014,509,5659,2202,5511,3886,8590,1608,6186,0.001776,11247383660930392137,307,13,16,4,881,1073,447,1088,ok +modular-3,3,20501,1,16384,8039,535,5662,2148,5501,3877,8609,1606,6169,0.003410,11690117216618492072,761,15,13,4,959,1063,424,1087,ok +modular-3,3,20501,1,16384,8039,535,5662,2148,5501,3877,8609,1606,6169,0.002597,11690117216618492072,761,15,13,4,959,1063,424,1087,ok +modular-3,3,21000,1,16384,7986,578,5603,2217,5500,3884,8568,1684,6132,0.001485,12032433602295556753,550,11,11,4,1063,940,381,1086,ok +modular-3,3,21000,1,16384,7986,578,5603,2217,5500,3884,8568,1684,6132,0.001527,12032433602295556753,550,11,11,4,1063,940,381,1086,ok +modular-4,4,20001,1,16384,10724,535,3227,1898,9344,7349,11289,1483,3612,0.004815,16316743800069380881,1020,19,22,4,601,456,222,635,ok +modular-4,4,20001,1,16384,10724,535,3227,1898,9344,7349,11289,1483,3612,0.001432,16316743800069380881,1020,19,22,4,601,456,222,635,ok +modular-4,4,20501,1,16384,10776,530,3298,1780,9343,7431,11307,1416,3661,0.001449,6545278665695955885,947,24,15,4,548,552,232,635,ok +modular-4,4,20501,1,16384,10776,530,3298,1780,9343,7431,11307,1416,3661,0.002121,6545278665695955885,947,24,15,4,548,552,232,635,ok +modular-4,4,21000,1,16384,10707,553,3195,1929,9345,7284,11275,1511,3598,0.001425,15932849638750774831,686,9,18,4,480,537,232,634,ok +modular-4,4,21000,1,16384,10707,553,3195,1929,9345,7284,11275,1511,3598,0.001443,15932849638750774831,686,9,18,4,480,537,232,634,ok +modular-5,5,20001,1,16384,10609,72,2639,3064,5956,3812,11341,1602,3441,0.017479,14539859018350938763,577,2,9,4,2650,1883,24,2,ok +modular-5,5,20001,1,16384,10609,72,2639,3064,5956,3812,11341,1602,3441,0.022410,14539859018350938763,577,2,9,4,2650,1883,24,2,ok +modular-5,5,20501,1,16384,10917,67,2451,2949,6283,4045,11618,1540,3226,0.011279,3177999051462689531,610,2,21,4,2759,1760,24,1,ok +modular-5,5,20501,1,16384,10917,67,2451,2949,6283,4045,11618,1540,3226,0.015615,3177999051462689531,610,2,21,4,2759,1760,24,1,ok +modular-5,5,21000,1,16384,11269,55,2322,2738,6398,4326,11927,1423,3034,0.011464,3450008235584598855,629,1,13,4,2702,2054,24,0,ok +modular-5,5,21000,1,16384,11269,55,2322,2738,6398,4326,11927,1423,3034,0.012253,3450008235584598855,629,1,13,4,2702,2054,24,0,ok +modular-6,6,20001,1,16384,6927,507,6188,2762,3941,2138,7660,1902,6822,0.016924,2625225261725384295,375,3,18,4,1431,1451,24,100,ok +modular-6,6,20001,1,16384,6927,507,6188,2762,3941,2138,7660,1902,6822,0.012168,2625225261725384295,375,3,18,4,1431,1451,24,100,ok +modular-6,6,20501,1,16384,7078,608,6018,2680,4124,2283,7799,1947,6638,0.016360,15259089094901122187,498,1,19,4,1482,1368,24,100,ok +modular-6,6,20501,1,16384,7078,608,6018,2680,4124,2283,7799,1947,6638,0.009874,15259089094901122187,498,1,19,4,1482,1368,24,100,ok +modular-6,6,21000,1,16384,6872,549,6154,2809,3898,2145,7598,1978,6808,0.008573,5139624633597254587,504,3,10,4,1467,1403,24,92,ok +modular-6,6,21000,1,16384,6872,549,6154,2809,3898,2145,7598,1978,6808,0.016989,5139624633597254587,504,3,10,4,1467,1403,24,92,ok +modular-7,7,20001,1,16384,6596,1289,3673,4826,5739,2424,7856,3729,4799,0.029709,10305528355439009293,227,7,16,4,372,193,212,151,ok +modular-7,7,20001,1,16384,6596,1289,3673,4826,5739,2424,7856,3729,4799,0.018533,10305528355439009293,227,7,16,4,372,193,212,151,ok +modular-7,7,20501,1,16384,6804,1318,3410,4852,5954,2390,8196,3762,4426,0.027920,12482868172572717974,85,17,12,4,377,224,169,147,ok +modular-7,7,20501,1,16384,6804,1318,3410,4852,5954,2390,8196,3762,4426,0.019610,12482868172572717974,85,17,12,4,377,224,169,147,ok +modular-7,7,21000,1,16384,6628,1660,2479,5617,5866,2103,8239,4514,3631,0.019852,16030144911233971808,157,9,31,4,326,143,213,121,ok +modular-7,7,21000,1,16384,6628,1660,2479,5617,5866,2103,8239,4514,3631,0.018885,16030144911233971808,157,9,31,4,326,143,213,121,ok +modular-8,8,20001,1,16384,6087,744,7020,2533,4394,2588,6580,2015,7789,0.004368,9678229960813273146,409,5,6,4,811,798,4,83,ok +modular-8,8,20001,1,16384,6087,744,7020,2533,4394,2588,6580,2015,7789,0.005623,9678229960813273146,409,5,6,4,811,798,4,83,ok +modular-8,8,20501,1,16384,6430,916,6365,2673,4798,2938,6954,2267,7163,0.004244,12822830550967485530,503,5,6,4,682,866,4,100,ok +modular-8,8,20501,1,16384,6430,916,6365,2673,4798,2938,6954,2267,7163,0.004134,12822830550967485530,503,5,6,4,682,866,4,100,ok +modular-8,8,21000,1,16384,6584,834,6216,2750,4862,2843,7121,2209,7054,0.006794,5404957458423946019,536,5,6,4,747,891,4,46,ok +modular-8,8,21000,1,16384,6584,834,6216,2750,4862,2843,7121,2209,7054,0.004315,5404957458423946019,536,5,6,4,747,891,4,46,ok diff --git a/docs/map-generators/refactor/validation.csv.gz b/docs/map-generators/refactor/validation.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..ea6614b8ff7ed7532618664feb5293de945bc714 GIT binary patch literal 590054 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 z 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≫-Ko{9H0i793QQ=* zhSg?omSOf6659G+h6jDrsuEpNc`gp;xAx7Y3VVLD>8qYaF!csx#uH-%<0PUuM2-Va z%-Ka41d_Bfgw79}TY1H{-+b9NwDZ>*m@@%K)7UpnFtx6Y;i)Mm9^{bdx8tAQrt7@G zXpSJ8Ny9!bqZ?lb5D(5JfQ*-2!`DSP6rz0B)Cm$mO9m~w^)e?1B4ys$liYhc;A??@(JY&~ngO3Xd zR}J3`&f|PuVRL72;KFKeJlbWnyC|AE43L@`8H#6g*71H-*TJx&aY5PSek`@PZo>o4 zidXw!D@F$qv^9H1O+fiFwMTm_4!65Ng9cr;@yOAevs_mgOwOGuaW>;+(<3@5B8t1n=r2Gk9$v3cei3gGe10F4|I{4!W64F`E< z*MT(skT-a4Ve}Dc#&S!ZB0$^uZyVN^>TsGK4uh_*vvR$G>a-C;)Q#k1j`PST0&#e$ z4tHI&L$Zm1`ejfZx{fEj10H%X2kWtypc?Rl-(Uj@lhj=8?J5}30z+jT_FyT!tfKD^ zfxbsCrLKI2`bLLE)y8V{XGm2)_Dx7;@quhoo_Z4O2XEk}#TB%E8SSO#+K}$42_c)) z5i22o?aUDsLj05v1-?AVpo>V zG(c3(yGh3n)^66$j;)A5Oz6u*W}<^52p6Q}JI{ww>n@!7{E`CN?z(6mvVIlih(Pwr zUJEP_WM-raRLXui;)xvOPEGb7JQ)UR_vQ`cgL}t+lf@y%_gT zhTyJST;$mE=2#trEoFXKTO;&AiVQwmI2Dv|Zc6)L235h4HqkRZuNuBCgWjA!&gLJl zW9-Gqi^y66M8+`-+Kn=w5r4>zdUY&KwY#0xlLsN@Bi0dnRrdr{=EiWeSdSlMVRNND7iMVA~tO4_PE)`t6C;N43{WPXxi z$7eD_aIBp*uW(T=!+Ftiy>nV16vR?Y_!<)xNd6D!1=B2V-6C-_9Up z%ck6RFB<}APD7KCd}M=MQXPXL5zv{LHml3x%}m=j`;n>C{-uoOo|hMsZe9_@XzL#@ zSc}zp+VTSaY;m+3GF2w8wi#cKVDyggLgtFIAeV!~F4NyJDVtB=0T z_D-ovq2zij5&7=6hxISnMc^N}=X4ln6>j|OHc?53n=LVL7XYf2;e(t!hR*S$7u&Iy zupC945OcgymA)>4MX*~~ZVace>BGb-{usA=iv09`iO=-)BETUvID><&5Bs4r%wXL` zFU*5#XnfHRt0}}bT1(6d? zG6!eAKI{E)6WzY_(YVo`{jy;tmPlY}&ko(rM;q3yOireyu0~<5P6w8IBbRid4Mv^32aFkk8=D<@ZvkMHx z5zUg|267v9v1fj+54W43$cTy6AkZO~9<&Fd2ii7p=MyCJ*b2B7*Vr(OT)#OZ6&J83s6?Ks*0}J5U{KDO0fMBnx+xiyjRX%BKv1{Z&}F(> zA5PUBr_N^-lBe>J{{=1aZ9JC;nh%W4w3ecwExJbhXloh@xG-bI`=GVA;UWJ8Eg^28 zhds55R%)XkScb+`7{ea?MrH(7E#EgJZ6QMNXuNW+{8 z7r4b0wfcsZPNTImS?}QQ_|U>7F;5_@SoI`pVOIa5fct$3DxT1QVp#>`%JL=%H#wrb z^)v}sM|U4wV^?ulmGrFaY^faA_~0x%tmFnWv^Nc0yC+8TB_;RpT35hXi+C^mGgKH@ z^R{QcIS$M(C*C1!pb$v!ob2md_G91^3HiozVstBC1){j{BXKvZOD9cu*Sy!r-P35e z`X^4JY6rOb#gPFTp|grkrKKca#I}=`BiZbm|=CK zeFfX^kzkpvgLD#(cKU5rd88qcR#k?A{1sjLN{lv$lo!Lvn6p)0C0}MWC5Hmh`*wCx zfDW~*)P(s7jO4_U#6uyjz<5XHl>f1PxQnEZJ;WKDB9A)y7jQeV1%#*!Ni(7xU{b7E zl8h6S4y%cV-USG$N;_gfjrq_&?XPRNYDBrOq+@^8wiyjbI$tVyVPj96TyEBlyIPRg zhcbClCs!xc{Zr@1J)_m|2mUShxJtxbp38=D3VizkB*h$j88v(xOp8Q-x%-5nt>Qt1 z!umj019uois2%J6AWFc;CV4%jg0_FDq@gF*XDX{C(}Nts0jYX@nXJT(_W23515}Ne zAWoW|ml|N5Ge3eDP`3ySA_mRm>R49gVr6vTk1Dz5rw3;Zsu3hE zj4j_b*5>FufMiok2Jn#_j=M@%v~DNz`jKt+il_~pnl z-CyjL?w7!BN3sxS-?{Z#EikX?*~(j5_i2C{69cC80oK(siPpj8NgBX{g9kOkH}QxB zxM#QS+cktKt6WwSyXpFC+F{~D5en;=#sRfMInkr8hm2S|y9OadyzQ@lZ$NU6lu|w4 z-d59J0%fGVd{6u}vd3gB^&}}s!MFp)B?&C$mb;z~FEA?#s66=SubySn9H`!pz9qr1fW`A( zA@1^2nu0@u0<2miT&7{00B(J29r|&Fz#Up(QJgXr0vGq9gK20Tb_!h5!lM0Gg}5h2 zhropMMW(Z04-*J_@d5K65a&nnj}QyDZ8gp$yqLq5p@~+rK;PJ+ujALf|Fm_mis5z9~WcC4|e^B*lJfc9XvZrJA>jI*FTkpB; z49ZOSilBqlUkR4DyOr(yMq+e0|KWI+aPg;V7!EhDlvX@kNg`rre3r)&@juW>!|i?~ zE;TdIR%Pk{GW2+bIoyqlylxn%Rc~WZx|a%3zh_w`8j%EtBT8+W5XTe&N8`Au9lS}a zOF;_?o03Cl9?Hh{=?wFOeqLU|TM*p+DSQ$_Ezp!WT9gE1u-KZFNORadBh_d9$yFQ2 zIs?|ovyP>PM?ky;b7%dG|8Q4o?ao^wjkNQpd6PL(bFg~(OO6tT<{!0#3>G$G6R|um z$vUI}igl`jbfZqYyOLm@d&z;jUjjZNpd#)d)+;d>LK=80o8<`3zJ*6uFpt%?=~NAW z$uPuHej>>($ST73%1zEUXV!KPkH&Ej91WaG10Nz1wieFIVng22Wt*-K)Ou-2IH@zy zv*#88Q#V%a;qa0Id4AD4w7Wr`kOt>=p0*&xXf zG8yv&^5#ulvUH~Rn4fmfZkBJOXM3Vl!XD+z<@X;l|AF4y(Y9%LcVj|+I`UYeemEnl z)Ty+&OD+g_4zCHJw<~D61s7oyDI&L!SVH}WsGoL4h z7k^w?#i7%7(0FuZyFv2MnJx9 z1!;B27O0Wo{7@SHrIc2E^SqtLAbfqU1z%S3G}e1FOz7DXG&w_gr?a;Cwtl$BM~|=+ zqf3-5<4XWJ`OvWwM-CG<_R)9%34+J1S8$yL5?YBJFum+&n}^%7oVE=8S6uI(;?Ta8 zaA&`auJG0j@)80|^3s1~6+!CWw%viF8b)|jrl{Er`+>Z~?kD_HhKc6qd|p4?WyxZwxfOH;UMS!erV^ZnC>xpx|y{xFel+rB{ z9i-3EW_*6>mk;as-eS>D9Q0`GTy=y!B!MA-FFMPwLFqBlf81AQdg z)~!}215fhsDvSl%6i$3iC`jjY8JIP?ByLH4(mb?(g-COVL)3oMJPex2^>-L9Fj1(g z4`zO}LV%HKt$Ii#d9Dxl*D&SqrY;ylZHpys6Ics}cXy=GZfr$fNh1D%S8}kUt?PJn z$qh+iM*$k~LY2>N@X}o=eVkkLAX32e5a!)OiS0UIX1l53a%42G34>D2q9EfrRYo`u zxCfa&er1Z!8;HA3d2H81Y5KYonIg^6y0_E#d**aG`)C72r@DS<<_$as=Z@Tj2z@e7 z!K03#e4qqYC)1c)_o1PAhC&so)*dfx!2kq1?S)YV+;Z(rN1Ofv*FJu;Hxt0&i zR6#a-Rl~Kf5a}eS^`pf0#N2gK34`m;(o_2oh(j^MLui*C(HkA8>cR8?RQLON1_uw%t zU-zvz+*HbVfOx765iV^Ll>QeuBZoeCtIloAosP<_U_*vL)^%E)iXsbB^)70w@6r6P zz@K(k$<`JQG6`_Ko^d&f+c=WW4Mi)KqB=-`BiW;pU`%!`$_Re`5y1x4>{?bpr&^@( z=%R7EgxZ{a`*x&d)2?~6EMZ|zslp>&2CdUavreY2Y8=i=Vp$!D+#6bAvwi#rv^2gQ zT9XUyc&kIu81r#~H9Y0B?JPJ9#XIGv($MhDtgf}OpO}(|q!jjO^xUOADW%)LLZq+s znuqB@pab9@a>78vwK`gD9xifg(SKL1;4E%0k0SPagFZ$I)8?ffrs$JLrZmmo@D{cRb$ z-1UG=xUfpf(eH`|Ugw6}MVx^75(`*-k~S5k;TS;zcQWT2i`VqC^Srd0xN;|`IYe==9^M3BHgC4s`eNuCx#4$M90eGr3QOK`;IEc1Tnqm&X1QVXYY2B#< zGz|D{Y$u7VE>OAdS)Z9px4S^23yT#PogiN)KpfT41(~;sc)h2+bqIaT`Wg@_r-&WG z!r%uQ?hcT~=|_>+c%B>XLTLv7ATGhyu6Eoexq~YKq{q%7ICRNu>oX9Y1{a*Km)sZL z!SJmHxd=Kxu9y$GyYd54n1oE*T#bfHYBK5ZHL3Ld$9bRdz^Jy)8w9RY9 zqdg_9d5pIg-rK6Qc1S#5#~IoqvU%1=X?PhD?cv-&=ND1N{V@)%-__Ej46)z{cCMnh zknQ5XUB$VXXLq1|!j)kS)X29eCAj~aT1-_Q+eM>?CJyJ}XWr8N0yIF>8C7vK%yuY5 zfC-aXVQQ}wDdv2Bf&-Xq1B(+Hd#G1)b-X$5-578916fVlLuKf9wKRX+-LDD zLSK~Qe@agt?P3ur(liky~bx16KRg}`kU>MHsDS@}iMA*GrRv!9h*WK?j=^9J)oevs$$G`~I z1gCsyl=0)&`*B9zg!^|)s|P&05ge_6cYB;(CjXTDP!sy`uM{`~S|6tc)~bl2%4$aS z8>{#TkLW?tMP^lQ0#WIBt_k-C0QumI0EVXX-i~@f-Gu3$0wB7;%XDtLCMP^$I~`r& zeb{Ncv4>7UW<~DOe~D5*CE@ES7~b?57npaDL?z}%8X%N+_MLM_bRzN^xbm`W8WkF6 z(o^Humf_ksE4E4JV{~-CM@O?T&N7wqjt|K~8XV}Z6#%|!>KC@{tAf~Nu2~H*Yh=Gv zk=(s%fY&|_E$H7ShWj<>fear;cVM^-tw)*>M|zXb8G$nvMKRY#cimTeOlD=%=H-x7 zusuA);ErJK{UVHARpBm^)`w?Yi)h)rWWsC3VNOk?M<4l;!mV0`J{N8MdO&XIlM=!Y zt~_^aWyF!aICl51@aV3C6!U@(*ZGGTFzI#)*bFP^t`;W9Ndz*(vl1liv2%clvK=fm-GNP&^^9JH2D`q6U zI_}+mJQvTj0QJp=5SJ);?x|{@j};ju(j_4{QdLb%{1{0~bWfH0FpUlvTOzGrTttwa z>5-yzzXX0J#?V#nuL+-g(iur!M>8v>ldZ~LGN?0HFN=ZV>w8)a%jEwW(N|6j=?H2B zhCq*{;Y%akbx)wPp$C)o21SX6m)bfbW2PQaBG2%H>>MZSv1@n6s`84CcHhENkx}s^ zg6sd1NV}zaj*do}lhy~&sM4J!JZf=`$)hmf$CLFhK7_)a3&X#nqa7Ch1cWk+UFn>M zB17Eb*Ox|!!7*_0ptvGv450&c=BzvD=LJaWUmEEY8o2HcR_X0mz^aEiGy+2R4p{5_ z0YM2bn-cjlP^@^914rty6E`gAcq|O}=x75%ofVES$q&mtov*dN#Nny3yV+*lsz?rg z_jPuy+DZtDa-bgL$)+eVY#^_Dk-*)rL3K&6hd1%Gb@Qr0#3XL{g7hm*BJ0qBZ78g{ zRWcdcArx_VUw(H74dp4<{{>0vcab!+0#galt|M!Oo`_e_C6eYZJhyFou4l*%j#>0^ z!G-a(ZO!fDuH@23|`GiZZ~7W>>#(xbc5*F{8rob(!9<@i9suk<1Fe2qnTjzxIZn*vQJxrhZz zSW;kUCjv@3E_)@&Bd<_WzXvxLy}iP)B#|8#-vmcXFG5MYVIwBU_5P7loz`HAqsuxE z3+hR*)#|Yn8aSGc?XUDw9PZJ<4%;C4T?0U9b4E+ZX=Y+Aan8j6nUf|7@OODVafY1) z2^J~M^KxTNDq}UQdXzWxyGq*z5YY^njxPqtLDrKROT$F3q4+@{N%9QJ?DanYUTd13 z)Gw{7f}zBrwh7*kmEkVTl}2OySfsM&$Z499FT?&(r|!CQqcIjMXyVNfj}ZZ zDF=bI8tW%_jLA3CZx|i$QNXRkpT_ne^J=xrXz})rAzwQNY5-=e$BTW&*Hbt`pRxTj z+&>!`Ufe&QW&tpXm5roWyVmcHNx%$SDh+paG%KDeJt@W` z-eQEpH2iYsOn&KCmPJJO5S~qZ`g(?aeIfW47u)q1U&~a4hwJ%^I=bK0(PgXOqBJDE zmCjun)Q8sCXzxXj%IAhId=;gRfd>YJO2U;gJ2TX1-v4X=-0$k>P*|ysW>wPJF<_2t z5X8=Lm>QtxOA08ivko$*7kO$3V=pF1ic?Xq=@cHvFZLPzJH(oC5h#f_1DHF^vy|0H z#)DRyOWwYBQ)5U@gsk>yJ(jZ?I?c1rVXY(;zlkK}$I5Wir7BZ*Zs4j3CC}KBCe|M& zo9jc`<`qJxjq&<$usmCbGujXON5t*N$Bxx@j2fcf)njS6Ym+q-R8Tt{xn@|!C*|rh zB3hA+1}!6Nv5$oZo%=eNI(EiQ)!1S09GJ!-FaK0Z{phW9zXse}`H~vnhL(Q?VzOM5 zHtp&`ec1sDsNq+G>T2BvWOv9iN$I>x8|0KoN*IsD;Y%KUZg|`mN*`V}@ZEq=2YI=K zvEw+6&A^%%W34qiVBo?+c1UUWUsz$`9%*&RPwb@rFL^Yps`)HzP=C*kX|BeF)2X8D z*8?ngL1C^`CD%#Hc&-lluN;Su_AROdV>&GgNvlfcaYf(eN{WyOErxgvpIyLRBKkH& zH5P(30cJzz3w`zLIUGv-kCwuY9!RML528qw3rp5*C8NxXog?hB>bD8H4EQxZVyCXZ zQa;|yGkUpGMo&LvI$kiDcT-j~; zV|lnKlWA}yD;7TCV*Ddm{nTH`1;6UI>k^LeMT4b=B%{+Y9tbRqxxY5R-9TUM#vs`| z)rY$}8vSU!me6WkVhIg82KIZhZ)|71m_a60HUcoJ%vH>V+F|_KC&*#@*IuiJ=K}Gi zjy9*1kAtrfdf-GD-EBI9I4Y=A_XGn^PS@PJKG?|t0reDG!Rf!XKfHRpFX$HD3Pkxr zmH1UrX@RR?=c>KRDSg%VloLx$n5dG)yknx9_%fhT<|<(A5?BJGkx`gOzk*74b+kb4 zxh+Xeua^0Wz@nS0F@on1Zv<`~{KkZIf$fp1(UkHtdPgSTmE<4`IV6{VHAy#8-Cl`Ju1Mm~RoZ zfr{QR(a=Gm2K5%xCOya4Ma=CUu{w5UP4(d<{WRk~ z&p;73L8ycuh3ba>1}uH$cz4S~4NF)ZyK97+6aa)Ue?B&{lXU7?uR4oBDU+s$F>aK+ zomn|3O8`sH)#0vpXBO0Qe!yD%C+k&@VyQI@_FLiOyn_`_UpXLPHChVzP%?ZcWJP+^T(Xpwz{pE!y2 z9{}wkjDA@|-xbOF&!Pu7T^6Jq+}*Bc#;Ffl)8wb^Ir50yQj7|)^J&O)VUWp<>TxgF z@0aicdEG*PWmx+LK*^_3YbjcI^i0g;=?vR|?aF#0L*Rfa`t1$S&W0++P`b>elTYcQ&^lcqAMSqId18Y=ydE_#xK!w6rc z{8$)nl4$qpkP^&Zt}v&YOwL0_2x@5Nuv}$GA^xI+fRHg0L`I}8Lo^utAZ(Rl9oMy@ z_=W0RWue|T(bb6EM$4C)zYP~4MMKDsH>96&D6RS3e5;NbBLZz0b0l)spdiG)!DUH% z-$dWFxMCM_CeKZnKIzoDwn~I=1@|su9IDvXdJt<|D(nBUD(U292u6&+9uA+fiQ7d1 z_*E0?zby9{Lff;X!7|PO&Cl<2$~o&9uk13bJI$t(6?2TAYrz2HA&F_5vZ?x6>GdM)@+(R?1s2Jc@utVO; zL#x7Gfu^qSPr_hrqV_T&l|D0FT!b*p0HuFnCf)VWML7|oEWS7f9TKa}RM=~1(U$p9 zGzi)jGS`~UP8i1XDF?$!1_ZXeMKjIUx^NRCYb#cjAm^~bY|f2cPQN~+FkAa2$;OHW z&&O3qpb*VfqGop|kv?H6r}#+?h1YD)d+56$x^KH~Ya{Soty+2#KCPGuz_q1(!JE9X zgL#pdEF`c;)pDt}+a7vE;+4y%w`IeZ6q!8_{5r#mkYY&O>SijQY+*5l_%kw`$N_ba zfi{~l(pmGAD?cWm(Ec%Glonme+Zy`oB6^EtP|UjByD&&*zrIud5{{`1Kc6!}#2J|# zTGVw%FiOTQf{@?x(KIykV$OHL+wBqvn~gOzh;NFOZ`Zn^hYzM&;8s#JDi*L4nH$XA z?%}lFj2hu@J4eCTXoU^$kkVIba0(O=3hKH8V2pf+=FAKpLNSFeg-yZw&=FvxvLRBN zhPAnj)>`kH+$gL}{QWh`p?^h3BjF^>ikcqCR#a*bCH(LhMFXu% ztmF4p5q-)k7rrd>W4WiD(zOq#I+Jgd&}rd^NuA<29iQmv?7~4KtoDW|{nDjdpLN^= zo*SA*OH!iBw)W+gA)u|*9bnjrc?trDpOEwfaTS}1A9bV83x|GZ2*FUK1ZIh@W7}n5 zNJRMUwQ%TL^M~qUBJ^708n>>k&K-Y|K;nYGO-Dx_Nk-N%9u;r*OVE;`v+-6>c>4dN zJYXkeGGQ`$r^GF&e6X~RU_KxyY=kMsw29UF1j}$>?o{V%ZMch}sSBbmKqycdtW+Ax z4U)S)!gM-K_<(#Uf>!uxEot(GR&iQ$n8NL#m4Z(xD%CRyI%`&i4 z%i@^YVrHqUSB)z6CF`4j*L0M&zJ~5usd@6)inkPw=uG!7VGduHp~#I>bOiT<^9hug zm-cMI^*{+zuGnUd>H~k7)nmgZFJKxp+W8VAC)f%nRtc}wq2HrHr|#*^-SuFiXV>7? zqMMchYfwDcgNdF77f_Pa0Z~retEuKXQ(az4&AH*f6mR!y;1HC*sNaE-pDKBfDZcAQ zKKw$%U<>6_bx+COXthU!WwOCAkc~}qz;OGF+R$8yJ=TYt9{OPXW1ObvaLrbM+%snE zy=t&eghbu1L)GwUoe5>~+fG(T1syq`&@K-t9`Mg!ffiSNxC^4q{gNhAw)3WYm8xSL zXs2iDX7!U4Ok>WedBC#5xtnf(LyA3EtU^gJ(Jw{zuk!FuL3GkI?+sDPyaK%a*x6$G zkJ0-4*>OK9Mh~Zl@rEdU38L*YHp- zF6&Fy;wt;gTXneI1U8!i>dErc-;pL|x{dBX-rsenw%Hzk?>d}WKT@#!n&^9z+%k8X zgK-=`<1IJ3-?Y%DZ#hI3lT?x}bz$hZPE{8?04=Chf=RDoGiDM%!g<{X(yrKT(Bf|r zIa7$?g)ImTPqpE84Qy;j5R=YuneG7DxCp{Nom&lkWhH|lLhD|m#QMG03B9gB+k9KCfkGV-Z9e5s+2z+_>fC5cnALMYS2KyMC#c5Zo5+=hmxp%NW{xJdhS-#NKM z!BgTBID8{sw+(k8vU?ZYRs==O?sjz04E79RIA1^;pEL)?g6Skpn)&MK8k<^a4Mwn2v8lVpV|jubakrP?e&l3jadL_ zH2q~1ag~Oz%w`~Buss-QRbZgWD;HV4>?T!050o{2>cPeDQ<|i z<)2SPPQyvh3QYEV;Mk-5 zaZy4Vxn{+T1mDau#Xr6Pc_yArhx_YSY!hBk?5zuHVJbh|LTLJ7=iM6MIx` z7H#*T6@noryqJtTuDUr~a;`9o7IEM5wO&iZmm2zvUn8Hv*d)gR?<1f2natwChoydo zI1#Kw^jjt(5=1c1NbO^Gcc6z^LS;zlmBRX!8y$NOPN5zdXv9NsI-3Nq$Qo|l22==# zGL(;O$Tj(3owHh8Vma70M<{zm29D&uwy4Hg4F7(}V@w?KK*k zfC1yUUq(_Cu0*{SN#IOjm(d=TwH)<7io?9a{f-&n%80j&vNmQX8^6lS!M_zu1J*Un zrf)R@xDKPP!j70spqjbvy-zmns0ZWyc@cfT1o8_HjkO$~a3-!IblN5|jU6UzUE?%w z|GGZnir0zS*ROyksyVJ~8f!Sm9q%iLyApT)3Hs?z}#lS9zHK-a5EQcq$JNaC(D5y3Xd25x}*wH4gbQQmUIa z^cDlx|4xtYmmu1}0A&p1?FlPUb4Wb9a!RYV1!R>q5|7sB(9K;g2o?I?I>-apfB&sO z%x^lerZ?uZG~>tUFtYxgP^$IOIj-Vn1D9Gh_R0Uh%%Y?*2w*6pokuiqh(BuTy$7RnB+ZzuN4E&-;8nKW@E4ENCy0f{pf z|C`x~yGouqahAwsPvaGLRLhbBl7 zj}3GiPhuj6Wyq{RC+mucSWzu3fUx5j^<>_uOA0-Fj+24JX#yz~)?ap)!Rky`CxdUWuDGV1k)fj0j z<8_xEt5?b700DjU!wm6AQ8-aNIUH-U@K7AazY?U;9lj`|*#QyjjPQ?u(57cR6qyzq zae5UREH+wiET#MX-}*O5QJB;vtwVc@+90tpn{hw;HeuC!YmJU$b*rmm?CUJ+v5UbV z;^*>95es2^^LrzF)rPN!kW|PVA)_m%{0Jv9!QtIE@}FmL=!9QM(8gfhEX36)N1pFx zv6o~i9VQJum4=%pSv(~MG&$&^4!rj^t0Q#C&^hr*s(sI3Yx+4Q7w{Yyn1DD z2H~TO`0bg(dcOj&uNtGn;ew(82^GGG8KQ@3sXF#Ly2;#v3w&%WGfjs~MliHMpFhb~+0gHPWD*?XNwyPXNG!{3F4+KmP*HPJ#!`#NW34^{L zuD+fLzjPi~S@=qAW^G7=rIukJ%>XC-SY;@D)h#z&3iU7vhZ!;nG@l#=MBDa=2kq8{ zu(%3$?#H>|>nia6k-17jgFXQaUfD)kn@9dZV66!L%014y3sR_In>m<&EVHQaJ1-N^JPkw|3h?SS} zU%?IcE9kPs`TfmRpE~{-%IIMY#x*@GLXFv^I+n&!*s$f!nYNizyy?6jO5~mDgO~W@ zD*Ao}z$YZCvC@JVU=TJnEy(%D_Al7_io-}%`ij!vvO%d?lMI_X``XP>LGJS(!42as z$entT`{mJSgK7&+qUzwz%yCmir?6<{9pyDW@EwHbKibut7*hzal(xR%$uVP`|Jp@U z7hm)Zu^T5@X1%myO=k_Sa%@wtV9vvC#)r`Qi;dFPTM*0W5nh8kxxsA5mOjg9jnl>z zLB~iqh%Po*r;u%5)hfowKq`{rXx0V&tS^P({I1TyU!@|QMi$FdO4~#!%(<`E0QJck z8IT3r&Gnz_PZAhLz)oZ=vBc$}ob%B!W2A2rC2V`jkR8KChc`D)s){W+;6Y7xaW)!7 zN3Zn>LaJXSiiN=+0jQzP*7sLy?(vmhjf7{P#%^!BaWZZzG`!c}v+dyK6d8kVyYPL( z?uj+!(9-S7<>V`{>%>Or@T$Bgso z`jC=)D#!h{o!xXtts7K&q$+(~glUHKsTk>rv*I6IC=LtW6 zRKB8MHh4-%E(by zaq7)bNIK1P*XV4swoUfVN6(D=HMl?ER&WZZuF)Sinl@ao_5}T+jjfQ^TBEIx7?c^4 zvsED^LH=1pBbq`p_m{ZistxH|a)mw2CmB>p_mueecQPoU9Fq?jE9y#$$!b-)5jtn^ zW75Wx^7hPlvR)nk5=QgupU*TUd%9)DY#VHr$2LBZScvEr5v? z&^^LbYU)79v1@qK=Elh4Atr9Uh>Q#!#bb?H)HjWqX^95(#WCZ40b&DvCpMoBAlwB! zWeIHKP^9Zy^N zopBx93x|A0C%eOqc<2DD2Rs03)H)T|oQyw$GdFMW2x;D4Yr|cWOs~zknBCbWSXM~h zn+EBTs;E0}y`<65&DV@hiUz||#kd+y4_RMVntcm#gGb2=o|L~z!wk*`1GjC3)DrA2%CFNeEWJ0gQYYhL%T@VjMN$gXL}o6@7o4&=Ym-=o4NSgdA#b2wptzget*hk_D&a) z8HrufSkkA;@Rit%%?Pi*?p+wh!XOSSBW*AoO4GQaFcGyi9P!q3U$3C5OPq!{kD^Db zb`{+c|F&l+_s9lpEMdmUkpzcDP%c|Z_uP|Xay%p2>M&LbM$n0ezEJXplSr;L zR%eI3J@Qw_mmV5hkg^@Gkz2!GWrlOTp5ij73Y$%xwXUm&Dl3o#=6fjiAO_=S#+rc@ zl{Vokh-2o#@Vz?5hCc5omyuEsluXeW^FNjh zH#M{i|ExE~DA6#rEHe=mV>MSyZsE3JEl7k;vc{mTB?B?n-Qe+J?in}o~A8fywtwS?0dBSZV+zj{hjqU=2ug)qRx0zBll4@?Fy%)QTulF!^_P|M9Lo$YEJH(l;v^UPIBW;or zs^mt5?&@RFB(EPIJLofAke0eV4K&{!Sihb_=}{F_5Yea_I(Q08fo-aWJ@&Dbg`5*h z$HAiO#K3{o;}2Z(Fzn#Q>foVu%Ts0e(nA~0gGHoKpkOa^+tCd_z@88CAACMc}w;lkl!EoZY`xqVoM=ok_tTy23C}K8SP0jh8eW( z{yLbk4grEY-L+hDLt6hJo_E@^8s?b0G&X`G&A7<9fb9B!giccru>NGI`5!$p%?bC}QsR0?|!lWgv+K5xZI4cevptCbr2|(S+?!(-j$MSHOMI%iQ5uA`x4+EKmyBdLRY-Mw_ zQ6kNNoYaPbjJ4o<2-9ywhG7g1oD9c!&1@KVMJ|s^J_Bn&hX%6jSc=o`G?#*>ai6{e z>{T%E&SfMjOA=bVn1TpX$$&_X5-#DNlZ0%}-LFo41*RN(y~Cd>!(EX~eMVkR z7{!l$*Q7!3L#K-5$PVqRZASZ9P0VJ|u}-RWl0lc?mp+p*U2!V&H&PvvVDU4ebiax+ zTZA?s=4AC=hb&zB#Zc549#=L$s7NWsyCo^1Me_w+$U*C8XJ{ltG~>K^sSNjoURkp_ zOmEE)Me@y`XQ}5-lEQ;6ON2J?(ARsox}=7g&uM}{+rReSMW^z~0e#y=w_hn=irM56 zbYn?1?k7@#BUhKbymOk9HpQzh*)YjWIkme)XPieP?J$U**TDVFTn&IHjq~9}!HjDebjHpaLN> z322$LKicXi;+0vx*neGmrFYK9pF^LV}9!ZQNk*33$WNdRKc zb?Uoye3ZCs410tYzQRg3S@P6POf;&6y;<|O*Z*C?3VjLYmOH{n1! zw4JHyz72{9G~`oHrpNMd)1ZQdSABd8F3_f&irb`>RC6`-X4<2JFUk4_A@h?T-`5)= z;8DkpPA}0bbzZJ8n0>AfU&?6IG)WUXMd}vMs#lTqK_Vf_&~{~Mz*B&xNH*Z=Q)WYA zFy$CH$*B@&k!c3z`{Z!D3bRBxSMk(JY%m*Zy@D~9tYNtBY(>XD7|sZSBdl_D%?jfrlS1?8%%E6Fb z?OQL~j3efUSKEyH6>txwZexc6w}b-);?p4ulVyWl;Leb59$X9ybscB}{awfCjrzjK zxf!exkYarpkB&;WOK4!zJ%kP}9DvGbx_T^IdO&skVu|!-cVDQ1t6f-UXM!S+tu_*@-lM@}c=QG*&(6-KdXwGQBn86A*_~+!ZRMnnSW!4rUW? zGvM#zF8Y5E5}l9qY|`y=9uJ)gRf+ZpA>vQo{GhFg-Y%?fn&|PRiB6%{D_lYBqTQM? z)o%W3`=bxi-dgHH-U$qG^MMHt)AD#eN8E84dKja`;k3o2kY4gR+4iuiXMJpF?xEDsXnC z8+G?fg;1NJCavI+tVN(xLtI1MCQoW?-zvkGD!RD>M^T`P!Sn`I(c=9!1y#n-h*93$ zj&%?ehMVMrGWZXo(zpkKVwa?x8&II>ucj8zFmZ$HfbTZS2*8W-{50lj zeGSKIRoI#q?WlKpg8*rBaOlsg=({R9ozW8I8q(Jub8XBn7=~w<{TLSMDwTC-PEhI_ z^_q*qsB*{v47}A0N2BBA*`?>waJvYaB3MYxojz!#CNhL{u$ZY~_JoEaMu#xJYlM;V zZq|b;%`UB9s1n03!=E5hEu zhc(G6+m01*w0*sPoIxK^rGJJ*OP_nJ4jjC_<9XVVX|L1g#9j&ITiZ*AX~HzWRr@tL z{izP)FTcGWfKoZNO-Qz-ME2s04p6v1tq0~r^=~VJ19UbT}r?zm8)Yo9W*tZi2|YUap;%zR3E-# zqOC_i95B$UXhlqqEM=2eHM_ggeNIz93kyj2)CE=QnRGOe#Hu4QEZZ2Z>Fa$xEj6Qk-oL+6^7XNB4xZwhs0`Sn0+G`2MuYq6O4)TjPlO@h+=;!30T(4!(zH^p;kIeU^GbSt znP!|eW;w@#ug2ii-HDq&a2hAZpcoM2;Mbl5u~kNa*tzWbBb$LU<>Q%QYT0XbxaUOE zThVT0yVM9OhQ>!LeF{ym3p0()xuF{^Q3p91gDWC18|aC=r!3%~T8s_vMy%r=6Fr0F zR4L9dMA`l3un+28fQgvs8aOL<$V#3kzE*4%Fs(jD54-%m@;HrSVyb`NN#7;0X^YU; ztKi*(3MGda#fz+=syaM^P1eIl&qT~t(Rq-79d1nM4L40g8(2O2ELV@!;eHzkWgT0K zo3wOdGEG~%18~S@zUE8h&`QT&e@WlaNSx}5Jss#kkA6kJk?irY&Co6iyM#(yIa)+? z4u_!J#bX7cQKK_8K71|?x2F($nk!_f8Bp%WiQPf%;du?N6a`97-MVXtU<=+EIKLF= z>ySiZ`)0yR3t{x*_%QFn=+l0Lg`Tei0YAvU@K~FI2uY8vt8X5fFs+gRn;cdJMlPzB zrDlZo%*ZPB)-P_XUsu3}`P4(n=)e}1HU-()40}`l{yMJl2}Q;#N>H^J8yc#V^SrGc z)Y~u+z`<@N$UUkY=3N=hMPXQxKEG zfU+7We0lCY&JXiT8Z8~Khhq`dAL1eDNnOs8;Dz$DD;RVys|zYreBQ9>E^+~K`-Jgn zO25SFtYH7|h6OhW0_N41={ue>;7?L0Tdr`iF_W@j|TL^}s$IAKG01L^u@B z^>=CXyz7!pew*<>K37shr~(j<6&48{N+0Bo4D!mmaiv8B;^ew2W= z=HuewehJg~+oHNcHd_t*Zne-;x#>`>7(1X$+!`qHs*Yui+`JD6*N(7v`!RG)fN`4k z{JTc&`f$5~aB71>dm2LLI5bRfU^6#*$Q=5yg{fg+L%I*VngN>>v)sEf8c0fN1jyM_ zeVE7ZWY9lW-5#LC!E-xMfb9IY;4=e!(GwH3v3iJ%>0do#jsH`1@5g{{{b?%gOa9yZ zmohr2)sN)?ZtAK?Xo88E>Fc1%mhf_MNcqDId2DCz16PHRnwIf0uS zm=2&nw_Czt;NnSPhRTe}Wqr2Bprj9xwMM`XUBhes+x;o9s%w`gZf~Ci2c&(RS3`Q@ zrOvK^z2|BY2C&LHQPIU}+Gjnvvb%qLdEO*&^CnF;i?-`!ns!Ke()i>c{Q%70XtuP$ z&D8bA=uk}ucC94O&v1Slms>YV7o@xL^nWp`uljITNAvhqNQ0(uFyM4qsZvwhIrbix zuM*}xX7-Ing9DLmy|3^USt0ek4~BQxwEK87&6xMdXcc#^q-Ut}E7y}Ti5Oe~EKQE9 zO&ilM`RJ<X@^SK4SX6%Mxh9;Zl$GY&Pjm|<_h|a(^k^e#N?fwO)%S~e? z+Gl?8Zziy@L2sO}=tMjum2FIgjV9Vw!USK)toN%>O=E`*vuB$6fYvap&Yd_g&^m-ytBp4w=Q%qV<-|9;J!M?~v2nOv0XB{dXNK~R+snMn&JWdV?&+OIclGvSS-q$lD z=?La*_}xing;tJIw!CPg=lNUX0Ow8jT1BzjHKSY_2d3<+HY-=RF<~V!k~)iaxJNxU zxP@iDx-E+OZ*c2V8t&rgvP**`6)2!RIwPofy1WYSZ3SA1e{4PYOm8;noIxCoDcp1= z#)iK0eh93kSO;Hbb5&WV zvfvjH-2EC1J*NP%`2>ox69i31gf~JrQ|mqy*Nn}NffUnM2SDV~dM+IAY5UzNG@#uA zlIszG`dA+BxzPgU)F+od5~whG;{6}S#qyc7b29@B-!b8A6>WA7XEvtIChKyCrca(n zx#oucSRd}fWXM>iMn`=(WGqo1bWX`Kp_+5U=1nCx*JuP?Jw&jDJc2WlL1QJ&5|ULE zZ|O6%biWKxDwIPhtD~%L6#&^yAspJ8rV=3!f$UU0#R};rY-2O-0eFs6Z?DM#CaxdQ z+vZ_@WhPJEsjA{m@RVYZHr;7g5_XtWz7^VMQF5%QQe)_b*2)p=-3riqXTO#A?6=zm zlwyEW7Tr4dYgo?#{3a#$McO=BJ8=c8I;h7!C>RjQ*7G1+6+rZ-g(s=^R?UFp-(z|B za|K2nqI~RVUK88TA}12kRa05Ogg(+`$I3Z|gFcag9k4O8ndQfRv<~Z2k|6QsopHYb z>}^6H&HBi>;iN4Izr9|r9ETumopXfG22cV@pw*M0jctmbFt6YrPI_iwHer_XLwQL5 z^3H%m*PnqUVgV+05kStPwADTvn#Z9htmAn;BFCY<81BvVYjQBR*jEVwYIUgxu8Us~ zrb?>k&+8HzR=ulPI95AbS91!hRnExex7U(FZLKrj1k(4rK+``yO9weJOwgwxKiK+4 zN4Mp{x$7gI>CaABI$X(_kVve@w)sM%h4|ulEf4uFG&s}_l8M2w2%QyTdy!4r{jOlq zT+yU*qsYcvMIq!qbE+I05pZ)QP1SJEm8bAj9q!4|tn@8OoB8syQxQ;-sWULxx#xWk zSe~yxRMv|~1R_%XqJekuI1IiL;=wb_vtI<#^D8-8*A(ok_)Z<2G|w;@G_qgOgw1XF zhcinjG8^VG_I4N}w^NJ7%XaXTmtuf%j|jV`>hP7>Y?=j~`H)D73Y9QLdE}=~3}`t1 z#Qd=Q=a_=s-f-7qYt2Ib{1ZZl8Hc=1gQFkmN?#X&)h0luexOcV^V&He*grO3=~3r!zl704K->J)DNe4&GrxcL?*e0KBg{~7 ztw=77=em3A##LOlN=W*z$$<>|GXA4Be8ol|T1rD!Qag4=vjR0_y&vmjDE7%?XL*1y zvY8hI`a_ta>2d}v!^{y+7MFslR5k^Kk0+n>@v<{F? zhx6%+7wi2o911M23XRQU?m8#P3~+*r1h~ucj^<-MeRk68=w5dODHfXmEqMj~hkJ+Q z=Hsa}lzYYxNF0cW->;M6?8AG_m>(-AhFdv`&C+8@H?QfXLaA1&?)>U=nPqX0}etqB{a-0{3jc@QkZ8 zlrMCPuYi36>py+cHG$N)AK~v7yKJNEoa`YWa1yC9klq|BwQQGljY!>nLu&7};r1|S zca?87rv{Cc^xR|jGDBs|1Qd`4yxLd@(Wii`VE1-lipZx}FIsN#mn$MD2OsffVHGf*o}t zwR+u?V;e=n-`z7aTj1&J22 zxtT@*R#E??zn$7&$a<`=>gah_M{}B~yi>_iA{rD>tV&ffiBJEmvc1A_`1cD7B zkeG7jPh_4)k*xB1Kp5w1VW?m6K5ShPdZ@)^drj(f*c^5PzQmKFIIZev^ROP`T;~xY zu+mUu&C9*Q@HeA2J>yT1mtzH8;vlt0+%dDhf`S4!JF&Lm@mymfy2 z)llKBV}66`Cyl<`8?~S+%}1uv{Sxr`#7CqhLw1qDlOvtjhK5C}AIQ z5p{BNl}yMyG-GWsHk7$pG6y4Nr!XIM`=p#Cz5yY<(7&~CN`MgB-|(a_ZS;uJz?;+c z?ZO5}1sEHIQ6nZMUor#31O~?W_-W0_YL@n7Tq+lfy#@|1GKYB+C*u*oCnI4SgA|&* z9_iWPoq;P)P1oo)eP0STck91Q1xl;YaOrFxs2_8G7AH8=ytuJ`U4v>jA)FqWmJJGF zMe(3J%xQqqMPx0JNtYcSHHu$#bri`Q`m4=7J_|!N?ZzJkaJOp+H`csOXj3xBgh)G> zA$x3KWNBEr*Cu1*X1C6p5B-UwzXX;2 zG#~YpUfZ^tOL4rwR6Y@*j-819`9pB+IWk`Msuf)U@AA-ff+_z|9&XZD_6Qir;hE!U*rz&%?R+18?h#zZr{d6l#rwqjdzcijQ#3aQpgS2?F1vn1N>la` zqVU>D5Xi{(l|Ed{=+I1Hv^_o|k6*>%U+QRk|NMw4X=by|N8Y=M-lf1+LS+%#aJz7o z4)pQ#F3-iG{iSk19gT4bSdr>$-^yyM8@W2e9c0>$@z#=} z>xs*s=j!k!QV&Dh!{ji<0bLLE;Y@rwSe|ym!SSa&VZqbzl|mSMMEn~v)}O6>M4Bu% z@>m@DH=tzv+cfPQ6CXef()EBuq$Htnn3o35O)=2>#8n9ha%kD#J|H{#X}BF=4sWI5 zo*CUEG_8v1^YSzU%W$y4{W!L`+84yCVHj?P3J=K>nhyma6GD8@i8p0x4s;%b!)7;JGscF<_Bm@x=D2=PTLFh&<-hmPcWE?{G>q#q zn?mCNKVo(??k1$F1a5;R0`XH zq4-6j5#|AUc%l0(+gU0h`UCgmI9UhS`j6FLpZaG?)d~TeKEJ0)?&tZK<8Z$Qe*6H& zRB9n$A&?XSDd&jfVCWb-G8(}{!aUx1*!Av`UC2d#<_4mUOIYbYmWKYlZ3x>vn0}!U zVRE9yC~O~hibL%Gup{hlB@1_Y;%7sRI=1tpsc>CJd#v-H3Pb-NEE{Tmo*JZ^u2+vR z5fY|S)3}|Kxee!>DV7aPoNvLP_uJpK57Wq)8Cbh*lg(Hs_G#Q^mchDNgG@Apbwf1H z6?dJ6>PLH{8> zt6a%g3$Jc+@br1@uc*@aH8_|%%wrR^ZuHi-g5@rSKy}@k`06;G^ zIILlZ3i+280gd)^!p|JPNUW#2s~lflh5C|}S?jBW!)QA{jui&HKuPG(ilI)HXwA!K z+tA1M0g18UW!o^mB7UY^sf<7HIkdH{cnM9%2AX5O%T*mVs1!4gffm@vxQg9$)6n5r z9kS1Fj}6Z>*6*u;AoGI*1T4%RSX!P&dm$k^IqGxXV*~yGph^U8urKPQNNe;3B`Sv= zF*(;A$Gbjy-gT)4mE&=E2y9WB+?_FvYgJFtZ6ll@PpxK7NMq~QW)0gKX3e;}_3?+t z%E${GlB8-pm5194AxV?Logz?Kj++j++G0CTHk53JrKX^7UzzyrxPFpD*^{=^n= zh%EgDYF*V~{NgETyBawT?jl>@l_l^_&!u<<$#>$!(4VX^LSNPeB#u;f-*nDX`vWd= zyf}5wf2pH`r14lE*ee#zaSuWeAsLB`WlzM-B32#EJa)d7hriX)nB<*WD}g!l%mFS| zdB6c%&%JF`eNcFDYgnb5R1;kMsnuMOgY0I-QRpL&X>i1L{zG9=J!7mmh?s@0#M~H*r$ZlNH zo|_6c2gX0Hve4`hhGAZ0QuetMesDR5J;oaSg~2-S>bIn*zZ%@A>t9MciF7ubm&-_d z>&|##fKXPozyhiyEiW|p+11HZVf^WUMt^zpUZvqn8XeYnp+jX)6QzOLING@li$9io zCyF{~VkI9GsKI9ZFt6~uWjK%k0p#sy{w$52U!l>08N^@koq_dFVCU{Y} zRhkCgLU<;)a^u)*S7dY*xs~$%R2gm;K@DM%r(+as%oyJF3hMn5ueY@Q`&%^}aGlKS ziy*rhkC~G*L1K37qG7qZ19(w5+(o&1l|=AZj0DV!EF7TLx8b#Y%--;ClK-&I=9)0e zCbswpbHTRlSmo~>^kA3r?^k8`moggHnEssba~K^w5UCq!GELoL#W1(6QZ8HIfN{o~ z;okj4l)oR45_wJi{on?52Bh&^ z8@>{w^G*c?jqFBIP#ZX43P}Q@xyNN2A>SW^fYtD?9%`M-Rn0aR`O|-PhCoQ88DjH8 zbx?ot=V%_Ps>{?F3DUM88Sm6o`cH|e`65k%yRfEaW?E064A?QSgvQu03O!Uxdl~)2 zU;VlOR523mSd}Wba+OKm>#R=Xt^JX{C3WM7&DK2KsU3{)JH6)enA0`)RLR_4K&AU7 z4BRbnx~nZLKzI#sETUx{PA*1Oq@7KgXr7=8X8eosmCDVyE;aA<4l42X)ch70eZL68 zLyf>=Wv2NH0$iPhKxj@EX=dDGOT783$=aQC(dp}4Wi?C;OV`818fDpK8sgM{+%|k) z#WdXt3swENJ*juf2^m8({EII^@Or^)2b-L|hL_O9_KjZk@MX+FVdb&EtKaUIK+h}6 zh~aIt$%ZEAU)Lp=bB(Rn?bHo`Nu9*7fejeW6-N4CZxh%k+i4g4~JjOy@p4M;!H zZ#FgMm~R$xw#3jjs>EwpuI#U-VM{~UF!2#7BgriTO53PXFY!kHz5`Jl?mrzLlC6uz znknXnbynzzQ|JUO4h9^015RlQ_-P$$bOGuG+?zARpb|uohOkYodRF~i9qvE>1y_P3 z@odKm2-Mb(>=QahSBMEuLJ&2JVXn9ttiCcOw$bBx-|+nq^4R+ZY7BadU@D=a{9A)~#Be!JoFDu8 z3Nw4M7`7a{NzMtn>GnL0M0W)HHO+y)OhXOd@Us&m95)4 zj8Krng=g-V;kYNdS2uyRv{%Qy`!zsiIwHj(n?bHJy6New=#}kg7E;<6v(|T1CtRFY zg~C9dR@eSaicr!!9a}I6GNAO2$)LY3hyd1Q5X=`%seZ%_kGi+-pRYN!PORKO=*Ovs}%OOG`sTyIvfwj-}qg^v6r|Za) zG=|7Z7YVEzPXdu8L+?%8aR2G$=sUsms4{UYY%}gxu?C^8sCjQ3iYY%F ziHJ}Ct`6B$*)~>-h!_R81Ol|rSZZJBT3GI7LmdrZf2Ua8O5cwBMsG;k zwU67})YFh$(osd)E4{OU6Qf_?)`pf+{f`oHdn05LM|KS*LPr+5K3ZZxOArJusfQOO z80R7L9hTDiD;hC*JL%bpR@7UXbvOHmfWxN}k?vdZQ=L#lnd=l+2zp6wL=F0Z<=n1N z`^KlYY6#k^i7h#_DH5P?{`jn*&t**N=NI)penq)%jz zF;5Vz!r=rQHqGlgw-;%onJaC52A1wW{Z$!_zvmOMRP8;EHO%36m)AE5)sGVYQAV4% z4lYUq`cfb6(b1tH1}skIJHnUbOf-qu=vRqTb6%qZssl1K3iN*Vz^hAOSpY5>RSF%P zne+AUfYMKWxIcpy#F9g{IDaJjO(-zO$4;AwW9}~@$u6xEJPXPpx$CQ$NMXAj>mLyD z@tzL~qw8}z3u%(Z%HmiblA9sx2t}@BrQNTN(3O#XMJ_0&#+CaWp42YtojvClWO}C{3C&Mbg|A(=6OOjkkvh5}mAAr4WI20i%az!}* zZ`xXF@Px+!GOOgqa~5pV}!s=9SQaD#%-js|hnYeG9)y*zBdMnRes@s;PEkOFQ`Zn445UrR&&ZrwS6DqO5I zjB(&U;eOXO{xWR5hF8F;h4$(eag<7ka`Ajacc3JEtHXn26}h@EIY0Mn=rk5mRWi#y z{qO_jVo)05RY=|uVaNDjFi^wLXEj{wDMD{RV5@?+E!MAY#7@_dvp$xFKbKHBdI1qS z&enoSY`A`;P&Y7z1hv;JvIzRgXr|Zabon#LqRVcaK5pF{UQ+=4(XIRb+ts4G#5?Gh zOz7q3(!>F8RWz$ohr@1=xq;3F_vS>{!MX)&ZVBO250!%vw~QM#J@2FM7w}4C1;u~N zayUc`FkK|GnO7CJyB|NVJ0Gxs5;vru^QAc4wb5w)vOW;*qiK`HmFU&B$N^g2ah!i} zrHaZ0^AxO+SF2J&EX*U|05hXFXsYy99KICl+X%^RpY6^$LVy&0dqxtoaS5`L6#vyB zt|zY%Ov%|nW2f__Zg<)dD-=qPrQsgjtjGaiT=sX?X4|&S>@=|NLsjPGc?jCLPP>k~ z2u0ywx(eTot28ifAwJ{Oc$*!*(yNAE-kG3F&{@B*ys{o$A;59P&g?|u>MkDx^8=hioH22P5C3YJr#MkDjw&Upch+#O69PU>ExCaJB2a?^v!B)x|W}QgJ*H}H6$Hum3HXH1a zXMN%`v#MbwgwkKSHx39*Fg$uM4L4D$k$=^^5=z6NuN5Ca3e~BHBn#kbKX6}0*-P%iO zDF3m|(0!1V1A}ZeJz7U4Z8P$?l?K{+0+)d}v0b9=W>eST3Dqmcft$(d=0zL*bq~NG zrLl&kn5k4(wyMQ_Hq5e&J(LgREyHkZ1)dc{GO>w-_c5o^g4GExnULpbofp1KaN?q*k-OX6&4ic+kxOn`{_`!ax;g3V6em>UYXdHR9~l}#($tvCY6!8u=+}5(VygKRH@UqMiFxSk6`{q4j`ewYDq{TWV6hcK%7tFcbwR*~O*EXP}tqulLm>zF;v8?q| zDwmy-L`09UH;?YGOE?T<@-&uTq(S`*Irg;O)8MFGwqm12hVd%Gt5-BE*F3nuQO+Ps ztxvzB97RQ+O2b_k%@dEQA`{~lMtdYFjMg;+3!n2-AoXJ$`0fz@#(}uXJ(`qqe9nRh zeAYigChqgPp?&!&HAzL;O&}N|hR19q`=x&~SE3ykq(ncno*OvT2OimYjNw2vM0xGm zX)mDav!H%Ml-e&Y?a=rd^&2acDYjXjW*3GJ&IG5?{0^SJa7(TsnoD+TJzzX%7UuWMi$@bd_VQa5#&0Y4E|2 zQF%U=hPxz}VbE2Bp*AkewEu$&1G+p#IgcQ>pGw8ocQaUeRc1J|mIz__2~Do~H*D`X zQqtql(EhcIPOkGGU09I}xcp>%Ne#Rk+yj4?IF&uir!I$W_LtJo{V3r|^_Ngx1BcqE8Mn-QVVi_5Dl31Ti&2u98 zEzkod1&DBTCTc1m^$#`^Y}WQ3dVOa`C!TMj==%kX$@33CC|nO%5cEhYW$sYGRg|iy zYtzxSxPxB^l+xA@n0P9j9-iI-bY2+Bb90~VGQKW>Sa_(iRjLQGW2IHhuxbX1Hymft z9qhblR&WR&!3_9s@f+i;Xi}NCml6}RsXj-4FADt&O*&$6YhJX9J?5~80+wtaQI&u4 zq9MGp2~Vg6+nA^T4VseZw`FfF4q2A@Z&l%*7tOM+XxV}S*B}k3lLXd6O&6}G{Vi8^ zf-!{cgQ56_k&N2V8g1%?8m@bYx|rgQV^0jSR}oCUO$>KYw0!6A z*~5mF-7AZ3xmwJn3?TH9bstM|M8MiBtC=;c^|_8jMD(nn-7?_7#_LI{PkO_W`Y!|4 zSTpEll`i_~&$eQY9A%xQ^j!tlA@kd6OjYQK%Jq&~rz;-d9_-+4u)1GRBt`t~2-&q4} z5UhM^?p8t%y#u3%0*qDi&V#ufjka|(p5Q$#upvt#-Oq7vUss@*l~Wm6nC|3NUmRYe zp&GdNyEGJcjd1#0r@L$`>1{KF|B-yAauE;$h^W6e&bc!Uj3Q z6viGVFH(;AY3osJyu5u?Xsp0l_7H}N1q*LJh_K9JSVTnKIcID1xSIYNE;Vwg_0P^}~#uhAL=rYt`%OgH_ zRP+o8AvKdy*N{ib)B%zJxUP^Rxq6OcsfDB=|MwcMs&G$=UVq7?XqBiK(UeF9MVn&k z+u?S4Th&LktUPUl?x47U*XI2*D>wwU-84QGg}V%wPt>DPmDc5gKdVJF1bey-+)0?A z`Qd$)F=xj{sx-YhRoj(|r+1pker?`=6@`B&p&?a(Y(5qRdJn{wnfQ9r#!w?3Kfy|J zru*3`g|F^dy6NDzYltIp3H;NI8UdY|>OwADp`xX=Z zeGedaUh995lB|VsiWLtYK9+>>IgzkQrZ5gO#wJt{)(iUWx>y>>WP+7+J{E<$6#B^F z$<3DBpaS+40qD#g9V$hkUGNDTrZp{tuYjRx@5j1weMNoCkTHA|z^Sk6h52p0$~v=# zlb$cV`D7i%sd#y0@L&!pJQ4I8Y~(*Nu-F)+eT0T?QDxjRcTiu-McI|x~-3AlOIzVYv4!X7&oHk$8*f=1dS3Sd5LUW3n0aBFdnt|`b znXjv!2-4vh-va5_GS6vGs{t!o(6_JvE%9^FvFLkdxjCt9KEuM#i?P5{`KMnvo@p3;6J;#5%%n zGTFrRVu(&*B@573M-Y@NY`@eN6p48)ktyd<14;^Wy-+lOu;NZuX4^oPw87P;vvKTWy`2U0wMX!1-@E6X z60HF14MzGBq_-`1^9dtgaCoi{Rg8OH;1G`Dz(1U9ds2d4+jWV64M^M|8v-wUHY-~FBep6#B%rUeu*q|p zZ!^R9HFWdMK_oVqM6D^HAhQP95X-ykHcT*l`1`9WwWBbyt;t233-Ob{_s_7&X25va zF`V~I;Hv&PTv9AR`h-&f0D5&!p#+vo_oozVf}vn(kjPmgKY^zq?XYYHH^KQZ5`QWW z=kb;FwyUbv<6Y;~b!&Pttv&n6T-@Y~B&MDLj(s5y^jw$Ivz8%8WM0PhStJf{i7Gsw z3dH>~(B1zueHiFcbKB0`$;`p76VbROy+KlyGm3pqLGv_m5}(6gR^o{;8VFYD>)>#^ zga$Ihx!iaOQje3q?#u?YRm1d{n4MN*NBx8ig^uCC2KB%xAY{N-@E*Cw>uqoE1>$}Q zdgr6&&f*`sGM8XH@_bi~*O<~rOb!DM+L{O$pAzw~m`|Q6XW`0;>Xo1rc*618Ka_|2 zBdHkY+~$@hfXcDRSI|Gi#7NC*OyHN z)?eQ8pwb%5^^C4{@1M6>NU<$H@t2uQuIt+H4CyuCq*JSmA%TGq0nHlzTOiJV#zfP( zJln78IZ#cqfs#X*&A|o~?ezsHL7?Qg9Jgg55_^D4gUdi9APqwe zhTQ5|aaH>Pn%&7(?0-So5hy)-?AEx7^d52kjY z(vrEc$v=IN&Oc5{$cf=OWBgd3>%(0TZSn&gs5AOmR>ABEEHOb(f^f5Y)`Hy_Lp4@X z>uaKUhmynj7cP5YTGAr?L}v4vwI55wU5nf_0OpP;AeLuTx7O;EETQNy*Lj(>C&{Z5 zIal&J3vV}_&mk)T{XKfH{<;cO1tWajvP9w}t6O&jyBnBCHMKw14DBGGshpUbgtj%gGb*~#jrQK*>=-09l zjEUv(^l*w*v}(-}+|~23K%DmtFl!@LSDITxovi}BJ`u=mq?eZ(javp&-~>GLs>6(x z9#F=zLf^j9qMHQ+pfo>fqVJc0oPxbZ=V5?w#lb@1COXP;M+`%s7i$mZ^T~|6s%Ikz zj+a~NA5T>n_vb^4vo~bvJbzg!71s^9@A}$Rco{WzemqzM-b*7W%$e@0G(fiX?IXj8 zU>};L8v4~Bwmg-G|3MP1`@*B*4d`H^OXf(TP>h2`D&2M*&M8H%4|y2ng}Qpb0fF0? z!g?x35XA6ocR@)j2hMRB8TsO1jgavlg`BiHozaVd$``oAD56_|An8kexND-b&Y=;z z*O4`JQ%pd7(5vh{bh-{l#au*}1GXEzS7128Uk!Wi5e33k(o=D`3!+(jYvK$0WfFDj z2aY{-*~Rm-hfd5g>#SI<%`BLzttsO)6Ixl`WjzYZiQ-V+oRn^t0KY2C5sa0}8qjkx z&}ex!>OHEFjW2;&N2CEWzpw>vKnl&wnyWVSzC%%RdD}$aE}_%tS6hRSL-$MYwtnoW zYA8AEE;)3ukXHg5SEqvEBTns^F>B}S9T#<#91~3!U#P2R`l1F^d;NTRlxmj&?Lp&6 zXcr%TQrhzPY#!fw8%%$MMU02OMlU?=RqAR;EAX*-D-B;_=()wv5+x5)H`5=*8}$iy z?i2{u`rGu`Kw<08t<$46Y^vqIP6gy#q+grpXj-$>BNbPNHpK+4Zn6K2bCHB% zIDJr;P*wExa;~ZFj0LEsDTjwQo89u9>U00~&yzz5tHMWUiN$?4ONY;|6RxQjyJzRS z*KP(J`&VklpTcn0LW2MyKiKZvU52Et`MB6J5kYpS$KL)O5;rltq%0-9KZl>-v^ViG z>ez)LS)!i{L;5BV>vQkK+}(oFC}1g6ip$y%ylr>v6e%xqSO;V^|7D^G^J|N>!;FIY z82N@R*Ee?+ZJoBS=7i)}$agqOX`?!+Z@rz1s%%pFntBJ^EubC!Mn2}F;A1k;&2D%u z4mT;$(j28?uyt zJ7w@V3*t0MT2Lao{u|jfHxDH%X-kw=-Dq=>Dw1I^v5Y^2r_v5BD7H!To9%5M{dEzP z63%d7q3o-41hOMjN(m(UX-*<>B@;g=B6 zu4Yw8xOZoR2L$jb5UBXw958vk6o`9RH)ksB1jXSc8CK5^7&=k1dZ3jpd=pV1O0e73 z?Lifc+1pHJo)~}Fb|kEHOazT@g6OaNaMrz+HutRrOCmw+_BMoL#j%PldH zQFzy6T>dl}DpV#R1zcaA%0u~f`e1kjl?)A54AI?E9asM}^ov*MnH4@z1UbODRF< zZ-J@gaZ5pa_E0*%LZU%*1(XEn^=N~$shr`2C}z$v=gYZ3qGxuEq zOWMBEk-g(dHw~&P%guJ8OJX~(-z4;1`Vhvj6Z`PG7Y=GOM}zdYCdj#pS~efRthc$_ z*0V);{Bxqu*LQ&4e+ZlDmPxP7sL1)tKGkfI4tJzn^i@4gSF<`H(J?R#Fdp|!Q5DuN z*AMFV=f8?V`IXsVfLc?c!+A%sZzh4V2~0LSAq8JOA`=7{9-+LeOZv?!0E(~o9lRUPibU+2;_XzN{vYn>rz+f~(9ZvE8tO;Z zzySw-u*pi&MK_pZg2zeR8L zNsDrVexTxf_C7~pKenzM7hou+G-@-Ma~{M1Z@#Q>7hg|hp?;H<3@!>WbSYcez`|w9 zQDHe!)1F(CDHEiLS%j?OXFDg!69zlv;#ws z!BUDuqVtkA*qs0z!-zO_2XD}fdKgk0RZWQmvoAYIuBxl&F!&~gfxQFeN;h4PL^&Z(u=RGExx;u0&LH-Jv`EUrqv=VPKt#sNh zPN#*m5UJ275hA_7DPd)KjvHC&RvOx4X}D)JC)0ndjeXAyMxX-dqjL--iRmQBO2Y`Z z)wmNaW2?IEmTG1!62e=LX~&c$VAto7;dTjG1s$^yfO(a~sxu#?2>B{B|GjMI&1Mo5 zc}7&wTGuJao851r4H~-2VI7achO>Uh1(-5>=>D$d8_Xj$UYMK*r}@a6sbtUv8a$5?zg~X`j>vT2uY6*b{`}O{_7Tybf1C+A9vSeb&*$2#X#(l^v(UL12qO zRd4yFGTd*&Id3Z(=H#IznPi#BxW!Mzr;4&u3#Mcu5$PB zp|Z~FhJYzOg^*EN1<_7t9vb}Njv9G;1+xz7P(WJpWAs;~-^=q=^>()3rGcqIf7zkx z4POS;I%g8*2XDMq4QC{^mi4FBIm}Zv@H{*)fb5^t29O_2Pe3+ajJdzA!X({OS5e+k z3`=^r&u|syvMBqiTlTi9sTv$mZ`;;`i3`Z3-!h`LT}baf8RuPxss_`|h)IORFX%U* zj8WVaBitQde`^0YS1+Fl+9AngLKT^Q@i9c6?)Hp~e#>k)?^5VkYJ#fyIap+YBP5!S zc?iMc8rDJ^zy2f>(g4B$Uz?$E=-^RJ$&>eidqQ_RJRYA)!=FnK*9PzI+I6u?E*KTS zMnlcfBMz03b#EFZ6|O>^*VnS1WC*>`MOaW=Jf(45S#Qr^(pN}yjY4+t=$u}ASE&z* z0ma%Vk{J!-*}^QX5V8ohe?7&gaGIy`PXto2{~osK_UOK2VNtDnq;P8aS(q4z&CnX%Rsc(zfahiO(OK416`oNLV8?N#g8G zr#6D)ih9HUhQ5Gwo!61!E`{E{3J3J#lUkCY$t4ql_~jx0J+B>fml25o)7bR&3Z!-4 zcz~fVtGk;HvRd{(rum%rjA-TXCD5|cTxFng&t&=Al>dh`=u#4AHFRokU9S9-gOc1Mm$)0ZE7&;#bmT zEU-TuN~y=hN{wsmwvu?wvMf(0XJW(U0&Kx=537j=|cUf^xfFVZqK=Zo5p zKUt{5tmUz7S6>zlU+K(V2IhLFo2tHLZ{}8<@_UUzXjhMbk|dE;PHcKd7LMAa1x9|E zWPk`DokTG6HZ=6#wyTg+;2`VBEr-vFpzSM0uh4AcOc)Tm){6*ruDXI;f1Hgq#vIq8 z0mp|E-sfvw==Vhf1VdQ?@D<^e2qy{6YFU4nQyyC%77ZrHV}1s!eoR;4EL)2*qQH~p zh)l=7ETM1uc$6h4E)mH*m!b$6#1u?yatIK($|^oegS>A5RsxNZHU*hnqY!KDc8$Mr zswn%jhy7g}Zr=k37@lAaXg%?06<*Io)$eDZ66-%5dk0wI3am4Cb*(pNt5{E0n5Y|q9{riGcpwprpIm{M4BC)A1L)>FmSf1U6JSL}G~FdI4_ zzI(QsA4~FI#i9Re(a_)0-VRkTOxE<(Cw3jv*7+ybEA5RXo-Duiu5q~Efo;6H!>kJT zt%f9h|uw?N?zq*d`0OLJv*{mw;pC8 zw2mSC&@zu+td+}yHdvMrIBB0@O=JyDik`}sz+vpg^n6`J->(8~6DI>dC!1qJm#$2o z-yz;=w92+H5_UwUjF9n&9f;`xG`m|NuF8594k4ZLSRcMZqMMFZXszQk%ss_(v%d^n zOdz9dXSo_-0+4n_BCP;Wj$0~kGPVunkGvs07Kghid8n8Gr9d5!L;YeB3$(*w)~^W+ zJ=pyaR|-Z#;~)jHk!bxy z`xU05U?2U-#He8%Ve4X=&Zjk}kG`R4HTj>h<*E&LO)gS1F(pN*%!p@m{|Ctks{x~7 zX3Wu)|H!Vdqpz=;U?04-^t;kVDM$KNmFwO&xZYS#0K(YO5rUpBCp;@g zggfDy@)Muru|nMC(amH>cCoy7MGh;g;Ra|2q zBw`!=i#T`Hhxt_>m|HO4VP)4%YFFM6Zc66bGu5EM*=|Zi0Im9n0wK@!;E^?UDi5DK z&Zop=#PSJT?x{M=Up9XX@HLC!!Y<}zJRIya1mc(?89Nq{0E$3$zpU!enIx(fCOQY8 zcXM$2#Q7XE$oQ|j=y@L=Lh4@q3o`%}HYU217s+%4#>OlOsx{1+WUyC;#pbz1^5(T~ zkUi2JUgq=UaF41^f=VgQs4CM(d%&z0vMn4blPPWbD#U%1>Fan}%{z-I^3Le^B}rKk zDOW&r;Lqo;8SbhMUs=`Dn0eg)AqF(wh9kJmT^-G#FYXx?(p?xKacg*6trl^y-FMlT zs?A+Ya#-J_(dS*1E37H+G#oWUI3Y3?NVnb}o1Mc+nD8Nlo7I6{5tjfYPHXeFRK^xn zf>L=b4gZox_g90xLj?hbqZ6S%C+ZlnFshd;H+QpdhrRZue7ijZ3>SvpmQ!^Nx1t75 zh)}66KOZZlwuy^b9~G1hFzF7CkOMn14zsO{W(ZNc`NLEETo~rBAkb-HaNx|$O^SRJ zOx4TXc}sG`=L?5Y_I(7`(=1Habw3+On0Q*biB6CF)pU<&KI+ysgsCME!4Owu+i-ri z4d>M|$9YjXzdO!0y)_pEwf>3W#4iKK|Km07?duWXj%hz*nvE&;fplHDS>rd z-W{|~10O6@cZC@O0{o?Ih%ven?KMF(I>fa~~RlQ}>S4y1wnAa{E zmNZ^oW7Uy_AT#?+CwGaPr$nwQd6LKZX0!Ko6+HG@H57B6_)Di`4HM)PmL86qHLNoT z1V?sO(PPrOpUQ(}$fS)bsH??J@eNnH-wbv$R%wEW6VpbpyrCs@Hr%Qt+9qpJPqRSB zdgj(Y9)#MSG#W)}`{lBi)Fk}{R=QmR#rDU{yo{ewrImw~$P3CUbLg)OU=y>;deZrv zEa)>fXX{EzcrquMSy|NGzwh4VH&bYrhWi!p#j|DMp28-^^OVx&1gjB=u~%ag_dG-4 z3xbhC<;9sAtt61hWt#yG;a>c?Hl({A7j)8|%=Ala(mkBuvXZg5UcoR_+6os7UZY{` zcvkQ_oH5LNbkU+pfB^Yza7f4huyRQ4S@;H$y3VD}&ncL86nG*@6?T~1;hWi7Saa7G z`f56U8|?uMv(d6#R`DIczHPL3+y@6$E335{bU`#t873wB*?jSoWUp2^V7;8r<>7uG z&Sa#bs%@*B;^I{*VLe2p&FZ~>Tv#6H;@J< zBHDCW-RxU~C`8iRDPia*OrUq2-roRBD{zL#`fz?ZD@n;>kmD%~ zelJdhzKe+|PV5`kv#8c|BJ?Y=#?}*?<$K}$o6HK&VL2|DxxQA1^Oxn{(5ZnJ+0ZpP z&T`Vj+-a7`GthtZ&<+Aga6%#4N$YKgBIghIJX>>PvzmRM`cjOPzZT}k$l#O34U2R+#krK2eh%gJ-A9006Zhl z?cH(@9S;Opy!DWq)_FGpeu$f@Ndua3<$$u_O2M)0j4z}aO?*^f z#FUFcni*tOu^uk9o~y%`EShOumkzezcFkQ-ph<4TbTS+Ewp}w6L$1efosxMyfuMTT zs(theRYEEuYp?0}#SaE*-Qbh?U^28|j! zMN+i8&>G^mM;dFo38Rs<^PPn-TCST)O4e@E{ia)poHm1#y$ECD8nF!EqpTEoFtWlwCs|J#R{hlj^pi{AwXg7 z(hIbd?%HVL|8$q+7-ag(tjAL@J$#GM!ED+VKRbLeh&0A78jiiHn}~c1U!__8M7}*2 zh}#wD>#NR1ijDEM&mUnvN-LXtT{WU;ZC_u>|9oLNo)&~+x)$j<}Fho?Js() z5cj}rXxIQo4u6zGx?`qtSld)wu%=dag=kpKuZzTu6iduZSm9wN*+>5t50bM)daMxX z8(q5A(hgu*_bNfZ!U`w2n;Tu?C<8`4kYpDQQeYAg)l+=Q%=aD!l=XZ`15N)@N8=HC zym+#TK(13{4ub#i&+ncbJ5tAWb|hC<)wa(CBHeWkXQ1i@g4H%SWsTZwZbE&RsK4U_4p~d7zB%uun?5=VhHj0Ye4MTa1oBw$lQ`>Z+4AfJ;KGg) z>Le`tyq*Ne2}h_vLvO%F2MypGxRk%yw4Md-n+;snloKi$+J9#!%0s&lmEzG%4Cgwp zu}NsatUqB-w@E6x_i?o1GWwhqo$m5z`hd#tj51iki`c@{71vD<^ z$!49rl62Ef0CWi%gC4&NV9*!Z(${4erwUxon-$(vS*Ky!VHJT{YE|rHRUQwCJ99k< zgpL(i1eNt!Y=M$)Wuv?scBi{Ono7-D8gWlzj?6I26QX4V&=Em}bx_nNGqczGIM~;Y zKFNp`M#-0(&8dinck^}gaKC^WN?$08Ne>sri8ZW}yT~cJ7Q%o-1ec9sWM|Bx1a$@% z!_u&AdNZBn5y~=S4IgVm{$FfNXw4Q&WB-W9+W`;Gqp~hP$4?@Q502Q5c)`$Q6Z(Q}KHr($*%5kJ= zpVreQjiyhM93Zx&Xm~S9yR`F8S5wImhhkA5?Z9v;gW*FX*Mbr~o~y$>Ecy&8sC?4X&2BML zM%xQ{YTTB-ovJgZ1Cr>up1~8*0Ur8D1b;+@e!@HE)Y>?;H+^)v>!VLSlNcR#D6OQe z48%NS+wfw2a3y2z@Z4O5GLy#%z1@N`>nF!SUsTf8|E7;lu%dl~kNiA;7d_(xd9>m1f8VSXEtI=30tv$+{(BbPMSkANBuMxRsCJ>Ii1wGHOC#JN`x1^u+C6hi~3WHd| z1?F-vU5f>w#0~b`=lD2Gspwe)Y&BVhFwG&MCL1;s;8Nr(Ag48V{gJekzI4*X;Orj_ zv^1Cd@?=$&q`+!Va>id`U;y?^n84{hVYdt#7U39ORqKJo*A7?r{JR~h?@cP z<7Ng?9Y8_7=T-qFYeZkAjFx>(Q-$Q<@R%M~E~k5LT^-+a(&?^~hUvio%XGg!HPjJI z0NG%WI~tC2twit@78>D%w7S)aOq|$>rX}mVYE)zr&_5<^DW59DJ+B!~@Kn6UEiu8| z*Fk;~3r?^!kf*Ja7H!1KW3@{i7;04&sdi?`9UFFSVO&5?)URjqDiF78Slu^Mt&!j? zr#V?rq2Qs^-20!Vu5D9P*yUQHt=kN-lg(s<1;eSecMl-07R+CAz^gvg`%d(5i4L&6 zHJGMD!U72~TsVe>-U5w9jNa=)2R7rL*w9_Z=N5ouK#GU;@}zf2|B^|A$vn%X73jh# zW&P16e#uDJ&cG>J`IweFXf^B%KHhmkN-Aw9tI-C{*lI)gz7(z-6xg?C;W`n zS9L58*YsHAI<}8aIz_(-tDwA14|kn(4ISBGY>+%aXw``zqe2%Q2lCPP@*y~r>}?4E zYZ9A_nPma-8p9?jks_0xk)@kR*=XFMqF7%r>O)eM%-%pQ5@gf7^AxgX=4aJuZK9?t zl7i<5k+N>2hl~nnj|f;F%fnr%4X;};3-&2`WJ5({W$M$4qQ8#4|H(wHtlw5K=W~Fk zTb!YVJkRJBbynvqGeiB^LnGaF(!k3M4P^iA%|i#nspsY`LFj9s#^IqMX*xlk>h!j& zkLv(a%6a2m*iLm#eB(_goxXI^5RCZRQaJji5GzV*XL_jH4z*p9^%{(1gj#F_$NFY` z7&%VI=CdLeaV>qknJe9|!Ps|gy*P%3AweVnq!22eHq{)8!6GYFolwFJ=#Vhea1l7= z-F(Dw`o6Q;dYv6^8fAggY+(@BIwGDi(FS)PQL{neR)%{+MG!wSP||9+QGu;Dc!Z-KKh9gp@4XpOVsP@irJo>hd^J8be|mq zSGEnNTb=yqpmDnda{bl=1p5-PIgi%Hm9Wogjw5g`J~41rsnT413N|7{a1b3E_*@hQ zh=?Qq4OeRS%5d($x@g7Bog?TA;y;E6+WTKEtJW=;$Yo8H@VHsOoWNw0s_dFIzcwUE z)1s%+@XyR>T+Z8z6b^3yo(0ZCBHIeFR3M~5S<=pw@@-VuOJTUnq$TNTCmO_(7S3xY z&U$+(odlKUWy#8UyI%F238rjWqcPRQl;-r=ITT|Q`uU=D___-OfXN(_7>LX|;%3A& zNskTI&bSPk*$hm?sCaBa@x~dIo?>o*6-QR_jTdjF)PDuRk+C}hdvN36{;k1TiaPkC zCHoFRQ-_;y&&YJ`t69OI_9q<6!}5=DSO1LmmK%M)1l=**ya{84%_RPlM*aA3#av(h zMB!lyI|gDGChUn*HaqAHmJ&Go3^O1p7H`PXJvrK9U@U44U+B;}C@s~jC`!W)4=-9B zA-_EcibZ^Y$rbHIip6-qtgAhe7N6&|8j1A@s0OWk%4BuZoy{hEs}A>zKq5Q#x*=y-RC9ZTGYR!ekrOdD>wFaV z8mN!*YOBd;EQK$W_BsW;uxAWtJHvwfg1#?a57 zScO%gO409RUA&!j`B<_tx&hZQi_XX5aJvEnX{JpMH8qrdGnC^HQFf9;AKPO}(OzHu z2(}-S(`&u7Xe(Kd#nJ(;Lawj&O6f}`eQc?Q1A|i^^a^%IqDtqHLf~naLiSjM*m^SH zvqh|}Y*^j8`AOzz2VLge9qX^+@GqS-G`!hgQ$nTotj-y7!n$v3ew$mP-rYHzim$X8 z?r*SC|IbQVWSF@@az;^zJGT^0^6`5bvJ)5ttC#A~ zzn%9~O6%K*ivsl^;mlIl{ISM6&kNW}&`+ezttV$)>$-VF0)l^e5JYmc-rG8PJQas~ zbTrRDH@q1xRLd3R5J7N)7*G_p+#&NhSD(5%t42aCnT=<4j{~=-AnT|=I3?= zLn3Jt#7RM)Ma2Pc2S7%{To|uCIwS%iBl)LbzG>1CFf}-N4tOq(<&=%kV-}m`S?+Kb zO7ms&5VnY~Z}AOe2Ino5=BzrmDK34`iL2tZ8l}Q{bg<#UwkFL zNL*P{2-JX-N*Ji0md%LG;9BOI%Nig08bJ?#GiCk$`bCX%+~dM8sZfrnAZ(=|4WK&C zKXD#P;j&%IQ*HRtC)aJf7vc!$p8z-_iVDVg^XlacoJ>gu&Ry@zDq06gN2)Q+G7ZCm zu9|h79~f~wOQdg?Fi4Sd8w}mb+tG3)-_ka>e8v21m3HQ_ZMrKBRA>#GqZUfGxlKHJ zF`m=H@GOu{cY*XFoW)XIGu(XPNvxf!5tykOI0^T=@5CX?|LbwTe7KYPVW$20j%EG^MYLm@-gMHRn)Bv)g2urCqmKFF-A<)GQ1@jt!VGXWXv9l!_^SqhBgC)>0tX z=X+ha2lwg-GzpNv6m_H!qa}b>_;H)Y+V_b#r<>qTy31-8X>LlD;lRU`Kf8tz*1ms% zl+l0c~n;85SXr@AM;_tyW+h%+`a|Y{RXNEXMtpJ<%Xz-JMJMZc-Y zSZjdxK4(zqw@$m)&?vK?da>;Ox`KK}cgwr4l~iMPGuC~A4gza_n5@N=$ThT|Xqs1p zxBApzN+|ok%qw_bL8Msu%wMvfT_5sYCyjyw{S{_)NiATt-vic*i4JAf*{<+4jky4Q zxq@TWk`ts6rNgFnm#|GlC8J~Q@vBwb1tK3``Jm91z?6!6&jHVvQBFr_sUuZCv9{?- z(%$-DWT;G#dg~N9hDY$D+m8CtYv`PBQn>HS0KEmEV{|%Xlp2V~5v#+~$taa`pv?%z zd_}cS{ndczh_kGeml%vnkllKGFw?#x<6*|_92&#X}fGOUeXse zty5w3EbPKZ!T_Yiy5*DTAfm9ztd9}W`7V<6u%XST*RCu=7271f#bI$cWuBB6U%G<8 zbu-Apz$ZAv`UMorGL>ZQB-Lc@L(lVZbhuvvL<$t84TeKXUS!HOL7yeu--R-?1^>k5Lllv2s zS0AfG{%?TvPD|D1)+hs}<*jt!sIQl2_5r66*Ls|3*R{~p*&5MbM|EK5U=27lNO`Od zUpE1-g)5}h>cBvWPYPas#}ywvx8qvZ85{QG>Thwq!TJS4bgny~^HUyDdvj;accpYQ zQ3ZkDd9juByCZ$5Yd_yL@!BU+Adwsaot!5f3A--m+yB3nZstng)qePG!Ry7n0S9tx(Wc99`o$0E##TAi|I1u|nJpqfyqF+`~7F%h2>z=i6UssVmM^IW* zj=~%l%+`YfQY#QN4_yoq>!lPs-GWf zIKr5UfYpl{3UyyS?ngSPN08|WZiZowadD%iE#c%)!@he4)9SrMoIl6DgyZW!Aui-A z06)kxniY1AlHzz2an&Keczr@X=e5C6#2EkU&;R>>|F8e^|NTGz_kWASU8c*rOMffh zWw38EF|;?9s9*>hi~&F2&}>r$m{1(HtYE*4l^v148wXX4tJfjn}3%k;xlff zk8tBh%c8`Ur9<6nmq=7cVGet_L~uvmA)$uk$>7TSD>!32yJqQCF4jplI~F(ayI7ZO z!?#m2LO3l>RrRqr+;!44NdA^UGGUq>uBi#wGLt;i7kzto2ag0#lhwCQm<0(Wht>ME z6-SDP7!U`$S|2OJJwV#cQjU>HP_OEe&7S?rbf7X^PFF1AQ0(K^K=xE45Dz?8t?ZUM zz%^ihSVA27sDjJsi^uBr7qZd}&HChf?_!Q;3VS0wmM)kNJBRgM7c}c*J^r;I;A$nD z#x_^T)-jLmqVZf9(&rq6Qmt71iV5X8W)i7x6wegMA{O~Uk~mF8Tg7zs?7SHufGX(c z%K>?cSDVeoPT==BdDV$~m@jt*Kn4GaWVDMVMtf351Oj6rn8b3HFgiUjR#jd9KeI5I zWk&GQX9*ELaC+m7#|8E63Y=k149K%40zrn-%o=X(sUnWK86GrPQv~GU;0?`tHmI=c zMu`XIkH1N>h?Hyf`#4M7mD}Vzu!dl}?*vhZt4lJ44!32e+UlJst*UpWBQQ4}ds@l*XAJ?fR3B7K&_=~|Yn|x?Slh_+ zgekG6>6@Pq|EpO1OEOK^dwJ147^wp*AFs~!Vu8pSrL`j#2=~mgXkZS#6^ryM8ZPU{ zC!fzOT|T4C%#ciL#Is*c_@rtg2%aGt5WBu%E_JIHeW|Fll}Xr-7y8oIO=vBO1>e5! zCk);MvLo|f7meex5{Ehmu)Av2(0y!`kLgg4t--F?X9Pbg+kUDQ_dsc&ISu*TAr3t? z%ymHaaBAQI({Nk-tlmVlk^Cq07USSm063EmycJ>iVY5o14!`Ig?$u8_GR< z>Vki)wakoi1c31kOflwFGHP0nLM7>B(4)JYmgbM?G)}`92)wMP^Ib2E-?5d#I04Dy z`1RnzJ>b}$Xa?vMuQ=%x>A|b($3+ul4yM45fA+F)02+h|>Q3qyk9JErf2rd39zrDy zP2^-F0(V#H(y=Y#E;~LV65(Uew_zm!!aI0!*@&FwCqhG;XM@YkU-r>gp}1Xx_!QK9 zNLaCTt*WK{huW%ZGjS!unca!CKhs?%HLXV;x$oqb#Hk%7CFh_uX%dB7^I4)P^`tTeL3ryM)qXBY4LHQ-{PyOF-Zp2pCIl*W;W zNmU>iYQqbCwgAP7`u%k(A(U;Af_-fuc+uJj()&|cxCwP3Xt2PfER4t_gf9A{pH7i) z9oy-!o61a)434ccQvvr8Gn{|SCd1S-2mP4b|QfnbjwAwn8O|JhqvpLw`m)NuD3(EZzb^l&~Nnsd$+ zNKZ#6o1s8_(ti#C)YZ~Lx-5-w!1d3>WT~uC9%=;NfbR6;#i;du8xDaxl_!98RpEx3 ztTSr##+bGmtSEv&xmWRXAuCNQIHuomB>)|t1fb{Y@RcY%eQb-?ZPV&t0ua0(w;j<2 z=62bLgRplf-|Jmi{|uKcp)$%emAQpGk_q zWyDvlSqFSi@-_?WlMp02EGUGUem0hzZ`M**EUH zum~flZjDeG+BNq~=-E9?8rHkN5SZ?FgH;mKF)r;8n9BC?RuTtzn6zum^Wysxzr0EV z+}`zrR%=ltHnY0=XdWW@rTlGpxLtx7jFhB&YS5j*uM}1 zlL!??q1W`?yJ!L~3org^hl2L8b%iW3Uo-5T)5KxVK`kA#A0Re%XIloHjqD$*U8Z}p2&>-SygA$mBS zJDH+!uL3SEh_r^L5c8tVRO1C!3rEg6BkZwCm2|UoahLdM8&i5N4(;1&$pEA&kRc~n zYNy3PO$hZrhiL(B4aLm1)nI14rq;`#ovYCTIrhS|7Yrekt2v&@OE;nN&<=@XvFT?h z?;XG!@=_QK_3|ekPV&3TV#0HYa9UlAi6{u2VHq2kIVD+`a`zk_db@-U#j2`!k>kTU z9#cwUrm$M-mxcHEq&fFk;jZU@b*#wr>XceMzsarZEj+hc`q{)gf2pO7x`62dmfKC0 z<^<5-okNiGjoG2>yMZTH!IEm7N9@8@)7ZR2S~kQ^SPl=Jdv34A;VzeEeTmRGJAN{6 zmNSDY!w$A8T6ES@hqXVAz8e!21V&Ne9sQg59DEs=qmWHo5-{HQbDFx&Ckv5$0 zP+(e%;Pa1%cTnCEY9Nz8RxK=9BSpuoYb(tghq)o^hDXiA{c+IgF`7O}rm2MCl(wr% zI174{)WUizt2$aYW7Uf_?W`}s_JRM74f7TM1GnZdsyROo4(;xufhLy(JY}BEb7clV z*Q)2_kUkzKBcA@4v7Q)17MMeW1I6~{T1{iC`Zp84{FNzvT!RCL;bwQ6s{UlchtC+T z6KiR+d^`Gdkykg<8W$K`DyQ_xigG+NFw1Q*BUZ_vBhXb)>Yf0 zhZh@-@4LXW$qN}CTEJp8@Kvn-esJ3Pkc$svzfz5;$Oc=Zp=Y((-TVNN1}nqcX8NX7 zcFQ&96&wnd=6az^?2vDd{qq$Znxv+oyMnPa*VDpcvTNLyKWrW8LUjC01aE`GT`rx2 zcd63iWV6w18d1|&mBy>A0@`_50s7@=pi#}{Tj$}d-7iZW&fYoTvZ`fNebvO}uTbY6yTcqR zFU-zBGP?|8#4ZeS=H0hzbZ`xku|Hyp!k^hWh2gmnSd}1 ztxXUmdb@f^j^P~-2N`fRRV;zm3xqr5DpytX=bBz3ZM%dS;4fa|RUmF*NpZ~F*3mGSUf0q6yKCKc8e@NRfww1T zhq}9;k4@`Y#G}YV$$gy_SmGfBNFGhDcF}9I*XKV!)rWuSrRhqv#e33AC*#A@SZ697 zE0>)tT34>LUKFNiY036nAntl;S+H7Bc&Ay!>SGiI0+g%TSGQ~HP)&SD@MqS;2{{3x zEr)X3#8x#Fj%2TL>H9q(T*5Q0MTd$guI0!mQxrEmAdkxiPwo&l-s>J@cG(^3KVS;< z#}BTg;Ziz}#{x0#@e0Xg5=C*Y*1F6cu*TWTK8{dgY2fRYi5A!J|D`qx6A3OwllJrIG_brz?35 z*w@l9?xKe?G>X!^4~^6X$vVXSqs8gi%D66|yCy(~giMlip$#9A@o`$xuB%4A#CVK% zxL<+acXA;Pt545a&_zL}5v3Bg?RKDBAKgi1wb<*=uS0qwSC(Z~{Ou9$P(no@&ET;v z++(Hb3|T&V=S*}4bwypl`kz<3N9a0!DlB|RGdYGc7bvqt;N=x~=y zJFAt;#epF#Y!SpL@P;(ReYXAidr+?lPo=O_E9rZghk zmY<{Hw5C`U7XzNvtKi4q{bPF*3@%G;fKvHh7sg}u4-&|vBg83vt$z@u`NzB4M=L`t zMf$MjmlMUliG5{%|C(zX=bV+FJQs#9$@=BLcl51A9ke#tb`yw%M)aeWwpl|Niwcat zjLXqsKp=o}5IWBtDE(sJoo{Ms9$jQc1ywBGAn~*btj%Fq!W8ZqS3aLiJ|op9ZUU$x zh@zk&$$_lSZIw#2a%o`Y`lBSDxy^T`zs4P*-BQ5I!2+L~QsSrEaq&TnD{?bh>9AWOuIN5!1l+v`!83 z%2k;{#%Dej)AD0sxXWZUkI*Ie(7-}lbp|i+vr|b|R*^Hdb#+ogcZl#}`-D$zxIy{K zi@0-3z-rF%%2k?oFOP6iZA}dvnno8W{FpWbrw~<@%Ysj`EzY`Q^_8a&!{{bGf4sZF zDA^bO&bGKJ!(0 zQ%)dfwknt6fls%^FVM7%4Mm|0xVj$Q;>u^97pO$$qsC9+Y4BVd?#bQ`>KC#HRV@#_ zS#=O20W^}mw+O48MvMY<6VbQ2-MpSt&miokgBfrbH$rRiyl^OAV(AfTIHzAsfjXY5 zRqHy9N?426+^?h=9N|#iG!S4K<|)D47jJvM4J4IQm)W(M^fe4phta{(Q?;@L~3n%$6JNCT?JVw%!7c?!usaaGrm%o%U7^*FBlCGBRr9ECSAT=`0LX1fC$UXx)Qh9@+iF1t-W z@yS{S;_Q`zl*OX4v0sDQSdVJaFKUEbUbGXsonZTGf%v)z?!&M}g&_{0H+8{tnF!o2 z7;oOXhz=kYiz2d|JI577PK2X?O=<5ORM5?LveNAe%&^fA(M`hy8#-XYf>H{y8*bj} zHd3F4DxwHm*M$Sz#EB1uHyqoKSeoX`_|msrxZ5Q#0q~h-Y*q@+gA61gG#KU{ai`0< zbKz{H@I4_>dHv&c3a{p^=>GrlWYn9J7hV?Aqj&fHYY5DM0SUU22~rS#NQeC~REXRw zpwzez#~J%7My54RHUm7tRQVS6LK@dFCG7K&%3AI^UGXoErsUFriHD!YM+sBOQ0i{E zcrlxiUPXE6gwBwRLXX)YRK2n;HD!AY4lV!6mF5<-`8+)Ebge(n!Gx8^DB)U}w?k_L zhkB9Y-MssL18!7SRnExT^!lSSxkL^%T->8ZxgJe@Dvqn`Hb5rLkz{>|9OUQQC$#y( zWxd~r^VoW6R=Tbo^k~&nJgBTXMQoecEUAWQyfw+e_-~lm(=mc5$3j|fk0`WH)#1F4 z4rj#h$%~KC!Fm4VWS6f`N1L}0tPE|Xq36(^jWs__xo*t}x>)`(Nzm=7QF!*!xL-or zenhp-?c>Nx4h{lY37GiMg>UbIlPwMJ1P;k{J%joNK=9*4+)*!V-;8Y6Oz<%^wA>@5 zIY14p(ljlotg_khP-YTltZ3(zuQ6;*b6?};fRX^=5&Rnu%?sA*W~{S~3RcG>m9=~c zr4@{8QOq0%B8P9c#6@^QGYlc}M>X!`Jm}U2@q{qk*AyB3!_5@YOtWBHzZ8f2Rj?m{ z8q~A4PXKMF+cz(Q$Aq7vRYhd7Dffd@oN*#N*1NnOd*t$=QZAoVI8Oc7>TuTyqdDPN zq%%ouPjbXLH03KPtZv%ap#q8oCparBurw|8Dw*Qa>q7Szqtd6L}4>tB%d}WT+SOkUOsOP|cKPrcdp$I^4bnzQf`7 z2v;88$6~IYhDWD{>=`=Vb|9N{0gSZkPNf2635(1olJkemD*4Wq;juiNzt~E{dz)qu zAx@#+!%%?{QkrugjjK%}ER1FWy6*sBo4A;oo)c}W%NBkl3!?NC{Zr2WA^ImjR|hVn zV{8O(x{>OPG{FT&eE6(>sD_tDe{t~sdJg|2|8#5nrHtg&9433@af^0{>VQ_5@~lmFhs{P~JAHZue+ZRyDNTkd#? zdj^#~Jj?(Gio&O-`f!&^7yZVl5A~`KNt7jD^#Noj*G;7bjpQDz?>XT+HYZM@RmtJ! zV1(oYSE8gh1>F4-45-&3+4DV@AY@cDN7L_$L#cvwcnMi7Z`B4EJXj&F30n7cci~dK zTsj<>I%_%nLRc#ISZNJ)c415)K@~-Bh6piyKUhMxbY(!{wz5v7)kYN5YU^8Cby_>X zh}VvdG+GOutVnlw6ik=9V0s9t@HiP8>K96Gm)_ zROwhqd&r%CW3%3_A?sA`>ZAB<>ga(Aj{ggPi^3kRAAP4EnXh})Q_Kic_V^G{Vy~Nr@}-a-8(-R+2e&3o+t-5i(rmGk4uv!n6-_hOO~6Z~Q71*-Jso%U zPYarhO64bjsr*AE%{v8I<*7oLY^7=O>b0lIBT$D7L5tR%BONExb*24y!I$n&;`ZW0 zpHQg~&JVg}rFvz1l3fGHTha~%K^2mW!K3pKPq4)gg7sgWg5=ajTPE|U(ta4t3 z8mCTFaCQ%S9b9<|=PJROuz?9H1Z}KJUHi0PO`%pnaRgtWK8mEvT_lZrhdIbp2SB+B zIW=2kY|9X3wXF8J!_I`3#=QsfX)?PyuTTZ~M8PP1G{OXIu;KJma~0T}hX_N*dY+Pl0~^gg9i% zCN5IAJdP0Zt30r49%j0%GZXbD6@peP-a?B*gwxIPF`kF{%G)oze-*GBWJpRTKVXtWmnYQlmB`%uj+979w6;vXa#nA@9PWX}0{zrHnSgps5?R@e zBtwDUb!y${S}OYN^h2%;%P0JnrZhGAN+G~^k7&Add8!Tn(n;6w!FUc1`_Mv)#Uo=Md3|ENf>TvrS(3#3u%Cb5(mz|(380M0zeQ7VX0bEbU*qX?Rs<4b$ z%dk#Pp~gdD+0{uu=Z2OqkuoT12VFhF=b%U83%U~V5oOc)+y_#`$DL!6=8VJ{+fdXZ z652zg++FK;smU)k-uFw$N*QR=lCwDOCr|{yiKk(?hq%xA(flYdTkqh3AQMBxfr=3m zyEEwZs=+YeA^o&IhX0hiOnQVRQVkNONEd81V-^LB^!iEXcF>m0fvf$&m$1&vW3_AC zCyA@oOQ0^gpjCVFi#>SNhVnHzoDms{PwYwZi+tlAX{Z|WX*Jo}_Rr`TvoS0bon(kw zpU%TjP_GFFu*%LozZ!XeT?L9iq*rih!+Z1SJ4N}7Ttwy1qxRl+{D+>;jWT4dnpHl8B9NL9L^tFcaIV3aNG>yyKr5>%f4}%UHjT46ZU`^PGic^j4+9(%Dqa?JPy;pCuqyp* zhP9x#4n_8(kG0`;4cR%q2mZ}q!~=?7D-WU@h(oi_-6-fRAfPjv-q95;&JmeI(KK>= z&Lalv$I?)~Y4%~E0Y$)mK-b!0-_Kq;jX&NEn0}o;on23Jj8xE#mDoGEb}&mDYKL|* zPb#=l{=JvhHT99R6gPR*Dw19Nbv1b!&pYXRptRPg+rXVH(n}=WRXQLiV5OVVj@hu6 zJJ4}wjl=%S6Tg-pop0QX{{@pTmu3dkB1g)$pZ61{VHGG-0ws8FTt+!qnaHSm_s z%&-PONMv%%sRf(0#8Ydd`&u0C`rr~AvS#8&3;M45H%N<|+w?Iu)Y7JaxdwKOlQ*+r z-&>g)OE^3QY02Vnj(5`1?FtGSF8onq-2!07is958rt-d5#2)IyaTiJ&6Xwp&1=qwi zHUqRF#^Co=aSciK8bN@bK9+{tcYyl`i?pU!Ak1N0ks0M0M;o93gGJhaZ#_XlIhn8# z$XJz@f(O)}l{AZs(dJ*qhI^8+hFBg;8WuKD=R>wb)P}SFu?I!haIl>vaHgy#DIpa< z%pSj1(tKt(DpZ;Dq;n|$%9B3yCVmMGHKlnS+bla^@pSAlA((D=CF32^cF>qFeAfF7 z(C-{*wAeT%Q((H30!jc*NBV2bOB<_y*7_!7p=7#)cyjN}_Y1lzvB-(^% zDHmG@tURW!-m1{bMd{TfyTcN}+&rxp<*fQQdr)mgE5NmBmDHKHz4T2cJ&luQBRk}! z(eU$I4~1Z?T|&LS_YUdQQDW6J)QG)tE|bmXrYOGRpl;D5RtwKdsPz3JaEiuBh;=)U zqHB;dC}h0tw8sQ>ZOO?A`+hYNYlJ$C0m|GA9_3vAtUeseTMtCx#YCgrb zfDbyeHvWb{nI;Orm218VM1!EWUSu*BCR~xyXe@NJ&Q}UhjhG3B?DV{LxL*X4Vtzf3 zLuLn2X_mA7!+8dLz+QUmlpEd5#Ef%23wRNh34^!Ux`%Z@l7~I&U$zc+h3Yh2 zhyeZ!i|N$rUFRPohC-)(dTb$sTj#=rYFMESoch&iu+&5BU{FxFcjw3QaF=T58KpSE z3ayg7MSB6PM>ZA?0gj#ArO1kn8=rpfFr zj>rVWTo0ixv~gAq@vgBcmCfwE)i50}TBnjkyDPe`EtO^_Wek({{!}5_FJIP(07_#! zR6*|TL&fj3_4%x&S=DM2n#USaxncYnc9!!e#Bis>jb`lH^gdOH_CKV<^_L3a068GY z6P>z7`Obp;Cp_ex9n5UuQTYq3bbkn(zUxPLCv^pk81>MV{{y4vN?bW)zvC1@YAU8z z2TN%%!b|Xw1LrfR=FNVuwC@}yi?pcOsoopnOgH&3yWTxD_S`%bb$Z0iuNrQ2CashN zn|%pzI1dAspM`Mc9xFZTo*iars^DvYMjYjn|D)dFypnG?a_aii32ysAC|5FxZJeKQ z1-H_gQTNy3;VzU$Xrve}g>bI1ku03`vy2Z5ZlBA9S%VR*8%=Q_%oJgu<~v4J8Y9bB z%^nGe?nMY!zEY)g_>{4=OWF#Vk4gI3z@Os`7S1Jz>`K5vlLm&M>t?Ov85_aFjJ&;* zW^e)5aHx6?l`eOIGDm@pxl}^Nv=|=T37!~VojS^GPtYS)V-W1{Ra(tjB9aIzj}$YE zgx0VXbiMdCJA5TucM>ZD+|FnY>acqkKKGAQ^La0k^r6ax<_h{&Ca(+`wHkYFhoRC+ zDiv_ORfoGsnsFU=u$~hXfjkWRCPh_)L&nMgq(EE0wjT(*6S#u9CNA<*4}IUNY4Kt4 z6l;g9(|G>d+To^>R)3E8J}LC;nuOboGNmd3aj2=`ONl?84{?V@2Skt66dgwj2Z8X> zfUGY0q<@6MzpKMnuyjHc9u(hHbaFIxgUZQeX;Rz!c2wy|r}(Ywp1|Z9_(C3JlJA#q!SNwx$l`X3HR-@!X>}=?Nd{Ogj_08g zt@DoXzn^Nuzf{sWzX3}JK_A2}vkbz?c0B?ebD^KoF4yjTAc1Z1j6WBL{!8!xs9r>d zPM4C}^i1P_Af8W0rg0NptOAOaX#!C}AT?I7y4;@&YC^q*@$& z?`c`m6%L0InM^%(O-$u+w-2k+nf#kkjGXrGzOXzTt z?YqYNzB+@!){&qKUJSD#HniqmmowFg{Ee&hC6Lxk7Os*BvWMGfev#Dptl#T7{&1Cy zN8>TXC7Dwo)8HvWRr(h5lZJpP?dZ_W^U-8Tc*=vnt}2Y8AEz)|*4*UFw6x>o5Z zJl2!2h6Q-(Cu9o1y!wg!`Qx4ORu;xRIF$NVgmavLdHnng$-oZIr3lOOm1tqL0Eewr zUEhSg-BclZ3Sf&B)+x)FCFk=t`gRTU0YmFmfX>F~M~ZdAX41(j>EE^;5LX?Fa4Csm z5Ej;ql5p?V@$ZttnTF*~@VPMD6}pBW_fM#_;WJa)+#O0LKkEvW9@?O-hU{3kh$Is9 zXs~7C4wP6rv`|QBG~AwQ!}zyvH!dbmONS}SL7iMB5p%-49!R)6IAFvjbUPfGV(mBJ z|EUdkr8JAA7#vW%{0R-`95;=Tg(M3$4rCOX`Ky7)+{Sm{ojl-gpwd0q zdYxNP0c#Gi1Ck=%M<9S1o*LD-?`w9W2c;;i!v=6h&LmO!=p^P z+!e~~2J8%EA~TCl^o2d5NOiX2kTp3-~nyEjyFz4%DxSTdf^cD#BbqP&r zAH$JE4K~1T#1Wa0;~f>c)G)bPeL^!tW*y6{f!P4iWwJSJ5!Yzqj4h{L5Wj`WS8=$j zbTOL+UnL+YAtx%+eXK??5XnvhV|H=S-20T>zWyEfkuJ(u@OHWHi47f|mGd!Gy3AkR zO4hvUuH6t?qE^6kiW=AC6LC!qjAg{Tte2yJGoCuS(4fw1>7dswWSTb~>--;daN~Hf zWgQ}iL|{zpP5ABRc%D0zjm3P9*CaxPj<#>ts)Z#bWEgOZJgEKnBB+ zXicuLiMfb_83SU&t_?jr7#TeBOV-cVU0^;I068|4tLVpnR-NyCU3YOJ;fJ zC$H){rZws2Hpr6cns%E#`cxX~U877Jh~l@)j~XTt8t|-X_Xq_M0VsBbk=RP-}P2H6_Zr4r!!*VfRBV5V!+ z`IF_dCR2Zm=U|w61D5JNSGp*E#7YBFw#qXeX*%T2R`vucY};pwsy1w`)`v$Y)1^A& zIl```d~&6kgZuH938H?($3Z?J21Xh&I?XJx?{_ehEf~LEIHEg(g;{B~zE0?1>|y1LOT{CK-|uwFaDZ zwAe*MaslezxDv0$@fWQ_y=!ze^fmTJ1sT9I3S$zHFa%yDqTfnH0&-Y|VLb(4TPMS; zf^L3-Tuxn?7`s&CJWUVvpGxU0EBjFkH)$KN4_OcH!0iQ23STfo+!?!oR>L^wXFKRIlA)A^URbp1+}-r1p2 zqbtk<1F$q!!PZ4;^s%{|I9oBq9XLu8NZ7&n5D(?P)p*|e;g|ADt$&q>FQIaH+7{n_ z$;@c~D;?3qul1T9#$^YJtYN*Q>sZ2MELW`b+fOJpHc3W%qHVMEoceRW1RL*$prXy~ zq=D?aARV8Ppdi+D<#swogE6DeUG)*(8dA7VfzU`pq(oLT6U3tyuHMDc$mT?qC=ez) zV0~Ors1IL{E)c#0cg$pBl4bN}X@Q@9=K++~u&JcH4>N^n@7vy?eugkh_eEe1-$Q(rakQ_L z`HajKU+%PBBGV3utkMkgIxlGAM`8^$88n*Y>Yyw&7o6ptvUI-}z(sIlFhq#(Qqt~_ z@~vYWrC%ai?UVYGP`XrVDin8UChOd8`YOW}+FJ*$S<~^(XT4oPGsC9nXe%e8gj8DJ@U4l7h_{FaGko7oANshzy)s46Yox^=>URTohYe4M8j=6xv$M%_TE~i$i*$;!GEf zb0-6KpO3)+iE5j#WHquWk_jU#^E}o3L#i11{XEo%e`%#tH=E(PKF~+@&h$p>+ciY0 zW9#r#s5B2W*$)nJ2zo7}7VYom zvzp2+Q}~t7TEF7Gw|jRSH(IFG2p1^e2xCTM%uXYlT=FW81HSY6&oKR8CXJ*#|{($XSx24#$ZmOFeJ?Cpy z=+`A6d%9$3yq6MmYLa@`^cR|cj5M-Tx>Xuh=~_efx?1Lr(@+VJv4A>(eKoan_wYz) zt#_R?Bv2=DO_umi+|^9b#U`{eiKHF9q^mYg$pcRjHV}nyt_W~G^A(Vo{4|lr{u~xs zzjCGdwWr>!+mlG0qVNA_^^k7ftddwdI)r)7^%|YiifMJeGJj|}vB-(Z;c+GXeGwA8 zFp5TCI#{R3!c#*b(x(Ss3Y_hXNJ=tfJKkHJNvgZQPsfMN%Hf*u{MKRqsWjYG(nA>{ zM*>4Je#=m1kvRQyabJw9TECAl&^DjJcsI zyL6Pz7ygW<#j_R5roe8asf=GRFD0h}-ejv8$tA4L`+04{igqkvjCr<4aH-y-tPS9l zL5Q1&1TcksbTP@=VEol)+E0KVEYmY%+v=tuuP8V9_(;&*u-$sI?XF)MY1vt_jI_jV zCUxu4U(9?7chQ>#W(GX~8e0a-<vwTWv`HSvxSd@^7J$?nc6my>_5Y{Y(z(sqsWzC&8-Q z*p|ba3J;Qx$I=-L6=Sc{OL0i|3Vp=n05Att3>p?5ODW_wlF5%trk(Ui80Ld@o{vPE z%^pyFu{Dy8yQqVECM?yvO4_9t{w)Gx@N(JvFqY6702r||z6eXFGF74!PL#Q8#^va; zn-!GyD`2C_QT_p{Dj-c+G0^o{kC@!Y zUQ0(??G*V|9P(GHLzAQ5R9&zspZb`!ph6gggMU!~|Ep0|zG>S&aZ-mH4wG_ySC<3Zcq$IJYY|SCABnKCY$fT_xR1_{$y5|BRhkmgTsuMJE&= z5_`^nV&>kUZt6x!nO<)_+vO@pD%23b!D*0yG1K?eU5~DyP%0~aVL23 zb|5?)^H`#a=amLN=w1aG5&+OrcI zM&u9*b&Q2LP0b6%G@)yeiVHZJuKK8QsCS99;iNW)5Mn?@;m}Gd<}QaWxUtxEe%F8( zc$tQgK5cy_1^TmPr0s6$ln1M->XouozvRgVWa4_qbP7#tW?_Kv-!nZ1(6P>Idg|%njZc)9c{~_0KyAt1Bj#_5QX= z|1c4K(Oq7U66V=fsmGOcy^C|tKjwRBN3AyHMnK)z%KMrhL1<2SDmLM_9)rHI!}D;K zAG>8f?RUs0v!pyCO8*i`6Kj#`Goo~eyM#4fWO9}aYz7BC z%oQED;N{rN{YoI?hp&cmJ-)O?3>^Iv@UVfxb61B66r-H?jj?pU1Uzb}$yLuM3O-oX!c3^9Q-Gr$=;a}^GUnlhXIo0D8>nkKj#uFBKz|An}=biLjC(WKk-$95n ze8iO%9CC^ug(SGvqXoZ+W2U#1)GML2hYq=o?@3Po_Rt8x_Oyq7(@EDaoitYg%$EYc z-4CeOu-{YZtBJ-Ys;qLr8InkN$LN7e2w|q(>5dESQts@!-c}CvZm@*(hGis1*V==c zMxAAxAYci(#gQ)=oXc9gba1mrh*Y&|b9=h01HAl`HPGwmaF3E!yM=WH(+{X>LHMTa zRmbB6$G=n$kj-z9)vDZ*z+KWhEkI=cQc){&Eddjif)IDo+>rQ*}h z6to;=M?e+a16Z+SGLqKoLQ0z92+fs;OHr7V6wMSy?vK^sE|X?PiO?S-CFox`T6~HO z!-hx7?~3M1>*Xk6=CBDnng`=6l`x-6%+F*188oy( zB}R=cws;jf+#iB60Tw&_vgZsv8z2C80RTK6!WD+rykV$pdSKmTF@e1>KyC{S;Rql1 zGh^wVBn^MFnHS2}%d(lCP~?3&6rFObz&1n5jR<1@OV>T8B`_LSQ{V>{-toJ3@U~C3$z%-XJ@#T%|9S zc8bb?q9vQ5N^g>fk|&Ki#l33mf^%YITxT&hpsQJ8k}9CA^-LF})FMavFGQt#h_jc= zf{D`raNp1snh|W~_@&r#m))(&xT8xg+Ej-sXiPOpSe+U%&{Bh3+hm;;ITY-Dvw&9O^MvG)19JUOcS^w~w zx?7;TXGkq@z6j|3F-W@J<;$sVU9`6}xcViIu9!1ZkzJvujVXXF%@Rzx^_Szi0j?WBSN&%O$}>!9HfJ4{Ia}Sn^k58H!Qpt?JSqjLGCyH0380OswWeEg$%Bo zP~)8YrYZdrq+X@rOCFu0vkP0lsSFO|Ew|7{kL-2Bo}YBUA+55Zc3Cgw>fnzfggAe4 zLfsaPzd3e)T?F&RxU7}5z(Z1CFy`s_7RJG4Y;m$JJA~@SIKLiCJ0l{4$`X~DY$!@y zeyO)Pp>Hp$cgHj`O%&7 zAARue;&2y%;hrGGE{SHz-+j!5h9JJq->+Po!hfp_qNLNU=!RSJRb^1j202tInS-ov_3rY zSpT7rrc+5Tk#O!P84n5XO{>==%~;Dwm*K$@%Jm=iN?(s49HCtw>z-|WFfyoJj@<@F zZ@}MLg4zdQaY_?gr#Cov+T!FAin^T^`XY3=KZWB6f{zXfqN_kMY&s)$iGvRj-1Y+% zV?pz=iXVen zGX@LW!ZNne?Mp7w`zoMS!#i~hZEp(cuS;MDYkhFbaR#7TPjwDvXsb=^muI<&z>^rZ zRU3CAI4YPki$@$-%6jsv zam_|}mrG-ibhy}oq0a^4E|UhxVf{xIN7B55$vxp-*Y)0Iv)x+o>p2tk{waicAAQY% zxYWvVsvlIzoi#wsUr?p{B}`ASbsOX<89=9*2L*DN^$o(}D#QvAG257mtdf#Y?lH=B z0qzm+L!`K#VW3eT^FnWzP&rH`+-cP^~4vb#bPzf8dxzDJ4QTZ7(JUl7I6-tGc5yo{tu_l?GSmq>H8%1&16iL^$VJutD( zdo3JH@^v=4T!X-iHRhUklg2KdK=X+{Sy6Ww_qh)wtq? zLgM3TYE$CJBMSL3>{YqACTfqxVcxh(E$c^A<)q_USR=(7tdlklUJwN|Z<2%-I>)+K zm~M2&xp^3gSN~YEwlE>!$3IqwyFM4ZS+0Q+o4V=3%9)CGAQ2Zj`qo0SE?XZjFJgsL zmN7WW-UItq;}K`9Fm+;i@>iq(iELsdZ^1g)uvX4UMXJfPjbU(7wdUEZ(&*QX>sE zm(2^F;d{)T*FpNyNFRDyjWV{+(@CB*VxN;&SKEW1;7b_>jE(0n$G2S|zAl3!jb2@| zjABS47<}EygmkUb31%;Z@ph2uFII@uC{tb4yi3+W+1?M)h~=?bfD)DOIKcK_;&xrMdN-6)=0SFBm@8GzTEuF zs;i`qz3yV_4wE*lVbI?Jzmop6K95*oU&{|*!Bcmf@^P;IRUU4aQ0Yg+xLuc6BBiTk@-oo~8?x8op?xW&kBiExLOSa5Fv_cdk;u3B z__BuCja;pn=-O9{ow~8Xf=lYJM2eO4y=bJ{@$caQIOjR>ht~iaEo(~$i1nAE?w9)> z97Me~G^u(n5AFERQhIL25{u+2JkW26Kk(w}#aPmA3`1DXMDqG@WBlpoz5VgkM-ep8;V(kD0Zxp zeec1ss0jEiGLX(~#~869Lz?}nk{N@UVMxfh_R^6erE_tAtPXdDH1w)7ZAX>xAE8U| z>qN=r`+Xjf6N!|(nT-Rte1HcQJk(DJgza*q1PJZ1czD#owe$ZN9p>ZWK}!h+zEu_p z4O<8MNw`H|iE%WW70Mgwe(R+;oWHDDk^dIH-U4yBw#)0NJ5m>^p8IgtK~u#VJ|fU* zk-b1;qP2|Gg9$=Cz@=JV%ERq8pni6nV2ZUblrC_9mDGZ%gy7hyM&Su-?8N5ma5YbB ztq-aT^At zYTy~iU#5rKB`9aHKWeHXQ6@W!U^_6iRp7r%9G#*|%AZjJuCE9QC%Qb$;`p34V?)t4 zSAh}1cq|S#jc&J52iY4&8O+G#N>gJE#VbuC!mWbRKw}+Aqb3OMgisV{Gsbc4#dP1) z4<@w7@^F{uB2iBfyc+`Kh$MsLT_X5HI_s4d8akh{4&@2N0IxP>f!4V(0ySDg&zeZIF0>{ z^rLNu(~W;}J$7*I;aR=DLjr(w+G_efCEc`2R0xGKa#MeLdXY(gJ%@P&qt!hYKxzh2 zmdso*^)kTan44$cG*TLx$HP4*^LD@zlb3pV@Uec-zxlaByz9+D`l0|)O7^uDj#S{CC;dokZFi|OqEojI^lD~XRHs;; zfM6f9Ogr~QsR6*D2b&^^H#uXt#C>X@o{q>mo%(WSNBRm%FzvV@eM#S$lcG|a|Z z@9GgSNp61}ulO6Zi^Q42nVIp9s5pS{&uGn(zHTWI-^^2u_{z0DuW$$CrAXJoyL9b5 zPuF|+o#bty&9X)r5T7#YsSGa5l@-nBBHJMur1uxaL%YX0D}Pk=0Bp*;&6$wsc{+ok z@uRIo>kN6x2oiFH=@?G*!eUAEcc0{+sUU9rr9|A6vd9e1pcn)EYc&NEfRN&K)P~iB zCKXX4QoItl$xqi|Yj(|)3HU@m8{^O^mg>I_`1wd)x?Mtrg6e9eh$yH@bfs8S-pt59 z8fRqF!CG+j%R~?4jFB`=?N7-clUAbG9Uz`#e{O$zap@6}WX!LEW9Dc{Yy~-0V>H9T zAZ@cY+UoUZt^*QksU*$uXEhz|T0O2`{#Sj-|Bei$NqF8oNDWA`#_*9o@lZ^%iow)TRJMeD0Ru32q9@jXZh9Qj^2X(b^PV>(3X-m=Qyvup+ z<@9{15BE4}$0G5iwDY7n)_eyFlZZM*Bb7FH=_D$dEG*pjFy!oW2FJVVUOins89DoC zvDEHy(sIhp27&3M>3HcO8Q%tcwDupZG|dQT`^XuqfDr^^gMxm}jc4dPj9ab-e2>hf zuS=jnD>D%4WcSd+7q?sh(+K=KwjWt{WyGYboRQ<%@jdf{_D1%t0K=H>$9TJEwe;;8 zB)~-`9FR&1(L>q;M74E7^l_;wOt!nWwVQg)|ClCS|=d91o71{kZ7?&RfF}0Eqz^s z*&2|}QV0$-kcOqk@ZfTs6Ep8bxTf&Q1mP`+)?FhQ(S z=Yzbbxb-=is4-%)Ztd6?fT_ni7%P$h48{?{=;A?z-wGHQjVIENSBIthyO6wn_)OV( z!w_gBIVLDWfJaah&yR;W0=SG5Y{DBBvU|f`Clat($%gZq#%+;E_Naur-vbs^9Y>pJ z^N@Yh`z91T+~glvNtDU5v!wAKz+O7EOYu-do&hdk8^_YYh3&;v@h$9d zrq<^H^ar7AsDZue0z^6&XAp;8RMKA$K^I%^Ihom#>JY(;$#dyUSgx)iZ z0KDuI`SR0WenvbfojRA5^{hAx(e3R#%Ave`E zITqZgQ0mM*Hn80_vZcO`4Yw-@{|r=M_@ZZVDS+}uJTz?Fzxg^b(PzjQn# zy24^wc83N=x^#qz#$)i$?GhUOC=;_O*4Ti9zUtVF27rew@wxBXkTg@SWU(A%SgkW@ zLM~#NbzWK0Kt%Mq?<2#X3#f+@7et@#*)Y%Eid7J6$xxt5>m3PDrO}E*_v$d$$is@x zQ6cHiHV-H^EgUZM&6xFm4M-t}VhgGZIHKE@Nm&ZXM!uhH%S?gPl!2)?eU3XQX0? zbF&C9uBAUocqxJEWv>u;damr9yF z72giPX*o>=(=LG^o+)7iis7+^sS)Uzu(hgO01#J`c`QjxObH-Dp*1^)H%#fCB^_!J z=J+0lYnf-5_zDZuIUhQUp{>!)-rTWT)XB~?8A|8^wFGrcvN~I8UV~s1wQ#+yN2O2n3&}myzLq0TM+C-il2X zO_c^3Y`vO~jft2OddIP$k|oUnpu44-nVad&j(q`|?(t<&o60j;=`NB!K1v6caG@%u z(O_JMAY?dZOUEV!OkpdG64r;AA^V>+2^6=g1WoJ9pn7!Ej9)9mmq?l&EDJ=!A#i44 zVknXMb#YlWy}Qgz9HNCU+8~yZi1lKQ8N|XZbd;-$Q{jKRMNZ-4fp)sd4>K|BVLZnmleK5 z3$lj}Iom8+9@lDTaF<7cR4N82U5f!lx*yB#zp|BXD&49v`QiRYTO=m4-nEo8Hc@jW zqp`$Nboa)|<-|+|m)v4|*R?G$?*~*)L(KCRdG0C-w;RAb+O?iF?uT_4jS;O7Fh_|R z+jo%n(aqa~A~c9yVX)(`ii_i0M(7Nl#G~h4yQ^~*cv}=y*GPSwMjUY-*~~dGgv^6x z&6akZ4D{OTFkdIM={!HttVW}fRMYbkJ(Y#_f0TvXpPiL#-Pr$OT~D~mD$M|Z`BJ*j za~cQ$M`gaG{oJ2`0e`_QV(>nX3(#Qc!&pLRRR5S~WRA$ys$>5PUwEAc5X8i0Y@B8-_ z1lR5oX&wZB(ilfXv2=|dQ36h5S&akBd*4+KM-et$t1Pv1;3J?s~3Yy5WRW{a1u)ffB!83A1`jy?xa+`Gx zoBY}z>%u)hT6gZbgMRS4hAJJVl*8>ZjH;9neuw|`rjO~Zv)K>nga%f-L zSXoVQ0%&-s?xf)od;?I*MssSf8PUCol+^GkUSDy6&6JQqV38PRW5yl%N9dO)RP|UL z?n3Q}!(WTkyTtp6s)ykL%fX|?+NRfejncXSV>Q=SRah61aSKE4Q9emO>lyy zB}iL;U-HjQ;x=gB5`^+M~`oek0$^f5E4+UEDODBE8|m<>Hthks^B zL!5b*z^SJ=Ou%EK5pMpN3o!5mi>j)^R|<89N>e}_4Yez|DjqX5c(!bjY`du(km zfsCO+XXiwH!q6YKWmD3CwMG~`<&bLY5%%WSeSZu`ncH?REHtoX4OT{uNWL*|kKrh2 zF4ds5Qvz})UE_${?G3ESd_4keeqJ@)Gn~P4dH1FR?~n$edp1~kr0T#`+8P5mn{I~J zaZE&A!~YU=kp}ydlS#kcckt4SW%vCW%nxkJ;Bu^fS~Z((z*9PXNN4yxg<&=t94wJ5 zhGSR;q*~dT9b03NcZMFcShL_Bqke9e&}Z#i=U}J|{|UZ8W>Yb$jFsy4uWiyGl7ZDV zU|J5oS`p;wBpO+85sGJFpRRJ=d|BIFo^132i)I$yM!AHC28i<+zLxwWxz;UN7r0x8 z0pHM?0b0+yhd2E)icr&iEcetRk>-u$e~l0wM0)K&4_y>G}N;+0F=pG(B;g^<;tBdYmJ^3c%B;z%CpfEmsZMiDVPG(*9t z)SYDqPeo==*4M^MP)Dt+1V&Mh_2KI#oIm{PsCn?Zf=q@_jhcCL>I{WcDws zNZ>R-c^Y|JrpF>W645yqn0KEF#Q&j@2F806!6A`nWxiSng2jh%>_H;d#N-HKF^c#8 zRv_+=fbAY1q&&SsyrFiSvIseO`EhvfrR#g-^9NfcplKaUV-i+z>gb;7cT2v zB@LyW&7F0KF&gKt@RgZ-8m9~T%jWBB;SLnk-h;- z_cUuhzC(JHya;!2o)F9sQ_t!eKv(q5i58iC_<`tZ-HlbUaomwBb;`_P{NppvIDbD@ zhk18c0v~5g!LRx>E>$>#V2`RWSkIr)xBnqV3Uf`;F+47SvDQZe;)h)9Lyc z&pL5a9IOqJ^0C9UE zJPnf;8dZZCeDIV8CT#5VxoVq+?l$^V8;=DG8S7Y1kq1WgF6R9>KWK@V!cgEjDD-v> z*iTC;fESGF1ZnUgPl6{I!X$EMHs0HZYZ4%L2n-o=#OcZscGR99jufNp`$BsDnjnHC zGhNz9vLYlCj*KXoh-vS8lEg+l5&G01bLMUWl_HO^9|OP+Xl?l3J{5?6rAf038}+$B zsIptV55+cc>`thaN%rUtE0Wv+#5d9u0U1A7zwVc43J7u<3fq#~0GKo^X{83F-&k%~n zAmQV{p%&_^o>kt30g04@=6(0@bqTX<4?Jys8LO2Xau23DSb{e&zr2Fb6mYK+B7+dj z5rX;DH#^m+66`-lWHG<#9r|4>oh~bl=3d%VGDKK#0Gd3ksX^OCBGsZBK$`1WqekIc z0L?eN6Q2YZ!D%Hi{CSAzUt;NeEvBo-_i|LV8J@O@me6>YT*(V z#>F&nl}nH*(Tv@As1sjT!8#SR4G~Egjp@16tl&IcBIS8e=iW#{_`~#T&9>ysS3K2Y zfBUnYO_C}k&75Dv()}(`g|}7Ac0leR(c`dbrc9J0ZcW?PjM+$Yvhdakl31ii&V?G* zt4^H=x0SuVSBd*25ZZ9sC=i@1UWCXAvLeg5X@Co#O*{z#Zm7x!pmQahZp|K5Z86<) z-zd`=I$amj<*uEcJg1rR!^f2mwN+bt-nRUNjWcc$B4=<<-Pqf=KR!$ua}Ua2r9Wk zra};c^kP)`@Eh?)(Rgzg>CjuOxA4SK1*4?`woxR2^KiWg>O&3p?(|Y0?zdsaI{Cw8 zZJW!=WKmd3?ud;mOBs9R^(C%j8KLv)TlpSa7)-TTicV9P^lyG#LG+taS&CoQhu^^V zgS|9;W71;P*@n65yeL!b>DNn7=+SCw>pBJ+{d#t18RMj4X}Vi>APh%F7YP&5nXgdC(_28Z4fJPL`Ng+>1@?21~DH5+@xcem-|ITrS2y?)$XA6eb zgNA4OEM+xGnR9wCgib$6wDql$T#9wCyw)(mnLGK1<85lp^Jdf9zZKhjlk6bioh7F` z&CI=s5X13l?2!4B&5SIaiRNIeeu1`O#-AB)WO4@rHSBGM=y$2K_rHWGZsJGzAQD|z zmo{kjci&UY!4d@a!g_NjYPfZZLnABZ9c|VWw&EdaOtSi(io@-NFwL9wv$5fY*T3_Z z`7}>em=90r6zzhDH^?iXBTXZACd`!z8?O;Uq*CF1|G3coE)KWbaLApN7jB$1dT>et zR7lVxyb$nX2(dC=s;Z+{(gm%ypN2f*0kg9mc%6;=__dq7s>59>Jw`Z}MJ61C=<~v> zVn1Ev%Y=P1DY5`!qo5HRtB1$bo12yDHj&UQP8-J>2j)Y0_?J|g(XE!}`5|d#f>-W( z31EzLg6Ygyif{*gH2A5=&&LbEbbkPjBv4k$VVyY@K*3_yceKlb5@71r4Aa;^TO(l# zNMWlnFJ6hEElj*wExtKvd_9H|Qt9yFgXLiU9Wzh?10Q-vY{B}md;Ef@%#qgz-=H|R z;E!%64YEEXF!a&FyQg34heLR&U44<@yDk`x13b>0FGxeu=$Ke-tmlN$aFvTK43wi! zgMh<21762lb+}!_q5MvpB}s#1a}$6A+&5)-C4=WLh>f7|L2X*oHzC{|#~+>KYxq2Y zNmRL0k&N+F8g4pi@uZ@`RtvQU8-Ji>eb6-2%zv^SiYpi-7MN(DNyTW&f5dc}ZKDx! zyHI`4SCggwrITj4Re_hZ86K2|VZ@yulVwN5Gha|2Pkgnivt#uH^R`I0^G`gtOd6f9 zn%DMP8SWQhuno};I;?n8dJag7;1=fFV}BH$35Sx|gO%iJu@b@;nNzIE@)agsT^PKx zS^GW7xh3bD=$ADs2-W}+xDrcBCes}zZI3^2P2h2)^)n`3fc;XyZ93~20DAh*v3M1R z+Z81H9MV3sVNmFJsuBxR>6(lK#5s8@9fGRb4xoRYqBf-(pHW@5{3kihPc>0zdNgRg zUj!UNB(S1PiSLq}Y=pkrWRi9PrL3RVsn9Ke2>c6)Xa~{abZ7F#16-P7<|ZN^qok+O@GqIP`{4Fo z8lYB=zDBnbZ@l4wK|$Is8Z*=mIzqpsN#CEqFj7Ue1N}kLXGbOKbTUVgq^Z(oj~%9U zLz!F66zUH=8fh3IHE)+#h(LK~!j%^njjzYh3`Pis0JSPJRkQE>F)G}+GvL2{Ffwuv zaLvKHoAnT2$n{({OS;8s@p#Mq>35NI*M@VQAu7As>QgSZ3a-nUG@q?s=TVQ2P7T4k6ZDL`(@XaYSRP zjD(qMZoqpPqKC4B@#x;&zZKFoLJ^D-7@WGcoh3k}@hJ}VuDzk1TEF?~o(-Ch#l7KU zmm-i`TOwAV<@2#T+%Llr8QpB44e=gVcE4F4u0zvaM%%otIaiCxKT;@9sZul7xg=Md z{`xXm5_1P-d7T{YNxoWnp*m!Lrs!x$?^o7n(icRm9+Z*9cZvUy%`(m#nKoPcsiRAe z&9pOCc@KE_x&n?16Nt@^J!F;f?Q+}?T6&c*B}XXSDpg1Lw?a8R!F3Qz3<1z|Ue5p~P{&<~IRdtVmm3Zz4`d#2Bu(vH3kT9a3c% zvEkH7{c&*kSDv)WtT$Xq{-+Sj2?jS|g>xI@SPqLNyVrvbx@q*Tz5Im_USeJ=l&Eh;fdm)%PR`$%3BDT z2y;?+3Lx>hO*JXrl3qJXNP4pr>Uv+mUpU6pk zT`pzo4ZxVzzaW^$(iDlXoo1FaB+i(^bvx|P%42OP_t_!zgH3^ghQ-}Xgag_V;5XZl zer$hOmt?${tOp0vG~`ktdhJ7YZI~h4N$%vHYQtS6Ey=nf*#S=&QfIXz5hxsHwsPL| zEro-@NyrC@rR$mFJm%B~M?dWX$$s+`s|i={r`mAWiknRewnoQNJyeHOTO;39EepZb z?avCG1>R1>x_ccCdN?Ob^ffumO>1qBdD8t$B^^Rq2$jNdlnRvC@MTbq%2PV_z9YF7 zTsJIJ7}qQ{X|WK4L60f&TGbH5F7-w9(Ep{A<_XLfEu7&WN116g%j-PCzMH!~HkhIk zPJ1h0y0?`>`IQStw^~k3FO-1IEj{Q{j;u7+%`%5q=$3x2L#dE1vRVb8|8Ufb3Wb)< zbco|!3U_}B(0?Ozn1TY{j5soueAS6LA}}}6-P{w5bvXnMS_9%syAeef$9qKbPkEC` z_j;GYHHWRz1%)w2QuP25F|CtXCHi$9(Lxs}d%S!J;#xn9h5+Q>R%d)c1_@yx@$A3X zzm(dCmM_)Aokb?g?JB2J!x+Q)YR%Wu1FL%!)a$$=_ZQg*=&U{}VE_o5!}Qnp(r{Nv zV^*$2Ud~ch^q@_yVmOne#~;oTloW8UNi-#qbU72>+I}J-=t_k%F}82#?w8W=l_Z^1 z+>jTn>IVNyVUfN$P=`lFp18yFNHClVlgl>wRGNK0Q=Ax{oe ziB4Tt?x|e3TAy^Kr3ijdB`eQ5iZbRhl`zFrY**!)Yus(z&T+m|mTnh7l+;h`{*iw< zyO{3{UfucJ(<$8{j7&}wfeO0lbrX=*vyy_SMYHfpg=>FO;of%9_e-F#0Czx$zrtqE z^xUv?cdj)fU@%pNnYJm#K0N8rC*Hme1Bem`#Q1%Y38%VK1x_>V((h&PRTu78Fb_pr z4PP((2>2SnjUn}h)OxgFGg%h~C;rTPv;pBo4Df1LkTTh!LN0WU78fVkSKzI zge-TcTb(L??ZX8lQ%I!yi+#rZDd0gFnLj2j;bzImtfUaW_LccFu2^}zt=HE#V<*1e zhD5+Os6$8+=P>WFzqxkzdycg3IW1_^JJF$Oeu49a(gRwKT&_iQlm3G{0`_l6e<~{l zALS7pA-BVbN#LPhu-;V_?m^NlNHzN7{Y&PB8u)W!mj^Wju5NATdGlpGIz9;#P^YQ; zfpX3p&g?p@>BPt(yBQQ^9%`~%zIc|{RnXmh@W__8One{IB9H1bk?j%;H4@s>0-R8(O*CnTD795MT&%i_#gJ_`qhL#z(v7d{sr@fp9eO5K` zR2KfFkyi5mtb#MJ8(sRq@1I3H^$Fh<+C*2-5+}lrwY)(|_m{B7<|am`V(gVYpZFdI z@)G5lzq=nZ!c| z)D;I<365m>Gp8mpc(JYD_Y72ZcopmA*|ewMVX33#_{_~=y+00_0h)Rm5HVyYp1srOCM z@*l^`spB#Sq&JE};E36odjT8}ZF@6S>UWJau4=3rjc(UlOK1v3;LdW$=J{S%5UhIi zpcJmO3Lqsl{OtaaR|%#v#`b)l7rs-Zbpwvw?@5%5zHvQ5RM>^8>&n+M-f)DPS=dj+ zPQ;7QIs%Q!U3JUA`&-BMVSV&e`nm{)E*(=PumUVY+$*l1b=p%Cl+)O5ckbr;iGelV z@Ax{gC(M6V4Eh_haCt2Y{chBq)OoBQiN@j-R7O@zxZv>d=qeOv0_p0e$1v8HA#-)z z4N?t&mCv06C->4cyjOD4J;pi`+tB|(zu57RfC!q#oVsc9F9Bt-iKKTS@*IjcG?Sy4b-Si@Qaz^7cBl*7 zy6IhCITM~L!@m-wwR=vlDL*yb+ClLKEt8_jE21zXGSRrYg2v1TP zbZ5ter5vor&juQp24~L6zMe%okm|S%y8QKA3`r@wOQWkuwIU-7UgXTeogKpmj0< zdXV!HFq z4%-ZV4wWV8LZr>mLs|HjDq5e(=YfIawhU1$<3T6;cik?2woQR`A$oe1q0e<;+<_z( zba08FVwi{Lk!UW)WvbL8S2;>k4InAAg(Ee$XoqUKzQRG_yMYGNwbnOL!}#9w1d?=w z$zP^dNODOXMx;+2n@*n(l`bIt)+thU4t>cn`{_H3e{nk ztp|KMtyacIrEwXt4mNS?N94)=yLDp_D-7U|^=Ie(#i@{x`9IZ{V+0A*Jg>f{VO{f8eG{Jj?ZG^3)6!v zWDm!6^%KsFrMBo9cN@bY+;+tUb4r7Dpxuf)|aakkbJKkDyEga(0Pg>IKX zhQPMyzS>1>6y_>?1w>6;6@2F~w0b8eB)NF7QeJlqcUd&2S;mGnu0Vt35jw;!GkD#3 zrpnlHVA+q@5p!bm<=3l=7&1%}em0iG0e9rT?4EC$XkD|;U{oA|sbiFbT-`VofP`~f zH*hjCvclI`<+0S&(Lj$F!6c7{#Q+0(4*iiprXDN9yjv<|gCAp25nCLW(3PW2H7San zwuPt_^nQ&QfWN0sn9THCe7`1!p>2PC*E7s-wq&x!muqLTtfuLuM`ehk{HX?Z z1+D_s)Vqbj)f&*N@kfLJ;@yr?aFoa;`zfE|V3hj#S{!ay&`nb5h%ON?4`bw8i8zyP z{306A)a`OIHzst;ooE8yxP}AOXs=NDqHDkqy7Y#3XHk zL#EcO|30X*)5tt-Mgm?r>N=g0j=JJWI~p|ddU=`0qaW+-3ZQp!^x-XN$Q+cO)9!q5 zYsEcX$j3W5&WK&lhT8WEwAQn7%lQXUidpt;)o}acwK7d%5FG&yaxlVq-4LOvY;9Xq zLmghAHx# z(@8NNX)m?m>nY50tD(&lQVk6)#Hkp&_R-p(`RyRw3bF5c!%p5_f&(Ft+W0nSml=kd zlE5E}!+AfKL8B-Rz6?f#>W+%mOmh$xICYCY8P##hA#4FUXF+h&1VsZFQ&T~t2gWb6 zzClX&E6}}3DDR9}HFATK`{Q{n8m{aFZtJOy(JBr4#A5`hZ(`9{ZS!U^ImYh)c+)hD zyB1oJC-Fg2|C)L~&cnmG0VoHTqO^UClEBgW0{d9S2U~iUl613lcxSm9C3$SDLtW=R zFB`^PhD=kyP@uo;sYqN*NjsZUVz4r3*$NTJRD`NgKcO2c_?SR=cV z=F9vBz5&J#4y8io)De2OSi>JIGCHXRa9anNd(ak(sWxY~`_hwGn!PwEeO&;5xYla&WAl#Ytq3i`LCN%nz7aJIYI}tZ zP!Co;t1y?YF6_`YD$eBTM#y;1x1r&l7_FGXU}ZiL8dhOdfUH5IEl;zZ+r_EiZ%ep4 z!2T+at3M!7r+nlL_GRH4T>eT<8g~t{1TB6Fq{MU_mkX=-VpoJWVf z9>a+ShP+-yN@*KVQWHWyJm1(!Uso}dQ9!rskOXO8;eO0u_{)&u%S~Gae8^mZI7~xP zL+O>Y9Pe#M)FvJ%dD5?|hH;kwGd|KS<7<(+s|I;Fu3dN|!I%1OlukWSY0!>={)}{) zJaRZ>d^oGn6E4PPv}e*@j0szSQ!DT?lCl@>D%@lH5m#zw#hPEasg%Gatv8$slC>x*0Epq=0u z#a3s>%Jgwf4E1?pNWXT_=lnQi02YJnq6IZkX4bG64zKoP_*k2W;1hKLYk{O`l^MYj zF^{=jy2X< z3@BHCEmSrsb;ITXW1@N)RGd%%v2jiVx!W3%*C&&WR4yCX)FIhF>3C0l`+5%EW@m=; z^2pF~5;UL)RXHN5BV2*_$l@PNfJy)^0x)AODg=yVMVel}1VcO5k93o{%JIp@L` zHCS3R=v$nbX>UGJv#|XF5!SG0eL30nX>w}r(pDo7)Y>o}X{+}uC}z)a<0-CP2hkqk z)!5OXgkxsd85(Q@I?X&*h)2fDtmCOt&Gp#=vQI<7{J-lw;#1 zMasyxosgWMMuA56i2{~9N=|5c1q3GB9Xqva4u72)?h<64x6D@{QRY!s;8?5DcBt0p z(;WdP6)^dABpzczT_{}&uSpp70i;GBQCwuq zNRe2DknrH-reif$>|$59Kf=l_sN~Yag?N1}9!o?1rbyvlrjJyNFX>8*ZPH;j_iYIsKEUHOC3^f$iFV)>%vI*bv;>r;YmaQGV@9vK{!qHI zbrTSsPQYfg@ztJNIYSQ8pU-;p;)95=u8Dc1`b9hDDUk0tIL@msjK%Z z(1rT=W08U4j9n{*ZOAA_*wxO9Jd1S*wC%ZCF@wG^tnp{W+AY@55N~rN7QeZ3kMgB- zbDu&0R*<*wjIpo#Q|vX~lu=If@ReA%wT z4~l^SWF(?kvxv|kp|1y=vbVrUFi!-3b(K-%3~PQoU~jOy`7o67Hr0Nu4R`r3y$G{8 z{RrYMdhW9K&}wo?`(eKlBIuz*?_B4@s+y~$!SCh9v4g#$8zgyw67jJ;E~4*OU`~d~ z4}?|Ja_oOmWY?Q^iGH5ZF@kT~Ev!Rw=!`JsMd+%gHvrA&C-cjtKzd}a-Y=r6)@1VS zYCC!`=rd?%x3AZQ2LT3e!#q87@D^P6f)iRpE5$%_PY$%pBL%edSRd{(=srJy(%6Bj zf9eB`W2F1+fnBcQ~4BuS}9GHwoj#P(0x?m_E+;vrUuAcU0yv?gVn zgz*c_U@e~7o44M$Nsz(dlB;%|{TjC12K`bThByLy%yLRxy#sCt%K=I7_^ga zetfiEG_DG!>T7hv{TgI=>5A^og%1K;v``|Zw3skLIkvK+27hZX&9Du}KD^jf)(>|S z-bmjTR`VArRVtvWVF=VLCH<@6~IS%NRX((rPsVbU>tX#rY%zO zV{m+JR;@SV)$tV_q+>jO8Z}n+vVmJ3Y}x!NQ-&9o zAgaHO4qtL?JEI>l0WBifum0qC4|R9Zk~MAjqO21+hTND!tRJyXaYi{FV7Y|(xK@x` z(BI3$S6XzlSgRySL|Wjo3qtDt4&U90vD}7vr`|}bN64qUaCAc_-rPnUebO6DUhC;^ zo9Np$L~Pj+NryBGX&(&$NF5nx$?87E#~vb^xiW+$BZg%Yi&+IcTTRJhrY~XWjcBJn)rb1; z`jE~?3#CjCR7vApFB>!D>JbjD3F}rj6j>a8MZ7&P^`ZWU#d-!?C=#Qq^tsK|Mm`@3syaCVJ%%@oyDmBWgCdR(Dj3t;xqD{53AHx70-MYV8=RhBcJ-Qk z*GqvxAJUvl0arMm-N3vTiJL5$Z&^R_d}t-bET}Y`(Y}fN9K!Hn%^aOlK5!hwB#~?= zLacRf+HvN&hbOWb9MA6Dw<|#4yE)+sv}smVJao+O$xI|UhCJSQ6}o2;zHao<0E6hn z?GQdCqWh6a-+I|TwBOqY`ARWS<>*n2FoSn9{97IYCeCTjW@u9f&BDRLjt$vWAvn^TI-$R;q}%Taq95C2FO<=o zNPK*If@nr#^b23&u zqDi3#&*21E_5w-+(vjBTcj6~KQ5k@7h6HZx)x!)6*2na>ahD}S=PpA1Yz`CqlDX~X z=B|B=`=1fQpqeW1&bD#Afb0OYR6E;#1x$F}gd_|jWXNP3!c)jUJod_|OJ$A3t!EzF}YQ+FOkc^j)z zWciolqrTr&m3}V-Ua)%AfmjZy7vinuP!V5x9w2U)ar*I~k~rLl_TW+$gCn$WKPh$j z2(}We(yH5BrlX&&a^vazjhx&5+>%LpefM9zT|sAQtM7+C^til}Q2rxPt+N6#-y6f6 zSUtp0!ygRYe{~Ny$2S8F$43F@Ar-RFUn|M0Fx>O2H3g$phrC>%H2|sq7;80vjn;%P zBThJ+l&BTN1*~~_)w7B`xNL?Pr_SY*E?#2?`pj0kCl`~#m&{4Y5!Yb4N`@fRDSDCW z)AvlbmUWhd3lC~s*kd_hqeW**@P{4<`1hkJB1Z%+g_=qXf;FFV}Oh5xE(5?G}A zcv)l@gAtBf!^9*~IddpdlhGFDHsmLZz45Q)=+hn63t34FhQNYVoUqr+FcWApw=x^P zh|*~2W9>!caKHb1cxZtuVP=b0R^K9FhX>YgACbe^A_WN^@#JE*K4v7b^RGNRdtQFU(1ebNl zqa`Saz^z9|~Z#jyye8IWwjV!JNzLCvZ=jJ50UF=E0qvED_>+dy?$k(SCJ| zU%?GK@l804Gm-sYr!*9{k~&AWO=KO$k1JwDk^|K#>FHN(!hqLYOiVzY3d21y8cm6vQ8<0Db}*81;+o+Da%R{h z&J!BlTs;9+Uh7Vv)LMT^H;weO5SH#Nx!}cR?|ub9`CPk633@~5=vfapSTg^#*V&;$ zL{)$kwe?!EjIT(mL2$F=;qYPR%OhVP3d5uO>bOgzDZ+h|QXmOKSfVo=<(eYi^4pXz7rctti@pt z7BlFYXEHh&DAuiQQ|wqsn{3+uGBMl)sfw|a1U2Awold21Rq+rpXBZE2ufz?T>tq&? zh{{4FSBM>ExnBAPPI|ehuXW+3jFvLY2y6&=nO1|ob>gm|h!=#-I(O6?!gI-U#CNY3 zp{&N9E6pKxITYps9jiFEZ+$8Yw`+i`cJeduL&4Pwa&H#h!qS%=5(rZ}<2De{=iNd% zi-ylH`_cr$l3XMn)58Z#S}xq9DU^?xA7ewM16-`gd3| zZb95)uz>Y9od8!QaxZ83kD=Ng` zAC5WKyycCnkMjIyPU6wOJ}!(#-N01UkP`qO-e8vLE71 zu6z;Mk>38Bjb2N~wt$taTnVBjWMd|L%C+H+&MqbdQudARlLWYdu0>qwk z+O1f3&)@+^c}jVo3Cd4rID3HsRu%;3Wwm!R&1mVhHq5&s6~=|u5@7@jEdY4}r(lPH zl<-9a$PF|}?g&W2YGFtZ=|sYDgbZUqx5z=vzrUyX+^(RrO*kdmBn7AwdP{ektOIL} z@M!q_v1V}B#<^hB7t{gzH9uoyPv-?CBr}%y;g7Z%_qzb7=PKGHBk1D=A6r!4Fui$& z+l*XPzls@69P%4Bk!Eh8Nc^G2*}fTk6urOtX3Q^Lbdzt1F1lTj4aRiw@j(!`SHL_Y zazJV03fiMJJ>wI64hi2!h+Jf&1HJy#KTNIf$}s;Wiw2x(FH9xPjOIG~@hRXJpKP5V z0{@%=Rks^H>_b-oigbR)E9P#6NcM3``viDQEXh+ZOuhXDsLm8o@J##dSV) z()meo4GcU*OoEU4xis8w0~^vUaWLRZU5)!6e$Y0tgD`d}T|lDnm4Ye6Aw9{lZGDTe zZJI@lLy0vX&GzP(GJ2~@x^atiC0XVURGB7^UOC$Cm^BJsL+sH>DJX=6uNmLBrhTZa zfG=Xga8aMu2W7$uBo+wN6SThDMl zdEG(JuXJW+C7M!`)($fpa9&A~^_JS@nQ?_nwJ_dBg1Wjibjg(@4`tyOW6Gtz^t<#X zi@slkw>p>*w9OEPVEqL(<N8ZdS#E$Pl4if456YWDmFgomv1k&7G@kp@`K7f)oBv7v+hx;X% z`wljZX&#n+b9>&gdBd}c!5&q?j=`))4bne*uvyu{Ruh$3`v6TU%x&tP+(15(lx|m0 zDF`TxI-`Obk-})7isE*=xGIe)-pEebnCs)EC;;ZI`cxcw>V+ju9hZLoGd5jy;Y*Gi z0_02mX49(GZi%(~tg&X;yQb+Uic1ExG2*#LJCtv#f7FA{FBZ-jD9ux4_?IY}YdW6| zGg^2*C7j~d>$v)t_1g{ucRqUtL3(p^c&(l*!};5sTM{c(*`gNNEjnhv(q|iAw`T}r z8$aEB8^7ov=p;ke1^n4zK3Ud+x3<@psp0E6v^Yl$7w2foUGsk-z4-}ZDP@Ut zu@r=A;_eQ!MK(`LXAyPLk8Voy9uVycU-_q74$vfU5tp~-h_Mh|$7J8~f(YCq4RrMs zj4q%+)wRymF4N*s6p8Adzl_bUvT)Cbu2|#Y_+vhFVQ4i&g+V}@0Y0jAR9_UOVb*cM zDrCJn7zfN@H_@&gG)FwQ}>#Im5onr=e(Vdh2cw*tc{g^89gHZCgiLhi3tW67SW;YylLLrY7V*p z#yXLdiOeu2GL^L=w_XmVKVyJg1xaU2q0(cNVY-aF7u-#h3Y0~OoXAT^&x^fJKV zf=ZYTY271&XVM>S^pGeLkWzUp4e7U)4hR@{0l9G?|N3j^IFgE2xn0IqT`C4ui~_B2 z{kf#mqE=3~?N=s8LasNH*W(iUehq&64!;}+ymMtJljfZ`B9kRY=@3!shXVd}D!}1I zT`=^4j0D3?VZ~*^e&Laxbo=8AOn?;0nw*lVTj>$pTj{6M0_F}c-m21bOiXdL_d&;n zK64}ecquy>cM+wtKUIf+DWd^6T7Ibx7IVT#;bc&+>Y?MvWpig5rdyNx9`*M967-MN zUjIN!z!ETrof|yt=UzV^Eoi>$c@46yGQJ@kMUuJ0^|IN71Lm+qdaey$49;a_7l*qtIwHv>DCZ7T_$sY=OCH*88vQ9Vkrprav3i23bB&lIC~K|+%Ey(peQpMJKMNbhXC;< zb1L||;LKqfvfdcx^>q%c6O<2^z{MlP$iYX^fC}kIGu-}B|2FRt(bNOu8-T^>Eo${l zVrygT#Omeanz1GZLv9nQGsvq*UIuFLt`qUccM>~?6b>qH)!{25S}$6VI3{6Ef|<2U z*E=LR&obk+Ek@K?qrf571r@KWrmrrXtVtujNY{}> zUnlkpd7~);T73AR{p+O!{hp{0hU?EQO(yLoe6?ST!(Eli&>;m$ZV&n?k{6S_e)$`5 zK87+ET-kNm`fdv*XRE5ME9CeX?T@~}(|+W?KuWi8X5j?%d-{-9C4`6iUn2faeMo>- zMzDj>!O09E>#ZL8|J^n(Tp8OeNn&Zjz7%zDGU-I{pWT^7gExW zCc@s{whdoz;Vsq297|(WMr2cM?y5W5el0v?rGn_N{KI;3V8|MV?Ata2b@QwtnJ?v~ zINTGWTX=NaTmg4yE(`RSLH;nKiQ2H;e599mkX>Zi6ZRnaz*A~gX!O~AKrcDpd^2Xb zTPmGmafL0B378!q$h77OLF&}!H%#Pe3jD6ITIO7OT0a>)l%|Iopr6jtc%wKeVenaD1~&WoP#`JKQ3H zp(ci>Qc58`+?!}@OBB3NHriRQ=_Q8|>$>%2b;OO8T$VvTgH{l+dnu$wVQBm)f16+G zXfRFE9^LrTGS7fGf{+eV3My=F$3>W%a)(y0K=2%UA7a!Go+?9>oXPNy40PjV+i<@I zO0qyXQ&-l9=}`UF94jJ{kQOXRd&!R;ShWmn_mI*z4oTkN^`xB}Txrg~#DLze0EiIY zL&zO7q(}RwI0_-~!CB|I2Bu~FVI_fOwf1yu#PSUlxHn6sqsNE-%3Zy`45#jL&|9jB zR>%gE^jWk<{oGzR8KaZfM0e^&Wf#8gO(Jb!-4DgOTH*wGmGh0)20y4rS;PGT;V2N#@sZg1%IVsYB5%z8tW%Ku}W@k~)_Q{lu( zYCAp}5auc^1P1kiDzSid;;-)(-S;QZ9la5>upTK4kr8MEGx<{aL)pesTq9l{Thx14YM;mxGZiz@~sPJ#y% zk%3YB`g}mC=eXR(;7o+0?D<+8Zp!3o=D-NM3PKg>_|J%Km%th7ir%h}=*XR0Zz1|4 zcG0zIsBPxpmC>wYB{-S#hDY7o{4I}GrLBWc6I-G_)Z@yDKGg|dO3Yykg;>Afq_PUz zktM$JICsWUG*}y0dNg94U$SJJ{!lLK%&3dhCo4A%x7qX=b8j@w3{-^~EG9 zvqzYw)eUV#oe7QfQ;E1;1L8OpJA2zP5M;|x>R~-5qpsIINp^rw1{OAvIQ!tlIvgER zy=6snvb~QF^?M^7(2}w%H6$*uc&RNC>Wr?9^pILl=ovPw8TBr?oY@DgvCyfy;0!O3 zbCX1V)rk7<&4amTPpP0nFtBLXEm$)wDt+RDgGL+SZ!_|BbfSM7AnJd1(p%ghh1i&v z6YChPbMWexY(Fm|D_e?H#n(iO0UioPK=yHLeK@ZqYiv={^tN}nKLtRj%Pl=p9FFZj zSzwvq196%c;|M9hhWZEQ^Xh8qdT}DA65kYS6nlpnY0-}~)_E5uV~#eo&FB5dUNqPe z9@oYZv&{Qr$f^jTv8OpO4(VjKnOcbD%DK~RVEAD-0H^y!;c&kO*->z3Da`cxP5+i| z=MPGz-A|vFB`fR{d|20fhZY!1XO+wFTlY2?-Do(nMnvdSb-3q6i!QqoN3di;L^O_I z{F)x1&uq$ANq#RFMG)$O0&vx6b}!}Xhs31I?=k&{%oYj`4?66{e3WfFGrvTg9z1#x|;w5hD)hV3v-t3 z$;x!!U@9{|EV#-0CrRC zD6aFxS?T@&d})fTSiHZE%=e$vsm z`0XXe;r<*TWQ_e)ZfuGEB5B!X#*Ear4JSle3Tf6xh9A&KnL1@wT}x>rM2rJ}-b#Pz zqK{y*4s-9CWFo{;H8zZTL!S zZo2=jC&`V37CSl5x=vhYHWe7^3&wA;|F922>$UpTgE@w5Jjm}gl0g+lCtWYxkG?G) zzVzADUKEbJKvOX+jE%z{MSvZ|ch0F_1=tOxUqomUOe%3iu*x0FlT$LTTEwIhtc+BGtP?ozc2repN?7^X6^`9iSAcLAkUQHXgS%fszH z0F@85ZUAcC-6HxEm_tor1+ZQ}x8$XpMvbB$k<=;>q`_#?W2KDQfxgkz!wZ@9u8mfn zAC92>4t2)4Xr1EWdf$&3!H{?ehoAepQ_uOB_vW zbUxRISu_{JPz48E?zVbHE$LIzZ{?u^udg=` zhi0si`&^ev#x3iPK>0!%?IPiM@i4v>aBz$0gSTr&HZ~dg8t!{%G682NmsiVZVQd81 zO2pKPLQ$a+THw78g)}TPo?)e%C>ac-8@LuwzDW)<dhB4U5)5{|?T3`Bs zG48q@WM%HLCF{0_#(>B8;?{k?0Hb+^8z3>MQ8{;R=PnApAL zLl>1(u>9lJt<#?0rEcBxu`qn8qRCukl5}1&)Eg-a97np= zMzl?m4$Uw0S{UwN}{hIv}jPv)sC+!Dl3W4!o&`Q{7xM(SHk>mABanC#0DX5CqyO2b%ybjXAG=_Dl6 zK4Hr&WGMEzg#*v~=-W-;mm&bMj7f#*6|ZXIEi#07WdDHQb3fg6|Tiu|j>Hq@+Ud%>EDrS7o&H<0sF6UKk6l+K{A z7LSD33fFqAqvZ*2gMy=nA?CVC*lcvwU#-_z;9Y(hBXrR?NB**E_)`?_y6DnF7tOeE zqV~#$?IcM7%z)W)dmYQ-!Iex4)|6{ayDjeE5|$2eF&E~hft!oelt{`hPK-F80M)-lM$fx0+Fz0CL!YcXUVc1L z0YiQcP>hPCkN~2gULOW(#Y6xYZv#Y}u`^8q#R3}9rTWD!V}7ZkN2C{!`Ac)uqhdF` zwoYdeLmui5dh(dY_HaF=j4?A9GdoZy$vvROthLC-k{(6TUzfnv$B1GutwS9F2O#BX z8|-mVm^~OYC2rLE78Kr9?c+z1`maxhzopqTtd!mi_U1h;T4Q|NM@i7MDWBdAUQ1nl zOy3-1FB&w2Uq^6T<-hSN${XWT?tjcO^86U=J&U4mSCG3CIUj&gv5XF3j@`8Eho~nH zGBi)wgw%<**!mjQYmhA0+-|dBy#;7M8Fx6y&R@K_t0vs;LD8Nca{x5@Jc{Bz5zo_x z4mt6)^&Q!b2tg_qI_b7v`2$izV{47%@Zzr?x_-ZJ8RoAhb%wuQ5AN?t;w3Y5L8~(N z1~j0w#2JIFV<#vZ=_7hrxYjh7Kar0s;qa8gI-at9=KOzHO6R*rHzb5gn#lU^N%C-? z-aZz4UEU;%(>C6qq}wBCL3(C$EHw(eyE-yCPKvLhK0xWP4d${(B#k07JW&qjrn{@aCT^%GER#!^U6+!VjTYTF$ZrZ&7jB~!pm?<9DTY8vqc7xYXcmk>hs$K2 z8>Rb282uU+-LlV(va0t0OXO6Vnw}QBtIMJr=_8y!ZH^xk z9PSqZM=8rS<*`DPo13HLmO`HOsYlnxVlPsO3XXQGMvsXc^OU^X!>wG_93Y%A&HY9A z_I(v}sBYVY07=ZpQPy0Un|4c1(dLs zBM=+)BkfMpZ<(L1YozRv8}8T8 z@TV9_ZNsjQ1%-4XVN%P0AZ=Y|<2s+rLFO#O2$wL8qmkIi%d=}k$DyFS)`t6^AKt5c zk%#L;H_)1uaRAWignDONk0t{NS9hXDe%6DbLEcuLm%UO;Ot*iWHWQ%zT7gqId8msK=&+ z$e~RcvYZ+~oF#liPJ{2h7)sL1o=d~`LpV~DhI#HCgQbDdRY`R4SHUDB+sAW4GBJub zd?WIqIVB!&7D$+<)74Kt9&5ursM);?C=w912dPwQO zgsPn%p9;2fo-if7E}-vM!QsNod?0-@21Z|QXwa_rv_Iu*|3pQLIidssU0ZjJgq=z` zn{f)pj=|q1ZS&RV(r|v6gX3)vxDGj}C~o-d7@BW3)<7w3S_)>AgqUKAU00WaBQYxi zK(Q40LP@7a_(U&L!~F_6mP@gL4(jF+`B#a{;E!Py+9ONZ3-GzuB&vH5_rZ8|iUEy3 zzT7B77-U{|4EJkjSPa97{n4z7!)BdNEN&1e4A_Udos|-IVfD+WpUQgo+dy*DNwWur zHVr8NEv3xj5z{^BQ+2pqgXY)x8EfifR23{hQZh)m;EptmfW9P_lbxTR!)Sk z9U_a@NT(s`ZC6=mU6nyzLqFzg@A9+Lg4EjdkD7oI_N%|h1QfYd>-M^}e zzAR*5wzQ}E@Xx$x92;NqqJcL?iV2=Y$7oOyMMGaRN=E)#z%PTMUjg zo;#G&HW4!YYTVa1v$|10O$fd&O6U<-5JL?D{(Gwq_rPe@43RD;n#7s`7$$THVS70O z`44tdGT3B&j|VUU_G$72yY`5e|0+jh?C@U)hr2ME%0Pv1geL_<9iB|$q4XzmLC4^HKcDFJURKKWk)P={gPWmmeXTx_>x7#kJ7ME0gHPE?oEri!Tfo=wsuZ2 zbAx@um|zIiJ@}gX4lNZvB6U)B;M!v63vKABINYxS;}8uyw9{K>PHdoaw|0A4Xu(%Q za&>C#)9@q)XJ5Z;A^8iwagjPYVgo%R7v{A(+>?4`%(6C@Sxv{sq#(-In+YSFnLT_b zNv`0JA-B9U$FG44jTV!~4$laF(hODkU#i2OYf$6@#eh3-FghU)Es=2tR81f6wEicx zp}PJhm=G#!JF00Y3>{(Ok`;$?AJT7c?5i}~ZUSkHe11*$9jFGS>(L#l1F9QZuti)n zgy?r2jL;)iJAWdYWLcO#_N&k!`~3NjzT+wk>9_r=V}qn=OI&z$1ey_@&y0=@?9*LB zM~gbiHdzN`m+!o(dRqv3YClmQbA9d?@RHg)LrZ>||dnE0b0RzqE%Y42_qkmn) zkWy1sj&YJU1(1!sevwp<$M9!-z&wwllu&2G4PhsQeaq0Fz_0+5zdcYGVIEh}_iGrM zC3fR1N4$xleuD~5V;(kls2MJ(KY~L!jbKw_`-kJHp2X)S)bntuJe#7FA1lO{G#a>q z!khVvKx}n1_@Uxh$o3{}r#lNo7x;hi=l;G3WfTw( zHLTF{l({H}UE1Lx=|x;i2UH;^p9lMf)iSJEOgqr<#oeAt8uZMtt$2o(@_ne+BZBz> zaEn3(vMcJ`n-K`SV*S`x;PuccVB-OkcU|$!L6XWJi*Ck;9@x_PSRZcMXfgM}nadx- zEWn9mPA>1Z%Pm$^Ktxd=uoYR88yzd59x!EGn9uQvdGh{319s#8Tq15^=3S$7utwg= zF25UkKFuIzYGFU<+36mV0DM4$zwapF>BDnaM@7d0a%#DC5m@y!?$NzG)`fHOAHUP+hr)_t%Ah6p%2;w9DxZfKB`KTEek$%SCfYONj3UaApRwdmY#P!=SE8r zUt@g=NjR=nq@#Kc8qP{rPr26(_kDJ_zlQoojWZSwS}WEb4N?y~g$$k6E!2j$s=Jjn z?yatJJqp~aVGQ@QNxb@O?9RRC^QA!CpTbCC#j5!sYk(oYhz`1H+@N*DUIwElU}Zhh zIthB0m{PhtE#R=Q%%^RaO8T7qR*#!58N8D4=z`D@oMxMJgLBM?gVgNjY=5j+iKZht zW8JTTaWXAZ@pqFuI&HZ@reCLr+XWnk(YDJs!=D8uS&wc+wuns;U~OEdh;@;SnmCZx z3zEix6`u<%u-P9+oOkXzA-xopsp>5}4GI&;H4$8yXPFRFv3$v1dcC8j9!bo?uadeHTIbzt|CSGCnk94#Ma(z3PM${P5g zbPm1HcoD*-8&-QcUt^=cEfb7?q<$L%;qN<T@bmu1*t_LejwD-_6S5ELn>w5Zx*I5< z=l|WhmMD+&Sdi|Cth|+(5#dJTLq%lEe&Bx?!IBIEd-c)}nfPZSy%f^qs;d|$xW$Uf zzzobL|0UMv{?lvPvBI4AdHb+NkopY22F2h+n?-;JpYVDg@UP(tA+l9Hp8PW-*N0Yo zg2tJW`A{1ErHrQgKb~iY5j4CEO`@@_B0?i5{9E*9(0kTU>5ZoJb^o_(2AC?pbBd#3 z$KGvFDU!Ue92Hm3t34s%d%ZU6edLQIt73a*R0Y-mqcX;x>q7kwZ)l&vLB_N_ABzB5 zr_)H6W7mZ;9LfmjrG;CEGTzYi2GW&xq)lk>a(P}y-_x4eE}6Za=QhY^CfK$qVMvh;^RXG@}w$D;5piEeiHs}X6JBpULni7K!k!n>gK zJT6s(N)B8j)=AhgNDR#@`Qh&2XHCg#Hz=9pBLJ%NFP&InPLI;B#K20sIpK$wll`)6tP?# z-DlxHlaH(;EW+}iE$raLjwH(bAHRsJD%>tX*IaNEC527z#V|%j^fS{1K$p5ReY!0^ zP5Yeg;1Ip7TIC&$I97cmjj+fztxr|q&n1x4WOp@yt1*+LO=qi*A1v1w+|_f>(dT|| zlbZC(o}ujolJuzxSp?#>DtskH2ici@88eqk3{*q>^He*ztCtoU^13UOTu?=$^~%`j zh_btODq*3iXMg*w;(t|ze`=u*O>CbAhC_s_L7Hv2Ijzf(x5ifxkwOZQP0!Gz@ue)Z z-)4(Ii9-HKpixjlzYK;{`sup)yGO*i18cGx)WQSZAQ}jHIzgfBo4xw(!us_bj&g2; zZozWKuVJ=~>{W=U`S>F&!Lmo!pc5th8r(33tbF_cl)>U<0&#AC!;@}GGXRqM+`cH>^%y7wr zixfkWVobL7AlbxK;QJR(o#S8kTQ6Er1m&1CSfd=QZ^GGJw4%M>!-}4nItW<85To zV8fdMwc|v@lNTi3P}<-L4Sg?%U|J~M^qfrneNwpRBD?Fu&XluY%Hw8qVHG`U1luCj z4nyJyx~c<5goaN#bpI(3Nd=1@$zQBQReIYp+*QzgnCi=B$U~_1&KaG-5rzdZY-L6! zEc~mi2kk+Zd>#W<0K-IfLfhfS?GnG6QY0OfV2lQLk>m5 zh7?`OBZ5C^v4FTSL!&>MmkfF+1q-Ykn%2NW8q8_R*ldz{zQhjc4lLX=+ zy%HR8jS8zng;Z|ZVvLGT3$sVkn=a<{sV?;YVbd_5>q64mvJ)7;4(gK~Fm@>+wJcZG zTbM1ub9pHX{b^dzX)bB9=I*4J51l`vhJKX=zP#1Jke;y4C}@iquKk#W`l$-q7t!F1 zm#Wae#|7@QK31oBK&OD}Hc>Jj`k33|PWwfeS*s}k$rH$#e6qCkXT(GIA6w@%`(shK zXEVc*HUT>c0v|iF0r}3fXfjUnqoO3~t3$FG0i0}nVA%rj?7)T+*bVAAxN6w zhcaYiYB5T?kb^9P)WZ*eTJ3 zbaZkUTC#SdW+QXwoXAN+;m&~UDeBn70CfFxuHW;Jz#atV@&Qk7y#`{l+}xiC8;V>j6m_Z3?ORH!Ui?T zI9Q$R843buU$_!5R#i~ot`5#!YcHi?{3ixf_G*G`pU!}B4OPR9C=K~-1$~c)M#bk8KcXaw*z@bLVL0Q=JLdN4FPM$7R=~nx zjme|rutYL?iy%rdNXF7G5v7CuDy18?I4Q^ZMfm!a@_Kw zLvICxy+GjPC2dpg^#Mk}@O;a8yI(_SpxJQn#{ojJTQFKFxXQZC)4aF+B4W5}UNQNs zGilEabHLT!L9--jv*l1i!-shGPWrwK)*EgYx~n=qN`;I)vVBcnM_?8Lk5wy+t{of& z84?jYCv6E3qm+R4vPo5dvaf~Vra+wral&PmiLSAkREi07qOTZHhBwR59GsQ-K5QEa|wro$+e;o2BYl51>sAPQynv-Le-|8CS z)+=2Ixj8i-z6^%m-j5m7HT?fs>Uq5*8<1*-e1p3I*Zdxp4mC`#j-iEkkB2ZZ{ zuUCJbdYl;b7oqXQN!J?e_NT&d7ec$ou`p7fmeAx zp*?F#(zgBcu%xet26=m4tETJBu#uRY#QgYtl=($20Ue2 zMFx#bf_-@kkI5;tta_=GJv8W0fD8`kf|&Sq^fJBe%iN+jcP@}w(o4|W_cer^HjKBa z{{0laVs2@Hbv7I<@-~m6ADpNvR#U&mr3Ms(7f04?RIkxXc**qgMqItc@v?_#{(J5y zC5+m_2jMg}0?|oNmpAz-xOa3+`w{&K=J2-=wDFl-N(mwc`0{f;^!*}GC}p@fG^%|0 z7{(S^4J8~h_&ZsrwoPPDJ|9?Wl|bJ$?5x-4Ala5Cn|k0!0qIeT|MH3_eI-OQE^8s@ zm7ph|+N`>I9+y_XMR7paq4fqVkPH{6QWe0t_UdQ%JU(ZyW6i5J-CM3bs|g{<;G@S#HbXWpgsLO8(Q_ zU226>l)Mo=GWPq6anDFv2X(iENnivF?gB^s!aM>Z(>x7Rl?MG#__8bSeJ{ySqxmJs^6ty=RE%OWQtU+JQ27 zHO;o&;l$Y;67B11sTH@Zb3LiBp*;%IRl_~lFa+xHR2=5NH4Wu?(SYy~1Y)&7*@&$S z96@43yEs8$^vY#bg}fApuZZY=<@ua6-bW1!9UH~VIqAk+mEf?x0DdqE*aETw8@*nd zL+V0oY$9vyxi;Kx!i=%^5GXQz7RIIa@IW?LMfp+glTnszhEYEta91qCi|#6Un{@nW zB^REnkAPD8N@qSTr`0Z$p9LM1eek@DlQs=#CvcuJyUzq~8-DZpY_&+$ab~kt;Y~~x z+4Ze9+^+#dfLFfx=OlA%S)73zOtbBhZ{_A-D0N*=WxcSFx(6pxrN`8hdy^Ey764NC zeDT%0Uji{80m}|id0Ad3nzz!nNd_N9&lf!nL?6v(9T*Vzj$1-D21(v*AWALF4ulb2 zCWrK;L)Pn+?_$b3k(Gq6rq1YMzmh|)$)OCXx?EbUe^-MFV+c8OcV+6Stg0apt(0DiL%N6a zCbPP66Yrt~Y&tZUr;}z6Vp8ZCt}#%h;9=qZ%*nL6_SH8bh>$q4c}XpQHqgJC#$6fi z7oaXp|7PqiTF8jZZ>0C=8m+nE$RGElA$4S(iepVpkw}iAkAtpD^@%Xlv0bc=h*i=nWnl5Reg{zJGq(8~Phwt?nKU2uZs|)6 zox&mZ@a<>zaPyD+2$?NeD(*hFCZ0MDtm*Q7U1L87ypZ6mMvly8D{{=`Ct_0imm1oD zsh5aoHP+-W5_VOo&Dk_%$Uy$`KF*M;5XbOb8txBZ8)ZN;Ek{Ihwr18L{Td44!i-&a z+TEdR1L>Em_!O3q1My1@*zK~)kek#)@YeHlark-+tkq<70h)p;3}?rr^(`HFzl$3B zWq^DDFRkBTH7&Hqre((5FhghaRvPYVXod~dJmcfInvYO+Op}8m2lU!>81d_uTeR_h(?j2`fzK&W`KA<|`4OoQ?wL?JjM4!9u2)9179L<(5Oh4A z$o9%n9oi-Am|Nl#UK!_Obx6N)CB??_|C=H09X<5?a8{FE8TnGDYl;hPx25$aWL8kq z)CFI6C2zdn%tPqqA3ayUFMh^;fH_l^gfWgn^&>GT| z>h?+WagF_EmXW^X(0T7BC_Gp&V8>Mg7&|v;%yRSCL7?;^YjWur2Q>8RoljE|(PhYu z<3=*5(PW3m;&8hJ{7Fn}FtmcHw(^#i!|ZwPLNeNCOY}qag5%B6`A$*B!;c~8@4}R# zxo>in%42o-(&3J4X&wk$SUt<_kvFa}!?FEF@%n9gah8cFX3aMRm~qBC7$*5}7^zKv zisP^9@Xwg&?e+E;1nTFshR$^yA@AUnTEF)3SvF{9&2euU{c~+d|B3VAhS@VP*DEOEFsxB>nFjIDyOIN@UNU`GQy7W;YJ9RlA=1pct(1)N#*pBUi6B#ko?T$uJVeVnLQb*OK_gvtB261MjvoM ze>@T%cMhrC4Y{+rv?_*px^v)#8UdK=4>Mv@yaiS$lr#2gtj63<=%q7N8QuB>%MSXM|^Rpt>v0 z=OlZZ9KK?rx2r(7au2Qse>Gf$PaPJo=IOWTdc_uuO^Il$D(mZPUf_ z=e0iE6M9)Xu*tyTs<3q}5dlgbJK}thSuo-4FlUg=xoU>auP27>*|6{G+9QCao&qX* z6};WAfLAURQDrDfhE3U-P4^yolg>ZDQmb0c0C=wg-61Lwc%oZo5P4POW`7CMr8i{h zb`iiL!+8((tz{lAVxdgo!_J?DX<5G((rT7hZw(%YO?xBmOI`oGkfo$}-a7GE9PSat zdXOAfTmuw3B7%N4WY~3s`5a6vq=V*mR$7_C&Ws?@NP1J0o((|_pS|O8cKDYXIvJqz zxR7=^36e9`FUUj%KwS(|&#OR5 zDqp{TJD8prpjZenX6D$6v#pw%j9aRo%ftOCtS;3sABkeLY(pc^Y}{VIf}$^^x#@Cp z!XHMdw!VyVURft@+8+t zen^Y1*+umZE8v3)gXh(u<7MEkx<8qC)N z)6z1o8vv;Se3Na^vn}`i0?tFHN|PQ9iS}G;p*=jcgKo;4=Ou*B>XSjzabX_Sjzv<4 zXM_bBA-Ix}{JM=!-x<*c{UX+C9=4Q5675_BXLUu$8~U~Wallw_XJPZs(ikBu9M3Zh zZ0MF?rZ;)D`E_up_q+x}bfF58*Fe4Juh$||NggW%vXbfrCh+#GH$-T!W3@&QSyRS) zd1R>Lp}Tsm3-^d-8Uc`LgOR3`Ma)1st;dYR^#}{Py$uJ$gu~0J0O4{NZccFdWmw`5 zka_eH68&`rRkd!mS+9}Pf(;6F5(lKIC6Z*hosoQ+(wUGwu67kO($!=$F6v>#D5&(1 zF^t*soR3AJ-V|>xs}SwdBe+j2T8#LcBmCC9{=8PvKue_DtslEuHEgLTu?=@)ZW_vS z{$*mg3!%4>1H3hduJZ#b?1@22;l@+&OlMb4C>Vy&8OXU}z|}mFU6*y|6;1x1_R#5{ z0ntN`vS&PrJpzUdSSOL$fMpGaQE3a_VtF)mu^PvV`ReTwFt_+|#YYZ3yIch~f`Ymx z;B_|p-9kyh8$X!&Dx1rjB?!WC30SL{S7Y3GXZ%=OR6AVL073Z3pcWOy&ms$m%}O=-qfcn&)!vpg@w7yr8v zHfyj!$IqQZKD+bXuCxZv*Kxjte|}v-a-YMt!2yM74+?h!9iQMjI?uLIcAt#z8>(Eq z`q#||f9AmIu1(U|=<@2GHJd)AkvpGacKc{x8nl=|B0CmL6blzKyMNG&?cF|W7|d@#ViCI8c;$L zu2qO}9U+J2c^IzbNqcUGy+;!3{VGz9Z%Pv{g5go3!A%h}5qTtgtQ`Ggk->VUf$@1YZ!^Qc1kvu3(|dh@4lyc>2zIdD$QS=m>C~agVPRl&z;k{0(&aZJqAqy@GsAFZ zAWeWXqoBBN$M)SkEGX+7IdOPAdRPE23-%s9`*z|1x@K?7k!VX2It4>r5m$8&aHha z(v-qENJOb$G+LTyp3S-@OmZ{oe08ik!^Y7(w70L@UGGu*mhS1$lqVH`(}e?&(xHkH zc7+mqs&C$8=2b?ebgov3nC`AG)ieG%&zg1#`e#be$)CHg5El$zg4!@t6z(|ji?F^!o6obfI|X>$Vf*N^DT zd{+HbC2T!chPx(O(;HxAeu#b<&4hHy1BgY0zt%Y}6Q^u<-{@xGc@JpzDiiwHSi5em zStr}SOb&N(`Y6Fu(lk#qAR`!gj8OT`H2ET)tGXE6gWVxevskW+rHanIJva#A#gZCb(2B^5pPOWRwo z#>f^X6^sqA6X8(984BOk)uWq=UOAaznI=k!%0VnLX5d<`402#9sXX(PZlYW~;$hx( z+60`+_(Yi>M4DVg19_)yv)JN^uj+`>hJ#Ubg;zRVqG-+DyNK_jC_3Gfp{veSI{3`J z%0H8uJa(WJSYd~Jm|G$Fx)JtOArWRn<5B444YQvz%;6|#xA9aP{uK=Ek4Vpc8NA@x z2-4N!t$(@$st`PKaaesLbuWDld;7ZoH|?98d2Lr+$hQ2M8q&>n)%&EUSzglu%3Tz2 zwCibK1D`J232j%$8-?}$5RSBm2B*?<+XLYKTREPvwfsY2ZSY%3862Sg93kr!9$LM& zU7Qe{c~f$F#+2@oWQ8?(zjn;Pn4p06n&-qGrW)eDo#hNY6W(r<_?^M&$f~RArsadc z7mVdYeP1-3_dWCsQi{g@MT;nYB&(lUW3=a~lx;7_U&NRenp6jVNVJQ?BlyZrnf3JE zvkrf&3-@qn%1WOXqQuYy_Y5CVC@DIk@k(#gluBE^UVTXC?Zj%dC0A1-O?&%GXe-FH zW*~2V8R<(GtrZ^4I($uhG!@p8PoasCK5}fK+pf}GBpSB{EFX+_s_!t>I+E@*4-k)gUl9RN{poZW&jy`C3`ScFvVI zE2VT-MTg06Y`-&<$HF9GDC%&i!;iQ3xPW|^dBhR5?iP^+C$aU2qEAC)LiiSFM*a(b z^`??P41dRT*YFT-AEZQ*WWbZ)KB0Z_HCUHGb_-69Cs9wUXPg8!OgY0&Z#pRjbrGL> zMNE%%;jW41!E>q{VQPd|26#JTQ^)ypXG8ZZuAP&6;95e6%Rs(uzQVk+d~kNcj`7Wn zJKYu09OuDKitHgfF)YqSlOV6^4i;{c2~ROvx9zq)!2B|LwZv99y6EPSbvd$5yER}&MF706DLofNAE*sBrH zeiTA#G27zqtYL+$sYd3WlkaC@AO`6a3wUL(-mU?~y%W2!ZanVZG=i;WHO%;|8*gPG zIh2ky<*etAg%bdfY1S}Mx(#BpXFh_%`b%AMm4&+?I;?U6K?_}j5uiiY)d(k$ySfqn zuEcI2b{l6Uo&Y5pz)Se@2a<#-zz8GkSJB&DicCAg)L0ZS;b%Qzt23Ip#oN2o(vM9B zQ1YT7JUq{dCo*fuLs_#rlNh7XkA=?VC92`;GWdi~&vNvTvrN4ETY#tNxi7{BBwM zo!CqfdUUqAA-wJ5lmy|Wh5QFgx$460eGt}!sjoRuMNA=ANS()NR0wa1kz3S-luJZL zuhD9i$#p0AM1ao9Wg)}xnT7NiI-dp6_j|ws&J55YQ>=(l!>xi^V^S?E42TB-Cb>_l zy?a6~UUS+I9gUNRbH z8;2l51Yl~37Ep09^XREG{3{+>e6zij25@AwX#8u9dFvBMCXDot@hZ$FHBq`hJ{O1i zALCW;#TmvZVWY%Z5XP%K(srcFc$J=wvw?tv38$=LjlOM^(&d(ML{2#E)N^&XE27=L zUiJBuxz4$Y!m84vY51?*VHoJJqdmt!s(h~FiLsLh25J(vtE%C%S=djdA>YKv5N_c5 zR(L#Fq+EOga^wtvh`P6tgCo>{bA284P>#&{W1^fJg6lzzQqPHY^DQ6x>k1eTtEVxA zC~0oR5D_VZ@dEyt7#b8~^xCAEn0E-F9xJbEvYkK@+zq0IU`710zTK4}-__7X>bf4= zvFdE>fePdD>2cq}KI~(9+RW7$goqyz%By*E?|;(OXudHUw_BjSdMXY1u0-~iFr@)l zkCSEU7&MQtshANTQa*-Kpfx1zWoQyGHdobT9W!S7RH#S}mo@rkmGN~EJUk?qP-5Kw z;)6lg#MBy~bK615c8d)6lVI!BU(hEOs1dC66(i?RLpy?>X6I=?yg+M{?GZ zH$*Ki`bqC+gdD}|pgjfrK?K?aGUJzF0E}6>+WnV%6jp&hWn`9Qiu@VY%*>?U059q1x(=F$=ce$y7uIw zbReY_qBUUe&*pU^aH3Pgcc_4(=Co}p2qN@@abhgzLs`iG5<`Ovo%xxv%8iq!>$pdsZYMI%u!}0hDR^ryXsZjq!LyIM@S#x4^2tTVJ zdcuzzr@gy-jhDj&>%aS4@FYE+vjn4fjfE`Yx&yM)P=Dp8-r_8n&fo)3kuv2+Sds8S3mI-yy6aT)XPm$7Ep$ z(0r^6_iSh;ZX9Ou+-ILnVQfKKJAtE8Grs4%^loe-*gI$JnJ44*X{Jnd{)>fM@0xNe z@qSr8=Pwa5V6f@*I>uoN!@^}mz#Y1h#PL8V_~nADZ{8Xfz?}{{n68RsUYhWx^t6ZT z>az~|>k|0K4Fn*e2c<%TxLWiKNKEnxv3S|EpsfESgjphK*QbfO7d-Y;0d5UQEyJJ8 zN13lU((MukOGbldU|?lBj%x6^Nqwq|DGvFS*FHbv9$gR4xx`3z?^mK9iJkFCEP0m8xyTW1rnXJ6+KC&kS>d{w2l{?1V3TpyUP4$BK>GWt3un&G#yWOvWMWX9x15j1FA?7NG|H4Cw zV%gJ`xET_BOe?U!gSN5$7+z()K0}Rd%mP4ycUQVZH5a|VOY$RE9AYQ4Thfu zeQ;{sa^*&7*Gi*Oa9~Shbsip}KQm6G>h3uGz$jX0Bes)z=gdFedK}4OOK&TN+ZFVz2|L^z!-$NJ;Fxv4W)uan7-3Fo zeH1nVV@xRO*2BrXk2BnQqBx=Ye=ZC6WODwC0HMJMDximv-q6^?j+-;_xb~i5T~OfV!DuXn856z+-)9)xNjS}c zBbB$RaF;-z;kxSTfpt@4)#b=((;n6uMe`P`A zZi^n>cZ`(}!)VZllK>kOtUvY`SaPs+z+RKL5d%JqXpL7)pV(2pY&N;EVtC{veO(47 zo30pC#Bc%&)kbB)@)8K06+v;M(xf_J$DrViNyb3wMjmNt@jI+pRDgFxn7djAZrWgWSOw#lK9R1I9_|{byNZaOf@1N-$GUJA-vUnY zC2*K#P_u?-N1*}@0=ro!aj*93@eC$nCr}4yX|5Nz!(@U>b?q3EM(1zK=Ul#1>bI0( zlr41A6WmUp9RF4Wb46C;P#>A z0Z3W@V$JQc%Fapi!>l1)V*wbg_ z#3!o04OnhGvQ@usLMK7d2~__k0Ib3KX_bJ%PBcrrIrj_c-56c}Bup8t)Ie(OLrcPr z``kwHkCovrzUBP3*aVg&M;MzX4ZbI9hDdDNZrmwaf%TWy<1)Zy9o@Rf{{)F%1LCQt z2w416VW|JsxaIcfsh6~JC2!T^m;-Tr22%}5#6-AckhDnp(~D`+*X<7^iF!vhO9ld; z?OHP6QGqz3ZqHfJy!8;F1z?&I`W_<1Pw!@v-w6a6`SaZgX?9~^c(~3eW}tP5ySK5`L4kBs{^MyLM%Dhq! z^QC_-%79VJuTqNIt{z%Q%W11ySjEm`>bBBfJ@xWk0L{L!NsyL%=+H1={@P{%%V`^0 z`epx%1x11|aPZs?P;O4<8PVKZ;027*caY;7mUO?0xlNNyya`{z(PQ8GV8)#p*I0h6 zGkM)9Y;w+gYS9mU<1*upjM9)FKUao!H(V_-=`bA?=<7AIfJ0r>!g8{g?PNA%{c_^t zM7SJYVykn#vTUyupNNh9TlnYKE1@!haR?eR7#TBx<hn_AhXUAB)315?VN0*Q^2?06`275)N~(nhPRjI5)01w1DdfYUuAf zc`)$L`GP;(q*GJScWP4p3WdhyR*jWHVhv6Rh%-Zmk=PG(ZXx<$ubbql5ty*r`%*9` zjFU0KKe4@o>GPKTng1by<`WF${{d0r2n9q!)d3E$Dnyg*d~Bf`Yasy#CD1RK(6>wA zaA(L!+jR9%`&y-qYsih-6Ifu|3Lc?$TyF<0b--Hd7|5|9*iIqtemGN#Zo#bm~Il+-HQ}8&Q37W&JqflwaV~MzDL(^ncwJe-h zkZ7B=9xc3Hd58!=kuKv`zL-if{rUdg(;${TwVy5;@_^s|0NdZ<{A zZxDP^%rLo)XSIRqtX;ONLw`TaunB^MO`K@)o84Ai2O^|qDDkT$_x&oc>TnfZ#2cp= z^AVBhNPqO-WDRh6G4s05yt)FeVO`818I@r0@h7frUJB0 zw9UGZkUfc=-+y^KTX6b`2(i!8Z-*(`}-_4|p~!KO=o?mFv}dnAL;N=G{G%0XertW zeZTA<_|tjstnl)rec=DPxy$x8=3kq$QwjIb=aB7t!? z6tQt>%3|vpngACEUISRww&IZ@1&M0?*mVwgSTje^ zdTPq8WxBterjv|oTYo%_COzS7;s?$CiI}nQ#6LGp$@sq^ z+3eLqDSzpp@gnCH0JPBpcj^{cf+VC48v1JAPa_ir2K)$z$MlvpQUZ+XeRc+{)AS=s zy6?ZZu6|tv7!-*)7<25U17w96s}EKW_-hvSdmKy6w5g1X)gfDpRqiPonbO4*%E3E7 z-xC{dm(bY&(*-nc9$9`7hls`Vy1|;B7tOKK7Q78()X@K~=fH6`@UXGOd{RI)zwCdV zAAZS@d9QxVyv4FEdZx@dF+zjKDOH0ohU*^cGvdG+sS%Yrk)qF&t22JW@)JIJR!;Z4 zkG{u4S3>3VbRfc=8)+(2_jJ=e9tf~((N&+?7GT0RSrnrB!7U`mo4x0UVgMoa@3B0L zZ>Ey5Qd`+Tv%^0-Y+UL6w7Zx=+|&A3wQja!TSaOWA|$CE3qNQdfp6*WRlPk%HstZ| zs5cc~kG8Aq9ZJW$B(s0xvrt{AH~o*P3qKkw80hmxV*N^p*4Tsj0iBipo?(BA=DC4?@V5D{Y{z(#)J*86AX*Epa1C14pNsblCld2VTTtQ|-vP4Z#$Uj7tU zi)1aX$JC*Ttakq}2#i9o*Pt_i+YwzOE%{S*_);P_#eH~)Nn#zJ%m%eHZgz$w){CeF zdIjK}f&8hYz3DeQ`z17iZUq5K&g08Q`n-=0^hF4H^>p);V!Ct}Ey_kHcKByd9J;&S zH8Y&hwFX`l10+6+a~Cf`3raJ1UPp(U6x9HoX$&Ykan?34Y+@U1E)i9?Z`)HsihV!v zrXIk)jj^AZpED>-GR*zhnz!-1k3R2l4LY6YFywZE7uJ+ed$L0+JcYVbE3(IULhrNg z*E$eROtDX6-HzDFYhR;c0{rLry%N4E!+ArNAg@!17(TQ5{~08$xvf`qv^h72l`JWj zRoGJE3cZ^9BUBilg|t+&9tiMbWw>i`&GZ)bmSLq@tcf&px{aSe-;OwxvaQ2HFndSJhqzT$@z#Yo&N(@ta;^@0 zq!+>4{S^%3RcKi(3oAMa{c~*C_BJ};iYq8Usp)Kt4T$n{Mc^ZcnB~D1-Bq24AiIs1 zx^P#dJ9R={r0BX@WA$YWD$u~GAN8^CL8hC>!>#i{u2zV>kL?{dp-0Hu?2+;H7}SvG zUDyDd$e@OZUxx)@Wi;hYtAK;{@<)!`?-Q;-C5J z23$qqb_trQ)*Nm{2~vM&3Es4Lt5+td4xC;B)=b2Bou;p@mz84+&2B!!-||rM#!VXf zq`WPo=e;VVh-w(h{@g!ssO{y2wlpR~Zh6U}#cUB-ebu#1(;yNx#8lW_?P;`Q>ab{g4w2>m`GZ$M!vdf$mk z_h{x8|B=y@4U|D3*;v;815Fe?uq(#63H{I;*<`Io#u$Bu&}j0}?S-^AZ-&PB1yj0R z1oRZ7n;m)&Vm=~G*@CPmF_Mv}L-)wEO;`CaR$tKZBd_ERW!96%bYGPrg63Z{K=b^L z@re{7mtLkxG4=IWIUFCZQq)FtPR4w$2CZUag|FB-!jvJ1{hI2N<3oDkV`=zTI<%sj z@@${P=!#d+8Xi~qI3ev3EQ5n^3d>W?@J7T#zxgNSFE#YMk{Z}cYqtr7H$}n{U!qIq zxjjFVwjdnt#0jEQoaiN40>}%AfatEuf~jHTx0S=ZC;0@ULGQpQ570HTWrkND9zsg{ zO+1kM8I_qr4-CEvE|sl|J-GcIyOGxL81+`}5z%bkk8KE|4Z?ut(a5@(kxs25dYk*H zc4dFM&dzo6puZ!yJDW_#FJfB)Dr%hR>)3F=1S*7Cwlrf2mL70nn3_%ZGs(KW7azst zTVo%Lmt011r4O?BoAxg;I!)vL%!q#8I+QO}^bnZeX+B089yP)*&oDk@@&Czs!vV3X zv*F}M@U!~$#QryZ>=E#oa`PdH@rABbzVyg6Bk+-O9K?)&(B8}iPsItD&04>B#o5E> zq?u(Gco0`D=;PCj%K#4G;!!xh3=Urx!J-GgsCj1L5r8v9_hm5Th6(ONWi5vxFROAx zbniI5BfdH!GGVXA^Y8;d@~ix<9N!@}P!%wQ!2n$P*@(=C0e1#61~dP$Q>rm*VWH49 z!G%Q$(uz9hG6oO(C;S()@}GwaaleAh`q*j9S8)n?DiFrC@t}Wg=BkVyb{U7Mh=9(t z0_&|=FHLe(kU8*^WmBS^;Wl#eJflkAx1pKz7%9<^Pa4NX;g5tD1-Tbm=ANgUjSXD` zj+-oGAuO1ja^=^fah53YTN%C!M7gIEvmls?a*c3o(yUHbSb}M@=Re+VY$7%KsmTaV zsslRac9Xwp&&rL03Hwwa%D;5c!_2{#oiqiaA~+kaWBGrF;qKeXiTWHU#QGlk3zzkN z|3~DCr|@c_2MVse*omIQ1=~uy<{k7#M#j2Hv`r8c2D=}X1$%%2rZm+GMeED(aDNC% zCN3Lkp{yMhlC{p~3prcR#x}?-uttCp=QDe%My?Swfb$w3fy1UbpwHt&xv7$Ah%Jyq zVlyVH$c)TNo^f|BqIY=9)d-MTF905w12t&Hw0TBAbA&|KDOL&;{IZk2Uqez)bTw$8 z?OoA z53Pm0I;fJq+*F!tC0(F)0WGY%10^93gux)a9v~{_j6F4fN2}3fCX~ij{M8l4F8b&QqBl3T|N3ZhmrA!_n2+n$B z8Q06pkm-|n_d=AX8=WDTQ(&2B@u9g_#fVhS^C2s-?Vw@~(Qr$AM3(N?Z~&A*Yrn`w zfZu_U6q|-x9Ds~4bOAz)MZb_Gh1-*cb^ST3tv^~gf?@y{8Z7LT&|AN*q{~-C@%*Tx z)jBXvWafu6wxN3^9R;#9HEx|0UA*218AFv~RzUXOT4soY7!&=ZbtviI-rVS$AM1mq zER)Sm4&;Pkju^}clBiG&)^@Q?x$Sd(NdK`@Dv_;I6~*%LDLNp)iUFcVPVAo=l|u%J z?t&d4n}8p~jeG~_6DXR9qkmi7eG0@~q+WKak1Lf(JrrHC*kBj80j;f#4wDz&Q~Fz6 zUJeD>4CRhdaFHXA@>v>PZqjIuE=Z6>y9RC`lY0AR#I%HrYUhR&33{unt}hj_DEFY6 zm8v^g+p#`_6BpG+d)r9guYqsf;1MNnFpS+cHX`I^HM2rv+_L4_>T@z30?}X@BZYN( zP+$&QMGquy6NVX)>hq|CD|c~p&ta+59ow7L3VaD0)vhf+C(;$VD85wJ^oT?s&po_w zVUi<}x`NhlJHfCjv@b7ad*v=o?h0B_!qIX<%bC2KL&P+3C}9CkBflR9vLFtDE;Rvf zD~u?)`;lNFyp-MUtBHJBNq=1g|Bbd8?;Nen4(h^?|M1PsbdMRhJs;ADHG? zxq!>+yRrS-}`dV!b9U7Om}KmsvAPwD*de1yN)*rqak-#bob3M>p3HI=Njb5-Is zh1F1{=Xf1xW>`tLcTc7JC7@sB8{DZhpU6mBdmjA9lKE)y52}?C{K$Xf2 zQ-!s;2jFC8o-PWvuTx2aFtZh#_P)l>6lVUYL3Vxy*~YtC_~U4lx^WYQqWLmB+%Lh$1xD)by&7ra zDM^*%o?**Qyxz6`@ykUTy-GA@{p9b>{_!-@Cq6zbW$Nt9o{GagCmKozJbMoxEmT}S z;9_$&nS8X~X**-5h;dnE9JhdqRJ=fXKjcxToiaAf@`)hFSDkeE&WYv}(r<4DcaIe> zID!D1;4<5yi@N3JjTF+^NWEd0TZa~$d<)vnzvH`T8pe+|i|((hU?&W`*<_cWFqR_R zRt}bgT`E|KuBE zdFAiuZ5!^@Ft%qs0>gRz`fRA0#^lGvs7;1nm>c~cmGD&_ZkIqmf)M~4v>N51XCrte zp&9C-+fv#V-4deK-vO>d78N}_Tnq5_x-mY`tq~rWznk}~K755`uTCFojUGq(V}p?v zM<@f|SW5JHWg%+TT3IKqiGH=ct3x;pIxw7~n z$Yu?u%<<*XoyU3=$xqK`Be^U`WI&{{YQBl%gp|sM>QMeAhRbh(Z{hew6zsuI#vmNx z9-d~WF5yb(*qDV8>npPKb^m>$2L2r%Bi0?(9IFQd^)4e;)o>if4WAQ&!ZVB~6MV0b z=yLA8bk@=Kxj5XP0%Nos&IqenCsJ1OLbHI^=AjSkHWk9O0<9gNVWW~^7I3%*?mO;{ zLbbCSjz^R3a#u;ygq*4!%w?j9nC)#)Z zV4M9`8tyUGGte_*MJa>>^?rlwFx~9w=h2Ug$SMbDm4Rl6&3a!BVjPlo17inZqAcF@ zrP&_s_R2jZ8ncC|lhg>2Ny81KKo>d#b9HK%7h%j8yh3l70iF?QJL@obrOOr?J7j$Y zv^sl%mG0tXHqs*XNqu{Vc9=G=7m$p<6Xo^>bXF~5A*5aV>a*vXB8lM~rb<|+1)2?E zIh|wQ#)hwpAnj5|43^u>knrQnFw!TVV+X#~v=!2ns&!fG=A*sAT4~kUs>?K3Ys?K6 zCU$$TbHh!b>OCWx8H$39ayiXBH~CJTG0d^EUj7+J&^fSfAc}Dk-&%tovctxt2UcU~ z-iWNXE3ns*tb$Vy&kt;e->YpUa#lFXKvuh%Ikb| z+`IjBU9@7tq5g&~EhtFWKoY=nV@feMWhnlW)_}x=nb}DX9dc7>3?e#|cG)HBlgOde z|G}ZVzBqK#wVVSf+PW+BC&L=T8yKEOgnYX+;wHV6h5Aj6h~RhA-uP*IG;8L7g8dp1 zxxtKB#9{1$og3Tu^Ksj73kbvaPJ{fmB@WMpp?zCBF%ULwNZ6@$pdgaVSUB(^%{|No zi`AwY7B^_#Dko)ixm6FE3sU1hF?r`mw|c4z_l#&N0a%XdVr=)CNoP}d)?SD|FkOa& zF}aLP-cVB#h_pRfFt#QJp^2%Jud^vnycVv}xh!@A(#2tR4;AkZrO zgtP!F7x=VCM+B*@{bkUt@qc@_RVsId^fsqJ1hDG^=sz*t>l zUj5c&*;4!p&yN)=K@&-vy8m!EuexxP#?lm+_CUc86G#XLJoD4INv0u4iJRU(1A`_b zuAxva>z9Z`i8Ld-!CmyUxB=S#ZrQK8a2H5teiU3_hJ)r#j%5SCcV}+PIvAf9xgGk) z>P-no+3A%`agg+Jr13o6rxG+SZFa~n z$b==Y%Fv`m^ayAUP}qgE>d`05z0&@NAkgC_2(+qLBsgSs2ZH8^^U|`}#qeVqIO$33Y%_jV8>0TT7pH^|sI9;t~aozLP-MDNx_PT@K8Bp`hSE<|+ znz8YLoHo?#8SBGV8;CKm97m?S4MdVVx-~Hz8F2xf4HSRtYFIW{8-TlkxB%OKHOp6R zxC^95uv&KBo9Fu62-h@U*(K0NAg#odkEEjX$mEN4={PNYTqE$CMk@s!{7nE??)vBy zyVC|sM&Xi(TA{L>hIjKzb(9SoPr|a8m21P#8A1boIzN%g6t`hUV%D*(Sm&eut@N*D z!*MMe&>oG5d_@8)oTL>oNx?mTY_1})@?5VAJLxRDzoc#_d=u`!R$e3Je@^ow7gCx(m?{?en$+KQ@g-&qK z$}}k24v{aED8>3;AO8#$Xh-D3KGui+d*KjVCHrlL z=RO0ZW3NuRhpV&~4iXlbUJJ^%b)c+C!f2+Hwg62XYts4Qj{@*lf$0CDvr3|SDG+d0 z)~mPPRI-OUkbgsaxQRmauswt(^u5pJq5o$WeO@6B_K)U$LLz3*UZj*>Db4GSag@Sc z0s?N3#!y5uBW{g0(7hU4^(imq;r?{*wByH24W1`Dlnal;@a-d|219=!p!nTGk=*KW4L6jIBs(e?)uxr?KPlK{(HMMI1^%m_I2&QPKZoh?qK)MT?c$~Mdj_%=z1J^a-_ zwLt@jJpQtZ9`{wWlxp<-?0+)<#~4F8lauDbzM-;zpf*g_F-Tb}^wk3r$9gJ9G-&+_ zS(?FO9m>HTOT)O8Cj?DdbOY8Hj5j3bK$y`zMXE&~7f(rr%&KB&IOmC}>L?Koc6oKP zCppi*@RaVdT$h!O4w}KZV`7dC-J^{~!J zkF&%0Ujlymvk1;D@x&s$t=A29<+=qCK)Z=bXK?E|!p8ApzW4POsF_P5Y&EQ{TGv#! zpF8)>VPHq!>3;YlJB19_b;auw?T&QB{BC*l87V_^{smYXcWiGbMVB*vZW$tzv_4F@ zK^MBT7A3x3tHs-$v7mk#{BYB9PUZcEdG-L<>E>fhY{ZOvm1+$YJ46+ z5=z#;bq+PgmYLjp9B~AO$bt7{xWzm{;TBx2{7dj#`HG4*(WOU-gO@=<1l#ob32``$>AP&U zBz7T-A*>u_EJTsC*=3=(2&9uO0#*J+2Y0^=rNk%+ax-fzp&2S++KbEIq3|wd3iVHD zItPjun6$X$u2;$ZZ96XKitJo`#wPl?I^46O!7soWpN zSbs>*aMosBlk^}0%tg-)SmIcQ67XIeZpzefOQFRM$vjZ^>GrGz;!}&q){aH(zLc1y z19z>DhyYc=P!0iEA(5^~)0*W#JV~@dcj_bErHf4mw5*6XaJ0(yfKyT zI`tNI3x@$Qav;#T3$vQdddfz*+&ikan|zL$ZG)FW>N|nSfw{-;OA98{>OTFR=Dji6#SVy|)95(}uI?SrXP6+8S%JaK2{zl>bCVtJEnw zZix_P{5TGQCE#Du*n7S%gHz(dV=kn{>G-7f+%41aso z=ztZF{ZS;9>nqQ0x9l-$y*+>g6^>=D8k%r8I?$h5`D3Z0H(&CIf2$6+yk;8gK>7G! zY0Sa|jxJ6+8*5ZA$5u(RHeJ(tAz6Yf1NIK)Qn4|Ghi$8xm6CED$R4(JB^jW><-{Tk>n;h_)* zj(bMkJXRWbQ%9|lqt@-^4}%4?f7Qx-t~PnSe&Y;O{8~rD{5y`n%no;rH2p>RDj7Xb zZ@}-&AyxfOxAZ^Ut@4=y;K-~O*7MJNpwy0hIjFdMb9Qiz`Y3Yvmr9yNQhu@B8%i-( ztGHg>^Tf(O;i(gQhjIZ+2HPI*#@+Wvutji3B<`$om*brzMdl*10@&;liHV2Rp(6xa zwTV3?P+R(mm25;h95%8*cH`G5|EE06Z(=~byt}0-O4U=q!#Em4{Uwsl6v@@8!irdb zd~={-D^U!ku|7niLwXJSsW*XgHr}>hTqT0jh_08MG~p*C7ZK$uPKTCDm0aW(NkZX<4xsozXaNOChIa+F~TAaoo7>@+A2kNRkWk!5PE7i9Gp8t9We)`CfU z1&<+X02|O-_QiIu9>3Sp?7AQ{;9W;`AjLrNMvIV&Dy&}s*Wu3U=ms?pj8cL76qWOy zmKf`utiYC1e#4f&FJtP%lFg1d{62+JG~*AtJB+HlX{?hUSs@`7>{v#4ErjM5i@BYlrDV@IQ-} zT^{OPoZIr=Iv}DDfvoWwrocr zO&m14UU2O_Ik+}$1CX;}0ZC4%d~y%PyhRShL_K4^pKHU{Qz-dbJHQFVG5=Jl1HPi~ zUQK^=>g1qld_v1)^40;&6UG~5Gg(QoJH6x2Du^tq=(> zYlW3`ku{vRH6wue|N1OnlC~#jo`t{R(zC#Z_Q$0=grch}8o<@ERW^rNtGrx-AYdkyvLb zyS!D$SSR-5=b{#~x4H_%^IE#z)v;z2uy{;z_Hy4Osw|gi)DY%fs|Pj%nxd}K2!n2} zu^Obutj+v!4=5u-xPq4UI6&MlVnQ++8_voakFoi1`$sl!z-|3g^S8SaZVj&8bx&CzI4=&SMB%PP zAWpq%6LUT@3--q-<&PS1&%Q$2lNHSYzBX9j{aVXkwITls3FSC8TvUAuVT5?mPHp6O zNaz;&(9f+Fm8hi(zj|Fax|^TKS~c@02Sr)z8)Evi(BXCsCyWIC)~0QsHne8JL6kYA z%0Xo7`M9#9#q9=aOj3i-)gzJ4Xdcf;0VmCDD!fjyg?K6s`D=FA_IsN9=xvJ%YqX#& zxdqbHeW%u*avdj&K#0vO>DLvP*uBLRDV;UD(tTSxd|iS8(R?JK#KSIuWgKZf7pu01 zZ9og@S8^k8aPF`7m!k+iUt~#bXQPbL7`p3wVaRt=R*-&tsyImTK`*C+2eCU<_X*4_ z&TXDiqJ>}Y@3~F`BqFU<(+N-C1bc;ISGVJ%=VM*Cy^*>y=RRFCRNB20F>8@Ef+_a; z^-YS6wF6XQK)6h_Mn_zLt@6>r618qviE&}XD)5)B&Q%m{5!&=R%EVuVomZ3t$T@@8_RSy}W9vK)eePG~fJk|);c*^rprn$o z2Oz+!0mmt>$l>~274A>LT?In}=c>DWIs{;Ta$9~_XC(P&z5=WkK)6?*l(Yu!@GxmK z!BSeRqg^J27WSzw{ECl;PG5ESWH`q>Eto;@fObacWvrzBgAAG!?;a}$l46rn!nKg@ zhim|Tg_ni&{SuO)ZM^!5CY46B;{aHhzLc&!+T}|d3e)xD*QX0ba^kzExTm;qkSYY> z#Um7J*4q9c)T3sM)s(<6*G}`NUB0ZopEcM@ z<*_u}E6gnOa{<6-N$APZPr9XC>$<*-Kfq;a=T5-D80IYkbVtw-EaE^DU|$!?1MVdu!Q4UzYzADwX+`)@ z9R3v`&Ea`I=Y-OpxNuqn3l_1fbRCLn`^95LBg9*XBTFAIwW0hc=I4w$iO(t&PV}F( zT{#X_&w)+zZxUZ=QRv}NfZ^I4YP0Rw?don5S^JAJ`u-4(cI+eviL5n+5EV<3nA+R} z7@ZT_U_ZO{y+X_Y^FbC%Ujj#KMuU)eCu4E*R2%NfXxbfZ1%S!~%AMi(JT*A>qs)~}xrVVrZIAAvrZs@@pYrpA9VnT}l+Z~-p` zn_!IH^oo$Vqgc6w9W2NBfT~9KS0AgwT^5~p5DtS%j`?VQ{4#)W$k^;@Ll@OUx6(1J z-=7WeD$ZKAbEfB=DI))yGa5@!7^;s|;jTy?#u~9XgQswc=8^&FIg+w|(`UX}vFhtc zx^ShDoFwO_*h|2Qa?_;Xr0D3xyYynW_jMUnzg>GEpfx5|a{H z_{@zD1u@vHjZ|aA%{ze2AnG*xdjwp)E2Cv5fF+q1p?67;eh5*@dnNfHD%n{;)dJUC zBk%ekvAj5q;JGZr6^EpH{B_xI(?(-9VcuS*><}!*o$NoR{}B_+`?1Z>7@SV=p*2@= zRGXrhn8cH1(%c>yMTaJ&%InpnV@?Xr?)vG9r_Ay*24;lE zdR&-6PV&dtHvCH+&D8nARO;1k0cL5VFyNX2)RQ9`({mQv$cSh? z-)h5M9<80OwZUL`%xIk^hj9Q5ksLcF;B40n(-2ue^6_VW2e#oV65=YFe_HtMy`@Os z9|DUG9|2zw6-{Si(^4x7puKFEpVEMSm5e1aS`oqxcV^V1{)vF&1J``v(Ke&r#mV^p zQXC{Iou>S_7DnBVqioW>yKLKv3Z}BAm35YK4XZk%hT$33KLMQ#7t}?NsrgG9eAS09 zjdqBY8kAOB-tH!nMpx}W>rp_y&7}qJNX6s;#`e`DjCCpOiFX8?35-)cD5Rd3Jcru8 zRSrzbEYpH6*F^oNG2Y6dn$8(Jn$s|!)fcW43kf8okVNRwkDbNGIfJ!5)>v%e-j>l{ z8fji-Zk%kE&^bVdE;W|XCfc?@HbRm}=d#+9^+}v&2e&n90*B2=Wz>&VW0^xIpEpS9 zOCcT0s!X*m9K=nnd*7z8CG4fdHf-DRD$L}su?1cS);hRQN&h%w@412flV;nm<)MA6 z9GX!-wA`M;bR3yNIffHZ9bxHi%#W;be9Bf4MV*I_ap6=p4QGRUn*D09Dv|H0I^0Ft z5w|zDTQ|RT$4n`+?^QBilVbH~n+xi6%~2W)DE2L@bF9@xqDX6Q+si-cLjeHuSR8J* z!Au(FU*q0bgH>i{#df`$`VQOW0v8(@6omGk2(a+7$2gr^nLj~4oN7j{oR5}D^`2ZD zQ<6Izb@(Mv4F~|S2h=HfZpXpH%x>raa!4^zk+X#vY7L@NKvcBOe5X%DrP}_L^z%A6 z9NdQ0RDrkS;&H6wN$cM9XK>hL0NVIpC&HiN(C*G9oM^R#(m?7#3-c~DUC`i~^2mBc zZke8Yg6R@Yi9mZa;wyBK-PNH*nghG&->cwHb+|uE~Ax?>G&qfuH}=%WY~3I$;}*{XC@B| zLii+>3C^NCxoyWf^$Xya3%StoSRL-t+7CwzUx#A*uHW3ZK&yzaFGH8b&H* zT3r(>8vOD0Hash}6c%_Z4|idz*-wySFj{64>nXkAX9{Q;c5H>N6 zZ}2*spTwVtP+pE+{x&__)X7uR5@Oe?3Q_LeH27r55Xe!ZvDFKu!%zS^aWco^Xgv%} z$FMcW#eG{`<(-cpO!y2e-R=XNJm}zEF(0Qw?N%&I5KomT+!2CPHs;Dy>%@Pt~}}nrKh=kipfs0 zQVB19h|*fqJx&k*43Hj%k~|j(Uzu|aPq=Zee{}T<+_b!TLhJ$+!SQFlZldo2(rR+U zJA*%mK6#rf9Z%*;26vvPDIQQsh$uq|1xnjDm$ru@j4bs2Rvx}&>R6`ev|!p2CXZvM zdaVO&uV=8FZ-soC6$oG#Nh3DFEdl-z#U1ESM2O~Amo`A<-4-%1Jz3!F6 z=pnkuYS315zZxc84Rjj0a(+0YIOhLS8t(6;Fnil3y4AGc6xIbpEH9JHM`K!B9{gOzxEprH1KsFk?7csjwuG$c1GaDx{j&)eC zN?FMe?uUm~aw{F|jH#Jn(~|YkKBJC*Te6BpKU4?01*W<69^f_B3C}(y^lX^}c6CKI zIMdfl7~K6C4CA3<@4(Mwo$kj7y0*uMLy2_#gDpXaF!Kps$eMMVS(!WohZxSbBj{_R zy%=bGJ%^(Pwv>CKcXW)H%O$c~PMk#e#rbC20Q5-cEQ*XHuP zdAMnmi#szagz#|g+kKDoVV*wR(n7%;RMu$>D@dmvOL^$N2#z-Q=Id17fs*!Z^Kjl( zc!&16Ewo+`Mk$Xdzd72&{WRicV)LQd;(+w7N`9;o2jLL9S=y3V)-`^=xSV6(_EaD4 z0n&YQ{pcxV#qf@?3S}olyufe!o}?>p(Q z0O{nM#!i~girtIWMI{VThdKvUaC@4CT))l);xIDe(7I~X)c0W1>(x21%4~WWyrwyP zT?U3;e1_`aRIO)u{ne#3ZwcJqlEd#R)!5XnU%#y)NT_+se56?q#%p{C4~;Zm{r7H{ zFb;JyY(1D_{0^tPILIbr9+%KzpN*x2?id_ulE6iF>@Yv7ejLM7$oS7dm-=IgxW`EA z7bbrcq!HFB>}gteHc}`c%mO=u;$3ub0Bao`@dF!dnr`Do`6zXfh`yS} z>ORCS9a)L+9A^#EQrB}m{5%FdiS+82^+^m|XX!d36Y~rGX8W_124Oh*yAZCPzu_fW zl-@dHyx}OEEs_M}v8KJ0&9X)kcm|7Uq?x#S4E|i#(hB!k$?=Chis0(MB+?odza;%Q z?d3AS6+uId+*G@MCBdB${**@k%jV(y4KA@h#6+Pc38|sLB`Or*yzGQ4&A~i(7IQ>E za59{&>}X?%CL7_ZJ1VsNg~)ol57H%l1p1K!9Yho&);uIV7z_Oo!TH9uIdD!#BR?>E z=T1Q?uUFO&`j6~2JQat#L>k=}c3Q5b351R;I0XVD>?*)s$H$tM!w>EzMRI+!0K#nR z2uHg#(ztw_2DJCLq|o~{fWwh@+;porWX!Pqa&6FJHP(XjzCB4b5GHkc&GCfCg3817 zj{UxTR)Zdo7^|^^c{b9hccC=vBc#@0Z6H<#6O}VZl5{8k`QTudLSlV+iwyV<#@nU~1rW zNy1xjHF*lO6BjQD zYkiAi>63Y0kQa$y8z+uu9YVdt}4DSQ?)bO#r7&Uq6 z+$Q3AhHxFaC7auITtO^_`^u7?N;Ln-9W+Or`Q4uNb_Fp$u+gFf5HQF1pnW%MjxeA? z$rJ~3irnpvB^vILr*`(OY*IkEbZ~mFwehZlyIn#@w-;XNUPzXiR&7k+BO;BG&%kj& zanOQqog-t7`)hRIL|sGdjNNqK5C?V`9Jxn+8y{|$&@dhdmdBwF2}Wj2g(&ViWH_G% zP;UTCuvS*bPUcfbEWBF2!|<_wxwAHdN7MWC(C$gnXWJ?`!7OD*HjomR3jPE6PaGU! zP`E=*xdF+M#)2!ktgU;5uDl;_0~QXa&av6NpMIq(0N2nbtgmK`&@4kwV@Cj-HD{ zx_fs6HrE{_owP9MHoPM?vnj=MxndQ3-}*sVmLmiv>u0nvF~KMZOktzJf&0Q-YImJ9 z@RBwFeB|n+VGm-74NvyCbkbvEeBJr75tlS7&uTR;b;d!wU7qN%xed;H$9%30U+QEt zhoCLlUh)g@P)FLS*mH-lwe4q;8I4AgKDKTfDswjWQ&RDj1BVNGRsjDtHhf(J4+8hb zxDIWlST#&#t)7l9B}(w<@LVa_rL0Rj4H)Ss+x%(z)IOH8sMf2Yfu} zMLY_!-_wYMOtTj@sa2H>>0ienq?G1ueUi`+TmFz*rPXdy>$4E}*JEMLyq5bo}`5F~f}bpI)}o_iKQ@O(JD?+tLga zWOoAg2(}-UVf4)yaRm!~8v9TJySnz2H2S%I0)I|j^ZpZscU6Y;WWVQ;Ud=O*uC8VD zf%lHy4Y8L;0jGIrUzMw{%++QJKD5YE-dZ>{A}?}>mi~vSp{19~fHsN^6nqtUrt1-4 zyihN~P>PWw?LFypWk~-?{JGW+9tgu^snf(CPB%XH%lHmJhuzeKR(%2ht&@O8x&&?O zN{|kM`Is-M4)>b?5*xcNFp&|Efd#vSQ!f&Pgj>sYW<_`mfpRrAI0rypXm0Y4Es#zs za%IuIc<;5F9yBWv!CgtHd&>6C5tkXj5&{yvEy7*tP)eOo-1ak+opl*!inyAKuERYX z-397f=Fj~a!iNY2rJ0}|LW(?XW*5!?69(H(Ej|Q(3U;5wwjON%)}@?bf|`#oSQ>6K z!1l(Q!BV?tNKY1SKK>j&B4Mzk6`jz{9vm(%sS&2ND%P$V;0aC3PimV+3DMbK#I(ip zk+jslG}0J7BHu7V48niR4Z35-UOEgkcFIU76LZZZ2OmuH{nYB6=QeldPdLq+?DbX~ zzH+R&X*Bw9IE4 zBcw6P#a&MZBe4YNJ-3%oGb6NUa52M~75H$S<_F6T7B=gRU^gF*Qd=Wk)x$)F^)sM! zzk(T@X>_&DlY<;>*+QuQ0a^+dSxbbq(k9mEpx4iU#C{^!37ZH*4p1M!f;a^H= zHcsPNDXpOwX~C;GskBNMnNW($#cAbijSV|O{FmyG|KqO&d_dGIl`>*N2~!!Yq6-q1 zyKlD|BqA9P;&P3i(WjFaHc%y7of>P0^Q}JIoPq;RJH=y=3w_f#LsZ&e)iAqB&jr2{Z~(bm#8%_N#DH#K$5T{sf3 zmR>U$dO9>TX3F@Rd#36&x!$0L(HJCFdaQj73Muza`o*{V>nZTP z$Y_6Fsz@vDkzYfR-tJ0coiW6Y5N(gqB`6J2g6mG< z;fZ`goN3z%h($1r&ASKaZzd2~w2l38=M|jR(@3Xnz4z$ceZL3%JmXi0fzDB~lHjftt=1!o%?PcoQd9c>~U+PdU+@~t}5Ux}fRY3YkH_E?g~bPpKe zFRxrWgkY)VbEhyPj9f2shhHqw;`}3rYpxyYyL0#b3amHy*JFCi6hg=l?td7ct(OkG zk&<|x+qoNHbuzP{hzlQ$%(P}47UF`UWn3B|mHzcnT(#jY*GBd#)0<(IAL|aTU@5aE zqp|nWmno*8FifR^xeHWV`X!!tsvo*iGtC*l*T>Ru*XSB>WTK%j-BiOTOJ)a=DZ856 zK|t9Q<`JfZt_FRT&QlB`cG32Cy1S^4duFq?`afhkJP!`Xl)fUe+ekTq&&ex;YGyAS z3|jmNufmtG(AyPUVWF(Eb&jXTc@B(VR@V@`)VWRc588%|OmPkJ>%pNOFZ_w~O6n9= zI)cMC^4q@Q{t)CM)QoMlxrkZJ}uhT_SawIM9maMIPPYqGi z0llY|CR&c0_hXoFpMNa7n~KBLldsn3gyY`q6-F=|KC5UgZB@4YPI0~p!|i=2#?FG( z!hxzh$?iIzP0lIwdpgaOT^&VoLHPfF&faaua%IV~`=NNCyVGxze?rF27?zjvuX1}i@7=kNZB3m3mCFRbp#m&Bj8DlN162f61sU+yy;Yd zIQNIX)D<8BN^a7o@#07Dw+_}l=Xy-5e_zjzEs;Kke=5VFbpCm0xa)K0$B(tmEAGe0 z6NiSnWuBXlPvsQ zvj6K5*{jUqe6d%m^_weZvn!m!d0c2w@-~(*N7GH_u>%3MCxHY}@%I31&+aMCrvOMWcWn5zuF-U5C5CKB)T>rKyBCKf&U5oo}FvpnC<|Z7WQ%#|8we?AP zd%uQ;Mo>6rc!;g|I*6_W{HxCW9;U^Qm4(juO9l%M}>+#9d23V;^ z1GYDv!~H4{GZ_Y_iKzvajmzdK^{@@6@CV395*idW)=z`@+!OB$GchP;Vf2*4T!9{TgcjhbT1?mSDCt6{KNZO`&%_f;7wgTI4FyUiAC6@5BRg#W_Neud_ zI^3>-ri~M-nekqOD~95MX`zRs2ivGXwT>=hVhyotwmPsMT(83kzd8b+LL{9o=da&J zB3FHAzx}$M9JJ|A%7b|~`WEQA!I@cqS`_5K8+Gp01C9@IRc$pe4`$xK$H z+|TQD%n?L7Sw^Elqj@-ilCe48OYxypIw_`kVX@xt!kiaXDSZ5(Ds?{wW(O!!VcETz zq?N;lp!roBI6P<;dI<*0Pnb<5Q#v4&m_GGXAo_jfK!S`LX%GN>RRdKL7%W<&vMb)R zONz|8!tSjet3FO+#cMW;O?zL*R{xqGoGo9paP>0e6eGY$bgDbKxe&=5pBdssp(?XEpD9`UaZ zw$30?>nacZ7GfzMXOijCn9@Yz&tsT${ZdMU4$Fh!vyxmwgVH@-!{CTM0-ecz8j*S@ z*uH9bUti`r1a1ATQ>WCi9y&N^F3(EouZxhvO`5wXx=)sO#4yH`eZ-(7b*HgS--z`r z2>3V-l?Iq&may-*rUymBdA_4c{qEcC(>B(M0)dE0PeDB8Pm**Y47CyD@*E1VGzO*t zEFf?*u(>&%q~l=}b%e+97p?UDjX0F(Ger%dJ`UwzOFLX~2k4*6=9lXR$$QG~mc7+L zDvh^?8X(4wpB??w40C$(XT9GCMlM>ErqCVZ;InC{ff8=I8`Gunvzzx4P!}}*Q^J2A z=C*#~q=$5ss-wnZh3MZwm7B4Yb6`vPke)A6%0e*GW}G97H-xv8+yw@HBoN)&&vsrjp96F?eL~^xIYEx3+fzry3HIi z+8o`uxw#|C^hM>202(X_3#16xyodj--g(=GFVqgNTZg+&8f##h4EgcqPr*a1rit%V z2VE}CZR{fyH3oSS_^qwt)Fs}rVF-)VnY{T$lcnc{^!Q$ToWbzNR0g6h;h>l$Xs@3 zNmoyf)1mp^_zVr>@|R*V$yukj%Hgp}wBtrhu)zw++v*EvpO+Nr+cl6>@>69AaVG(W z4HqkT2#%^>wS{3vGUjZ0$ol@)nX$eQ91Dn+w?Dg94%(7&gxNB`!AkQNMX0L(q_yD; znlfF&fly<~;#_(8D?u@<>%_;>coj-cY0y4ngM1uqvx-$G5)bq2&H8l}r_|YqD=sQd zWOOU_P029YW`IGQi3u-PTFP}JXx#_!QSx7@Qrt(34V!84S{ZJl)ak6kC~1tevB4Tr z>US*K;nHE-{~gBN7lf*YD@>$=Ri}c{|HvFbxvt9aymXj1@tNJG`(~Zp!^*EH!G^>F zWtp45nazY4*o|VROtSl_3h2;;gRe5>%V3Fip=)yRSQ>7(;Z%q1{|DSw(>ef^Kx)5p z#t!chke!AG8)R&udBl+t*CX(ZHP3^38)Fp;I2YmN)t$A@U#Sjeftp+ikd&-S+Gq6O zdwgJ}Y|39-U+wB_*IhuCQDA}uN5}jJBIVD(lpeRy|B^=cSSml!Sal_UI5v@fp}@;k zVJ1tmk2G448bINAS>t?~z_q+va%a^xNH2|0V3v{6C_ij+!do!+% z_3ckET8AB9dRs`}E@6O0Q->=1Lf-E1qGa7m+uY(vRGAhV@uD<&WQ1u`x?HXhX^x+W zTnz#-pcdRdrv8W8(C#791lsG{t@$2L^0YG~M#KZZDF8lqg+BC(D8SyQc4tW`VsZsK z14aSl&{dGx$*6^g+R(nV(U5h~hNOt|MjPGLX-ESx9KDDrr9W%&T8QGPya*MwzSL}$ z?mL$cfiSioVXgECDScgqO$z<8iE?UtQ*Z|zU1Uu(H}BE3cV6};M+4F~A=2v;LdMCF z2C|8wRWFzGJ=fuO35{0|b^w;cbjb(wL%4rPEHRmr>wN6^j?*@@FgDU# z6qMB+aoV|4-^HQ*OB-$43Z9&(8o19ke$OVREl8JL#|IkV89m z7$-zMDNOR2m&gNhn=4%>lMBYWo)mBj(@mzJRTqyf8t2JZga+;fQEKTvJJ6lA$mI;O zk_8bAVQOEem14PmJdB-$mpoGw_PA?`n)Fz-_FDosG`->O)z|8fZY${#KDrU>08y_Q zhUd|4gNW__kBwYxMK)B$Zv2_FWmGt6L> zTAiZl^w`~T+H$C{$zJbaqiu!AT0}R>wVO9ty|oB~duFlTuYu}-Z)jFa9FDijL$#VV z&EoXy4dNC$#AyoI>RefYC756GJ5Jr2MU=d!;V}AA8@^(D*G({@=oG`r(Y+hy(T&#~ z*x>DiMrP*-D;gM}6oC}tPJSGxLlmk$o%-;;Y`9;6)FYMyg|56xV{^%+>239>V7auN z|1j`Tv6w5WWE@#pK>_?I^N@SS{)f>R)sh>Trx0ovf9wS2m0 zv~2B^AaI*w-A0~dK)yEYy=o0HNwm>W(EH`i4NCFjeM zm5VRISHn-|xh~wr(JgO%0~Q`=E=y$2RGtE4pM2g{(Pf%bRdCT3<4>99V1#He*5S-< zybh^9mxa4D+SUj-6+SNl7A*O%w4Y|s7rN)`Og4oTJ_LRi>)<|k9K~fy;@-CKjoO0dgbzA77ELeEVlJ79K$4SDKq)A4+kMS zCHmwyqSE~;66Vu=XFh`x!6MUfOb>pkw3xW1h|*@ej8)C4oylq+PF@Lh{pZKAyNR*R z_cVugk4`ppm+>8_e#XzHg`L|aIno@?vWFIzg|%U`rzsVz?xgt6i1u(}t>xp`{Va{X zUxB6+ob`F?Er+WM7gONyjj7bH$d-wYxhhIbWN(!Yt!pySI!Fwq(eW6lZOxTW%FmS{ z-zvT^Q^j_nTI3!j>U_>j=u>Dyz4B?^l7e}S`l_5Q~ zT46b1>`mR$*lC9Jp>X?C9M+$t=+WeoC4d9vx-DjK`3e{!oeVD{Sn)G8{VERm-^Lj) zS#Sz^)_c#yt#S2aNrgWh3&1#;X{qE~9q&P*?Jm;S4uDmc*=r0@RTHxoaSl3X^go_T z#lqFO36q@!;fSml0^g6Jfe+&_a;T@b;*h^3h>`Xqm7boFV=-r!HmmjyIVudekp~D% z5y#SXEDgwV_)8^I1b@1d3m1a`@o;*5stz|})n zFRdDNz-(Q|JN`D8_Dw4l7`I^dPSU?9#aD&6YopN*GlDneZ#6}qP}q#Sdh}$5vAS(x zansM>%xik-LtMeJBTnT|0V-%d_Wt9=R;hi(NKd!nNTW6N^$BrU1e29#L=RcovAvym z>{#>CCavLM9URyoVly5a2L}hLj>FLFF8b>t*!O8s0Q)jUTBt(6?%QD0{PEd`vm}!b zhM^OND)Yp1%=ol+Gh^-H*Y;W)=K7|-q9Ybg=ZkpCk-yDG%(5(E!e3KUw0vx+Kl=hc7!`Il{i5DN#LG4Vtk z>#aN||3OG1DvkDM7o8#o%tHSh6?(e{TqtY}8znN^F|ivaPYKf{D-B$W-8Kyq?5as< z=U|$JWoD!OvCcTR;a-1av9^1Nw86J-qFHotH^q%8#xXji^Uo$a7ubb}XR84-xTfP4 z04^GDz($p@34X5*|I$XIyE$JB_cZzGFaXd~Gy?f5L?SBb4*QuAwvS=hC-zD~rV2qlvei9|z4z2<*F)d$b!qI{_bBXXs4(55tFRSCFJbWqCi&k}2 zqf=vYJfHY`fHiFDv5L9;Q{;lJPY}n!1DcxdlhRm6_;qKh0}t<)YyDLn&ilfF_Z<0W z-EDKs4#>7?_qN}33Y}8`VdW94@BW%+()~V{XhQvpa1*+_<#Q2|Lgz!Z%MfQ`aQZ4 z9vbU~!z2zipDguMzR@FG4BkUi%t;vGK_=sz1C>fEdW!88bPjViJrka4!d;FF@aG#V zD5GvrWkWWXdr0kIz6|}N*`;up%0M!SK$8)8FB)__Pe!W!}Xe-Im zz%V7OW5P%2@XJbl8!5d^3tysWwHc@h4#PBQuQ0_`EOnW%vSm0gTdj#t)EeDag8W}$mj2gG!%dY7C#)PMwW7jn$~bAaRqI12tu`(#z?4oBk%0|n&`qLcp!U&QYcDu; zl_K-vC-fDM!@}(zsFvclHCnPrtsFI=R$g%MPBe-|xgkYn{H>=@wOfJ+7GK!F-C}GIU3BRItmy8G6J^9hlzKNMdzJ=R}+n#*fn%Dqo^E_E*zV z{~_-Fstxtu(V+Cb54E90KRpRtU?I(?#OGhO6Az6AO&9$Oi52;cs70p@gqMNGYImV3U@M zRPqwC$doSjY&M{zV@pbRgvtq61s8#_t!SB)A?+opXHUhU-KT|Qis#vt;SQ6dDGtf# za9-o&w6NVWV7?US+Y@E}fTLA(D4BWtw4ZhqW~W&l9{su7ujFWLT+c|;q!(&hGXb@; z(k*U)=2aa!P5cR>iX$@CR~TkM`n@80>7^7+wO7SkyGx>h0;`nW?ZRALvmyu{HS#dq zAI00r^<)@=etR)C`RQ>tyq82GOpV26`$SpLx?;fbFi^y;J{^rsjbW4E^HJ* znyG~PLFv#RHGq)$G3bVE=TqsAqK5Vr8yzN{Y&c~d=$3W3yNlT9Dm=PpwsFXcg%q>y z3|#KIK1p!SW5+hfNy{zOUH_>x{4+H=lnKvBl2V?N*!zP@rjJj%RfcJBD8I>j^4r(_ zH^wp640WH8Z_A9{gSwPI5woWSf6$mL)Xhk2{J91 zriN}6J>CS^hXe#Ya$cE25`*b*J2j-N-=qV7cnEKYczDAm{=PZv-LJr%W7WaRyC}0p zvSq`ZTUCQbN*BCO^=go|2dY=L@D1*wVPt6s*&vHP!paJy@u@QOyV~tE9L}a7W3pP2 z9u_zl!wqoLt?c7xrf>4P8L$yBV=ZvWe`5Wyr5)~bbFiu3l+dkzW%?XZ7?Lo$RvGe$ zkNNRlt=c?~i$i>>{8}w&t8*NLjZqz2m?Ec7X0#OM_9kh#UqoVbvO2;GLfP35z1v$P zpdFk2If9cxxRUJ*0?A2p0kq;}Mi&m# z`4xi7ss0i#AIfJosjz)mtMpn-g!ZnFWubq2b8nxF9%4pbG$7`&`Q*)RC#E)(=ez~h zBvb`+8Vjl~+GV6fivhOExcy|4(f%cZW^^pi>*t`J!E8s=0{p+aLf?UUnHXUXk=(&B za=p}r{yPy=AEjw=;C!ORW0dY#(iiPZE)6oIAmfaw$^#P>v<=yK-LUG=9UMJ@VfJru z()}THX+#HFRZVIX7D+ug`DemO#|(ct?vIM;utPvp0GFL#gY|C>3n3UarsO`FD7A6V zP3Je%*uO^{Yj|Yf&<^#7fkA|FTg!DVn%4VIU^>wrxn|D~l_lHG4BjpGkH6_{Vz^%e zvnd6coH@|4#<5e`b5@SiI^1(z&a0ee#AWGz`dk>sUD43Y zX|BfcLgU@!BwduK;kf$f7ZlL`@T?|929?!`_bCUHdzQ5OWEk#0J{e^}{ZR+qzT%>h zC9&uYa*~kMz(py@NnA&`D4kbqgG;3KIm(AwpVEmDaCnHNFp6hAA&7Fkirv1hq3zXk z_~=Q=(o0T@EEw{Xfgb~!SukX7%qW#aVdPEvIgK+s6L%P?;^EZ)re_#;tsAO$Je1gY zY2<9y8#muHgADT9YTy{H^SiE2u}%w9C*+v_2xZZJ5~mW_{Fm5<+Z7C@2U&PEyLpE0 zIcscwxZ6AdkhZ|nRyUbs`E~r!HPBrb3?yD!XqEaFmVn(-q3iSFd3-C~kTo@y2^NME zf|o|yZMJb@+b?4kfz}-*XI9MS@?$t+U&Xh;*DV+zY67vyWE;s zG6t|!!=B5N`-nWB5GuTsg?msmeN}9V^-wzP6z$CV6%@_a4ih5Ag^qymKuWZl@rq+@ zl|UkCxLiomcDCawKG%i24*INH2W`qAccv>$I^@eG?h=vv~zirQxPRrW2Eu7>Yex)Lg=VcCx=vPFyzy1i`1vV7P$?yWcgx!;Tt~ z*g_A_6o!v0fWq6vaJvFiKBbKs^UAEp08}-2*XqIbPcO&Ky6BXdNVEcOG0M_^LPvc$ zWXx!g!eH-N>^7eqGK{sAg$t@l%Q|e$LkQi^KY>081_;b)oIWR+GJ2kQCWkrtp#tFe z32o2|Z*_jTa#NFJ1I;iionaE)0#4#DhSq!;>}B<2u-6ZYJWphumD)>w_VB7;k=JOi z_h`QQeGMde(cS~pA60K`nAFlDUHmkC!^6TU4=paqcp|$5=P_jL;4HCBi^^bj;k&zD zyX#L0uc>aHX)Z+LbO&T16?-5aMcXlBy)xEKT-`VkfEw{uN&3{}O)3f~1w|Sewua8iT2E3bq+FFul@JjI{(46VJQa71FYIT723T9R_Lk7Flok(K_jN z5glF^ZCIr(&oryHO2uGkZT1dC#ycGs-MK+ONU;a7J6t|-qgg&%hP*?|f7tK$*#}){piWvk6#uUT`cLNGtE6t$Q=So!Q45tY#F+dPV2ZRz}pmR zG_O~VU{Tk%^(%tZp8-UZ`CZ5onrp7ox8wP3YZIhr=$9cDNIwJ%d zHC8q5I@|#3ehXTSz4h~|q2HCzZTDE{4Ra2k1eMSxHmHQAgD!h~a?~2cqY@Chp5E$q zxkOhpZ8xh-3IH+Y!oG|R{rF1rNy1u1F8O3;43~@^Q_9@6;gb?wG*4fR;)#J2jDYpe zSS-x>@qjf?Lww*h?(OR$h_=#{dl*=!F(`d(E_+g%y1oIz&eq_<^oP5P z?zMZ-kS_}ETXncU3k=!GRcnVWbGBK`+enV~cU2&0H(}h8RH!mj;kmlb(u#ZjNtrbw z`PxBOJ(cTU5T*Myc-~v~_}fpj11-e*X~n)rntNUTX$nhQ}1C+i!NoOH>Ay6%O zpPV^@FOS}<{jS5sRX{({kvF8ovWFdOIEQC3!V#%P2rZi)xCfN_x@IQ|HoRIipfNh| zNzL>c=X3k%r!)xP+ZH32S(JJ6lZxgxmV+qKXf4^TY8Zn8jPHf$Ppuhz{k7(z)u!@3 z6o>u~8MG;0U^`DT=n*z4sA@Vbqdg=~sb2eEX5DV4`nyNQ{q{$7=pR5yTRn~_vd?#Q zpmxlwU(6cOG<-2}XgP5W_E-o0j*Ms>T)XB)J-uW$^e+|k2wn}apx9918Dvgnw>?9P zgyV`V@J+!gqZHh!S-L4aU}T9-F?nF{vgJp$Tfav{v#3$&U9Y5yEIWh)ViYMu41(iQ zyOBga2@Pr*t)VuA@FunRxzm=`r%?` z*wVIVFAmfQdg#%}Mx{vs!R$d;4Jo=Yiu4MW_h)0ho_>oNU`eYqXOlmnH}ty23~+s1 z42|uP5uTDVweWo_5Cb%Dt`g^C0jNL@88mD3XU^)^RS3%;cKgux8K~H}l(2zF!mWBM zZhwd~l3WG`PPpvw7p$qGc@uvkAvbHDixlT=34ObSrt}d|aT@h@8oukvqn|DZ35xrx zHo)AXT$0mSX=c_>16gzPlP`}9kLjSSj>nYf`xT6cfuJ(6{}I+dlmHTm4gXLA57xtR z4={g3RpQ&w^b~8&y;u2UTw&0V4T|kAJLvl*z^a?!C5&_aboc`v$hxj;hM9w@B%q+` zVEwVY{zaYwgDgw9EqBvsl@;XSSpDdc(eFxVT$_XHW{@7-@etdei!Q~C4s=R0{FAt+c%H`S0GYMa%b;WETSyXXt2Ed=5EE^Cc#H3ZW^e!is27Kf z+a(P8V>@OUauhUDF>n$);`-1xGC5rP26oQC;n+8=pHqgh1)3!hx~CX>`}B4FA)l+m zT??HQgU8&MwMx;dh)-$wk;8zx?V&+QR=bWbK*D3RYIP}B_UhLWsD!!Rsnbk)EDrg1 zppV>D`=q4O4SjXkVN<;s0t}D}XiICAH8kP}zW!)R7})HPwU2?4D>T~7@j5r$uR_Hh z=2y%OkSi*11OdQ?dC~K7E}mmckujA$VWg}-g5Tuoor!aS#&>z1W?i%Yrf0ZaLCg*2 zSDG4uC$Gf{Drz6%DG8Qfd=Zo-?R*-4V30ruu!ft(8Sr|J;sEG9=DXDGehEp|C1bqb zh9=k$0I~BVVaqIcPK=-joeD%5df}J!HqagD@J7QA{V)uax)1}L^7M;WepQEiNHL#< zJqbUUW2ssv4-{t<8I4GRueMwNgcKOM{!uBujDT zNs?=tKk5QhFRfb*me?#?LX(8*b|Sc|&?0LM9y_2A2X)nDV2f}MAK0;8 ze;kmR8EjyAY?mcCmBvIL`4%sw;rFb1i4eN%z=j$GrTq#Nsc)+}!18e2 z>TbeTQFR=1!}`U6dD@$`UjNcTo3lt^aS;xx1bw;dl9)F*0~0C^MYASk=n-FEHSnQV zk_^;;BF84#qJTubVM_NaKsTQ)Kap2Xod8rAvW`^O=cpUo*DhN4(ixcxWQ3FUTmk&f zi(mlnVT7Z`;;Ay+HPB2-!14eQ*`ujygTUJ-%E+jt zh&?t)S>;7G;rkcHc~yq5cxYa}re{xzXr+k*P(L-r_fUp2QQF7IDjF56?$&F-!Ev^# zc>%y~>XD^m2Y#6N{HB8bx(tKOI6{GOK;xSDS@9?cUN_8-?LkWxv*En3s2OV4l$#6r z@j)cNBBJ%iYvVPp;dTlA*qf0m+m-pRh`Os4inJ>f2ImfVLP7w1xL%<3auEH;JJJ-2 z{;^$kLIBx7Iqs$Z4L3Vejd3 zP&)0cU%9LOE`v7DTC`gwi$^C+dYV5Z9ZKGgnYZ3B>EKv8kxmojcil47Soe5s&aJC$fm z!joGLDVy(3GI8OF{;}4R1V)U`YU?=@*2BO_Y3ex>m=dMkuDwc8d9Drjr$D19OQ{Ez z1wnfUL{}|*!nQO0X_{$~1a$}gF%6OJ=>*+^y_&^~WAF7;8}8}ME(^3T?&0;h>Wyak zMEQj7ABV2b)N;BHZJe7~a0&MA)c;e?WCZENv;$iAqJ+L*1Fn6ppPSNW&~moVbP3Z) zAx^ikT_N(RD@s99(kqS1fEvm9D^1I7L-+Ajxfw4}Z~ZQV1^{u}WHvp6_$jb?QX6t) z3{sG`vZ92U2fsQIm+Q`#HDdP&-Awx$rV;C^SdUj>L;uQ$_D)k&IaMG5BW$J6Mf`3F zBb(ATNvRM>Pk6?`9{-?W#Fd~)6LV_RQcYVneHmUPZ{HW;fxv0hH88@Cfuk+?0YTPD z_yd;`exFznYsA~vV_40q4u7kWm-cmT2cxX}$;{(@Y`6(gIrB|U0?5pis^S>Lk^wJx z){@w+Ub=>^Y4>`8RvQN?023V&aL$-pQUv*!-FCdIp!+=^x+`GAr<0cr|D|?@10`c5 zX-Zls;eTMvS+Qyrm{=^rSw02*Hr0>oD#SB>Ay!AvuB-Q(00jjvR%eA6|6-~#8I1Vaw^N~8PU zo*UNEGq%`C{os)KTpG@MX-GT8$9-oGDZWqsM7DL6hI%PrwP2;M$DPG|;wn6UZoZ&s zB7#E!$VZ}5uipkr{xA)cx)vGk5AtUnZAA1A3<1WWa%jlC9tpm)>>Pg1omUs>$W{L; zHb*Z^rTZoD6}t3y?$4w#hXyX0HRvt|1ErnQoo&3jUD5hDP68X6MFjOjI(NERQ z6GHyz$mTgSJ1vqN=34req&OsfCp zQ+cTWo*c@v_39Ckln|!)i#kCS>qf&anVY4i)1AQ-wZ%<^`P^bnsKz7 zUk2X%3W8VbG{7`r3~4To<7wDsfadtwKQE)(x3<8>y}RSay^OZCo_a8uXsIN0>n@dj zB&N1i&DK2vO(E(&{e(a0Hd=&9Iv2T@I+=YWkfY5buoody`R zHT18^-JuuzVfZ@+@P-Uns|vp(HgNUPTkm!aT@^pnNw!BhX-W={5zgY!?uv}ymM-y~ z4q4~w9|^%sm6Tx9+qv-^a#N+_a<4_6CvxKRPzyVYVoJV^h zY}NR06D#R4N&8t(1N|&Zi}azfhQQ+ZOKtcHi3Vomvxa8E|5nV61c~X|5`*Ns?6o#1 zeSuupOldH1NYuM^ zUQ8$@2cZ#b@>eLxiE^QPG7gR7J>PV1-&etxNYKZjs~s}~G$%P%>2#XWvukHV(;{KC z43xBLQ-(6-A!&m2-Zik)-)qDdj?ztvilzhNAAxmI7KBDJ(|uQE07Ha*Zz!T?#G*nw zM+6JAe!ZwUDBxx~)Ql(E|cC53)a#fh9 z4_sGqd!$zo5~j(au5l|$ST87rO67UkaJvmK$?@K9_$V@(0p(y}#<@BISCiYk{q>-a zka`5ZQ&78{U5qn*ly9deh$!PTfAucLMOr7#!R+syQ_wVb$2Tq92adT{PQ~rV$N=1W zVHe#0g1&hLW~9)HnlLt=eN?FSPyP>Pkh5aIp{gbj1xoN zUgZtFKaLE{Dg2}wJW{1^5?D_Wr5)E0lO}`=f`IeY}N}OkNbwl2+)4dh9(gLiCVf+xrB z#K43Wu1F>}=saEZiwFGlRwxEg+@(%wfIV1$-2-53b~? zz(@qoV8jn2m?IKE+jeroFoz(C^;cB#)z&3I;TWM#-wOxPX?nn)_PmQ8-}}QdD0PjJ_eC&H6+9#6DKK3$06i6FA&o0{(FDX0<~P(~>4rppq|xl+!A9Vil(+J5y9*F$f~a&DqH6*$;smA!0G(-q&atUNneYI5 z=3^Zpos6}C)L>{AvTYKOX$2?bInSrx^OY}-PsgrO7&}XnD^?Cw?%pvA{KT{5zgAhnGA?7W0P3TK|r?)f03Lk z9JV|Z`Qk(KgGZFk8Sk^hS2pvpwb4!Q6-*w%s8t>H9%AhpKy0Y9IY)_kL$0$v-MM~z zi)tw5?h?2}pn{nL=~rs`RUU2^VTv%WzF=nV^UomRnu@43Q`Mz>XT1$`oI4=+x{{Nv z$13hpgD#uBZURHNAHHBp|4~P4!bkb8yZalh5YBj70qiWwXnZ|O7l)+-Qxqp zI{|B8m<`};3+JXdN^m$G&>tUREKd_M?)i@h-WEs zX73i4zm1hc?1#@km{LGm>;1>?2H(xqB#q`uUd3k)gEv;k5)NHRKT=u8@fTRh9kk7S z-BSzQ6`ZU<*8#4MV$i$`m0!FmbS zOt~&$&2jjO0r{Aoy?a&nKjgoFIKNnCjJqOvy68FBw~a88o;-+d^C3B3aH;g|27E?> zuS6~MCJ<>!Wo>59UcnKp4XE51*z-99^!q9}mC+9yR4-|WgjlhzNLFM@G}g+&!4K$I zN5Q)6CPjc%1ad-!Pd3d^UBmJIZQpQz9B!AOsu?vN7W`g;_xoZO-UazSE=0uyls z8kY*JQNo}h7Sg1B#2RqDIghx~IR0&$QPO;#8??DB6sSqeeX&URCI=k>y;#v%K!WOX zVYoj4c-W~N^V&7YRMHjz@?elymkhm82_nX4%|D}t4w&osRvS)+-FOhy3IxVz!I6!Sph;F)zBeaaToC7Tp1J zGt3y{z8B052hC!lYl0rQfqqr&gxR?GpnqRKjbV;Y;f&?$_Ne5q8S=~?3&WQxdec7r z_*wEp?!_v-gw3Ako0_YuRB9RyD+iw^zJ^B3SdCn~>q20i+CJykjl zSqZt4s@B8_m6UGhNFq&w50wm|gQYFU(TL{y5bqyh&F!aO-U~yz6$b2}oZmVB`0!4B zcucTxXrM&59OMv~^}}37NI5KG4d-P|tY7Wrm@!AJ9o`qww`*whLP>m>f?rK0k!!(O zi=!SbsNCQgfkvU{Ia=10w*vvNW|0R9xb+iG_2I;!PU5jN{JDe*&)t|pw+qBwN6ZeN zVw3ViCpQ>}q$vqA9a!|Omc6u{(jBDey@obQ<}V||?Z=;oiCLaY^P0Nkhh*Iv_`o6r zbnXyPqmWl)oyWRm6=4iDif=N|_7sI?nkMo*GmP{vyS+on)-$ApmEU?x$9itW(Ll04 z!Y09?K?sPK09*H$khl9Quq#6j1^k&5#>N-dcE}t2!#^W~hkE6NTt_qv^ffCR|KqWH z`6(SP6E$pjt_=5QkaIY5vr~g6ScLvL5cI({IqI{8&itLo3n&L`R#?C3YEE3y(3JcH zL1+EwW^G96u{PWTqG#B8pF^J)1EK@<22kQ)jy3e32+*Wtdh#@m^+UU(cY{mcw{ zCD1e?S%#YaGuxx1()f}_H_`qb0vefXHKH$v{)m$9FXN0;=Fj>Q7<8-=iV1pRStPZg zT>?1$Cyq#(BJ<*@LVSsm!6xC(9!f3dwURzUl^p6+K){{jk=VH*3SEaHvdB&_tyBUr z^GkNc>jd7Vr}HC?_3J7Sxf3M=3pKNVXpyreTgsgr>zR^+Ad^aHKmhsI#0}os^i2o@ z+8QulZl;->@3B6&OF&foLlAcbx+#YmbuNb86DG*K+<39ZBu5k>b-AJa=m*+NlFK1+ zah9e_8?8%s`Wsd0ehnIYA^gK=7uMVil8V;pY2XILcGyv;MBpivOruzGj?#9{=vY;*7l4kPeBjEOs5X6- zuMz={F~(nVqQ_TGG(W-}C-+hR3??baT7fFLOq=29#Bspqy55JmzPLhA_PP_23Fhux zRYt?^77lx=5cywA>EkCqGnJCKr@2xGEz>MX)`vp_7ORa_{DduQ%mB1Tr|!ep-yV{Wz9X+h%b5cj2k}1g)u`m=nX05 z)R}>sSs4(Qt2joA#Rq|T>z6@rljmc3_{xah5hxf2c??zL(intXJ88t{ z7mxLnX^cR6qJYLI^9-*B$tXKsXwKL+_z&L(h%bFKvS7W8s0XChP2T8UV}Q_jfdi}V z5I)B};F8tKksz@;m~}en7f?eg?$&|24NcjQ=;!(2eibY^RM#7d>^{4WNJWKjQg@d5 zxf%WrHu#JcLf1*##-CP4=krpaWq-nqFv7>B!|e*1&=&i1ShN|~x?qfM)iw!KW3AR> z6PS!iG>x;bE&{DV19YmUV{e$#Oe6r0A5)hfYr{>S4G70U7tSF#EABIz<9UQvI-Bx* zWkj0=jH?V6v1$k8>ZX}@&L3_7az;^HoN1oNhud8+0~kg)<=&OF<~B087}a=9b$E=E zX*5|?6L+l!!Kkv9EZZ(D#)pIN{cq#LS4y)? zj&?xbTWynj)7`!+mXfzug2MAq8~!DaCe*V%roLrGqie<`#stKpQX>cvVdB~HXk8M; z`r3S9EZrYKz&7UQF+>>eWX>*)#~_fR=!Jf4_0^%l1ZJG;D4zHx6q*!C2G(%}R4?1bgV?2lbv4I`_q_F)JeV{71^nXLCq zNEf)H#+t*eg-3c=skX?tRB#%kEj+uj-Dv2N#SjG2?EzUi|Zr#OXqMt<{E`nn1}K!v>c zUtrx+S%Q}q#Q#Gi2aGFSOh(OW8Z%612e)1o9?>vQFkg1vfY;p&_?O|~rcE`N0hcNj z(nBZBsa^NEB_F{5-WJM3qj54galMO~(7=L`J#7BgKZ^%fLZ$!Bko9&ABl{#The6_6 z2;G4%(;-fjo$=wSIP$VN_3EmO1bXNnfp1=#;e)%mV=RyWEsz;LQkL$wK~E4~J3M%^ z#go0yxI}tl)#?3mg86fm9 z8InsN2K0yu=Scw%knnp`$n5FUG~GlseLnQ3tkd64?!KM-Rs>Q`rFCJ z-h^5%ziRFJrj8zWbu_+*h#i_(DI7phEu8sRZHRT}B+apVi@`y`Jbtz4L{|GM$N6ZRqH#?Q z_D}pmuchIpO9c#I#nN=>rVIcS>^SpgX<)s+H&F=>eXCYm9|G>kB_Wjt1>Ih(p>-(X zO9jo~k*{=rA53=zQOF#pq+`GkI%19)IGq(n7_Hm|RZ>WVD%QZOB$#Aw`nU|%Jvs0h z1Hxjg|6CdBwfygcmH)%4sQ^db{=kcmzM9_0? zV)@0;4woli4xsEfDlXZ1y^qeWd_JygQVg#QUBI*JHoQ1zd|d%M{~*6^Clie0P)@Ke z$TI0#Y(ir7(tXo-1=!Ja;Er;t-39$Vugq$HIgzHfy>$Byg4CR@S(GV0RWoK^d4M!J z1x(_1h0JPX1~d(1539K33D4}Z5V1a4(!sui&H5;W8+T>$aFxpTO!7%Bo6V{{MKX0+ z@37Xd{Wy3ou-ZV0+C=;nJnyY!&X6AbngzZhAchtWzGV!#y$@^HEp@2wBl~Mf{wO0lk`(L$JaK zE7D{UVQ;$nc{t{)Uadc`A*&tO4ss|8+TX2}#$6ums779enYpLKY%=mqudNXs<5@ze zzTQKqRSoXsdMVFx(AYz1Dh`cX>fnnMWqLkFgO0DzYPVtZ8>k)Oj}8rKC@oV5k?Edr z$5{}2ik1>jjdeaHR4>xZsk3`-fVfgkXgnXQL%VZjkRAr0!M63-yaMAkA?%w4dd&Fo zYkuN8Zn)#33kC56^)LcX9xoi1h&r|GJ{5<5X`}g%7apr(J#3A}`ke=CkyRY4yq>*} z6;hmPd<}}vK|*R0HK{8HW_G)K46_O`9@?VrWr8Sd>Jen+20KhvcA0!;|o`hnw~+W zeoufqW91MzKZb+C=S8D24ULMNpK>MG(@kU*pa+s8602Pahw)W7R}6N+W+Tow^Njm7 z049Y8jAfGDFS0m7y)ZUCg8)^xJ@n!7oKP7|2^$D2&=NxaFqDojanQBtbTUKb|5P9P z?|4w%GvH0C1$65$K$*EYL1_J41oWiN3TRcqRYVXVm7$O29|f9(R&J#dsAE2FrTcFs zT-z_+R>EQXE{#s^eKhVLC0q>p2_K)#I))e;R~BinKdcW~n^D8;-_{QOt3Fg*JG1a; z0N>CyvR>qDCAHrx>DVU&H63JcLT15Qg@HQY!Td)F=b`==+R~R$ng}-(Jz-Qgwtt3N zhtSk10ob8S)7w*4i*u|$JE3Y!?6!uktow~fMrxRM^o}d_U(N~IvL;b3c8`(?{VK7k zKFw;*JGOTy6h4A0G{Dw~l7#fa7E>Z4J0QkLAS<}E@axyCZ75*St9F*b)T>Rx@W+<+SB>~rdNfn;sE;+`a3RXl z0cVTVRj?7m2kA_gVK#)=synYSiO)6SKk3n8LAtXV4yj=;Gf2dQCjIvMm0&IIJ26=w zAm&Zm=pS>Z;~F9fL{rrM>udZ|Bfg$OjGu?+5O0ByMh2zR9#{LAw1@e1*Fnx;?68Vi z=TfV<>OjwDjk(vtKXQQH(tgHWD~-h(a9Mad!O!NIR9gf;MjHE?+b2VS5{d~Q-$eFo ztTcv}xDpl`%>HM9RxfEkw@VoAdJb2?#b1?Af(i+)O73B7V-Xi^Vy|%Sdh|#Hl0?_4 zV5m68dkG;0Xqv&WJA0}S_W)_EG;+T7(k@1E5yG=(NFs;{VtWu4lb%`2$5~hzq)t&FzeQ}7pOat{aQ9{ zNx8-ht@Lw+xIGQYw0de8{-wc<`Q}Q|T(RRAd&YVmZgbqd;$+YEWmOPB8L0*rZAleL z`zlFV*uHj|>pGb?V zINX)esqC0ghxk)#-C?uB-l0W;i-)p;2hG0*wyc9h_smw%KktzzyaWjgzvFsMPrquz zzhu$?YV8GFs%9y`;aqK18#qW5Bj@(g19%nC96QoBUvzNyyC0qhtopYOPUGk!+$=f* zTWqXYfR-!kX9&YcUtDwCYGf-2+7rg~V0^-Qs?$k|T37Ri zwyqgYLV}BVj!^(raQPH&CVWO(4JWsEkCovjQs%sXa#d^8K+~$Xtc>p*yIHI zYJ5e=8>3I@2eDttNQTrmwgd`cz#qLMv1CS)TATzfY^dG)spk~4s>{)9PR>TFF<85h<&y&;B+=6#EI|AtFIp~k6_wP ze*Mf-DC>>kx7K|kM-H653Z@uT+?T@erPk(~qsQ;+7y)=N{4#wnRre~uTX{#IRi*Us z-1#7I+o~)hc3jUck{MS+(nQ}2!%dwE)2vDXIBb-4GqKN6LZqEEto3MFvDzxG(xHvX zn#dT{!l)hRbpLRKgW7}KsV z8K-BIYXR^u^6JC(Gar4s$6cOlGa-}m6q@6Ib!cI5fUy$x4ckVf^jMmXhIu(zi6Efk zqG;%Wc)0iw??H0>P#yl6A#G`VoE-T28Qho;3vLo8USuGuEz@&3e`XyhL0f;yr$0zm5W%HqJ>0wpIOC8|WITRW4_aoEQrP8X zV@FW73tmpTu{9=dMoIW8nXa^#n16&du^QoYg4giN>}r>X`z6Sbl1ZEuK(GL;Mip?v z4p~2h`dm~}EH%2HV8tGH6(W8h$rD%0Zp7j=MC+|K%)3yU*H4otvk~d^0$0KU*JtI% zIcD$Pw5In-%euv9)Vz=%1L} z8X%6=)nwprWUvK}Ur=X`kGGKT2TkQ`ZJ1xd$-_Mfd0MzP0-FLF%Nc4K9;^S>rRTxtquq+Z0&zuqt8n<_)c7jTPW< zo@_>ELd$w;obE4Zj`tDn4M~>sBb)Vh4K=Jx_>Eb2@Vk_0h2*4%N{SEa578M{3|w_5 zbI>ad*n9`Ev3E~aRa}2zv(7JxZkB0I zY#u?F12Z>_2+xu6kah~%cWIc%|A5Zg-v$S2wl-&`kgYcN7@DA5L{dN?eii-qUUbsm zuOVl(ju*Vd*}xZyor;lAI#gftOPs1h2-0ypX;orDZ)mh}lyVik9B1jMU9LE8%dJr3&mm;^WPQfXQd z0~yASjqga`YLke1jx9wyQ~UGd2k##Kdpw|gWV9IG%DsFe*Af5HVm8&|+Tnf)Y*Ec& z+oTw+=TUDJ%7Q4VDui++NOQSZV6D-;4ql^m9QX!xB_n(LHvE^;Yi+n|r8RWo;mZeL zZaCe9x`EV$A0>R+pjHY8Thaptp;LpX9ZnI+7`szFG_d4sp6{h0eT8%80F+piVA4bI zQ4b$ZLXxY05*^$Xdt@&`FkAt}SSRy90`+6u>lT2S;CY69d;0}*UzOp05m`&uycGt? zi7grD5V<1fHQmM@YJL7aIzl!I+Go%$757jd!)!&)u&>{1!(F24+7Y7E1gExgfr{Q)Ns3kj(RMS=wyD%Y3gcI7Z`)C z?$6Z6K-F^e7D;DV(sK)}6>+Y;i<9XNU16wI_?^*tm4>ef>l!LJfQ8J!ciL=Gby+1` zbfpdTu7j|e^-gXfF=(voPDmxpj5lxA9AkBRtPSaxw~`ytmbOSlDrME1)wU%99l~G= zo(h1`DwC6!XuTxsP_VYFpRSb&>|i$7s+FIV4)b66(dsXs^P}}YX8FiPPS~_qmy$o; zuK;NX*gI&c)t8LWuh;Mos3dKsp#N&>4cg_+N(aIa8y*@i<=DoP1+u;c#ls9;Gk=!S zr(%sDm%XHfem#bx?>Gkz9A5Nsxyg{u{3CR5T!|8y-4Ia{EPN*2G~p1i>qg&vUh<=3 zC4D@uq~|?98fR@N88qoTBsBCM=aY*@MBYQ-)_4*99S}52gZj~bfy0so!q@e}Sh`)p zVDg-Khg7HFTuS3rCF~bQCs{9d*u!T~v$PZf@s1AO`6H;MiAL%{5D(9n$HI_*i5$$# zRvOY>Mq^{t85-D#bIkp5SArF61WtY&xqM$ibL+X0qO22+g$}GB}eoR%%Jt zJ6Z)tH*xbLnw35e5mcirXqg(>2B$80KYg1AI0+gH|FUVg-vW9yqoWOzvp&%kWL~n? zt+OEcbtiyf?8NeG4VZkARA<_z9N~sh7r-#`{L9R6k1d`-#7_~hW{j92g;KYGlTze3 zw5SU+vbpOCAYVy8!N_39!jcyTs8 z%S*nF6^Bo0TYqb0?x(>J<OSt1#eR=V%1SYa-eRV~(0Oo3Qm>BIy0+)AkS{EqET}}2bkvL=iJpk@@ z1+B?jtJt1EQiAXnPPzEtGK8)ZhC7{i$PYOr0Od=w9{)I<#-2z8kXM zuOP6eEIp%OsoSDa$iN4kceU`kpL5GR2)t5Shaw94=4eh0JtE)o<=36{R8qh4ylD8o zh-pKJkP3n{Vei_wc&coxYr+jAW>xQvty5}(EXsK3(}*d2?^^p#1$+V#;$}DF1O|o!d48%ZBPnz(BjV(CzqB7uY*fnzCjQ z2ax7^6=m=-0q^28OqB?_-An%4{VAwU*SrMmP8BM|_3T04lwXObq|d~_256A$eBw%q zpje-Rh=nzslZhe49{8y;oWFcC7`<7VCNP;^^d=?MK57y(WZ6LqWZ0YN(a`hbTWwsi zPAPr`Q_~7;(N6utU(|2+OE^RyAw}s61BF2A4%ndcbY&&2Hf<0E)QoA@EJ&D1{3ig& z4Cz^@JTfsD-7jOFkMg&9mqtThxTX%UZ4!74q6S<=JZmQ9rTW!JmXXv&xgfs(5AW7c;G6cb$ezhKMt?}Q1 z5fZ0UPTQ+HKMEY~HzE0x#t!wVmq9kUlR#VgpkU>`oxb{^8F0b7BWWLhxCTTR{fYVk znwa&~k#(NG3>$_(M(^9u7Db8U%c&!#lW=&x<-;H}C2zzj3_#xXE|TPRg@f8sg0337 z{!|ginrfj;el@g@#i4%1z_|#gIP|dG+%a(a&}e(BV7u*I z!wErV%~1pAc6WeK{B`H9zEWj-DnZ7k2YH(tzVz8NH5&EuUKrE~OOKn$>@p(eq?|zt zILVNp!CHs*`Y$K*^g2Ax9n}*7U>Pr&goi{D${d$-$RfDZZz){&ScyLthkIl(H#(G8s=HGPAQ3OZlfi164sUgb>TwLt z*F#(HEB3VblESgA+c6?+m6Fn-7mCvSmpYmiOM4j|WYcE0k|2KQa8_+)_cyIir#xcu zoOZs;qraYj&JY^koMVd{q#+FryPX|aKwKQ9r2Naco7IKE>z7Tgtao1VpvBWWg5c`g z>~Mb&HH9&&z~*=aN3DiQ+c*#s-TX#Kqva613HN_wgeH*s|oCOMpUx$f7X8yw$)&@59zCtXf~Jku;cw<8hzVD->yNIOJwvYbcXbwNr$CS`{509>)jWNjEtRW+G z(KV5>2`dq8Id|^zD8Z^h{v|uA!6)3vlx6 zS{x!*n(~s`c3aEgXr4LB;Bxg!EY79xaC87LZ1puHblzpT;26Dam{GZ-PKWY|gTJa! zvh2~}Fn+7Ep23wCmOtNQb^9krDMp79^wlRvrP=-mqeJd5uu>9(K1OB6W)GUp%Znff zS>%zzQ#4w8t_=6s=)F#-MU(>z*m`V0L#O*(u(JnJIq;5{0e@bY5}E|$ zpB{Gor7qkbL5n6+&qMId)spDc%oI7pT9>II?Y3Pv*aWSWg%1vw98}d9i?Ov0#lpaQ z_zWubyIHp8RdD@yI~aC~O&=>{)A01<$QP+!L_H&l&$R24WSL5i1SfksHhb0F^Jc!p zTVc3oS0is9q7Wn23(Ynut4q+3$h@EH8(^nq{v5H%nKN;mjQn$3kZy)vWNBc$$T$5| z8u~8+tMSbM%S@gsu}%6KQa9TO%NiKCSyL2TFKG*y+@S=6Gi* zeML9pzoN-i7NO5u7cDK;0Zxv2X`dkLA}4aamuE%zhNlhp#Qn>z3^TCq$OnCl`MF<% zD-9M7d2F|a)`g|2Nv*=L98j}vy9UNGlEzSkRA(s8&;YQmJ@hG;HcT^~F{Qf}HHG&f ze4EvD%UNt~@1Yd{0@d{+_HV$gn55UC)`e3GlKBXqgsUz9SDwe;#)kU^xNo2YA3;;b zkk&7kfjqf^Hj_J=H3FkgjlIdwO~CO7J2+r~9v?;9)Wxj*b@@+ix(dVXh2*1zCZ=6L zrVOpq&Ro(n_MkXIgFVFyIb*efVYWih){HG$?eAZ`%5`>fWN7-!Y zGPt^=9D?X`JMB$fP*qEgmvGRpcR<5Ri-JyjX~%0Q`z6bYEb1N^I3Gxf?c2G|1cJ}7 ztSDOM&nr8cEdi%2e?xxbC*AIX3}Nf3gcHt%CNkL#2SpI=@@B6aNI7!f51kd98GQFZ zB&k8^Cp@|+_xJ@bU!~zLh#qHvcq8q$t_vZ-V4htTpKcP@)HK6B0()$i4eMZ}nWMJ+ zXjLACp&x(Ee*3xtU<(x`&^@t>flCeJhHV323POXv$-AwFsmXdU1@|Bh=9xiWG{xH% zbC61STf4IEXO7aB92z%2x>Sx6#{SiSQf&|Fg3%03Hsu}}G_0@K7Zu)ybx`Nze!?MZ zD{#lQ7YVxBF`rk_U!l#|-|!B*a5XEH^%=n#w(AT}+PGiKpKNw)!G7{E_h_sSqHXKL zBcxVMwfoe7ul>^5T$SN|5gY~labD7*?;!q0WgM~dp!iz!17gG_90D6^t^>ahsR)WIIvLy2Z-9n_*7tE)Z>m0FH}A)>^W z=Mufm7LI%GyXbL?*x_Y%&*8gZqil!@Cu1|r8VKd2TX19uE!!%~!`Fc{#&ha$aLy~S zmUD&%Ml+N18Bw|^auW!}w*WWVfdQoD^)08++ONhUP9htk%!~SJ(HGWYvbSA$qsH3UM>c4BV=0aQ%nj$4>P`HzUUP$O z^W{}>n5wdRBV3Qv%KDnDfh`qHK;5_YsqhJu#HXPB^#8-IV}$5`DwUT|5yU zd4VAl-i$bgY$qkaz_}%FkNG}dR{>Xz69_6nW(8XjXB2Z|lfn~FI+dUv%+T!$6AOui z>nvh}N4L{-Ig6R0+Vh`(nHlDt(LVwp*H{A*$a&Z!!$_uDm}{nycWCZR(S^Re{#7RF zW4Jh#Ix(0q}n`Dk?;?68;HWM$kBV=H}~woCA*3)jrNcx=5fWp;X@&ZofmS4YWp2;nP6?ixN@+3~Jj-GK(( z(>C;NXqf*V8p`n!|CaSa!JVQ)kAay{LGw(%i@HFI<}iqU@ue<&siAWuoJmkRspYoI z=q}!3nX3h~U z)ubx$v0qa|Si+MYk|@R;B6=N5=P-kez5@wdq6Q$tC>op$XOcs#2kSsg4? zkj#j6$lmf!lNmm;&ut4S@B9H!*lZ^goY~WF2p7tWEGT?rZ!r{OYR26 z3uG~kH+Sy)RWMQvr4^-^#0W&g_PN`!VP{C=!C|!72)J@%2Z|>*pTo%%Pd(KtW*^DC zIFTFnwqy9d2uff#il>LmYJ-VijKxSMi=nS29XddSa8j7WAYw&J0#vA}bL2H^uy;Cy zzn6vkCG=zKAu0v~*>JT3WTUVg|7fDy8Q+4IpM(Wg)|+$9Erpa$ib*YhVnN3U&0E&H z{=Ze>>k7hpfV+TKr+s?E2R0*VoS?)QP%sEy^FrkM0B^4b4m&I4pma#~DY;zp0-Iy? z!B29x`!zI;`LLg14V~!>*zKbJV_wiao8zg@JulER%~cY~bXgzoDiB@I>OZNVM8Q`_ z$nsDY?mt}k#E9^qQik&r)p}^36t0y#^?aFgACmnGYLL|=ouF!n^(5%~6yR~=2$U5D z|EDP2fBege!BPIDD4-FocoeVGs3YzmwE%PO9w^Qsb7R}DKKmzq-Tjb3gGBLjciWh45BD|A$C7YYAoGSznMu5we0s(-FoMm{ zP0}%zYgDj(ST8=|gX?*nCjd8sUuYV`isW|8_2k>;`RfXpDVdVQk;p0n?%-MA-c>#6 znTE%43A|j6ve_VD8owNjsabQ$q}y4Xh`s2l!m9o$(&v5=ICe6f&<|le#2OjH(t;7X z0blxLf7EUSW%HoPy8=nV#G>lya~>a3P;6^?tu#0u^|3H~MK#lOnrrK*H%9P^)psR7 zEqVF00~^pverA=}QH;`=Ba#38XyOYW=qD(kw$8b~>==Gs1+MZSF6ZR~%~zXp_+mr<4yk@!8 z7{GrpSc&o^tZK(D`yA>UVRvb3t$x*pf8|931(j!Nl6qiHPOQutQV%j@S#!52>i>&GCrbnL^CBMFR?Q;CyuozD&S=#QP|2qJqov5TVp zNKNvD|CEQ(LwT-}*#u4Pll11+aW5N5Ual}wdb$F0Y3jS`O-SNmM`#|I@&{Qq{Vmex z>jF{=R~E>A+y+RmE)8tDwHBm&Uc^<$X+RU&WJSqyRS)wTbbe%5E!t(^D@NBxV9!%= zxQ9hIW5HQJ8#~#yx1PnJ$-01?lGAk!N6gae6Iqv8bq<+|=wk&QalF;Mx^T{TRzlw| zLdBSW14oo5Lbf~bYc$WUV&g)oKlcz&%6`OA|# zYq?qsuz>)cgHd5-!`KG?C2gr!IwJuQmy}9BgM0LV_vyYSC|VgQU;=)4Ee&@WG}0nS zAIKOpE26@XnzcluT@h?)>>vN-uX%b>q*0R(RDb)#9$ z>;|w|hLm(5sVtE!(vQ9S>{Z?SY5-8e12xapv*H}Wth7f4pw%AU*pG$b9ughoZCzVf z9Z+{Viqzo5G+iSjR6MMcSV%%5ADBBrtcT`)*q&aD7=<#2kpJ1A>O%fkW&@+s%gkWL zCn58gcaa)Vv(AU$(bfgfj}orpocS+xA^*oaX+$dODYmSbAB1VZZ7HVf9pzdu@QP!a z!^#shX?2+cRGgjCHU(e55tIu4vi^%K`KK`4p97Z9o`^D{C=69n5q}rZU<84lJJ5zp zSk3Y}myvWpVOg&Oy;)#5v0;E&G8_n>%0l^86QCT|kv_3-kE}Nuj_Jm&xL&+;&}=@( z$+nS33G=A@lKn1Rb4)8^&Pa`o#1bl)~@l5dF{jYuSNNki)lW&;I`F}|YS7$TBJV=r+&_iMmlLvHI9LQ6uRVFi;QE{MgOaSdB*j2v z%>lHb_ZVS~ZhQt>aJ+8VgFhQ&d|ic>d@PQO&iH1%k#wC+Od6_Iphg|xxte0pK{E5X z?hh$XM4mC(xqSH1lOc;q=raFS8EzTP%vY@Zn*sEgiJkC$Go5)?Yk*f5j>CZ_SvJAh zkJS^eGj+A69T(ak83Y^RHs%0Z9-$nr0&;wo71gZg5~- z{{HLqn%lrgEry?xR_A75#4x52xqi zaDNQy*eMQMimnD9))&R0{6wP7+@7oF3CMY%!)-W0l9kYDi{P^aH^@`z#~$SwQo4sk za~yV^CF^!$S37{FZrUa8nc*^lj=j`605B~=jC2-~%b|8})+{GT6X>fiVz;j=P+KP( z3hFl+S6wprm55@nMN#S;LU502%&>tq)xEAl&cB7fYjmjyK!#aiT?WNdX*ho^8sx%+ zz|^CO{-p3%NygEQyVYyakj1W<7bYfV4FN2S*KD*|3VIm(;}UseZ*bCALNq0iQ41(C zYn;L?#|lTXK7$J~t4sTunP#CAuEB-qQj{h`8lkzV{+HB-GY2y%m6!eVugfTr^oG48 z&R~&XRICb#9KfW()7dXPiQM6-Plrx!ZT)l}WGjD|NxIH7QL0C=+x-d}hY!jntew{t z5Vy%2kl8wQ{$z#x6k`umqj5aQ+-}cYFfsUa`LJE!meK53~WZat?B5(fZ(X#Ya3>Z}eebMOMq+o>&oP(aD)+u3z zw9X!wBImL9yF3pK^_LZQ+99~w(09myn6$BdhGEBaU-qgcUP#7s9M&=sbDRzHCs4L6 zW|)4sY%pI7!|f6d^eCF@1$xO>h+q+PYwGA_5NX}BbC#QWBpUf@P*&Z1X6YV+s8oi%Z-GAx1#cCe)fCF%#&DD*2M%P5 z6Y>aVrmbXnUO<0opnK%8m5}w)U}>s%j!1a3+hOmScS^O9B7CSG%Jj2s%A^uENxLqf z;xAVM=<^2peigKWu8JKyftCOyPYoDSc&srD5GL0fJ=~h_MT1-%Wikvq z?{HGRv4Uig^CcQo&YY|_mi<9@uf}V6VqoZ^0Y6|OmM}#pLJ0Guq-PAfx*i7P|K8RAi5~9ar zakz_b$CVJR%2Px6D*KsLLk{oMWQJR$Wei<;*PD0{wnwWtS_DaLM2Q0tOIWwi>2YSL z|Jp+PLOj+6w;Bd^3DoUOwC|py$UNW7I2yRo*mqtLq746Fp5-uSQeHioqNu4g_1JNAdIh z8mJ5PXYY262?tApfowY#A_Dy$TE2wvQ>&+$vEU|iY~7Bce&US$o}>=~pO)Cyi#f*k zRj|hg-A71K3`3w$hv+`u0hre1+_}uKa>5XRrU|t+l`8N@4#QYLhwQV|_quRbKj*%E zxQoWqjr|#f)tsZ1PXJJT%7ZKK;xD1VUX zG5?7fdk1)nH$_%?z4v*^vQ)`rgNQhB8bx!jP|`gX*yI+8eVQNe;|}K z$yY^#yh`GF(=yKVCEGg71NDWFF{w%>9IVcAK9+^M`WY{fZc;i=gQ*Xn69SWdtN{@X zUAEk@0aQ(>{3L&p5V1-c?*X0V<-;vlJO8qRZr@nasgfj`u-?Q#mTYVkUF&?nPu$Mz;;idN=^x&7kF5t?+a?0^}yTNJ^%OH<4Y zafmsTvh&bEgJT$e{AbZ=%%*gFyoaZcN$lT}y>TfKM-BBLRAHjh=| zu7LJS9|7#2u+ZgIRKCs~-$UP&^ETp^qrdLadNc)s6gu>(e;Kc8WNx-DFAGd+{!$xW zrQu8eYydlB%AqkBDlOn8=&sE>W9;+xk}LLJK)hhA6PvLi;9~sLxzQw-7PzxPk$M># z`nQ&$YO1Km5Qx9dNif5PK38;34bS~R)C7UoQD}zGVNQ0D)(4;!#UaizP}%d%IO%>3 zr?!z*ZIz*phiFoxi9W;n0W`@)!#1&soNj_gF~-nsbzKjm(kLHSbT|w&7jy`3#o_ie zP!Jg2!YiMR>o7P%((KXY*igVm-9Fe=v2%0h(+i6$u1w>@4do=I1T+6-&2W>R!YHZY zNwMij;mQ$5p)Njn1*t9I=NrDZ4n^#!*Jz?8yEgj8yva>k1xupseIFTaSKzQg^Oa2% zG`R()aQFjE=_;ts@)kf><0zC;jP>;JZ*pH}5!*+=w zc(6H>dU0R!Dpj|fi>ovlzlyMB)FTonQ~2tg5@{GoUjlsY7w{ryV4^->@g(du6YxRR zoGuscmfU4!hIH=9SQE9M&&8qthn>`7fGS4GxZ9)KA9VBD-`301?u?Bf0S$#Ui&)0$ zTtnuTV;NpmZAw(@=X|LS_gLr>pK@ena3Dv753wgYSPeheV<%Im8U>0*V)Aw9sK(0& zvL8timJ^YU=b#4K!rLOc64*kIQFl%Xz7~qUX7|KK(vkC}vs3ueE1`C)g+O(y`bvss z*_e_ouNu}@LBs75JXi&Fp@|+Q@Xi({;KCDH!q-}_tcYT~Zibjq@iFVz5wz~hd-)M` zNn_Jg?OC7ct9K!^#$`RSUw2po)(n_S4pT{+%ZvRzl8ViG7bj7K>FYp4mk$u4qhSzZ z_zKv*7B{88R*zSO_)27U8SY2G;uZ$6mfY1oO+LI45^ZZ=lUcKHs~c9RG>nEIvU|Ts zwAct2zpnn`y83+$xJ?42qeV|_w404e^oHTGMh}-_8xAdnv>9A_Fv(vhY9%qg>;U4O z@8UzI27!mm=qTaggDXfFWN>Ax+sAMZz%S~qT~ zW)Y9|;U>iHkxGG1sKz$DKu>UTZte$m>!u8RI>^xdI42?f?6C}d zEDv9i#AggX5lUS3{8YiLb5iz``b-X_YT*+Hv%HYi)H@wR58f!q-V9$KUx1fg!#^a@ zu%>LiA9v7DeOD!2;{{t`@{O_w%AQ+XLGu$32_0{{hX15O>#0|BU`lmMUOxdo+1$^5 zMMYrwF!hAT0auIq5ruGDC$T7zML`Q*Un;}bGq~o42xO^S>Ba}Ar{iAv9P`S4r5gdv zTeHh55S9E(^&IoEZ<@M$|D-oVZWCVqBqf{?NXLsqVjN`Y5jxD-{GzCsY1@2em{M)7 zW)4%E^(u7iE>(NC&0P&t)1ex~{1?uCRfgLo;L(M;t8PPRMrq^#9PYuAk_}zA;c4Q< zSv_J~wR!zLe3R#4n#nI{D?7Is?rMTwey$Jm+j=zsW~oY=N}D`6-A8lh&+t&q|f>#fp^q+;{@meEpO4K&O;k@Bum)I)gA-#j*RqED!5&gQ|_0Yi$jBQpe0p(dQ z2nZCLqZAy{NFmA*H214%xr)S14{ahiCMAh{W_(B@a&+}llN7xXu_`K&#FvVcLev0c zogMi684xMiXET+wel;Jkzhr=Z-vvx^jv9K)2d^oVHv}l6G4%ZH6NTRg~6J9b7qf3dHUH^RmYgSoaEy zgDhc!TUfyMkiA_`A1l7~^&JC6Hl+WG$==Wnp@iis4T87(e4HE3<32ZxJvZ3#Y5;Ba zXF&;VKrSdtuDfAo9ny8a#oZA<`1Kj&60y)FUIWfX7OQ7jMY6n*g$t@lk zP*{oKX{K0Gpz3j%OGco^Y;a?eP0}Q)!7Y7&%s@#nmB8y~e#-v5U%@yGKb57_qcCG; zjD6B^CHc%7fmH*mT?P?4B&}EeN6ZX!t~q*m(h}w;di{Tlx~+06g6Ek_%-HX z!rKu+$i2r1!l`*tXc^~6M8P8YBU&j_jkC=9t$GtH%H!2O>FWx7?|JwqMhjK{U%BpL zGk7cz(*n0|%c1%HG9Tq!XZh@Ko5*q5+uV?tP?lh?GT%SW4PW=*IO7Fo`v-mt!J2-- z0948gA8ot(g8_z0*b2GnypnSakm9a@d8^)ztYZ(~>QhNL?@C;HJ7WU$sL_1NjYEc} zltW1hZDdEh(VWp*c7g*SbF+95KKThX@X4-vWaAUg<4N*%{!Z3p*y|wI=UMo z5K?banAHgC%`H1D-LAub>#Sn*aImE`yU@j$=&Ccd*RU|32WVTJlIfuhCv?onAGOlUHPl)`v?4E#>s+ zc_5qxRtLJz$Dp^b%YZ40dZCuc^&HI2Fc7ix)?xsba=pOzB;Z*J{uJw=gqX0-ZFU#l z#a$k#EHik3FI$K69t}-?qMGQY5CFQ07LUi}pu}0%&c5klupMZVd2!bWUDyAR4b!dV zW0o;&xX9|0YVhc~dcO!7PEMg7nU^~oT5oxS))hVABCFwJaFt3p0};iV4NF)Zlckh} zPmGdyGD)`_Z=llGHL$z!wZQ2?Q!w|M$(4@SMN<%ywu4fWdqZ(d1AlUCAY&G-e}bW< zPxSU?$9=y7HD74|ntclhA}2n)9Hys3wBt`q0kTI*NBv^g$yR7fc4J6psS}*2kc%$jtoc)?958Pzig!MPXNc|lUj}q41)vj zrL-K(+76viICM5y>xh_6izDtyX0?y1krW75T!a|a2pVgDSvs8OH=8(Ou#C9)W{16>D1Gt1oR3VU^BxYJjb_=JXnLd0f89Q) zhOQd*mOifFxRw;cw$|Msixvx52uEiyXN9)#|2x!L&tKI3t2*3O(WXS<1kU(Eoz}7d zhA@sw`e?9}-mV=|R@ZNihg3sjvHDjKFC>BCVq15t73jzoIXvp$&busHMjEBDy36mE zn^IMlK7*&z&VFsBv&-X-_No=kOKKYdMkv8+!C5-Zo7;IlkNUUsOBM}nYnbO_yD%fJ zqC3BwDS$pmY5R?2T3|p*bG_|JuUHu~kL^sb?DlR!QXzqHO)9CAb4B38`3G{%eq(V$t!sI#5O;AjyI&c-h${dTa7sel zgny$e9CkBpZgF&$VMCk8t>Lf8(5U53Wko||WWz65?Wz!WQLZ-PRtVksO~yIHGPD%6 zsS)TCgyNxudJsOE$u~IB2BFgaL7Ck(%|J2aE{kvDsX+Ww8+`+0+~T@p9gEsTf_Kun;kiJhZ-4kxAp^HOBBD{85fK7pO`DWW#MmPQ0IUj=HD9iy zi)~=n9+9o%?$%;EE*k%m+VK4ph66Z5J%4ZL@Wv2Me^SkD8-M)K58d(!Xe{e7t>F`T5xp%zMz^GB3JTl3puB%<`gU6%; zF?_*vZ0l2aHbs{Fn`g%T5@-m8#nPI}24*6G7HOp_PMDW`A0M|4(718&I zYzW&L(tON)@2^6K^D88py*OZY(zuMY3H%Qu89A7$zTs^P@e=RXi7vO}FfuBR&=mn? zx22Dh8P--oF;M0kXX$mEC~!Q~2v!GWc#KnZoEfw7Rjqq$ zu;yoes9M7v?~Y2hOIV+lVX*A?&&{1uY%e+7Lt1$&4?#yI{_qn{F)H;2TL+08ppg9X zs162RM0t8qz}+svaHueBMV?$R32Zp>BtXx>N>TGwWr2b##vHB3-+*OMDM6J(qQg$Yd@I-QQyZKz?ARfgC&jjPFAaX z2S3evTy}dgK3v*pb@2~z~kOQY36|HfPmN{S317Cuw>3TjEh`U5O=bhtXR(-E)>o~ze?p*A-=@P#-0l6n1?*}_Tk*N7AdlY za=koZ1zjQ6bA7xfys^d@&Chid<3NwU;!EXM94LcNA_6+YZ}p(g;U*X|Ogubt+fq11 z#Ka^{+;rBzy3TOq6*ZEYrM2p@#Eka3e7LJqvrpouc^hIQZND>gtj-%?>N}_T00~1Q zxGKh252Yb_I2E?mu?dZ5*7hSgK8)w(!%ZP=XaN#}#H3bF#pS$TY-icH6W)f6{@%xi zOT;QG_?)eC>jVL$)}Ll=rDJCmvq(P2f1Hi3>TtUUt?17{c~6_PgsD@a^XaFu9CU$v z?a{JG!LAt>z*QI;EJIMSd2OQ=R-S*Mw4UV`rM0FD_m*uADZtbF*b6^L10LD#`H^6VQlfy@x~_*T!P_Me1;W z1awmo5^ZRMOGo-Py4M0!Cg3A#9qg3D3R8vKWrKROtM+=04Cd+8AWXsmR< z1Sq0iCbc3@N&_Fg(e2ZfY_gZXma)lyjM)JkI!(gLd2M}(F9IXQ9xYrpZTgX|blx@6 z42_=4=Es}{MPqO^4T{dn;zKPQO}m&IR)Om&kfBZzrprUgJeT1fKit^!`M#L`Qb|`4 z3@|y<8z5&*`sGb$ZCjw+&UR7AfIpjR{WZcSyo`Z-dJtqHDSEoa+IojaW2LW)pyAd} z1z<(T?5V?{%W(}lO)K5n-1T4hdAv0U6xTA5o8RNC5c7|upr zD%~E#+W|Jh{FIxXNyoO_bFL#c9lJUGN7m`2e95@ADA95eb9^c3(T??Y4JfUD;Bt4x zCxAOe>Nyik({`OVs?SRttFZnkWGAdHo*rR_VwPurjSfwDo#^vZarg>~9zS8g-qOcb zDIHq67PcHS@S$jy(xCY`-Mp-_km2>NsRFgHDX2mkm+d}X)hv8o{>l0B%5$zXmMTJ7;c&y(AWLVF(OIG+OwM17fZ!<=bwY zL6B35EYx-X<{-;Njo#RMVV;GqDzXb+IIUlgfk0*z(nsv>41Ja+x}Ca3(694=z?V~z zI09of1)ZCM;|}j)kd_VyH@S?@yXkg!R!Sma1zCV3{?4(EuOx(0Ny^Yow>&pbjkL;Y zsSnasnz%mz`<8k~*+$&4Ci)vse@z*Hcf;~QgOUATuIT_;$cMo2pef!;~}{; zx&6kFZRc_@%Rg+^=K|5bRu9E=A6{m?;oGUF&2aBVnc%c?JE9;hSdHt6tS^HtuC|!! z*s5o9fJ?kMeAllP;uh2l#i5#C)wCgH9yim(eI7^)jDZG@F1$r+YqH0>2o9x_pgfvp z&3&RmRAb5VUtBcqR}f~ZxN<}8$3u&)4IL`chO9cWOUAm-#|%pCO91S?dRVR+dQ0lT z+wgoi1n++WO@1`dxLreIVumZ^A}+ZP=NDzo#ot5WxW2Alwgjgq120)o;K-H$b{aZS zO#nWTp!i(-sM)h2=OW_lzV{+vGeDtm+O$x>gd!%e8nTh^@1=QcrX z2v=4IxA*3}@W_Mq^nfW|9TQU_V3yM$#3P=#Ah@c1Pv&F0bwAUV?qX@p6{(7Rh=f#C z#9*1hahUE6eee_VQ%rXNB1{wGAQm`-d0C&gkqKLpA)Up~(Tt z(3N2F(YpAIYr}XN6YvfOXQ}w67Z!4Sm_G`Ljz4KKZ?qR`WyY6M6VoD7& z6j(nzh11w1+R9ostFuLz%1Ok}dLeOSn|Qx*578@vZtVz5SR3w=ZCcI{i~#b9 zrt(0bLt#?^%mR9SW&9A)NXF0l`DEr1n%rf#{23lw3U zQ&!;G9E_1&9AK&zgp3e02-+b%+wj2vR!&voz%(jWA8SMZ7j4Py)#KVBYlGf!KFOHs zcdX-;2w!ps=zt=`%_RHh+Hijb^uc2G$Ch2{QaEol#k1kPM?u_d${_I$ybDO?1yMdw z{&QGe_DVVIAepWE@wSxi-)jez((+2WP$t-DN@Xv9PKSqWz*vnKgRpcEY|Iz2{yOV7 z-Y=ze7-^iZ#o?w>wmWils-Kq`ZuD#r+NTyFIH&oXqiN>-7)G#(W8}VWm}?eS4jx8O zpZHk7fZm{`yH-1wSWCAcablY%`zyg&N)d@r``mWfY8x;w=$PJUyXU$D@m=310VRb! z4z7}KrD5C|t;pa;#KEe_#o&@oZ+sZ7ET#KJE!H}J{o0g<}t>p0?^M`@WJ(h&V>5MKkp3FwB;!<@RTt3G~iYqiA(vd*=XC`f}`kCbf>c z;Gs)mYvw1Aq@XRhIo=8$bTilXPr(Bnb6v9nz6D92?21h;Tf4{?aVZ_vt$$le-?h># zZ4!vpyx@MU#n>xAQ(!kN{=QX67>{AtLLB9~3Cx)^nk_7SF6};Ji;la!z63^p-3CDa z9RSA$fOpEK)Ag6E4|A;QZW?=$GrQix^;Z)`5|e}E_zz`?W)I`9^;gf7rMphoU>94U zrbos0WdDq{lD{X-2z@Ch$zeQGLhY6$4>a!m&o#s{cw1Gx5x5u6UEXGgPhjt4CYo|egKI{CRrP$e5SqsF>bz)n!TnP4o*n+ za8c`a;Y*hbA2@{K$T!l`S6=hZqRW)^6CGa|Ey=Vv#SP3TfD;`wJ_EIh^&spVS(aI) zyo@E(xl(0F(PMPT`E_)-Uk1-?=$tnK_tz&h8;W~M+R%Avdx|7*J(Iy;5v|8wFw$-q zis6uva`8fGo!@=C7U^jyj;y*Jn)(dbuO5d!@E99+r?D$Sr3cV`R2@;?m}fGu?!x(fhWR%E|S(F>_@I5WYfL-g56I4 zv%cR%JF({P(aq%bciQV%Y88kK>7p;Q!~Gg4W*GNC45h;W8P(7Qy+m>CQn&wA3=dT+ z<~2$pK7mF^etH{$B!%XQ+5jOG#B02Tgx)Sf!!llcstu5eHhlAnkM3v8lkMCyRMDHA zaT{hq!{HU1>M^oD|w9 zY|2)1#cMHx!nzZnN`%c`aS2c5;dTusT!E@}{goK6i*Gplo<_%H`bfVVLTd^1fmC~p zygR;RFzJ5amR-|B*VOZswv_H_-4e+}Y^JSqBS?)HIus5$B7)j5%C z(P~$B{Ktt8%#%c#dC|tc3q(5pg)I%^2%ZWAdq3Rl3uE*cxWV!#w#fS_yK~QGi5~BY zhx=QgAB~v+nmlQ>>Q(@k&&zBn-|~LuG+C!(keW{t+18&k!`^9o0cv5X%`m*==K}Hd z6rj(#ci`6>_VPRXN-Ota|8Z<3R#l?13e);IXz16Mi!*V}vHXk!dg9<^O5aO`xQmtP zgbu+a^gicqR%@8KWvwOa-D}C0fX*Eq80?4+%Z?MNeK@V4liXWo%NL*4`!%@v)+5!q z1I4?PR^=baOm)q!&!9+5 zotz;s>6?P|GRvjCA|}jx!1oIujajj1KF#QDN+S6*0Ii!HC`z|Slcle#fF|2y0@aXi z8;_lbWxy5~*N|L-E0JUkWO#2!MJT)GA6y1Bf%wULdZ@+)YTfiaJ>0IKv9ROzwNVO1s50|IPkjJSs5`GStuH(X8%s~r_r8y&j`*nD@UqI!brZ+G2yv5+0 zu0K=L$&Ts>N9`IOB)y}VudIc!p~FWKUrdv?*-3zJ6wB1s#68CT+%E!_7vDj{G$oj| zoWNN|UX02t%g3ddj?^|HhfV~;Fvz;z%NhZE;^0(h{$9*q)#3bQ9_`1cVp_#RGwmBxDwbTXT*3^_LLA%M6 z4H4E|AOHk(RhjQrOJ7eQl~C~&en??*sa7GnuS@a(lACr|D2kBvlgnxqv5>}Oe;A_5 zTi7mMYtpuebM~b=+-?Gk+sIf;I_wzHP%lRfLNLQ>=?csA#9ocXY7giv=lUQq=IbIv zTX}N8Ig{4LC=Z56mu!Lb%bW$b-F(*vAk zvwz~3ctn@dmtHzWmC71Au-~6R{KTv|J*1GW;!?b)hca~{%Aosn8bqy=Lg2#>lb=Yy z#Rcyz`R9HOCx0aDN#o1H-HhMH?WFCS6(U_{b2lw?9horvR;^ea2KE`JK}w%QIEj4Y zT`isNYH1|GN?i2QFe>G6_!&QnvkeDpI820#xcflN1JWvCg*I|M5hz7ap0@Y|HV2Ty zJF7L_mC8^Y(55DbX?0+RJuy)Y>Y_2exkMV*deIf}>N|&)cmcp=eu$W(m5}P0#(4Z~4mv-^9ZXKtr7}HZ} zNdGDghj7=M3m;o!S{t;w*q+-Q9G95}R07&7U+D2t8tz(Yqw7Wu&!bSoRnO|CO)F2I zLQ4Y~lqGgi1|o8%n#j_sWh6dBojA2?Mk<}(28VRtN@odPbY0whw$k)VNlNQFl;(Nc z{;Y!$>~NJO1et4`IM?BL>Z6zEC&`(k(?9fAdvaBVyI7iDMAN&bV>IUwTVe#lvkUbM zBjL8-0nDdh1H&a%OhFktDQ)LATyiaa($`5Ze(~?VUBie7IXc|7UqYL%LU}O;^o&6} zeL^z%Vvyb<*ye$)FL8#_CtaG0tOO3S_RCs2eQBkuNii&!8K!y9bW=nM7Y@cQF+R9b z_pCo~KPS>Iu^msI>HXtkj&&aA$WMvH-ADWGbQenx>?81?5JF;zQAyrar(KLCC~fru z*4~<=U)4@9h_Vi<;BX$z z-3Stx{y1FVV$sx0T0yosEBmx_%=b{Zl)t}9D={NtGPSvf&l@apc`(9OY1_jZQ{(;# z^VoGJB=T51M9M8ldPHagwV^%MhPzg}gg_N<9`=8DoN6t?nKdLGqhK$kZVPWWMT5e+ z29EV9kLrY3cqL zm=MM%`-e8(4OceLrpIWBR`?YjY& zCDuM2noTtlF!Dlm;@c|a)8*jm5n;!m*~6Z$>7!@#PzpYTu&CyMZ)=Bplr=5|th-cf z5UJP|p73T&dv+bLuACEN=TXD;RaI~bSR*!nm2MW^YBPC)+!1trzt{m+efUa~PTQxI zgOQF2rxp-Gjh52KOQUV8TpCd*TzwUx^)C*B`8YM5bjme3sHQ7>@9-P|l)hBTq!QvL zV|$5_hm_-vqMh@fvkp`7OZ!ns_y&(UsL}(8Fn9trvFMR0os?ZW`7Jp6z14^NRb*4J z`4C4W98``1lEbYJGBcmiwZu8AoHLU0Q>Zx5%W@&sv%X6I$(OE539WDl4-=SV6Ok zg%iML>~5hNbWY+Hbaas=a|y|NUjG7Ad+KzG#6mK$(nvt;AF(og!zX=RROIFSX$rpR5be-D4o6nrRx;{6w$QX zg07kwO5A@(B-x}8&Warkyklfs&_fubbnmnN1V_JJ3U+Nl9p6kb(P5bbVfU$u%B9Wip@A1VYnbb zbVJS(FoftmW*pC)rTYbFc;HqVK$~@ZL!DYrbb!{g3F^EE`6z`b?2lxP?p=S>>um1$30}K^Xk-S&d20yS^tMPbKMG7Rfd9Z zbu16%G|$TMd_G1yr22*TgRbFT5h=7lyKvtOk?~96;6n|!EeSn-puP1qE#b$w3VD$X zE$+27SX6F(EZXZ<`hFGg&G-t7Z-x@&OYAe_wit@M{nRag;LGTuhGb^w@fBVnkUI8XLt({*OhDN|l&!g@2NnIkpgnVL=x zS3aDy)HgBQ?Fu5=;S|?`n*@`!(ZeK;#KA&9;#%I}XtXH<-Tx}YnJifXkfxDEAcgxm z$owvv`?H*WDh{`sKpK1aTvev8VdqH@5J|8@d_fR+Hww+fYbJ*^rk>d7GGvWlqjGi#$;oOY#B10BuRbYxJaq|8bZU+cL9a0i z>1A}N|Hn#typ#q;1PUc4cqEdo0gBz++%DcMkF!ieD3NclQvJ`yp>HP*dn8kmiL%)n z2bD})b7OX=82iL+x)F3t8UlS~za|Gh1&+`2G?(nTFx($PnlU+qyHk%m5fYBV2$6`& z(`2+*Iq+h2Y=!Xh$m(Ke7LMwh&noz@RVm48Po?3el(w@t&5x22!pBDn5`Dl_X;orB zxFHp;3(nH9&VUM`H>(#`6Y>$m8HS4y;JeGx*EP&zZ=|y`Z$r(QV{igL+{vLxtF~vr zREI*kM#b!EA^=Lhhcmcy%)RVi>+8ay{Vojdqw}`Wus>&&9g)(G4Qc;;7>h7v8#$Fy z`|)q+hSXEzV`!i@nJ#&}M*gHPd2%+Rl2?unp^m+7C^bSaj!3S_mm}vn^ob+ga9v%- zz=#ml8RtJeD%~1q9G|n)$7g@mud6U(>x@8#OlDGZBX_8AmGsc0@kHE-$*DhDHh_HK zC&5nIIAkA$zbLa|c;vju;L=?qO%fx_FM5G|=w_rw8rO(n;XYij*PZx-OZ@r?c+Yn% zb}B}2MOOay->Vku9r*LSjlKzlc?Ks=8UWV}o_O^L?76$K?@OvJMwrj}(l*vn%nL|$ z@Cl_aN|&|FB{)5EY$2YiLc52Cdh8JziqL^hnQnjtdHN=^gA#1{bDIz{S2F2h_BA`$q$8tHY-s~aGCYE9YexadMGb&I5F)nnt3U79?XhPy~w zR{(|{Bh>}h4b4S~IdDJAJW!;US7|`fj~^?k3Jtbeg6?0H6+QntvD-RQ82|OaU6q6~~PXt}9_-XTJJWmY$J5_(1Y6f4N|)2Nt+O*rP08> z21Tx3;;P`mwQYX{QtZGo#m)`h44m0C*^3C>+MNJFR-5XJ<-+8P;iHwUBQy2-6 z`-3rqV+#wYc5a;i#I-gPHBnHyFtvB%2UxNg&m$K^%<1^qB&6qc!`CX3~<5PQF zRd9e@I7YQVL|CqDI~67dSd^6!YW1`eCMn*$d_+nNfL4cXBaO<73hwJF_z@h$d);7m zQjZYZ^`ZD_ARVro;NH6k<{In78P^BZ*W}WoRW0EVW}Lt~e_Ti}M4WV!>Hx7h_|iN--lnT9XS-8d>rdsCGd8h5+-OVu$za+MWKDC9R_EB`=hfH!bVx6Tnng zD>wDoo+4RA`l<@J9B9UKS-9(?`HGB0*qo&;~1Q ztJqc9n+E?J_fI{&NF44r0Z*|Ak~S`M$UreRSKUJYQO_hM5g0d4t#-((a*$!W>f;=N z7_=>s|KVKVb$?zpj5|Y#+(9G`Q&No98OCLvSEPdsrE+m|5Z|maQNR~EX{ktmMmQdx zHb<%>Kw&|&UdqB<99^nm4Au1VsT$X1f4#~AP*&SH+9d~1Z~f49CUXw1KWq*S8^`qx zPJgqe{t)N1r>bzzkZw9jUE=83!X7rm{u?y_R@85Oj|(nU*NC>dF?Q0_CQ=N#K{c1J zlCG$S!()FnWKCbPWL~svda=S(%K|S^AKT9&+_Kx|1ZZMEac!uO21zPp%sPKUu(w_J zfJwD-e2obGx(LuLwolIHSY5OvM~IX1qzQI(Ld0+#%2t2|k75n+c<2_9;Qrw!8IRE0 zyXoHjStwMO5dd6sSe)55!0PX?Ks48BC9I`jZBS3}|s^L7pU_=tjY&mf95 z9!)cD7r+b{Zru>o`7G=hBx57a)-;3j7E+Sqf;9hx_5jyfOw7v~>J z%GsU=hPyC##Hy@+;ZR)6sH0b4vZN{O&;YmCtK?-YRSZ69pVdbg|t{C{LOrCsOG29{fV@T``zPNlimV zvWmtL#*<}kZVdbbQc$5~@gXJ{d&8L5%N7=iGMzsZv-8ER`~DP|Q|$ZX@H47qCWOS& zu^?K~vPr+5z54`zJ#g^RIs!o?4h?LH+vW9^1(RNd(dix|U5m7u!pR0W$4SJ|Sa1_P zc9U9Dn}2%c_2G1a3fFa@tUwyB<5`%?>Xj|~TI$nF+RyzO6qoe9{>RFHjU3?O*t1h4 z0d`GH{J7F~JS-4cWyA<DXo`1Is<&HoHb zt$i;}NLUam0rvAcrs2d?>!Dz_-LjsNVlm~-aPR9Pj_^(*MuAmA+R0Bj$G*q>V{MKi z;Upa)16;Gk8Xvn*&bmKITM2LkjWfK51~9*k40l;Fhe8;26AIrI4nYkc9utyS#)~_^3=69feH1p zmkb0*z(AiwtdK_Qqe}r1`D6+9Ey9)avS$rz%BBU25bME6sub2BUFJ@P#BkqVb0B+t_P{Gs**YyXkGs zaDNEkq!N@2IGZ2a3{1|_HrzYrw%{!A{n>J?rexJ&4HE_hoGo&UE~qR*^>p^9+K_Le zEzIG8Rd0UbIB>#2fWS)*7eXmP)EU%KT!95=UsZ4SOECVN z(jW$hqfzivP~-twhV%sZYG)_2x)@j)t%5<41im2{%H_fJxU)fTkTGO_!zi?YS=GcNQq)iN(ksQnZ6?PK9CIGJkbNx_PYY?N~v7~(b^6Exr67O`M z8%x4zt+xoII3w#rNi;kK@_Gre#X_hiY(SeZO1>hbO*lXL`||mA1!|?jksS1*il!c> z|6+_oSt?rN^s&9VnRKvpT4%v34Q=8!NM>CG%ZG8vQNr64)qj5~3-@~fmEKL(45w}_ z63la052P9g0F$uoR;4PAV*S5HOR{d(nUrlU%6UxOB0DA z=&=hU)e#5aK9Ni4-KsXW&qX00|M1HQ+mQO~mjPliP7_*Ji?oKmbtnsMmrHZ!99kpD zA-`0G`$Hf}OFudFhGz9(;sR<|@B$sd#@asJ?xVFbV%>MpCow%G!!1?Jg$3_07cm{r zb>S{WhA;vYMth9T97gvUVxwuGdeShXlq-LZQxciMI;dB-N<=1oV$g^Dnc|pL3)Es5 z#N*I#zX-$0bjb!O3%L($S+*3kNaw~F@0XsWjmLu?0^=dD6 z8Tm^OUBmY%XZuT_L~t8H3|x-;1$69{OL*ZCpRhim)hH0=4YD@Q^W#a&fWcwT0sq2F z`nm?gDDo#Cl+QCF88t4c=7SpBDUz{{U!C!B*~7M$UjYEt1a6ui{r*yQO_1j+qLlA4 zRE|;ueprrZK`<-`cy_s*uoTu zi15=Opp~$^>&-}R+VLIiu4}zU>Xq1ybx^PWMMIq*`8Y?oDH$Bx1q7t4JXMGD?p0om z2d*Ml>fDzUh^f*j0% zBPMeV=+I)o`-gf9j)qV~n+?WMdA$2&+@FK^iKbTaWmOBoCW%DxRqbV`T%Zy@PTF~| zODO|2DEcoL6UM2GjVaOlo$}b9>O=Y#HsCH-y5=biOG&SGd{o(%C}gauUj_}p9tf=< z2eA_;PTiEYQ9t3B6vl1<&3qjkZffL#8;%%1SS%c|mUJm+{h;cN!OrBNASV^6$*T>( zHlGF1;jH8OHypB4MAFE;d%=`$S3tz5aBaelnQr)?ks8fATr z70#qf2&1O|T;p;NW8Oj(pUz9{aZ~cQ#=Q=+R2x>uS4{=j6>IiAuT!j;R zS{GoKVTQ5-QyJcdhp*&bKg4xVBVmRk7Y4-#aDc{0C>;3$BIsuv;Y`9MCkF5ewH1D! ztidbCp?&;VU#5q2FA+83ap}dFzr@yn;mJ4Dhn{Y#TqLE^bjMhSw4DO)$}zQ;ZU&s_ z30jO->5dQmQTKMggy|u~OU1)$5g^SH2r{!P_S3+PZRXP%q1N#{*W|LAtf|}sW-ajO zMO~$Hnz505stb9%iiAq}SuTG|@VtuUl2neX^`3bPJZ&`Kb=cPmwmEw-<`i{Vu>|F}95(pzBqr z7@p}>iFuv-ql|+oNzW$BcpZuGwLByZx!sQGO8R3L{b;6?^LNM(%`IBiYZncBkSR3i z;>kXI=U9NO{|GIUXFo=QPs#3&vKhD<1{w6d6!{mkjIRsuo*Qm+YS7YLMZ#y8vCpEL z9h-|$%$hR6yo2|!1771S=RnUNBEL?cNT|D3tdEknoPRlW8+|wB@|+iUmF?xGCd@za z(MqUK*RSiC#8ONnR=Tp5{-_$_NC#MYPY2C+F><&+p=yxg21Z8*ivxqeQVxS9NW)jD zHsG6m$LcOu%e)%Rp$39~I9(Y(HSGSPI6RV+?w5g)=FgD#tRm8#C!?&ghA9OL2e<7o zhC3;<1_>YQL?X0RD(w)}J+FPVJfc?KYr{Ramm=BagqctXu*4iev(s6Rq5;=9CsYkq z3K$6mBTzy;VAB@uzH@Mtw$4uXj=^(3e#Y|eRS|uE2IeSV5Vwk76wZZKjbnG13`PqqMsZF8BXQ1Wdu`=AR0)~w^RFaYi z&MZAykQqSXDe*K9z>MBs*u_gvm%} z`fv@*{bc_~D>E>)os|EN3oGqw(o>2LX8YUuHo^yqFc&nJpiv8Wzj zj6oiG7v?P(qWO3!4dp-SZ*cTz`q=X_7;_YS(%)#JkIfo9^qE>e`jjh$OEu!4X;gNJ zbvS1vuMrt1Z}HJz51~r*ir60zL;!t&Z%RWr0=xN{+bco=!8&j1nzPpniz%^NN_4wF zA5ja{rM;=4^IZ*XaY&H$*gAMHRNxipPP~k!fqz`~&Kb1U)1}L4&{5-XiZ^eE2EbCH zxV(O|P`)7Kmz#9qfAV`^@Qp8*$f8X3@((Vr=d2{b(|+ZXjE z9yEXHq48F5Y}_%$6+(U<`s`&#hw*12eWJe}s|zgnDD^cvv=RwDNZS8|IKU-1FRIdG z#!s%lSJG(8l6G8`@MyG?r(E7-6AG8HrK?iXtKf-?lU>k-3Uy3>#JcAOR${z(Qk5Qk z_VTwJ8j>giBYNSkX3hWt)($a6l@a;Y_RNyngtdrHJTguhAW&1ilVE4}M6d0=Hq<+q zLU-CDvw96Ld=k`bjqg<@9=hRNR!Z=OaHYi91%G#vADU_%9&my4k@@Ux=5=P=6yzV%@Vw zQPn`G$znaq=GAX&=yj(BGoT02rK~mtWs*;>Z(jY$YEz~`P=y{!!@rWFdFk4-L3iZg z67pTy3FXz4uP+sR_W0!ckUBEm54T?`*gv)5OAmd9=cjBT?B_S61fn`5S2NV#@_lr2 zTcZKXY#^6W^-{@s4^FBZvhqO$WG|(m-s^J@H8f{FFbEGfRPY)D50eSzhM0%ht80KG zpB8#$;u<)@aYPQM4!eUCM5XhwG~AWQJ%TR^C$AixNN~UnbqWd;^A)x~xVBFxSQe7N zUWwaUl@P@jy!hQQhgqpd_1nJT>k@|S%4!x5l@beDJ~K1|QX+J-|5(c>&GfYR4xz~(UL*?n}`dT2?Sp%ifEv8}ZTEU1cs(Rr*Qw0=bA zlkiac*$EO!rqKKwRq3Wgg|pm@wV_R(rW0xmaVP<_VJ5vHx+Pi(IVcCG@kEM60-UqN z4u1Q&k}VnOUu~4`mry0Afk*)b)>Eq%-e~s7ed-`!GZ~{xva!Oz8M)dZF}I@_KTQEm zDHeemGZ*d66~8?dhd)A|LR{{7 z8j*M{e0~*&_OIg5enOo8;-a()*%}XU^6y9isOiv)Hl)P5f!?ZVqw`!F+JC$!anB;N8b3wR}&-%WJzDtoi=3@{(+!u6x?kLq3Cj$y) z+~juA5|Aja$4|n>x>g)EG84>?vFB%0NmxphEwGTc%J8j+wyR8PX579-bb@55(^z@- zv$b9nQXOI^*!{xH9@27o@}tnk0RlhB8wcq}oUih9x~$)eF9zBxKPKV!tPk`UK_}i8cV>!8#aNfH+UIpVb2^`4`+7ud z^kF2tH|()EjJv*|{Dcr$dKI{xT8S#4GqsCE{#LeKg;Y)4WoS`WsU}7PjkvvkIs#jT z(V)qGb#3uj9PZL+Y?lu0D!M%E&ibb&B@QJHtv~v=WL%+!DT%Oa}Q zGJ#Y0yUgJp9Zk2UM?ba-hli9{H-*kz4Tg)B&Wo*-g478wgvSESPgk)vLxmR#xHjFo z6xVv=dF${cOh#yhD~5K)98BTSZSQ@iP*AZ}yHwhIwRND5UL|NXOvK=EsF@saS$|?` zOmx7J-rSVFE`lNjAp+~o%xGJu@v$>z7{z8-2Gm%^9(;1ErsZ`Dpj4i?aLo~vgpj|% zr`1;b&0_EN^L4<$uD;>{%tmS3fil>46KBz$NMjOJVj0yFJ^{q>U{#)VnWAa>s2t2b z-eyYARMy)SnA;5z$^@U{{uj$A49I_=u}7v$*?N;A5C;(*B)YPNS=|jhA@mG}$|o@e zhX&EtM{V@|5}*JfJF15GLHtwmgGW$w3<*ygdZX>nP2LEfK|_4U-+-i1L^*dXXklpj zi})>niKA2adj!Yc^It}R=_R%24o5(tL ztPWExQGnPm6m4Y`uyUHU8UisOJ-ylBK+BB!O+a<&d4+)e_eaC zT^@51=VKGks)J}FOv;npe{iKt>{1&+ukyBTxQ9kl4(SaTQxg3`)t_Qm}W&YA8;U?_!?$Id{_7;bH`MN}BJpn^K-%i{Kv8Xe|qLlY+`?C;ZtrV5*Wf zH-aGx?;un(DioZ7^e=kBoyAi8$4&DmG?IH+oAX7N${xc#$2hpi9E~bdX_0Jn) zTc3bnZ^p>z5YAYiMuyNXJTX^+dkd%@9J>74V;OX$Tnn@dZV3e%hgRqj8J+{L|F20F@qjtoJKOEP@gj|lCQ;~e1$cGSO|2h zQ30AI92BEnP#q(u)JK1oA0|Jo0eT(U2l=SlDAV_wT(aw@S|AOw$K&Pj-ZQB(O+M~AeyzamTbXK@&HR*Vq)*}n5_ z2QGCZ&~Pwl_1G(iu50jWoKfLr$$PjkY6+aSsgmI|fozROdBg1jno%~q^%)?W-znS% zlW|?ziNHfIJ@nbxB#ZSbPPZOJQLZne1HGB%9@QzF51Q=uGd~iQ%3Yw%cMog#w62>M zJ@iGDihKs&zt254G$U*B$r5sAXtfx*K<-W-3M&pYhZ@=OR2a(fZNVCV9gSXWqa)J` zT&a|J!6cYFInqAL1SsSXB3K9KjGG_g=$3*KTHh=)%JDA>>#+ZPstd4+@MRR|&V)#; zzQQR-mei)O>Q})^>m}6Z{tE1ULyKDh<%ScHP4d*6+YzMQ;7OWoKQY8<{aW zCNNNOGTe%=-cpcJohyy$AgiAd&vMlI8bHM*&kSQ z>nGH$3@|IoV`cbKCfkRu4}fhGze_g(mDY4G9B?PX&oLkjmCz6et2|Pzgy_-2Aoud- z&Ki@0&A)ha->-pMUiHn0#+8Z<$~To^4Ua_znQ?W?$p?)0+WG=G4w8=WY#d{5vc1JO zpkn}79!*%wJuUj^J2#UiBWbW?kd5pL9#2>*ZLU;Nz1&Mhe8;!QR=&{6`GeEt&pk-x zx!}I>sBkD>InCi6g>?o8CbZ-nQjHMbmhkV$7pWbQ7uXrCT5C{F*P97_!UDI=u+25q zile;Aqwg1Cauv=){5euc8$f&f%BPXhIuWvKP~Xsu%*a@-&|yMwUPmZtWkVLL;x||v zhLm|yJ=KPL7crV54C>b0vJ*1$5V+L3$M@2-pBHA zm*x5rxC<8yzE zRK{@WS^ZsJKbnNNv+FCuo+RXN`N!oIY=+0`a1V>th*?t@e|!(KF*jTji=EQ|w7gGd zV@#BUlUcHU`2t<18hP<<-E3A~x(xToRFYg9sM1B|9d=UZ z>2Xvb7~&ei;g!R7a9k`_67VFcyoemiS6DPSj6rLMba?1WoCV4Nfife8Ah+YPYcKZk zxFy18hctF{)+|#Gkhm9FTIwmn__CA!x(bNXU>39+Nt+6N)3R%BPuU$?8YiR44C~_X z`dH)T#J>Z4Ck1!QH!{f}lyUSnIix$P)I_!$J16sOa`cXvb?NNH4C2DqrMU0pfySN< z4>@>1&0)Z?(;M_}&o#e?Mc-csZSj&q#Bj1bw$gy@$KbstA)Q+iRHY&96T4i%&Wa0E zO^kBP8b8|u(v1b%EOw0Lk*ajR348-XS1bO#u7Uvip=Oqc$p1k&)eW|7e~|d+hD+V_~>o13#bM93NhqP@JBRa4hE% z&W*#@bEiNm!DEa|Rel7Arh5qpZ|BbSac2Wh_q8(QdwPR77Ptx>Gkd_p4akX2wz@CK zP&F_{uny|uoQQ*0=`19!iF*~H^%VPtXsgp-28Y}0ASG0j3sTd~O(2$l+5k;lvbpl; z8hLNi_%loZ*CdVlWj)Ent@vSsxuI701RQ8*Sdu>0hTCnZteTot9k~+YADY0V(92v* zo(`78F*sm+JWebiu_avJ(;4=x<1;q+ZFB=-AK6MJ|6WFitI~*+BVr+b;KG$+RIl{< zt2Ve)BX$O8jmXwjNc>#V)iZO$pb5B(&QCPf@_#6!^NTXto*hE_$@cIdh>hq$45==j zkcPs+EN=a6ZpgpMN_Z3=h9DRQV~wokA}g8uw4cCf?kAZ5r9fK0QtaW&$k?G~ut~AiH1Z4fE%7akvZA*}A$s)v1SlgDr*=5$(9)OY_*oecX|xsWmdPJ@e;ql=JI>rt{}5x~Q({Wl@c73FH5;wr-Y;mZ z=3j8gy}Gg9uR&dZcvQQ;m360Q9{cbfN!s-F7pkO$hGJSX0S_657hR1s-py9Z`@XS9 zQ~~!~Antk5L_KRb=)20eq@(B5cyG>ao{_dp-ViL9n;FXh%)5Jq)~f0;T@0DSB!_HN zTNo2QRfxMbxrPXj!_|B`YM%gnKn5gY_M_a>edxA70btY&n%N26l{>nS=V{6e))-)?)pt>1gIDeEno_+4?ILXT(X3_a~vUsdT{`5e{>pq0btWOgLivjlt7=kidVO<4(YkgBAuE}GN_Q`TlZ z;TOmMavkHqDzc-FKU}3lhzF#D^}%y_D8FKsjQBZs;#@Z5DY&d;eN=5(>*h2URd543 zHLFt24Mqre2O)wpoAXrXFdluC%3YhR>kIP{?*Ifu{(S*UA;!U5YaZsC zkceqemznAMDjmUVWvqJbZPU|lC^w?MFZ$TvEsJVHLuD5#4O++o_oY&wDa<=w{oqsaJ`N2dQqbo z8`(rNjBxqkO>if<=c|=5bL6~Bl(TsLY{_l zDZy4zC2f)w(0&Z(epiNi3!gK^0R+hqKfp=2f2oKOM)Ed$oiH^zATN_qIABQ`RlEN2 zYBJDyjSe`;q`zc>mb);QJMLMom4otrLC-^u4Eh>5GI2DMh})buCQ6T7F_@YTGe61E z!Q4XFcQq``9}2_2qN2NI0y(tr)+)RH{8|HbMHUH=}-w=@l{_3 zY_m<)%V9%{TH&pq-Jr}>SZ72%p`gt*QkqN9l7pmP#$QH;-#N`VD`(W0q?G&^0^Zd& zs?$7y8enAXXF+e@u(3c4!9PJ)Yb_tnP+{^lOfQPk@MxRybrscuL@I~7U`{DEbRIZatYDPIxk#`uA%8b^fYA)@X=I-U*Vgwala82K_D3~r*s7U+$tbkjx~XVkUEprR_i2JK^D zYaGNJg4p8E-7ihWktD>d`ndkaG-4&vNwyVh+8oxm(r^zdcK`4DP}OvgAaC*!2At8t zUOu3`;gA8tazaGM5|SW*-5NNkr1P>~D<4qpS8@24IGRkI{%EQsdJEn;*1f?uxUvZ| z8m#|>{*2uL-o`KiSQkAPhx>a7#!UxgwoggoDTYbO5#vJ*LfpryTd*w=q%ips833BW zQufP!i`#ya@UimZs&u~%0OLc#*bWR>zeNw&8}FozTp!V)P!-w9Otza%C)i_s{e)7U zS1#yu;Q;3*JywSP%^=14U=6E$y=S|uSot+QVliB*xm@m_hy71uSKfhtC*RA^_>>#0 zZ$?(a^n;D`i=)#05;FTOWVlvT&bL#} zQG(lo9U42m7pv~uMGX2l*LFIi9J;YGf}%eg>QSm`6LZtafy`F3K8bfCwfclTh|PM} zO>H-;r@@^ze6KM-rGGI3H1i}+%_K~i$SDK{r&%BvLta)F9n# zrguxF+XZyKa()Z~O8_kv4x9{FhjfFmjVrYmpfse&euKo75p^R|G!sq3iS4lE(r5=c zyp0Y0?r7wBNOAPVV2rFXiwS5UE2HS(kOECe0f(*IsiTvS1MS!q#WR%uvgR z;s3ih-0lOT`*eEd;j!@%FA(MK;k#CE4GU`)_WfAVLiU>2D*4iu@nI?j_WK_J;8h#? zFM)%$48Hw9Qzyc!E|dXETgolvXSJxfmjL&(4o*}8jUmI97>L(FUTatmm8a71FLg91 zTFI|A-K$H%8WA(%)vpijYw3%2GU$lP!0&Lp?V|6gCc6>IZ)1MC6 z!Q-cJGcrlZ?8Q~PeOyQ}zhVE$5BOzIebt8hB}h<5{l>J@)U^XKg-@Oaj`Z<;#|+^D z&q_SAX(5u6KqS`R@+#&IJ>yXB$=vX_$}oPhShW||yoVu(u?goW^bN2ZD7+U>DH(>= z%UEaLL<6@P-08kvKq+0ehlS~F?f|$Kp3+xnG>nE~XtaY7l_7Y8DTtzB0r3e2OIBXuPoN@iwIeCJuWX+47ovSKBh`^s5!tjw!B*WV=L4w zPwP}+{$|Ay#VH8`&GfE)D|bn7r10?}Hrf_Qgr!`e(eSN!16tVvi)m`%IulY~1!_GF zj2jZKqs!l`G`JG90e@3Cj9;;!Nl!sFXGoAGInh83p*CQWkKO+AVnKzs)dI#A2k|2T(i06wRH`gPD{1y|pN(f22? zw+$>egv$^#NaC;RT--J=oz8yoXd|-_E!298L3R!(-Z0HKg>@PYs!ryY)aXxT826ze z62p$77wX~cqgnL1ZLNZ*)UO1%ptMfZ(@5)C7G(Kx^G~+Sz^X28`17*iu8hVL3n{vb z`C&yXAVmBjF6I$i)O=nx4SZ%D9X-pAb^A#Y*(uMQMiL=3?ihcSINUFR$^bctLr>=k z0RG~-qw4BqN56UfN`Y35!Z~CTiv0rx6*1kp5)CkJX3yJlf8V#Uih(o0Djtwf& zp^ga6HN;_qImHhg8=Vsm(pho$87>0E7k3a}30t|egm#oQ5K7RF6BQ%pI6mQ^ksDk7vTwKv)s5#>0+;vZwV06rL#^95F-d+L z_aZ$u=XHrwO*D)trb-oOb?e`XDpwcL1MwF1-)pj9D)O(H6H=^Vlad~FMO0VF}HmMB^e2kOuYBq(otva1C9xCOYjwcivaTcw_a`8+)Qy{&A(wZgMcD8JYY(50#;QsgeN> zaJ1IvBfMCd8~8(L{;zGr6siZ zyNVXwT$fL3P6Q(Y?eC34(us|1Baox@;6`<85lffwV+EJ1hD&-;utFrx3oZ3>nG8?8opmz+qmlps_yIhOeuT$IfPe zHd`XF>9Kn7YbynN8+}8Lj*$abm=W_w)VQ4Oq7S;QIXUP^x7BZtrQvo3j*af*A;^Ny zuy2qcfIF#n5qiDGxq$U2qZU!ND5KZ6utue`M0&x;G$W+Z`JKdie;bJU)1!wdF?14a zRQPo9D3}o1%x6{QAc>WG-0?2~M_~b5&s*x{ z(@l8-+`#aBzHFku9>TdJ9NbjFf6!wLKkibLe1^zmn=>c9ugEsZ9FE+?Tx9oCIKxi} zc2x3c$dCJmdRHZ*;Ybdwz!(gy21mQZ1+za9{@1+41&zvyRSH2z7;BU~C!2HA%JL;H zXd3QmyYjq=zFz_YKXhXiViP74tqDmScKpLJwDt}3H!%J6N+dDfjza<}NKs^sC%0b( zOK~$-baQ>I40mO;Kf)%W!M^&qy8 zNFv|oS>I64uhP(@8)lnqXfP4|R#KD(u#;-OPx-@9GDyDBE(`QcmuGE+X-2qEa=P1R zzO5U+uOhLmp){C5NkIf&Kf)%LrY~4Zo1bz#Sus?>Bsd$lbfHRR=Ewj}|H&}8 zio@*^%$bf|97Gk?K+Vb`EsPrOQ^;tywIf9j(z7Eh$Uk<~Cf$P{%nd3rQu1L+34SaN zH(4%}8jhhxHO7H~5~UM1{Z1>9GqOI`?aZrp$?6u639=r<0??Ofj+>nl*<0_m;jW9e z36^DNVqNUf!-=RAhw;!whbJ!XQD9c=1lAP2Y9?skIRetzN>Hhs5#XDiqTlMjQlqWT zFU5iF$AHK+uv-LQB#ubC!)=91tK#55Y~ah_aDM{Pl*?cRmQ@ie&ctI`eW(oyE$nXF z4|9~sG=|rIz+uJN%76H0yGrFV5M~%A?FCi(dJHtA?tGwtm{siHU@SWOhTyMmQ?0CN zzOWj!p7n~Z3kF^?ld^A2sr>|v`y7wyZ#CUF4mBK=1`Q{lf+58f8_r3J&B-@e^S%QS z05xotVNoN`=cfZB?aXEge+B+yf2|Go%xENX=0crBm$_3fT3H zXkaqwE^RQxlyZ#G*fH(2(3fD)n!YGPr>SQdf>eSOEju~@@YBE*6waB1xNxP-Y8NUb z<^wq>%O|p_B;toI+T5-L za_mVmr>Q;ky6chev*U_hYeV{O+0;d!2HI!>OVkh*yffUPE92XZxuKsPT!?(u430<% zogEq{ZOt~M)XkDV{!$v!9n@;!%AKZxOT3e(upA4AY$#ltp_p|Q9Y(q9LtJGdNeEcg zOh28GuiC)oIC4ULDnYt~Ma;T1IF24shRBMO`-nG7p@HdD;C{cbJ={^i~?aBBNtZfQuZ9 zs%_MwH$;JyJEiX&`wEiKW`?B4n?S8xkHO5i<_i$ZXpOnv)(th^ZMu_LbsDiIj!2B# zs0EY;S9qMTX* z+9T@(C77SNp;nzWD)!xsE`?m@Yyy_v*dLk_pEJGwoNpf8^&T3X$1V-i!9fP*C5-mL zz*1qkv6s2{ry{#$QoS+=)1|I|S)Zx9!ugmC~zmx@s?3ygSg>~1=5-^TG zlLH=j7iTL;4@+{n2_ipMDS@w2EDmGmI6%4*%owkjQvTv8oz@00wKQ#V4^N9}ZIGul zB5K0g5cZQJVRl#6<*(7hK!C%jgF($sSxY6J8O>?cMZM^V z7R7-KP8Qzloy9yID&hM7oZUlm`}qk5-P`E-$WyvsfoAxb%@1yeRf;xlO3w2d_hvnt z%G{um<3%0#KQ@x;u%18V+`}rjVUs1pMCLPljEt`L*lLNArfA6Y@-mb};-Ug#3)8u^ zEjnsoem(2ISv4#eYp|9|e90RKeuJPvm3XWTcR}vX-~_Q?^%DN0sDF)I=j6PwF z@eVS%tZQJdAA+EE%prk=asFe}@RMam&HsXw{AyoVO4Dd6RHBDo@ttR2xKPNVGniWh zQecktQW@^DXfrnR-pUtf9`Bv*8jB9M@m%2eqyLGLDH+Bt=Sc$XshWNYvlnyv*v^n{? zG?e=$x@v8Xj&G|TvpZSd=}>>b65>C$x*<8ztS@%3LuO}&t9le!AAlK{BG~<33DCZqc4KHBA z*0p#rP%1hNkPG>K1@=~am)6h*nzqhvA>Tv2)_HtH(N)u~pZ`?Ht2o@_iWyA~{TkHO zBu}UKLkdbtNvp?<H4~6JI>TC>p54Dru?-Ng{{wp-PC@y*m5I863}D!;6naH%Fv)NSrS(#o;cB=H!tTMNr?)epA zBBNk~OUem4@`WhH+Rl!>*B5ca*E7gxupW7tPw@4O{bj;$qTp^bj0?W50INY*ArpVZ zQ_~8f+zpKUc{JV%xz*39R3rh0Vc_u z)(lDLF!~K;OOlZRF8m^iu6IdvEt|m_=T6%`tO%}#_FB{>RN#T?4Po%X>raR)Jie;r z^pG}cUi;9(ay<>*bDjm!^-B;kxQ)}I)pagU+R!VziNwg87Ag%Ed;xzyek?6ul{t#t$;ua}W+^CF!O~$5mb$jdcU_?sNGXhmJ zKkbJN{);C1ehsWIhFK;MhMFuT{%wxdo{ap38hLSKnwn#zk86B8IGV{=!pq=erzGhO z2HT5Acm0w?t27kL7+X?4?BKeTY1nh-G1g5Z){l?!&}g98(Lj8qH4LLYJO)|g>(=}@ zH2fGG!;biA27Xa=gC-# zs(`1ju`y8<=l~MHGH52qrlX_hR!H7V*D;8N71D1D)K6Ww-Gv!?`on_0(Iii&5~8;$ zlyLOT&++<>iQaCIa0COqn&2xNpv80dEOGjJstPwXa^zsm-~*k_k(3^T4-#Hv*i6?= z2RX77mGuA+;o{TX(0{V0oTkC;O$Szhz=9*#s83blo)--h(A94Vzd;-Vpp#|;qnSf} zr7oX6y@J=*xK0T^8zU4F2XS+13eF}&gHymeL+P%GW@O>_*0e^#S;bIrp3qkh)s~zN zeO?SD1vH)SPs^T+JX-`~3Tt^AYFMiWZ|Nnxp?+mGv!&De)8shb4BXzjw7JueBU5$i z+~X6#MuEUHVa>tVDhg+*@$oV+gvd+h71^qZKbzv;H39E=(j! zi*s3LR?^m13%|-qZy4YKC<1^a;ls73%g0k?Lcfs%7oN-uzt`?t#@)H%*!mOb^+cfXtfj=x!w?U6J+_ zJHmnkh6~j4^%foskkf>bi=#xTag2OHlkS3O#+%5BmXl@_4OvTlXHo8~=rD0wZ>y<3 z#EaK8(UYJEzZ_Kj%KcN%Y6zQVxKE|wUxH{hEpMtfXFP&PfpJ#p)8nbWI*Qf?6P;igpd`W z>mPahb2E5VFoUVi%A$=(A)%$xLSY|7T$g2O}A4lbPzaK7Hy`)lOyv0}gke zbIdS|w_rphX%C4>d#M+_rN#6Va7H0`ZVbA}kpmXO=+8n~!qpclrRR~-{V_@mBs1Mw z0qj?q9h6xkHzfO4XA8ftj117%2aOf90>NxVlyS3)G($y7Miq|!m&W0q)eB^T4arml z?XIDFfSHCl;-Ek(=>ueezv~GawWJd!f*bB z&mlAYxG<~+5u^z3KGgWFe=*iYfrSHs=7R-wq@hP_v>Spm2gU!XLVTqa2fO#OMN8nt z)yss0oA?Z@T;4h@fgtF~*OeJ(@;ZwfBJ=RRae<#g7V!O3OP-J!wf{?}l!ET_fXvW) z%PsDNRJtxB8l+*@oSWHUfCJk=0Ab^;eYn4gW_%L4z>hfsemuXMVhy(Jj#MrX>mhk= zHH(-bS36&IxR@pRVPq?cW%qYP7xr8u?%xCQIH~<+V+QOaJWYGZN zb%kWQLWBVsK|}5A_&B3aGo#zHPU*{*T%=o^hQ^08I$tVQ?rtdyMZFrR?o1xNBEjt^ zYsZtzt!dp`H_2x|^!J~>6Hg^#{8F)|5t%|>nJWDwKkg>r?%Tlm&XEeH%3GKJo3)xO=ABrnPKsXc%Q0(dr!3r7ihFWo$)W< z0j*@y)|@~7a9JNH^J}W_&BJ$IbT2a((L_?#~cOkTa5O3cmS@NLjC2WYeZ@B-9QsV5Y^1xBmN;n`2(* zQbvl5@f`o(DT5)@_YLoVUOD65;x>-ae* zlnyQ9pXfLFn?e_UpNv;^xV;W=v5oozRfA^gApf0Ve-+`vA;}y!&A2$7Y~Oz**kII- zAP3=hIsBOJ8|*8+2EJ)nZ=V4_DFL{otA+QCKRgdrVf^2+pTS0eHY2i+vf8_Kc&xJ# z^y1;}_mljF5w*L4c_e1s{&c5^RQ*w%^NV!XH#@A(&3Wx9?pUhg(+Cs1XeB>v2GZ;r(P<)So+y+;M_dEyMT7@S*9Ls9qbTn^72V*Z(8so6NX)4AFHd#hZb(K+!cq;TrPJyz zK8d4mcF6%6p3BAAQ{KC7s=S2gzQSL0RP1zJ7AWG2j)|47MHBEB=OApS=zDv}jgH{4L=$<*X%| ztD$R0527}1eM7FHyM<;kAf)%YuCy(H=yyc8t2Zab>Ze|o(er7n6xO$VAu}8~rmR7A zra#s#pNh{T z67*mppR_y)uUKt2*%3G~uDW1qLZrUEwGHz-S}5^5bBA>@>#YnDbJh8AsH0@A6oB#~ zBU4TRmOH{2*8$Fy0Zl>I86EJ=p5iM04ClT-0+2z%^BS*pR*A~}DI=w8O=BzFa*JHl z1Jfg0nm#2%lP4n0CNPT|!OB?2T*&9baChTMK&SiW!%Y$CMyw&fOH51Ka}}z3;J+v) z_?XK&o<~8x^nhifv6K1HObu|#0=^-fjTgm_GHhp>8;FBR=bM1_ z{s>BtN4|o6=oy-tSv}Ep4`hU322iZ~Xa}#%ScR$5SanC?UJ0G`v#Cj*gfFoTw_L!()dfD3L^EKDHXmc+Gc+V#U?Fr($vQP#M}+ zQ1ej6%fdnZ>Jg!Qy{H|9cb!x*wWC~;F@!02AKBrbuxa*MnAbvQ zXx`Eej1~=jgnE%~s>Jkr{VvwEeXa|9#L-me?IluubVInddRj-ff7zntRh%#70qPh@ z_p@uyXTZt$XKt91iP6r!&v(+^*EgU(l9e8kvK`&v51a&p*hhm;I`ZAzfFo`l3xJb( z2WoVyeIroMc<@~xUXp$8Ujt2gy}krQYK0=9n)hTMh8Q%N*R&y|+SS3sthQ+#2J6k_ zh4XH7_&IRrH}C2NnbE$&njIbl4m*KX?2x`I4bn738un-187>$auuHG|YgAdi(GluDf(m zrIk12j$3@P@j~tMEMdJpL+9Ay?Kbkys73dWL@VNZV;g;PV4YX?o#UFU0dZh9D(eS9 zzc_=e=HjJ=mkrdxtGA(j#YLlsVKQhxFQEJMu6!s|(oM;~U30r!(YWRdKNG}ilR7Dl zo+ZzUOqYHpJyi_lbzo% z%%y$+-Ml%kwc*Q+I-TxC-!Kek=2;R+1~q_?Y`g2uu}2M=JGk`KNH*4khwTj&aX$b$ zCTuzmw0=$YuG(-93ho;RT)4=cU3s+84K&1{y$V+bcJI~(RI_>#D@w9{;F4QsGj|;c z_l&$t5UwasT&U9{GvodcU>&<$%Vx)t_y-UGXI8cYwgZOu=JAp+6>+Ww1}XTIRi`|Q z^9P!--i=|P$FE$Qt2li5aSh4v6`rBf)7o;nkcRUcfh4nr+}}eFH0r`( z-^|dN{f#wph@UJ15Xa?jKtpUiEI%H8mp*|+q+l&(d4GJYZN99dzrKUCjc=yM;QmAx z%4kEi7zS6Tyql7c&xq@781yCzcLW+v_FKDdNx)9}Uz~649v2NIlt9mvv=XJ)UW$nC{A_Nb%b~Wox~X_U;dnG(L7%k0?zJa+w)AT5l1h zaMEJW-C@lpy2{&I9~i=J+HPjuhNUwhj=1RYQ7b)HhkIN!Pe3z{paHK0)#2DUB_?`_ z4TJ;s=v}mQt#hhjjl^;^g00L?=K}2s(Vu^rD&3!e6EGs%#4=@*JsLlrw8aajT0&lM z@6B!@PcSa)=*OQuvvP2-7VIXtzM^QNOEwgtJ=z;?&rqSQ(8+eW1#SJdGH!pH^@M}mJ)&L_& z?R^p5zS5#OO!^k|o-D8>Go0?L|ldAKI1=G*cZ@#};D6&z3Cy{dUm zxOtM1{53;Jl#a1+kWB<}VoE@9a@Wr-<@Juz83BVT$7@0a8S6_x4WW&3&~==;YhN&| z_ebD&Tn&8#fy;fTva!IygZSp3%DR`ZSfVrob=S`8^ef zufS+}NS3%L{mcWlv0HaBZP6}B4>ghdqd1JjCT?s_&6tBAidC82J~`2;o@7O^&s*bg ze+Cc>9D_}PI<4q5qgoPdHjKSNCmx%^iqsG2A<7m~DAx_XDXYc`8d;zU&RE~P=~!8BA)&kT4)*;@ile>aqUX)zz=np> zpiZkIiv!&tqPyQTO1uT!!0St8`1%(1%7KB1g#o}_&T^JnaA=}}5*qhaR4bL>GqsZk z@Cu75^X}NhR=WoF4Ua0;_7xV*>+20`#PYK{LSoyvy9WNbWuu&G1eG+5UVxMeCx#5I zqHZEatK*E39{tmcEBgKnbczhbc4a^v4>G8tqMs(}k(dVEaH9c=&rI%KMXN<3P}kh= z!0$Si4OWNtAPx1YG~AP-6{**lC}!GuFQbK?aA^Mtm{PmU;d+O|6n<6=Skwq1Xvt|# zi6;8gpsMVL-)V`$N%}i|LcGLw^H0K(rh01IYaEop>QB+lx={4$Bg5FKn_i&Kn1IF#+w&ZDH7=^-HZNE=zfO`Gh|P!K!lZz|PL$?u_A*DsNd^{gKw)&hN$aUOgCw*M+_np^#2Crqiq$8>n@B1Y+YP zUx@L}z*~Px9BjLTkp0bOjeqJ>YIOUTKN|dBf5vCHJ|{n7ofIeW;UMZb3%r=PdqBU0 zGMGlczU>~q9Mb8td&t2rXWbi;(vD)Q6hl+4-2*CqE(##IT7w?ar<==EuheL&y=G%< zGwr2L+`k6nmQ(1Mq;)&tU=MP##~5xUk4+ZJQViBlva_1y5g;r91unh?m)}7>;_yX$ zDivS$L0t!UMNI6-*WL2e0Tz=Lg#RTv>bFW! z@1Bd|OtXgXEh&>hqz$r6(GZ6${<}S6GibZlRpycT8=gLhOwb*?m!XiyLjW~in}~W} zKkS<;9l7Gms>zl{l0Sw@fxhx0Oj*TNTT7M)Q?B(Ru6XYPti;b@7{P&3dZt$-Yr7ki zheRidcKfSXM+74k^WWUfoa#Oz!eITgvz24r*--=~s5a|lINStnW&wUAt{V~Z^E_R; zJ%drX^9ZL^rc$*4t+gN`a-Y-1Fs2F`1-K?GK*%c%vDRghra@%n5|`-{_r_uCV0a;i~MD!;g+u5n-+d1EO^uo2T+~`B&TAPBE zc#TkFX8scG6dX(@LEjk2<_NN5nj`_PkyV?Fq5%YZ(K8~t`Mrtwa!KQ+Ftpfv052a- z4_e2QVbk2|=Dh)8;I*y*tKVb+!TlW}c91L7v$A7Me17!PkFurqFPpUR_VFAJ+B_DK zFOgx+7(TOn`7t#o7Snse*Ty@M_5Kn#E2#qaNHAg{2(F0fK7rlcS~DYHWA`y^h1RhQ z<8l29f*}vX&aWf~NYh>sB*MSchI??d$=l!)KDp8S_=+PwZg3G_ztKjVgu>78#vX_y z$9hwa77C4UY7fNy*l%)vOn__cUKuv5gq~gUGD6+BOV6S3MLOvY6Uim=zTg6rgjiUY zX4*D0w$5lG7zDD>q)xw;hW0z;M^ZXCC(kgpa6)iezk(e0jPIN=(p?n-)zl!}rW__Y zM5>2Vg&B6R^(7)TeCBnxyHA>~g{!8EpWt=NBaEKVFA?Qn+|RR~wSC#7v41)74iXWo7dKl}m-SGpZ$QCb5!T&lYH<Y(i$_ zb)7#U$2tSn^`>Y2dI)5Dq{z^Iv03{u!%4nO62WNVQTYTZ@E1$#p1|aj&ByBKxZ@^0 zN7sZ&Ce4VHpU3Fv+as)B!iZrXKOouxplK-v3>&Rfg#O0%qedW0hFTBGAZ0o!9Bjmr z#qkugbJA-5QXOuef?R$UZbREPmB8DB|3MNy+BFak=pKUxf)OkG;B~02pAMr17Z0EI zNlGjX3=^cEio@M5F5Rt0j)qFw)njW6?A+e`B@i0u&E%uKft6bSZX57eRjss~%MFhQCI*gHMk|rr zSuW_Ey%D(n^=BixKO*<+Fr>Ha+cL&;l%43Xo+`uLC9QFDhV{?-S5=O3Y2Da8HxFmK zH7?-#S6x_t1RbV(!&W~yR_+xOjfdv`@w|5U3XLA27Az_r`7K1#aHc;yX=F5)MOsl? ztvmwakkv`@dqPgZ>w89=gw8%=$n$GmxIe@xJcxBP`wy6HU7S<6@Mt1Ux2fT^$Ob;5 z!-fLN4}s7zUDF7(gPo9#B48HsS{ClMR6abMN}j;jf66eY66^roMCPX2_C*1?hD3_> zZ%W&Mu!d?YqZOR7L3q!YoftKJ;vck%WUSnhXF-p7?ARs|Mv9+tpqmo42ci_ z0IoYVXsQuO$*6Dp!%x@Z^exDQ&T5o>-n_}tikVeTYR>~xJCrWWW zqyswo2JhuJ`5bmt7ao49fScDbvGk=(@s2@p1Dd#JBuYDP$5traxiGX;fJ9x~y zyR4)ocbA8&Z7>dL-4uz3gL81en=0B(#L4_P4iYjIL#0Q4#@9p059II}GXlpNw}I17 zROnuckh)k$fy2XtNPn|x1$j4Y>Cw-Ly$d;^vD04o-xN#Z&R43I7J{sJg2r^n%o(g< z5{h`&TTB>His2M2EQZ(xrg>o6_r8-aIjaPSZH74?i^J^!irEExWYc$3rfsn-8D5=# z3>?Ih9>u|L9cq4EA3o{PB_aX!0;l~UkkE)vM^4Z4T6)|?Q`O{!R#mgITr`0y#!SQ0 zTxv|b34_n7lcYeMCODEe*hJ>5K;cU~ zBC?tJ_;YQz`_%OxS{rzG82$^M6d^*;%FwGgw>F`k&gM^IrVhf#sv;HcN#*JqI^RB* zwDhcIopb_8 z`K4z)HeQMM8ZkrwyHCZrUL$NR>edO+zOHiwAmjSV2wcLH*kjWLZds8xHwUiq$N9Q( zxIYBT0(6tZyauMMSlUd&3_5e_?f~|@AJ+}H%7aaQZ={Qm6naG3>Qx;CnKOBLtqymO zvO7L%F}vt4u}sknQK=62RJo1F<~%0j7Yv&Oa^Do0BdlrNc7K`TEL(F)q6O({T83D#(x6YD=$N{Dc;>@6a@0-tew2ok@`!@O-A zkj_*0R4B3XIuUK;DuB%2Hz0?x+v&li8ix$)^L6cA=W}s3NjJND2aKH~{rsmuUghDQ z9c@ak0|;?M9P@t{eX4 zlFmIM?jITlslR;^KzQO1#J-x4w8QC&I0=SX{y4iO?zuS3-+ESo-&91;1h1-$ny!h` zCgnzP2(cTP-LAYE_Z3Im?SeaTl~Zrojk|d+4tJY$F@WIEY|2|6aaUX&PgZEtnZj`m z{MEJ+zE^=S(nk@q0Q!dYTL{<8P50e-stx__k(QOu<&#H7|JgZWJtDVGm!C$taHOg> z*1vg@CbGV$S+T>pbB%$H)JLSG%o#b{pKHVY8GxLD>8Z`1Z3W}zfJQ8EcD*7#e18Jj z!&AW$xo5_j$UQRaHP)So3 zwF4Wt;>J{39=Eie_l30532!7hJY$vmS8lXgR)hf#1BuVW9Stkzf@g47_*kTiS-Hwb4)}Vh3!-&o&wk;)nZ$ui4y@tw>~^f z!JRQ*1(Z>OikciM35h%2W25`sn#x7jHAIhiR|M;_v|U;E@vB-95STgmeY6 zwF$=Kf_C{FVuN1(QWow{fG{md9n?G@*A^4(6sXfSaFHfR=h7%ub*AWf6J7kkp9^kX zQh@|{!=EfI9}n2CvT%O{JctybM*T5Uk`)}G>ZOFn7n;Y< zDcH$<@L>%5`y#p@-w^_l8(pRO9ebA7J(Qr*v~nv9G8#MiHbB;`5C|{L-HId0uq(FL z=NqrPAOAw`9pTA(;dNJ9B^WNuo*Oxc09CI;nX^$XD|3+-uF}_A$opO#)N`acT{Q}? znP6sH3X6ZIqaHy^Zb4bAaDdm124!LYY#M0UQmDUreW?rg??AzYQ@wWK_NWVB;E9m= zakB1J+QWe14<7k->?DiQx&$|qYWkgXw`=R$V%9d1 zXbrSWSw+f=T=M#Ve@i8=ig3@1*8UFcPtYevbIm&rcr-c#G*Z_7O?~9&weAP&R|m#p zvX3o+Gox?WitZ$6P|f5EinXWT5e|&^>46jGqKSK;d%H|oUmezgwxgyB3snTo46*2) z5(^TeXoJ(vn}`ILKuq+5dJ$vxKK|aQEY~rqAVLdt+xQw!HW*A2z_rv3jUh*+D=MnzZffi=LQ;y{)+V zE|H&>v`slIekus*|A(|!UkU=<{GiF_5K4vD(ImPboxytlA`tk_t6htpS>zi& zi2$HLU%x6c!|$Pgg8zIwknN6xqiKCE@-n$5??F-xs!{|HKGlSKShP_s`C2mcVIVr~ zV%e}g*Rkbd__RQ9L2bc#1zrBM-N$tsD#76fnI@#X)`XiQc^we2^JCcXW-Q1OMV|`nvR^xX%vx@%M{sIl;7ct%_-S$|F2wO7z5C_?nDSGaqk^$?s9%pHD);ZV)x)&-ivV;7}CwnOK< z94EHuyh=2`o#Kk)W@p&JKI5@6+>)YoQSo7CiPYT~WY|c24Y6zX6KQcvTE`GwoXy;c z9T-EbjV&i3zhu7!L(>=!GlhDr47bmK?}~=IF$pmM<3vH6!1zv5(kqq|;RF)lPN-QBS2o z$1wx1hJT9hAu>p&1km)v%jo}1ik9#ATpCUTpn;;05?x`ZNJQ^@x?l8C6eVHp_jLbL z8GbqQ6Nh@pk{GAd9+7WEeT&0n4R&0hImW#ZcXA|Wudm?DO=RPx&62^&nT$QWycin# zmodF;)?N?bOc?>+wJ;O|06xND(hK-);($$XP=nbZsgY1*AHnc?zJj+YQu)r+!CrcH zjUfE`%=TfPIn~jIBf6DaV8B>k43c4h_UF1+A7TC-A7F+t&sti`>lo?FmdziVuN4C> zgpVp?szrAp-Y!QnB2=LLtOVD~kilyqhKk_t$oVqBlVi0`g`-cU;ckj%hcRs$S}5kL zVh^-MBf`UT4ZcrIvV_aPbpx4!76wfze~)a};503f4<@cto!)rdF!cXqMMEA4brLu# zgxK9723kmJL;kaVE)c-inIIXPxgI$n)qMmNKV73FZfo4Z^D9v4`!VGEL1~yVN@)lH zN~kq3O()Z&%GM`^&VRj|>&G(9X+9{LkpLd-!|Z}~Y*+GQbtvEYK9yu^P5~M+j3q5L zKb;TMjo$mwm7#S0XQ>>jLtd7B8M9aA3feQSWNz$JuFdUj~Hr_}4_*%0?Oaolt zJw2-*YOLN$43}0(>(zRW1LP+dl=Eq6GQ=)fqdb?#oOr0FG z|0UYz>q{8HopNT;yzH3TFdQTCc4$Stj-)DrZ~bi6>$jeNtW$Cb=pf)AI))M*1FNG) z%3i;xG;{okEds9*S)i_9sSF`Yq8R3J1z~zPts4P3>FZxrn5g(UG=UsZ7{qf^-s)>o z^!*taM0T?hTy-@&dh4;_3-0Mg?0r6kf!F1Pk+3dPT-+I3@uaE5S%^QBYZg<7x59A# zax;X{(ftBgCvL{nn<3BjPvQ_SSVvO|Wx?!mET7iclcVUx!;bFp|0Y}gdI+Gk z%KnwX$k?EvE@4rs;awmxe&@%7^|X&q?3TfGBipKx{+*&uXL*S;P)3is)&Au`4YXE+ zGnS=YI#IG+c0kGWul1c2osCLGw@XGagr)Eq)!4XS(Qn@16&B!hL`R8-k^{*sOsPEf4d=gh4E;6MN2EmU z>!<>Vd@=(q@csg0^+%ed8~&%KxB77YUNT6VG5C$qb{qz*DfW`#yw!(8n8;Y4efGdr{ANZxk{NR#ktS$g$?45NosS5O{B#yd(V|k@2RNyM<$OGTRyAc z?Y%X^qQ-m8B1NCmTfhwop(p~<)RgZ5dkx?a%SpqwPS#zB*?Q<^ zJqxB`NK<{$S9{uLHI17#e=K|=#F2vJH+9Wzs})Gi3?>28X~VLb)cQ95oT9E~Rq zzg@6Xhi_#%#-nJpeM2PQ<%KULj-~a==16=T%oY*KLEtiV4Ior3 z7;=$){+HOd+aokgUHI_p)NgmTXj!s@mS#F9=Tr*=Xp-fTUAS>5r*(d=Uu{wN{3rHJ zK5h*}cYiDn^{yFh2C@S~;^u?4hjD#2zjm4{?^1q;W)H>gsD~LY9)|1Sw<7XQ;btT( z(fx7a#LVikKHMGxcaO zW@TF8=Dw>P%wAATuFoIgX17#&WiD5N!XJ>FkNx!iwr;pvp+^)}?u>f{Eo_m04njGj z3CsZqLU*vP$kjTTuw@1dr<_KIQ-1TxlU#m1SBJX~IqeP4Fb$^Yb}K&{ZOIiB3sv0% zgd;ieUH8+x`nS>Z37YX>DS#b+I^ShxuWJvl*)ya2{sj1**=wi`q(&JPRDJ)Nh7UUG zx_96OX$)-aVMyNUEjr3`2?*&A2iCRgG>haVz~S}?ouU|_pvMv2x}~Zh3K@nnpc`si zqsW&)M0N*Ok~vM+Z_*2OJN}HdoVPlRQO5CXcR;LQZN{4mbqCSJ3Sjx z&1FQ^|2R>zBN)Wx(y;Y3hYCV_vQ|!Z>#x<}ZiQCmg=w+!av~pR$CRBuGS@V_xnb9L z-ffoypn$p}i?t!4Ra?;ZP{{%5`Ir2L`}@!x6esyjh~y46Q#A-i!)C_Y!wAV90Z3tX z0_P2$aFzG!NQ zbo7%d46;Miip(ZYr7oQSBA~g=55=MVYa1Oh;HToiVLb-@9jpunaLMaK`Yl=OB~I85@$L0YS4Mz9g~ZDh?>w$WmUf&52m(5C;9p?6#dX38DRE z<8b>BMi98fGdq$kReJ&Qq+$muYkKcIa+8w98XB_8?C8sjtV&jpehV%OoUD#VyIb$~ zIM6^vo1N=B&a-sz2E{7MW}E!*H$nvWxDw z)`6t}GJs;M%1GC0rD(TAnz`(|^O`H!VK89qeGX`9TGBm?M(Fslh!}aUzoOR1>d^0m z?zRt6X54Jq#IT-+^=dj}XBnL?f|4ksen9SD-`ar<7rU-lzH-%QDieQ`dcH5CzY?Pv zXyLVWh6)fl0zqL$BsJ3s6{YBwO@k#tbv-mZW@2E>qb!Q~T^n;<#HnL?mg{+ z&5Jf==y2{2IKK94>Cx14k1bOYgSu`inHY1%&PX`nx)(`Ob}zXF8zR3i9QrrB2CZ!< zl{n619=eX?HpWP$My|ATrPwvEV{)DVA0scc_$piR4s9G#x&wa-0-JYPER)8D` z5g@-qwA9_=0RIT|g6jI*E>1>JI3{t$1B6g-CQqD!XAZz#4Jo;Pm|tJSRUP`B;{<@4 z3duaHnd|D!8M%dZf`3T6f^wPT*yax*lqUz|5CZ6GBrI=qn&~ePrGEEAV^tY$9gGDC zOg-%j<$;%^nEe*F#zagtgd+sZxF1nQ2-k>cpW==%0DtXx2?g!{iiG2F0d}OMYzf_0*O|r7zawNgXnNeBRYC*vs@f}>pv#8<%)NXz0$m;<+ z_pk|eFYc~8l=r9pSRB5xqHC<9QG}%7RH@Y1flbc?Fs;CUw{}yF^9j1Ei@Tgh?wsEs zF`{|q8{wIec)j%v_a}hS!7Wkvgg}Q_OfuH13)<%rL06rJ5nry zSy~JgPp@#j+cWfOcW)$oa5nSFn9a1c#HX^Y!23OO zGVV{Hf%OoSjz)#^x$&dU)P!NW(kaED?ch0^BEq$SO&LK}4bF_6!xjWkmf|~n<7C{P z0eYzrgjQ@8ID$rKI1ifP5rv_rovK1^{EyM~h_x^|jY5i}mW*rVpzu%;-=o>Bk4I0# z+@D`Iuc>-xW87K)m~dBdPLXTSnYe+~V74~v-Z;6Bo}w}%yy3|S-T&!?t`51LbA2Mp z>ztsY3fTc%ZB8XQ`%q??-G8E-zPR1)UjW~A($WvN=j6XvT|`y3Y2kJgy*STH!phDE zMkcrh@N-q7!P(e(MW9WIb5Kpn?m8$76pp2_H1Zimcim?)SuK%3*ESP-(jVV7zx4Fq#5O-LAb)BDn?vbL)vY zIx}_@6D2Z|H=V&yCi-9mGU*u4_J;YDoB&>rB`B|uwh)fQ=9jMPFPSu1k57sOx3KlE z*}ilV(?BRsN6XmgFJ}C4Zi>=#+c59!)zP%lGg-^*otofqEGFx>Slmq_%q&6I8|+lZ z`m+)1q`s=>Y}hP*lv?7gG4{jh?~G(#&hVQpViXyLfI!?mKJ>t{S@q zW}GKp*o;yO?h$+3@#|30rRpbdn{wf(n>^ePRvCvc{%pN735Z{g|MffZoq(9Wof+x`*&vttEr zSRR24b_i63t*J!gpR1YQSnju`;cmk1Zaps>ZuRyjr-V1E%%B{jq>tjz^@Nf_iWZ=O zDnLgnxMsSF?GC+={FKiF{HhNB@~6cM;d~cxE^U>KLk+lH=VH*sB0Lm^agS*At>D+fkg%v^UAduBuE=F zW=;qbPGIrejRwK4NX&|HLc+n}1<5#MWoFNx{wMMED z^hDMld@I?2%KagTYD$3Qh!?^N(^*3rk!|v1$dsr_ITnyFo9OXngr=TyGcZtPSQgF} z>D?*Xaq4FwHR+Sw4bG-ca*)=gu4y@osKzU~0mcbTZh;|>i|G49xGFT7O$RP9cZ#yn z5GU(Uk{y}j8A|-0&}*!V$$DoFmJRE>=MS5kd76ccAiH|oG>osPXudWx&(LM(>k5k` zsB=UjfPpKt3lnn;9Kj_sWFMf7*qWL|6CT_Uum$t=*EMBVh5Mhc^OwJU0i#z%#z@J} zurhfgPRwZH*7yYA=rQzX#Ln@6ls3BUY4%@So0ajXjep^B-yWe3$=?TKkE-}65Dl5% zskyUZ0-XCjXm|~vyk#fN1a-A~v01i!l`_)bU2o%Vzzt2AQw&oOtY~S+L^G0#<0x2S z+!ZA6b+Q)=RJVkj*6XsSvl5d4X-?y%CX9c%pc!tS=|!Q06@O$2EvqQQ0#FKGK6s4I zUce2`zoHs~{_QvInn4LRbs6_y@iwyAg6nwNw>7v)>w{lUyACd<+4B-8@kZv& zTWv2`y{|99-@Gi=1VJm2KDH=>zN6&!=bRDdkbcL&LMn;z|6q#@3k&*HB zF_^mv4ZW)u3(8NAeAXuHO3^wdDnoG5+O<=7T85 zU&cvb2iMom*S{|0jmd&KjMy_&+YNnh?FGAfe+y!g9-1O5vvf~x?iznWz8ZON=}&=T z#4t|U=XHHsH%f=IG1eN1z%xzp_p;5$9k&VW&-WD#@VHPZB6zQ#MSi zsaQX^W1Y^cZ^m^X6(l$r#f5Yd)?sh3 zE(YsTMb`qWw6U}6it~2wf+&N_vNWxiD4+426y2Q-^H0WXqJV3B(i+YgG|*DF&7x-N z0KnA->l47>5U5g}1_Uhw&1n_!{yI@?S_9R9q+H(- zwuW^Z7#)hp`rolYh){~CXzrmSy&KgqHu+7a;#@`2`=Np617*5S;a+eyv`Aq9~KnvO^7+t;TcFbWSZlE=y)2j5KDRF5EM8A@9}IZ1{cn9@mW z9z;l#g29h+ZA}d)*M-gEv>DfxOM!xJ)L!)(yvg4Y*e)&>1L&z&8QU z@X+g?(9^sRPPYD%2Ri;`i`HoVY;Ry#xVlD2h+J)`4_f2Tjmt2hxhPC_5cg6Z@_*7A z;Op8ey0}4x{$ivsl};X+*OANyzYE&FWlApci?NNU5Ue( zJ9lLHV5dW<`h(UxrOTL&R!&^%A=VB?{Q4*CWpX^iFh+`FBR{&LN4e87M#QfUzaCm9 zUD3|ync|40t>rRh+hqoxe_aDQvw0)l&DYtW2!I2_{OR%9JKP>Y-x&qKfiMQIR&`=S zu~c#o6;wao(s`b5l+qH zen3=`OD8SLLHx#Qu6 z%9#^Gc8qo>K4>ysPQ+ZRgc)x_dmP3FB)Jy4*S_IqN1aij!8b9I?D`KWbf=4k;N>z< z3VG6LL{K7q63mBPY}b0)6)0Bm>0l;C%`~P zN|O~o%_Ip%l^TufgimwyPv&6JHpfTMKN)Ag%fi<)B!Go+Ps&Z=KHmr|AJ$rAxu_=G zyo}fftnyHNH!H&(fQRA<_7~n&j2g4wkWvGw|4JXm7Ck zyNCD1=fY5aub~;WC#@{{Vigx@s71?cdpdXeCE6?$Y+TLQ#b67Hl)dA65Bbj*AF96l z-tM2mjD_JI?dcaoI-9fPTlAPw88&FH5Rfs6JX@XX`d54`RZwQ%0%FTldo2s+eWqm2 z3R5%Q;MhQy*!R*DdsSUW=Ddd666A75JI%;@-K^=OO_$yQbKN8u*L1HNkYR|NT;~8ZYOY5*lH*0-QpK*S8prx#{AgD6${0@lbD%0LQ(YHTch|jJK;z*A)6Spqat`sDEn^-Z{uIt(1w?%myr^_Ba~Ta=Bh|| ziJ^Hj_6h*p<{|eE%(#F28?4fg#;nT4)V{EMVRv5V&?MuBIV?MHF6epg zM^N>c{Lw!uSI0dUn&Eaey;#aOEraM$b#Alng>KoQ%;!{lBS~r?MmY!)8IC>Y&R3Ju zR&6`M<-IixcT+a&z96B`xbO1GRbC2c6#Rv_&X0XlE)H{=tB!SZTc7x3lySzJcZq}e z@(g7x`DFy42)@-vH0GJ#Gfvtbeda!k%tvAwrfQa;9u%3Rf z!C-wPW{fX0DyPCe8Kj_YNWEw|XYDRlLmWYRkS@W&*?1_LTa827`spiHiN5bLMbAf; z()|%mQ$URe;PpEkdh&nc4eerK5lfwY`#ZYmAfn>Yl(Q0C2Xkp6lvO4~s1kSPep!tl zC9JoH=v7{dTtOa3XTFTWsvp2=Hz5LYFa%rjE+2B zAKHmfH*Srx6V?xL46)$w!I>li#rv4=K1pxqi!1u;OJIJZ;YdNi5fOtK!ogVuw@Y`v z;#;T&t3*uE|jz2n@IfkGrThx3$mWFnhuQJNueqw!R(+j}J_b?e)piC+6*ums)7E?F^ zl@qW9F`x)FyW!BTJUrNhf+VjxZX(ETiw1X#OPbU1m!)@gGA{cMXtK=7!SG%k%~oGI4}4 z7bjK^6ecq#{*7H205NTD{PQ}x-7&2B!ex{ercazadWFtdpxF^~lW^w~Az!Ov0vN=~ zTCoTAwBs*jefPLJvIiVbmEm9M(A=BK`JD511UaI~E8)hN1HAPaDy5y|^Eatd`;WJw zMi+Illx!X5wTw16d8Uh{#Pv8c(;^D)Bx&k2lpOu%&H&~Ao4Q80+B{c=FK7Cp>&@G5 z7mBKOnSQpKT`rk)hn!_$nq(vQj1xlM-xNjb3?W2nQ&IU#%-gtIqUF}YQeuLmsYX18 z`I{l}=G`i3FT+`*t+H@1<8K^?Cq@9qvY!M{X4CA(aFT!UV&@&UE~6Y|dg)4qDqLzZ9w_#LThg629jW|f6WxD-_DB;hyoL2B``u8?H)!Y#CFPrs zj5iMw)VrO30Gzt@VZew@ZT_)DyNYgs?M{!Zz57FGZ^d}XeIs+kpQs|Bv;@l`Q)2p= zFCD9_xkBjSs)}`WU#S~;o_{4oOVEGbIeaBV2SIeh8oUtD1rLR+WJS4J-n#l6L2C!= z?*T}s)3uD2B03>Q+aR7t5bOEaHT)|en&SAZV?Cnq75?Z|s7P&Jp8#Lp{?h~C8dgM( z-{;zJ&xhtj^G`!d3G+G#&3dmlN=YP3x;D{5?HPVmth{8wW?>3x*fxdrF_cP4kH~Xr zxPJ$AHx2&kf|y6UST-=i@Vx!%UO%Y-nW z+Z|73XuD~^A3WVa?l7N?iRg-#2C$ITUUrEoY@o!}aC%MSXS2R&jI?iE!`&0jrHi(g z{*0HP;SvDm2nI>CHDjd<$LofsgQuWrM{C7+5aU6BDz;R~XjWnK(86B|#62K7UrPs> zaCT0dEzFm9dIN5&G_P_0X+YjNQ5P9Ty9yhsQoh`em^KAQlnLp}r|K}iGiCPHfeop59KHl; z0i>Q!>3^;{qBLFKtdT%(rglP|5>pg=tvhr=R$R9h#c&EdoszkUYm z?_b~*=xpIt-b&Ws!7>2s{cZ1X_eIyVebIc{UC?I4HN$0s)9sHkUE##tAdz*qrS)4q z464EEB6FHJw*z2VDk^w-tPpooGKd!TT~1VV?8%xd#=RXm*!Okwh#82r2uGeQ9I{*S zipV*#%|6%`W%6#eA5pBgMxk@~!v79)GK;zTr z2aLDeOd&8 zrVW009V^{GgJAj|VD(t@vgre}3*J+2xcqXeO}#vT`qpt;fa+{axd=N6YH3PWZ3sRQ z6PRac#{FYZ>$u(;y=|Fg`pHLa5=Jwv_K-^(0R5y%uAq!y6}s+jSRu2fwU04c8l7Uo zXA9gtAo?t^F2QggZa{I9Z1GOKN3%z`ZFCM!*i8Ne6>ms)pDLKzKAR)1Rs6)1UH(uU z=D&-BKIKPSbW<-jMMWShZUn>ra3_el)+m8t#YXk2Vx9k~4fGCNuC!V;i8#l3)dt>) z!clUK`hxTd%b3>5;W#p({OtP&7w3~)@+0K~A zSBx+8(yN7=Bb)nL>Yt{;4ZYOG`nqnI$1mqubKB_8zUgKqy+IY1Iqt5i_cR5;6fgR^ zCiB0pPicUS=igPZd2B#8>G8%qz4i>>PjSR9TFjog*@Etx)u~8avoOSGk+9pq_y8qE zCLGwq#z2&LSc(#vTX1vV^FVJ;5R^R@=gF?cR+`%~$3dsc225#$EjcOmC-8$#bFva5 zl(ByH4H4czEAOZ9Rv2zi;8<0vVdf7x1%`7)%Y2oih4W?K>1-KPH$n&~h3dTi@9R@w zqO6Z`^SJ{6DvOTu_Ng%3KL+OcoCuvAuUfAYlczM><}1O~p zBAY%N2ZXO9R7+2lVcvbYjls704W;e$i)nHdn`mxw0kVm!QpSo)Deu6snf%dNG6(j) zTnxK5W}rl}(SOUr{FgI&^GBXD-%fLw8ed6d1y8aEe~egm4~@3SD<<-;m!4rBzugUy z-N64a2GiKnW_CkD#A=@7C1UmWzdrW7x(Z^70V8w#)Q?oD?LG8qw8>*#`1%SM=t}iE zcgmD^E1BW^!iB^tEr^WpoEh&hs5HNv$$^OfA^-bhe{<3| zh3#z^Y`GdJxu%d)KzZjAsXMpU7BMy#brAjg1&x2vD&3!g=~!?xC@}o+5Z`cV?b6cy zr>mz5I`hDfb~1)pCs)GrMA_Wznq3~`(9XZaH_Urja}O46174r`naUi?tZllHL0m|Y zOU9oM4|DxCCM>W%*0(Uwa2Ab5B~Io)fyb{x*7;>jrpnWbqc}ZT0XB5`8DA*o(|}=~ zIX2>)!-nrDO{hf7tY3=aC@R->A>!ZZvS`^1JQj!BBS703;T1VUy;_Iv*k&BuWI}jD zjCEY}Y(WPqKjz@;b@5C`zi=^FO$yGhti5@6N9TxtA-#c1*A${Q_BtB(O`wG%loTba z#i^S{iHdFrK~%@vA~4z>m?OKt!1iYPSAj@!RG+1+N%&3Yiqt8S+45B8rBmwuoP)LZ zORV;%KBV8-KDyr+P7A>Z7;3Qgz<4yX-_k|s_6L$Dp>Ket4ybe7b3UP?u>XW@3*KcVYC0>b*Za+r5# zGD;e%gSoUuYgrv46V&NB=hY_@RIf1UT0KvOIUFRA*anx1I1=e$hg*~R#@V|+0ezmA z8$wZG^vN7%&kjD-rT8CbbQK4Bt{YWH%?~z#d$Ye?`<4)H<{ZZ8ArELf z>;}5{%s8XXaC~rQ=-c5S*y3FEYF*99yIOb0LL1{4<5I8+Z+=}i%)29ZD8$<@WkiCq zNbVMfqW$&{&WG5+tm+rN*gBi}DA-dQ*F65%-?%F>Sjtb^hWRgBG?nosE!zC@Rha0X z)`uLiDP0#?xa92|5QZ|1kJFClHilw(Ow!0mCsH^h74QcMC`C|H##%(+f-HWRCN zJITQF4{dQA?2*!|FM8g6(M(h6^cBkDrI80+g-vLK*MYFptqwu@>+cO0>cnjV?+@Rp z5}gf;KyipWs)!TmkEP+B72N~lRpUVCWzbnXK*@yWhRtBz);spD(PBDh?wk@O z^HP4c4zp5E29xz=n0B9mSSV2d>U|QKJ zy7FxYeR~AO2I(~PbjbRmYi5!rrUSv+ndnGr*{b;vz^%V1SnqFv(^@WLr?HKq zW4RP3;hTq!751OU{xu$kTNPMBo#0At>t`bX%CSowo^YF8sDcD^X-Fm#V%7%~tK= zufm5)mj<~*gtCFpeZu(#~#jQ z32j`MJW7ehS8ia8Dw&0oJvg;uJqkO5rX_g>Xb&^J;=^Z8^!)Nfr^p)UHc#BSA_*KL7oHx6f8CvSP(ZDvnWenHw zDGfb;rZ$wgE0!@mHrKzy-gT}Y(RwMu^VZDg^}5(1CR%S=eQp@eyYKE$O4JzJX&yVd z4P~a}Z3-&a-MB5$Ey5Mn=YCe{>?q{ve#^%aP2xNnc!XV#W#Mj#K7!N9T~%C$Ni~Nt zOA>sh`LD;Zjq#&KN01Lg7}ui#P&r&NM4C?$bY=j^wd(m*75*6#z5dSqIn3u!H?l54 z2avsWhUpP!uD>HAA|ral9O=#PHvemhJ~lARs^GPqVMwQ8D^$H(yFlpadTR&T(TOd7 zUD(VXx%SV-J0;tNHs5N(`HP}-NKr}w_Y*|mK<#8m7}BKU10WTYOS1@~bM=#Ksrl5- zi~OnIGl_aMbMUDs)Vq2$yA?ZejG4{efhI8)z!J|?ju}hdn$7I(aeZ8*M6dtB5q%y? z#I)S`D;RE+y?7Wr_*4{bkHETD&K}=4N;$OL{FaCm3R?%$r1PQ}sf#!>={022=E+DC zBmJLEgHz4JlEycI!@EBEqUYTgosH9C$+AZu0?G@+0}mTSqTDDig_3L83D<7j#MsWS z17j*RjfqmU&|Ll50!8PL9_z!GEt-qS@W|)XB<_#xS=Vi?OE)=i`UJkUz{g>zXPg7p zw`Wp6F>FNqA3hCt9ULq#6Q%nzi2k5A;P21GUQ|G-@oLHIr9|QN3RSxcP>v)Qz{tRl z$dm2$n)K)J0{?B*@RbxD>7~{1P`VZU>Egw@gPo&mj_f-NG|0S=OV+>3D2F+(f%gXF z#wt3yigu>;>wvf_#LbW_b~bxY=a@ma*<|#pY(QR65LlNM49>y~N!!z^)PVB`!T^Zj z%27A~BfP<5_Qxdf^4vPq`^x1sy`cg?&5=WF2p@@MysduIgqLjXpo8>-s4u9jxFAPX z+sB8wu^DJX+N&vgewm_YFh8WeS35|kbRRwA|kFn~eb2XZ)bge#_2KYm4+M=F^N_SIq`P)BqrDN?G94Pjmp z-;im^)&{s)oqG&l$NE`<0P4acV{jX;=n{&CSLfUP2~1_<9JO1helH4l?W&nopUrh> zY{J)nN|0N@qfZPd0Ye5pSjh00fK%!*rCA@ve3mHPqpJN0vq0oC@Dmy4`w{bX8jCKL zanth^my_H;1ZNUGIR%?2Y>3uj>$v~3hQG?f-4fkL;KXCheL5IHoRAc$+u5rIiDgSE z#u;=&X@RET`C=S0Eio(Af(MU>Keg?3DX2E7e$l5xUPtZ?HSGiXz zS~S+i-!X^FTkp`mm2yc9v;@{eEsP-*^zy;VAm38ZSW6ESWwev)gZgtWcjL5c+>~E( zqUYTgJ+HnYa>~sjMKRVHBjx?-||4` zS5Pz}DHwM-473ynB4w*vzbFpOP1}Wx@BX{zX=x%k0d_sAs_Tq|Y zqKnEke9;9bL6KZXrRsddwsx(CK4#t5BNhF?3;|PP{f`Fe7F$g6 zI}c)qjCyb1?9{S%98Me^U>Nc`!}n> z5!fL>i%OJ46X646>pfGHhuuD&f!_w*vtEMrN~|jb34r0eG|z-9h7I`k&6By{VMG^19oV2|PYEGdA$d^Asd z4Q|UH;vR$x^fJsu<}Zi(RUy9es)v!v0W4u!-ymGq!~0XRQ2luNhV>5&1oyLn@vpBB zs_+J3+`C4+?pz$uBE)`PN8e4+SY(WSQz_Z?IPn(pi)%L3{YK3=*-Ni;v;yN*#@q43 zR&!JRF0fU)fh1Mp3U73=>1MX16OVhbVE z;*Z0@xad0GUq(0DA({*Uxi9vY0+hO53b$LppYhs@1bCI5IjRN2h&4<;CchrGq0};qXl{){_=JY zTyG)(&g1NHA3gpJ&ESguxR0hf6uRm`WVmIj?HrS>RDvzabyH3DTl+Bn6X0N2d5Pq` z8u*Iilka0*>^0;y++=Yv)?w<1=_yy*Ip|2BW+MR$wz*a;5!YY5Td z^25uh&1AbQ9Kt3pirH)(2G!+=%$J|OnYcd+!)d@DYs1|XeZaLH`_0n$n(|WaL=nw)8R*Dqdl2^$VP`&KWhZzpm*Q* z_MEj&6RdQQzBdRo!Grjtwc$g}kTzl*On%LOyFJCQb#$s|SiSjgcp{mZg~5T{P}_?3 zqb66ev#^^U$aMzL@+L}^tG=V9B$+FluJhVG{Kpy1Yv%}JMr10ZrPIro_8s&)bw|2n z{$?i^6!_E$ELDpU2*()Ad)o)@(Rj~&ucshSk;E~21hqkjq9Mtf#O?xx05=rT zHlb{3ggza@)`)V?vFLXtZ1+%wMa*|=^z9K^aC9pS_Z9_0FHokj>B>1cBN41xAdt}_ z;lay}Eg1BlM++jGvKhf4ZlC#YlBJtd9{X2HuEp8JGTMslW}2v>cf$T;@8pney7&g> z4K@5gTvLQuvF8W{8^boFe-?@K2Jo>eF=)Ps0`NkMz%#i76WAoD^W9Pg=emI?8aI9b712Zy5 zzA66TG8`1sMsbTZQsK&30=j4SzplKE>|sXiU>^>9_mAWdT|x(a8}_o7!GJQ1y_hvtE8I5?{}tv4Z>i zDI0joOvITwINkm0j2-}VK#RYz(i^<_EeulYrTtf}`(G z(9vnfd`bJ@G&ou^F7qXS8q(soNu7XHCI!4L8uDOiWTYc!`ZMHW?E;eHRKN5X0C#@~ zvL_&_)Igi&PVXa2KA>0_|QbTL(TTb>G4$^?tlKvQo6haz!?x) zS>CU+V4`v7A`iXs=wt*slnQMhBBGRG%K_P{cZ_&_ z7)vK2FO&)MvPh@j`mJ3Y(tWZM=2n zyoi#ekC`gS>>7*=D_q*wA2ZMC%Yejr zJ&4mvDHD6DAuY^s-xpQu_fzmHF(QWrQsfURVCo|1l4u%E$E}=7zB!~J71DM{js-Q= zf`GY~<5`I^vzXWFa5qS+6R?j9*b7j8!;J%)K?T3?)BFfX;qDhAhp@ra#NX0wPi17xP&~ewSBdL0GCi-UxGKW{~ zW;zfiQ|Q}s(9icHP?4s_>_IC>qC_)Ru|~KDhai`rZIEVdM#MT%rU7p^5vrO^8C0bx zZ2Yg~u{M0!qif&+@!Ts)<1*w|vWH7MJUXCG>m3}b+@ZWBf%-tl+MX>5EGPk!ar4lh zO2fahqY3fI&#G2V-PQfmv+-|(fl4A{+s6c#Bj|_v_De7Jr#7VDs#eVj;%6cj^NiVQ z)CQJrA9r*R$B;8Nw;lzGM~n3Jo!o3QYf5g_P54|I?%C1(QYs#SfG&nmp(_T0ow2bjs8uGm~Pyji;^gyP|AH7|0{6h2rZCrPx zTXPznK*vtB{uyDjD+l^FgpO$MNPbP zi*6Q~Z?=G8Y~Cv_>uOu)?C#E^*5W-KHR z!z#^99gDJ7X+|=gDHDx1gQOG&Y28g+ zk+?|>H$duhKa!=FbmfIxjezcEfG`f3iANPtB;15gk#b>v1% zChQ0q2~t2!h-AR8jbm3M7I!E6x8pb4=_(BQz85cgoE!<@E&L(Hh$&m)6WWT#b&y;^ z4nPwOdl}w&-lJ@*jvLOw%3^c}9# z7~v!lO=Xx}Jr#%qHG^5u^k>ag^QkW6e+9smw`kCaNL#msbx&`IH`WW-+!f{d**OeZ zJ~Vu-mzTnjpX?5B7f%Lw6{*^uC|MY|a?Fd*?2*sxcQ6)Rw+Dl|^zXyX0dm^<;(`11 z2-TVRI`$$&i)nI-Db}q1_34b+zwWgI$3Ad3m20iZ`sY1OdnHeM%`T^@9DJT9-BP0g zrJnLR_}F$~(3;CY@6<_EZwm*g2&<)NtZZ-`b*L5A_3!}(-KqwPwMJnmkA>mOARS6E z7ScT7X*eijNr8^=aoM!|ln;Bbdn96u>&!b?+N|QDEW;c;FDfk4#ufhdTo>*Sfi#WF z@o;*flP4Kh>oCG7JWl);D=>~9G*;`ZT!$%_HD_yQC_~N*xF>KR#7gP0F5L5aM{)%P zXPCYPA0hh^$4T5)%{Ff{(8P2&s!!MZLzcz*)W-VYo0FIe1snTRb`G!WhOb9J`kR0q z!at{b>0!>#=nJ?BFl1O~WQiA&c7?G4h8N`RowJ!m0Nak~d{IHAlf$--?uhezKxJQe9Lmy60%?{_XFxj}Ib3WU-CetZ^SX?> z+*M{+k=rJnVe8yNi?X1m-5|_pz>3 znOWd*7#-cBV?Z_pJ%%S`2=G)G?ha`|?CHw@$D)C$#&Gw9iAZd6tU8RqMKl1g{=`5z zAi~N$r82nv#8^Bw)aJEixOBn@4&CFo}^%%4N#&PATJ@w(O>XvS!*<{>;& zrc%Cwizy1($^G-H;k+%P<)$*$69Rh()a^`}k68XB?uz5s62d+VmtH4wr#iBm!3uY% z=H28BQgZWr^EurAbT7*J2sE$X?_PtVIX9hSO5~r{{Qw_E0knlX7lF(my14zsmLnV+ zTKu_c)u*EHFN1W|`}eYCIF0UQl(_zU0OL)ve9(YBkrC{-Xc=Betl#h8XV2i6EUD-A zkCDWxLz=<)MPfBEhT~)O$wA4{pHbWYNv@2DOt=$yFBu|(5BZRVt6bHU#Zc}goD$eToeT~4VXLT5MQU< zq<*Lo$f{RwyPjJc;)#}5a41Kj_mLEl%}8tBYQoKy%#INSkfs8iZt=kYxt^zt?s`vt z(*l1GL{!-`t%K|sj9fm$q-ka7sxgdyd#Ma}YqXGVzFJiI*xbSz%{Xd&(7cc`wv4t$ z%m<084T{e?2Oy%>^AEDZ2Aini^V+>V6^6Sjd2n0j&>{ISaTlM(FdGXoU|Izes@S4H z8NCAPVf_c!e;;QuA|VwRswF)ScCZf})T2h}`zfGkH4_(%)uAAQD)xpL<7Vj|CGP?A z)b&D?$1t_Ps95gE*6)q8J|8b$FENh=nVsSRm`6FOhGAQ&<8T z3$S%7EsB+T!0A4tB=^5$H@&Z*Z%?2`nU=vYt7;M*kqZK{hx5GHj0b>{!0tnx^FYpwCDAX=@b z0+w6JCFHBGA4IpQb;p}8`u?Y%F^se@c*zh(RK##cSoD;#bwuA*xTN3@!q@=Hg?WQO zG<^^o!b;0W!@>}vJk^GO+M=aLyl$X1LZXXtzPzeApiV*UUXVdP`Xpc)v|FIQAEV#y&oETMuqV}Gl_k`7 zhongSbxly9`W~a4sJpqHz?@voJBHZ8GG3Zk+UZ6tB+%t{_TVq&q1}@~r}hLYEboa?SiyeE24)%DL3*84*w#R(eV%wr@t zZcmh=Z*WSKrC3}U4m$W(?Z)Y`{-YF9I;FFJii2S+^Ga|?kHz8s4D@h)EVEnRjZ0Grb>NC-$3ScW5kIGye;4FRfA6`?(c`f?+#{n^A=Bz01*%H)T%Z>Y z21gnx;wkqEI(xC1b^U@n=DF3-9>kanyxjU#iWTQV(k$~>9Bz+*E`-LYapA~1L|J+i zQm2(e@w=xj&U@XW?Ra8~2HVkyXf;)Uu6MT$Ml5Q+dDbVjZ)(OSE9rlu79dcG3@^kX z;dn_q6f`Ibjh63RFC^O0>es}sfkpH*a$G&M$J0Y`_?JDJ!?(UA`v79aq@CqZuFs4A z5yK>hc@*;&0)Y0Lg8Hcq_rz#Z!`(p-+YV;T{3R>U{Jn$r4tws}popLUOu^-V_i(k= zm;%OT{)twS3s(NJX}EjT4>L%N8KfPeIp*@pga?ECP@0l<<9KpeIlJ!9R_0KQ5P`=7*?!ggZJb zgk2tW3m$8IaN1t!L;P7rjSiPKnjLSWL^MY%O_bA@j6F-E(go8e1- zt_*ias#~Ul9*EAxm5-HySECsSsuyizGYI{Yny@+yf>>%~YG_=pe;S8PI(g-0+#g|h zKV-|E!?^>4l_>yIT;6Hq#U@y6ZpJX}{Fv*ZSlz8BZ?u$$3!b?}Vqh&*C-vX0%T*cr zFP;*r2!Mi_A#*i`OP-RlVtN&`)tx6{0m6=u3_)C90$nv=tNJlEtp6sKQvSJ*h2ieU zWo(zmCl7RPZ5dbBKy_?yk zy{q@CL`Wot`5&VP(^giLWa9K|w$JxNz~62|yqKm1BQ5|X206`sK?^R|3R<*^e+}9{ zSbb!d$AA!y9ObB=p-W4&Jm|NE*nS{A2A6Fc#;?OAG2ib~M`5Le3?qL6MJXzt=#Bvp2U zD?^4T^Yk$1>1_*rzXtjtP9tr5umNZi^b9LIr5RzXmUjf+8AeBaE-f(8u`;b<<;XX0zG-sH+ zr9$@!neI=;;dTu)g9s7ay(A;lo982>T?KVAAnOifth}ovabnw1A=WoiU~rsi*S4Wz z@oMzme69}T?$e#5I<|JO9}7dv`oF5b;wL>qR~yFS zKe2be%EP~8(bKu@MHWpr#1AGlk;JQY+iE0+?0;EV-67-a@KPS`vS@lxbI$@(GIv&v zNj>+zLFVLfr3CAm--#L{>14gXM7m^M9)D77p>x@DLy^MyTpsSK=oBo5SVPlo8SRu9 z>I$1I)#UC zOK2j{a=oal%(1MZ+JT+X8XR!9VrOi0L7+LDk`WyFs%n_;Cfx;+pdsD$^7vCA0CrIM z_6H%+g$n)u%ccAN`}gT`Y?EV?G1-_E6tp~p?v?P>pq6jHOGZ;@+^aNB;WV0vA|)vF z6as6mA`l`&>eXhd@J+y=v{8v84 zyioC*weGbl%=^H=td#>!Q!>_g{G~^#hE}8GHO+9y#=EX6zJ!gkj>oCsiR*+oWvC15 z1>5|5zL$l&7TWw2w9vS06M2}YxHtH$909S*y>3}Y6{H9797HXosjMRt9wVaRk5|Sp z)VckIvwBZxmV%`#w+@d0)&TY=vv6l0RJ5GCaQ39J5Cw5|;&E36=F6aCReh=8FV_&H zpZz~b|5X$2A;n}fsiM%Ex|ASDZZ!jZVlmggG2Yr`m#)7taFw3+tJLfONH=X&le>2< z^!);OspQ!1s$fnuTo8bmg0+n{-4|sth1h0Y9fgRbz`Po~ob!1jC($bVaF6G_xBJh3 z;U-}<_!8?Qg=$qJm}Q++XkFJ_Ts3UD8?bqpwf|*mnE%Q3;Xy0<@=G#!jiEdO-@v@* z4$yFuwXPw9BCbQ1*M-0%5FWmQ9qhv#Yp~DnuKiOMZnpuQrL(_kd-h&ABEqL38cBcC zTUi)tAt$>>qLT5t8x(ln#K3c;deZT|WJTlbcynPDzKmompMN(S8^4|jO=`U+hIM`d zL8D0VBBqJ@7K$Oe!fgqq3+iE>p(cLMp>N;UARR76vd+XzHu8eCn#RjCgY+I(-kQw^ z&fyeAGV-Q=XopL$rc(wAY~BIbbWcg3_aEPxiVZ+PVl0lb1K$b@3qElEMmu+DB=eD4 z-7=(ILitv2BA#oc)Fw-kD>ARNpO+={`K5!FAccR?F(Z~Y(4-o?RgE%EO^q&HMPr=f zNIc+ztaEvNku?PsTayOub4~@Ap>%r`Lf^0Ag!{&xA>l#;P}nq;0wg_8(PEPkHjuDB z0Zt|0gf+4p83!AdaMIb*1@ot-3bDV_lWvzlK92VxY)5I*^AH}TQ={f;Wzy4~ z@pI($B_y-&ScA_WV{)%WgQr4JSI1LU^sg6!31I5pQ$y_9SqbKNsG-e<*t?ySN_SkO zDz9E)khXSe(3vC$#hzhodv3zt9|wl}71)&|6Mh&NV3MPOMz%t`P-4%}diYcvz{;xi zN8!UbL+>?ju|B=#O9ow^l0ff2UB09VL`MWeD9oo%8@gn4&v?emofCbeu=7XTaN^4i z=nV#y;sKlr`9ttY167`AtLO1QU?@EUN``Y_^wrma40dTiZX$bOOOP!pk>N14!+b0a z_t$XS?0GkiFe$0xR^}0e%^v*dV@u5rNd9njiNgWc8*C>qGB1}4>p6m(fyrKZHp%#Y z44~>t8g%!QC0K%vftO%-?0pDI30`wsEd;zx>T+ZT2+4$=Arv-6ann;(_|ibv@aCpN zA)SY250Z=#oO3%6)?CJ>%!0DfDX&RaFI<9Yl_)bZhXZR>rQ|bwfs*c5KqC(4uIA8a zv2&xjGc2QF(-78-lGR}KCGfaIwlJxK` zHn=!X5C2r4QYGTKUs2WL2tq5HJzG{7wVIP+vQh;UrNvbb zlD(avB?ERFaVX{FSF=uI6jqQtk3xEZf`ju4BXye``;oT=^!*Z=xyZ1tPa{2acnn@i z=WXF|a+r_ZU&r!v-P=sQ)~b-}?-8hMv>T^xq|@3n9j*hh&x zlg7w5Vx<-7~E_5lm zPZ>4GR#b%4t!$~=i$BKKQ-G)ZN!`rh-0B~_x$jp1#VLRnLs-w)GJs>~zDmE#Y1qLE4Rxi#4KcJgAKJAv9NHp=%2CJ_|ZG4 z>dztm@P;RST?C{n_q0jxBn|mACJv3pvDr~)xCWspk@Rz#BC^hgL}nYVVJ+M-cII1) zC{23Z$I@_5iS9abu)1Q?U5(3aVMUGKG<`gah;7Z#stGZK$^ay+jy!doFI~v~ku=O4 zlD9YW)vrsC2Z>-V$GR_U+nDBz=2hinba1HRaY_KbLszoS#ZZ2+k*Hc+Ud&arn1dPq?A#Kz@}@nrjQj~)7k$<25h7}6s(N$HA`1`!GT;kTq=1igM&Cyt7N z$cZr;Av6%DMA^LuZ)IaiV0$kshWkS}4CDClR z`Gb({>0rWob-EE(F!d;OJMSUUk|cWs=;$`m5eQUb0B84{i9>sjZOrv21j3Gyw+_JQ z@s5CU;DTrsNrA&U^?Ac^zW_8r=ucv|1Lv}7c?zB|wU$s+(feLN8>~?{e^527PJNwa z8Pm7Df&S5yk3ZLg`o&Z4ylbG@j9$o@>E=n=OtYtnF)9(+4XM? za)LJ|-tamy+@z;XrxI7toP(#oFvKR-hWeu6+0&1GCRsTk%2o)b6Oq<1oijWEVtTXZ z{ae)A*A-}ANG(fDQ$NQ4V`C5 z-Ofj9()}V>`opp7> zs)c3uoc&_0>M4sbsWdz?&aviXmdT}EDbc((u5y8+UNj9~&mqNGB3T^ZkeQ4P0q_!#kJ#9}$ZA8j@L++s*cYK(VGsm0AlbpZ0<*PW9dme?nR~6@RV+Gc&Z%RsAUsIV@jotX~8T=XX+cgz+jD*sBJF8QQ*|=7ClN zD7RCG9`3!6SWbyPX!bhz8H*VsSyVI_R%pIcOV4;xxuaUN7@T43iMWT2;1Aqf6~A{4 zccCntTkb~vn}?exqQ%!EIYPz!vlrov*q@;2=i+eFAhTU!)T!K!%mG|<#Ns9B;g!KM zw4C`06k-n0Gb-sy#wwm`D)Z0GP&7t3-qmimD`;BP@IvJjj`X0Fv_yCr2|i}(Q(+yi zLB?=VwTe?QslbEr6caQeH{hgZX>gVKC}+4|!Ib%p{{_fV1YmR>^-|~n=|R&FfXwiLKQZauW(ZN<;FeSeF{-1p2cwm!w%q4saVjkg0;%_W0FJ3 z!oKx2)T}QD7Q@AtoQ4?y%1}efQ)T#<4q5}}M>&H5Q^Orgp*j`b1zg2AkWJteqwNkW z(4L>`!d(R|SzZBjIk(%YIy7Zc)1Y)vcihFJHmO<0EX}--H8<>+(3qtFbL2WUVY;5) zW`?^Cx*l5x%?KMw(V$5wGFU!I(L3EshH7Ic0i}HXFW0qn45UK{ANi9fnucQP;8R_w z_eBH8&^0m)xesi6Ths+*gHRkcw4HV6)y9m-Lnw0iA;1Fj0|=qpwo*pL|5g|7mq6eE zR>K~^v@3uk4BrD!$08tQoV-_%HM_18!PTmp$2v$c#~6`=eD0IrP{&@vQ}37Q?Nu1= zI%oh?Y)2xbn{U9VH@?dOZAsghmeRHB0gQ*hg6njy_Uw3BD48x-me0^Ybtgo>EE>KP z$ZQ;$fFb5ud}h}9G)2;zW|3NP*Gu!qF`X0K`H{er!kd5-e1_IhU3&fJ+;G2)AhKs5 zaVX6>2g;VsngYsZwB_;k4RQlDuA4E|0ah@TTMvF+1SofR0~v@?*EA@-7l(TPGP5`c zdVh+;j7M=ol+;rwZ1wuip-dWA194(tj$H~d7Dq5rE_5$&uQdL`TfJREm+ynbCY{Cr zlhy!Io`m^Q-To=;+d>MgnLIh2tEHQ~sH=Lgu}qke1$#G}8J&VR`MiYw65;|}SS2B$ z^HrgR$c!ApP=fi=#$G}zM52MLROvd`TeEHmUf_y-Hbd#qzWG0nvsZQal0vs|dI~f{ z3Igx9;VyKnn=iS2N>)wG#@wu1Pg=zKT^U=>tuZ=d3%#+5Yda``Wj31mqa)i_iIP=iTd2a1%QvATcP)XCF zT0pks1=66e2c*P4mli8M${NmgKb?~!N3r&S#6(IoUuU>+19N})NqmY-Q?R(%L9b87 z<0~6!H3c=BAqiL{zwM#hcT|L$98#b`FPq_HSOCo$LOO^M#|GGKtq;Pz$$xBE>m_Zv ze?~;oVsis8h-T|W0e$=N11)QndteZ;0}9U_!!jJEX_(+9T?AUDSMvWT8mmQJtxP5> zWq ztFd!|e$|8#GVRy=&-)d0rcKN?C}vy>_(Mc@Rz2yC2o1(RFLLgh!!`gMM(EggXS@l3 zm53hGDpnJM;R_hsS8CKo|uE?LBBN*KMXLCYy zW?-LhWb#8%_?HBlBj-F`W(LU9tiYkhG&+=Zy&4i;9@svykdeHSR`2hj^qZaY%H6Mi zYc=rxJvI1k?T-DhAzM@0s-T5~A(zCzbu`cyHK|A387t`aRut~fp~-Wk%^g?rZdYlv zjSV-NwAbJ@y9`}(YhM-N%maXr?Hn7fJ-RtFoPI@*HKG67Gq7o4Al8tfqwT=$o;^dp zhK6!QPGXy}PI4lHSABrEOqP+s^jGpFH4kbARMt1j>RpG;kV0n)nhmDabCBAhtR^pm znIWD)MXUo|dA$~EW&;R?43W3WwFEnfg)y2{Lx`R=(6=klnJi58I2G9%R^6-VSci)z z<7Cdh{W*Z0SA#w|3MLUg>o;|#2tX}VZY}b7i|J0k4y3Cx+$GR6M|m@22pZ;FxZBsG0C9kAo^T6UwJ&XV!6YMoo~* zh&Bxp3SDI(^)lf?-cVi5yqzs0o`KaV{>hGizXEBm%5b{|Za=yrqwaPVQ7hrC*Ti#A zDZIN`t#Xygz?_(*H(X+IhlfNpZi0qkHuMW=^{&1JJ@Pj&{5HZrhnVtqPTHCE@fI`& zxOL{?O+xY;c-CY;#ef#zZaTM5f|JLS5%<~uH7@AZ@;ol!4`42DYOo+kjR{ zv75m3QW^TUJI4GYf@bB`{bW-}4avhg&&`}>YE_-N9zQC8yk3q@SKqY-3EnEp8ubiQ z+uEnvFuoTJ>dsv*i_Js4(2iniMYMHp7sTGo@Jo1SRk{PLI7hgTIXCh>HuJGyslDqN zZW82)K+jD>NYUB4f;1bSA2t>TupWnM=RY75kH!hlz!S6EdX#z?UtK6Md;K=8&} zy-T3Oc|6(ziPJ1FfCWitP-DwAo!h?3EhzqJ5SAytXNkNLt+LL`{25$$>ZmZ@xk+CF zXt|yk6!wkmk>^S8*cCu)2hM?Y0c)C&Vy;=TTyEmBjSA%_2dsu@OwDC_=d6ASpmnBL zN06z=FgbJrfp~SEO?uQ&7#zf)3|Xq+_JY`H0PsyMtC8L2xP+wD;9qg1@eQyWqBOMB ziw}hN7 z%AQ=3p$tr`5gc=zrM5kW}#`T7_UWgzE)u zs1Ja|IGaQec6N{ctxNL~$ct(d}24VB@M)^hD$DO>D@5$m!H2GgS& z1EzT>1A|GAY2%f%dW&O*@@gow*=KZUO-3N+y6e}|1kQ`ZpVUbZD>pzpDlo&O{`8O_ zM{G{o9lYz3$4Ka}YdGayxMoOUjj~D}M;@uX`d#oJKM4)FM9dpr^qO#EKQ&MxC>XCX zw>Wa2l7i)JvyXBf!c`{!~hqFDx}bAb7`3GMz`gs zl#96fyl1!zp-=R+CYUKGU}={`#rU*`O|PrrP~bw|SCHROLKF0acSqIKp1%=2GmJ(w z4S(4|-xHZxX6$0 z7~pgJ@#WCjBHS^K?VZGx(eKLNk!+&+qBN8i$35V_215_vE zn0-GV*u3nx`I`r9#nr7H^Rz;^Z@qG<)j?0;oPNCRq3@4j>xYl(xS@SgclZ=iijZ%L z^j0;1yASl>>uv4pje}8AwNLM#HLL{tpYaypla70O&WHe?u63D_&@BbpJ~e{QG?VGd z2Eg=ewMDo^uLB)(NB-aR9c*@L&3KBT0`FyLxLv^d^Ln^1Ynf~w3lKR*_o4SXli51k z^|o+h*O5HXp@XPm%g>Z+f&n!u>;45RiNkRIA|&kUknXZqp@Vr@WQeH2PJuzc^Pn}e z*%n$gOty#tT@L^dg-+o#(ZC87sy`AYgehOSN$E=l-NJS%qrJ4xCaDc?oTQ`y{S992 z?Xo4;#J^D^;7tyKU`LYRWRcW2sW|Ou&d|~$pmcu~rg5=2+8g(0l(as6jz0P^fq-Xv zdXHsh&h<%5<8Q&b*fyx;hNKu;H`dsvsbnv8A>GBN)CI6T<-caKPq(3Pc$?-$1A~lM zKd(R5GqZpIXPPx+Xxyb5VbEc?+KsaBf2j-imw{#DiIZVOGz_~JHD|yBJ_2A>Jz;dz z>+!rmR-te})~PPY`D8};i9t+Ba>;4%ABn5?+W-&Uj27Ls@_OKsU(!k*xiuitBwn+hUgCMxC?8Mf< z)N7c?Bp1M2?S?w=)aL(u@l8tqQbAjgUkozf>Z9Ul7~R4fKDM8U1f?tTQ(ZU3_PH+H zA3&FX$X@BtEl#M>PumEmA`ez0;d3&YoA z*oG#}STH6PP)4F0`Lj;F;Xsr&b5wUzVjgJ;4k9b?VVlrXW z(gS6ub$zelBxzNZ-9!w(5U zvdMpVvk~6|w8nbAM2$4KNQ&PozpIm3UqcaQLMdmcTg(hbNFMKrpSMfsOwp(Wsy_%| zbn{kC{FGFob8Oo7P0c2p?*i-JGhAo&fKpQ$n36fbHNEpzZ&#qC#@{uQ=T`tEx1$~) zF*~`8ZU@eGQ2I|rp^%=LYgfm%K1$>5Bv-`#4LF>R*F=Z2J(h;sHLx+&U=mm;IRas# zD`wE}Zwg=XyeG(@h^>C8;2TpT4Htz;l-Zr%qldxIe{>yJX}E_$_b`lx7UQzv*lmf< zQBYum2D2Xm0Uc~}Rz)m$3PPqypgJx*W8q`L)x27G;HftJD-OCxi}Q#mq2)no*Vez* z_%BD~*ox2aPMT`okRZP1y4{~ZcXz;45V>xO8u-S^C)hIsY==3n)K7+tHC3$NaUF{5 zOrTp7RdZ1LKuVIhgR1{r8}3qQrcBoj{^7=c!e>Z=oj7cjd|NjpNqiMr7K;Wj_knGV zERHMoGuopwynvso!(9t47!7^ZsZlL7eSFg|mCu@ATnlB4-GbVNn$#WZ~-(rAe^H0l|bQbJ4+J5IyB*fjCCZX4vW{I z8_79-0q$I=Qij7n%HGml4()V^!b@2Wl!Pil2A#LOP1^B$Py>xc0@+ZQzor+Cg|_F0 z@PR&^J3=7;Iy-!2Lc0zh(Xu*2PiYR2C2)t%`*g1LA_jDg&v4fRN-N-eCnJoZj>FxsjBJe8Q7sH$_>uB#(ORKp}d2 zOVMJyzr#xDONzQ)Bgh@eTrLc`vAj2Vud8%{IoihD{(5u*q~IFtW@>mJu3AKuwzQ#Z zxzgXAGHzGUb%})6o%q3p+8^kS(uM&(0ny!!lx`ht7dVpF{}lgp{Ou}E&M5*=eGZbD z+-ZNZ3Vv6J+f86#>fU|-7}%=-N9xeK8AHLi*$T)mC_RyauVD{311Bl{YRbnc16F+aF?rKz!GTe>DT!{E7>DIfo(#}Ge~IK&#Tv7%HLjlX>311Iv|cFI%|CD z%<4Gh7+#U}LvM-Nh_tG-^GPR}AN8QeW=JFhcJjM%#{CLZxEYWvLdiiF?tVu^L>sIy zj&eyoxUTgRJA#)~p=Z#iY2mUtl;C{itn+YR^wGBq=tvxcwZuq|)NJ!vxrZWurWT)D zag_OESxR@TU}09R8I$apvJ-uR!p9s$uaR6pJkplZU71`}-eJ~|HQ+5f(dQHlw+)R# zu|vL+eObd*jWt*UFx>2v6so;c zE`fBdYoP02XEdT2aChn!4s+Dp9J`oQ_4+IC+v(dC=z~$^ZAxqBd#1o84exkS6Ia!b zu_Jv_k#4yTaVCD6FM5t)itZmH*26@w`fkK}zl3RUAD{;0yHfPiV!6%7JRmq`bo&KM z8`lp!y7eMV4KPT)CZ28mAi*&Hmv~>#!e#+~ZU_j(bj$K>uh()KFfwgS7aE7LM~(?U2fEm+tI# znr5@*T^w}xJ2iYRhFI-TQa%Bi(nP=k64|*XsS5IKNZ8h&AwSbqDZi$HeqDv~YS^b> zo$~~$!;euvk8{Vg>$bAfK%oYljSzE|(+QBS&EXFfUI?^FMrZPD?vK^su1;-E^bwRDocS7Hq%6an=wX56wooG2-K)fMMw~IIs(gO6+8zp}s zdBT7sts0yS%lGn>!DtbV6A4R|07A&NO)v3}2oAT6nj-n@sW{x#xjl}MGg6oZn%D;o zm($Pz^#D1-tQ}b}|CQ2QOkm&^J9;Zk1?i!)JrNXZr0}$xPXCffGtEA)Ab@xH@$39r zN7Gnk6e9Sa=u1G#J@}_$1EqjyAiH}y$)>w)-Alb8AyM1>

&&helNw98p=Mj@ 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 starts; - LobbyMapPreview() : MapPreview(430, 115, A, A) { w = h = 180; } - void paint() override - { - if (!surface) - return; - int x, y, w, h; - getScreenPos(&x, &y, &w, &h); - auto target = parent->getSurface(); - target->drawSurface(x, y, w, h, surface); - auto font = Toolkit::getFont("standard"); - for (size_t i = 0; i < starts.size(); ++i) - { - int px = x + std::clamp(starts[i].x * w / std::max(1, getLastWidth()), 10, w - 18); - int py = y + std::clamp(starts[i].y * h / std::max(1, getLastHeight()), 10, h - 18); - target->drawFilledRect(px - 2, py - 2, 20, 20, 20, 30, 20); - target->drawFilledRect(px, py, 16, 16, starts[i].color); - font->pushStyle(Font::Style(Font::STYLE_NORMAL, Color(0, 0, 0))); - target->drawString(px + 3, py, font, std::to_string(i + 1)); - font->popStyle(); - } - } -}; + namespace { std::string colorName(Color c) @@ -997,12 +967,23 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) dimensions + " / " + std::to_string(setup.capacity) + " " + tr("colonies"), "little", rightW, true); int size = std::min(rightW, h - 126); - int px = rightX + (rightW - size) / 2, py = top + 51; - ui.box({px - 3, py - 3, size + 6, size + 6}, ui.line); + int previewW = size, previewH = size; + if (validMap) + { + previewW = rightW; + previewH = previewW * preview->getLastHeight() / preview->getLastWidth(); + if (previewH > h - 126) + { + previewH = h - 126; + previewW = previewH * preview->getLastWidth() / preview->getLastHeight(); + } + } + int px = rightX + (rightW - previewW) / 2, py = top + 51; + ui.box({px - 3, py - 3, previewW + 6, previewH + 6}, ui.line); if (validMap) { preview->setScreenPosition(px - (gfx->getW() - 640) / 2, py - (gfx->getH() - 480) / 2); - preview->setDimensions(size, size); + preview->setDimensions(previewW, previewH); preview->paint(); } else diff --git a/src/LobbyMapPreview.h b/src/LobbyMapPreview.h new file mode 100644 index 000000000..dcddecbed --- /dev/null +++ b/src/LobbyMapPreview.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GUIMapPreview.h" +#include "Utilities.h" +#include +#include +#include + +class LobbyMapPreview : public MapPreview +{ + public: + struct Start + { + int x, y; + Color color; + }; + std::vector starts; + LobbyMapPreview() : MapPreview(430, 115, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED) { w = h = 180; } + void paint() override + { + if (!surface) + return; + int x, y, w, h; + getScreenPos(&x, &y, &w, &h); + auto target = parent->getSurface(); + // Thumbnails letterbox rectangular maps inside a 128x128 image. Crop + // that padding before scaling, so terrain and colony markers share + // the same coordinate system. + int extent, cropW, cropH, cropX, cropY; + Utilities::computeMinimapData(PreviewSize, getLastWidth(), getLastHeight(), + &extent, &cropW, &cropH, &cropX, &cropY); + target->drawSurface(x, y, w, h, surface, cropX, cropY, cropW, cropH); + auto font = Toolkit::getFont("standard"); + for (size_t i = 0; i < starts.size(); ++i) + { + const int mapW = getLastWidth(), mapH = getLastHeight(); + const int startX = (starts[i].x % mapW + mapW) % mapW; + const int startY = (starts[i].y % mapH + mapH) % mapH; + int px = x + std::clamp(startX * w / mapW, 2, std::max(2, w - 18)); + int py = y + std::clamp(startY * h / mapH, 2, std::max(2, h - 18)); + target->drawFilledRect(px - 2, py - 2, 20, 20, 20, 30, 20); + target->drawFilledRect(px, py, 16, 16, starts[i].color); + font->pushStyle(Font::Style(Font::STYLE_NORMAL, Color(0, 0, 0))); + target->drawString(px + 3, py, font, std::to_string(i + 1)); + font->popStyle(); + } + } +}; diff --git a/src/map/generator/core/GenerationValidation.cpp b/src/map/generator/core/GenerationValidation.cpp index 22e96ecbc..cd35c0a25 100644 --- a/src/map/generator/core/GenerationValidation.cpp +++ b/src/map/generator/core/GenerationValidation.cpp @@ -48,6 +48,9 @@ std::string validateGeneratedWorld(const Game &g, const GenerationRequest &r, const Team *team = g.teams[i]; if (!team) return "Missing colony"; + if (team->startPosX < 0 || team->startPosX >= g.map.getW() || + team->startPosY < 0 || team->startPosY >= g.map.getH()) + return "Starting position outside map bounds"; int workers = 0; bool swarm = false; for (int slot = 0; slot < Unit::MAX_COUNT; ++slot) diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 27c1691ef..143850154 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -807,7 +807,7 @@ GeneratorDefinition shatteredCoastDefinition() return {"shattered-coast", 7, "Old random", - 1, + 2, false, {{"water", "Water weight", 0, 100, 1, 40, ControlGroup::Terrain, false, true}, {"sand", "Sand weight", 0, 100, 1, 4, ControlGroup::Terrain, false, true}, diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index f05ae3899..d7abfd5d2 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -276,6 +276,10 @@ bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSi { for (int s = 0; s < context.request.nbTeams; s++) { + // Legacy layout searches may choose a footprint across a torus seam. + // Buildings wrap those coordinates, and start metadata must wrap too. + context.bootX[s] = game.map.normalizeX(context.bootX[s]); + context.bootY[s] = game.map.normalizeY(context.bootY[s]); if (game.mapHeader.getNumberOfTeams() <= s) game.addTeam(); int squareSize = 5 + islandSize / 10; @@ -313,6 +317,10 @@ bool placeStarts(Game &game, GenerationContext &context) { for (int s = 0; s < context.request.nbTeams; s++) { + // Legacy layout searches may choose a footprint across a torus seam. + // Buildings wrap those coordinates, and start metadata must wrap too. + context.bootX[s] = game.map.normalizeX(context.bootX[s]); + context.bootY[s] = game.map.normalizeY(context.bootY[s]); assert(game.mapHeader.getNumberOfTeams() == s); if (game.mapHeader.getNumberOfTeams() <= s) game.addTeam(); diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index fa6c47ef2..7144f94fa 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -9,6 +9,7 @@ #include "GlobalContainer.h" #include "LobbyControls.h" #include "LobbyMapCatalog.h" +#include "LobbyMapPreview.h" #include "MapGenerator.h" #include "Order.h" #include "Player.h" @@ -594,6 +595,44 @@ struct CustomGameSetupHarness screen.activateGroup(screen.groups[0]); capture("map-1000"); + // Rectangular terrain and markers must use the same cropped preview area. + for (auto dimensions : {std::pair{9, 7}, std::pair{7, 9}, std::pair{9, 6}, std::pair{6, 9}}) { + screen.setup.generatorHistory.select(screen.setup.generator, GenerationRequest::eCONTESTEDCOMMONS); + screen.setup.generator.wDec = dimensions.first; + screen.setup.generator.hDec = dimensions.second; + screen.setup.setCapacity(4); + screen.invalidate(); + assert(screen.generateMap()); + const std::string name = "rectangular-" + std::to_string(1 << dimensions.first) + "x" + std::to_string(1 << dimensions.second); + const auto expectedStarts = screen.preview->starts; + // Old premade maps can contain equivalent coordinates across a torus seam. + screen.preview->starts[0].x -= (1 << dimensions.first); + screen.preview->starts[0].y += (1 << dimensions.second); + capture(name); + const auto rect = screen.preview->getScreenRect(); + const int mapW = screen.preview->getLastWidth(), mapH = screen.preview->getLastHeight(); + assert(std::abs(rect.w * mapH - rect.h * mapW) < std::max(mapW, mapH)); + SDL_Surface *bmp = SDL_LoadBMP((output + "/" + name + ".bmp").c_str()); + assert(bmp); + SDL_Surface *rgba = SDL_ConvertSurfaceFormat(bmp, SDL_PIXELFORMAT_RGBA32, 0); + SDL_FreeSurface(bmp); + assert(rgba); + auto pixel = [&](int x, int y) { + assert(x >= 0 && y >= 0 && x < rgba->w && y < rgba->h); + return static_cast(rgba->pixels) + y * rgba->pitch + x * 4; + }; + const auto corner = pixel(rect.x + 1, rect.y + 1); + assert(corner[0] || corner[1] || corner[2]); // no thumbnail letterbox inside the map + for (const auto &start : expectedStarts) { + const int x = rect.x + std::clamp(start.x * rect.w / mapW, 2, std::max(2, rect.w - 18)); + const int y = rect.y + std::clamp(start.y * rect.h / mapH, 2, std::max(2, rect.h - 18)); + const auto swatch = pixel(x + 1, y + 1); + assert(swatch[0] == start.color.r && swatch[1] == start.color.g && swatch[2] == start.color.b); + } + SDL_FreeSurface(rgba); + } + puts("PASS rectangular aspect ratios, cropped terrain and colony marker pixels"); + screen.setup.generatorHistory.select(screen.setup.generator, MapGenerationDescriptor::eRIVER); screen.setup.setCapacity(12); screen.setup.generator.wDec = screen.setup.generator.hDec = 8; screen.invalidate(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index d011f6b07..7561492a7 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -117,6 +117,23 @@ class MapGeneratorDefaultsTest assert(rejected.error == GenerationError::NonEmptyTarget); assert(fingerprint(first) == hash); } + if (method != D::eUNIFORM) + for (auto dimensions : {std::pair{9, 7}, std::pair{7, 9}}) + { + D rectangular = request; + rectangular.seed = 31001; + rectangular.wDec = dimensions.first; + rectangular.hDec = dimensions.second; + Game world(nullptr); + assert(service.generate(world, rectangular)); + assert(world.map.getW() == (1 << dimensions.first)); + assert(world.map.getH() == (1 << dimensions.second)); + for (int team = 0; team < rectangular.nbTeams; ++team) + { + assert(world.teams[team]->startPosX >= 0 && world.teams[team]->startPosX < world.map.getW()); + assert(world.teams[team]->startPosY >= 0 && world.teams[team]->startPosY < world.map.getH()); + } + } for (const auto &c : D::controls(method)) { D invalid = request; From 44e191e411ad26ba7e6e10b9b9321d4a1e46e880 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Wed, 9 Sep 2026 23:43:15 -0400 Subject: [PATCH 006/136] Keep review reports and generated screenshots out of the repository --- docs/map-generators/ADDING_A_GENERATOR.md | 4 +- docs/map-generators/FRAMEWORK_UPGRADES.md | 12 +- docs/map-generators/README.md | 101 +- docs/map-generators/RECTANGULAR_MAP_REVIEW.md | 78 - docs/map-generators/preview-tall.png | Bin 93146 -> 0 bytes docs/map-generators/preview-wide.png | Bin 94124 -> 0 bytes docs/map-generators/refactor/RESULTS.md | 76 - docs/map-generators/refactor/REVIEW.md | 54 - .../baseline-confirmation-configs.json | 7 - .../baseline-confirmation-reproducibility.csv | 7 - .../refactor/baseline-confirmation.csv.gz | Bin 255191 -> 0 bytes docs/map-generators/refactor/catalog.json | 4753 ----------------- .../map-generators/refactor/confirmation.json | 22 - docs/map-generators/refactor/coverage.png | Bin 126170 -> 0 bytes docs/map-generators/refactor/coverage.svg | 2832 ---------- .../refactor/edges-configs.json | 1890 ------- .../refactor/edges-reproducibility.csv | 1249 ----- docs/map-generators/refactor/edges.csv.gz | Bin 40284 -> 0 bytes .../refactor/framework-regression.json | 16 - docs/map-generators/refactor/generator-1.png | Bin 42459 -> 0 bytes docs/map-generators/refactor/generator-10.png | Bin 42471 -> 0 bytes docs/map-generators/refactor/generator-11.png | Bin 39678 -> 0 bytes docs/map-generators/refactor/generator-12.png | Bin 35401 -> 0 bytes docs/map-generators/refactor/generator-2.png | Bin 38733 -> 0 bytes docs/map-generators/refactor/generator-3.png | Bin 44637 -> 0 bytes docs/map-generators/refactor/generator-4.png | Bin 42806 -> 0 bytes docs/map-generators/refactor/generator-5.png | Bin 44413 -> 0 bytes docs/map-generators/refactor/generator-6.png | Bin 44818 -> 0 bytes docs/map-generators/refactor/generator-7.png | Bin 52215 -> 0 bytes docs/map-generators/refactor/generator-8.png | Bin 43068 -> 0 bytes docs/map-generators/refactor/generator-9.png | Bin 38828 -> 0 bytes .../modular-confirmation-configs.json | 7 - .../modular-confirmation-reproducibility.csv | 7 - .../refactor/modular-confirmation.csv.gz | Bin 255104 -> 0 bytes docs/map-generators/refactor/previews.json | 338 -- .../refactor/source-hashes.json | 67 - docs/map-generators/refactor/summary.json | 364 -- .../refactor/validation-configs.json | 42 - .../refactor/validation-reproducibility.csv | 49 - .../map-generators/refactor/validation.csv.gz | Bin 590054 -> 0 bytes tools/plot_map_generator_refactor.py | 4 +- 41 files changed, 14 insertions(+), 11965 deletions(-) delete mode 100644 docs/map-generators/RECTANGULAR_MAP_REVIEW.md delete mode 100644 docs/map-generators/preview-tall.png delete mode 100644 docs/map-generators/preview-wide.png delete mode 100644 docs/map-generators/refactor/RESULTS.md delete mode 100644 docs/map-generators/refactor/REVIEW.md delete mode 100644 docs/map-generators/refactor/baseline-confirmation-configs.json delete mode 100644 docs/map-generators/refactor/baseline-confirmation-reproducibility.csv delete mode 100644 docs/map-generators/refactor/baseline-confirmation.csv.gz delete mode 100644 docs/map-generators/refactor/catalog.json delete mode 100644 docs/map-generators/refactor/confirmation.json delete mode 100644 docs/map-generators/refactor/coverage.png delete mode 100644 docs/map-generators/refactor/coverage.svg delete mode 100644 docs/map-generators/refactor/edges-configs.json delete mode 100644 docs/map-generators/refactor/edges-reproducibility.csv delete mode 100644 docs/map-generators/refactor/edges.csv.gz delete mode 100644 docs/map-generators/refactor/framework-regression.json delete mode 100644 docs/map-generators/refactor/generator-1.png delete mode 100644 docs/map-generators/refactor/generator-10.png delete mode 100644 docs/map-generators/refactor/generator-11.png delete mode 100644 docs/map-generators/refactor/generator-12.png delete mode 100644 docs/map-generators/refactor/generator-2.png delete mode 100644 docs/map-generators/refactor/generator-3.png delete mode 100644 docs/map-generators/refactor/generator-4.png delete mode 100644 docs/map-generators/refactor/generator-5.png delete mode 100644 docs/map-generators/refactor/generator-6.png delete mode 100644 docs/map-generators/refactor/generator-7.png delete mode 100644 docs/map-generators/refactor/generator-8.png delete mode 100644 docs/map-generators/refactor/generator-9.png delete mode 100644 docs/map-generators/refactor/modular-confirmation-configs.json delete mode 100644 docs/map-generators/refactor/modular-confirmation-reproducibility.csv delete mode 100644 docs/map-generators/refactor/modular-confirmation.csv.gz delete mode 100644 docs/map-generators/refactor/previews.json delete mode 100644 docs/map-generators/refactor/source-hashes.json delete mode 100644 docs/map-generators/refactor/summary.json delete mode 100644 docs/map-generators/refactor/validation-configs.json delete mode 100644 docs/map-generators/refactor/validation-reproducibility.csv delete mode 100644 docs/map-generators/refactor/validation.csv.gz diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index 160eba1c0..9a2c69b73 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -72,9 +72,9 @@ Run all registered playable defaults without maintaining another generator list: python3 tools/map_generator_study.py --count 1000 --start 20001 \ --output artifacts/map-generator-refactor --label validation python3 tools/plot_map_generator_refactor.py \ - artifacts/map-generator-refactor/validation.csv + artifacts/map-generator-refactor/validation.csv --baseline artifacts/baseline/summary.json ``` For targeted settings, pass a JSON configuration list with `id`, `method` and a `params` object whose keys are stable control IDs. A sample uses registered defaults plus those overrides. Out-of-range values fail explicitly. `--binary` selects a separately built historical executable for comparison. -Before accepting a generator or structural refactor, inspect fixed-seed previews and poor-performing examples, exercise range endpoints and crowded/rectangular maps, and investigate changes beyond the documented statistical thresholds. Historical PR #238 reports remain under `study/`; current refactor evidence is under `refactor/`. +Before accepting a generator or structural refactor, inspect fixed-seed previews and poor-performing examples, exercise range endpoints and crowded/rectangular maps, and investigate changes beyond the documented statistical thresholds. Keep generated evidence under ignored `artifacts/` and summarize findings in the pull request. The comparison plotter requires an explicitly supplied baseline study summary. diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 0cc3d1c44..c11a5c2c5 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -1,6 +1,4 @@ -# Framework upgrades for the new map concepts - -The final concepts from `/Users/bradley/glob2-mapgen` are incorporated as four independent modules: Contested Commons, Lattice, Maze and Fjord Continent. The source worktree remains unchanged. Each module owns its options and ordered recipe while reusing bounded radial geometry, graph topology, constrained settlement placement and clumped resource helpers. +# Map generator framework capabilities ## Generalized capabilities @@ -30,11 +28,3 @@ All helpers are ordinary functions or small value objects. There is no new gener - Lattice uses larger home clearings and puts wheat and wood on opposite rims. Its tiny repeating islets remain the dominant visual pattern without starving the colonies of construction space. The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. - -## Verification - -Synthetic test registrations cover irregular domains, multiple Layout controls, mode history, generation and request/world validation without adding production catalog entries. Helper checks cover transform roundtrips, radial bounds, wrapping, cardinal versus diagonal components, sparse region IDs, disconnected graphs, invalid graph inputs, weighted dispersion, home footprints, exact workers and gameplay RNG restoration. - -The final run covers 12,000 attempts (1,000 per playable generator, seeds 20001–21000). The eight PR #238 generators retain their controls and statistical behavior; only Shattered Coast retains its previously investigated failure-rate flag. Lattice generated 1,000/1,000 with 932 all-team proxy passes, Maze 1,000/1,000 with 999 passes, and Fjord Continent 1,000/1,000 with 995 passes. Contested Commons generated 999/1,000; its separated home-island economy intentionally scores lower on the generic start proxy. See [the study and charts](refactor/RESULTS.md) and [the module guide](ADDING_A_GENERATOR.md). - -The original 832-attempt edge suite remains recorded under `refactor/`. A further 320 attempts cover every new generator at control extremes, rectangular large maps, solo Fjord, 64×64 maps and crowded eight-colony layouts. Supported combinations repeat deterministically; impossible crowded cases fail cleanly during validation or settlement placement. Native SDL flows pass for all three control modes, along with save/load, replay and editor control checks. diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index 2876e77d7..d161e9212 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -1,98 +1,15 @@ # Map generators -The generators now use a modular registry and per-attempt random state. See the [module and extension guide](ADDING_A_GENERATOR.md) and [refactor measurements and map previews](refactor/RESULTS.md). The tuning study below is the frozen PR #238 baseline. - -Contested Commons appears first and is the default generator in the custom-game lobby and map editor. - -Random maps now start with generator-specific recipes that leave more grassy building room. The custom-game lobby from PR #237 and the map editor use the same names, available controls, ranges, steps, defaults, and per-method setting memory. Old Random is **Shattered Coast**; Old Islands is **Rugged Archipelago**. The modular catalog also includes **Contested Commons**, **Lattice**, **Maze**, and **Fjord Continent**. +The map editor, custom-game lobby and study tools share generator control definitions, +including labels, ranges, steps and defaults. Uniform terrain is editor-only. +Normal lobby generation rolls a fresh random map; the preview snapshot is the map launched. ## Development tools -[Exact results and control reference](RESULTS.md) · [Raw validation results](study/validation.csv.gz) · [Source catalog snapshot](study/catalog.json) - -## One source of truth - -Each module under `src/map/generator/generators/` owns its control definitions, named options and generation sequence. The registry supplies those definitions to both UIs and the analysis tools. `GenerationRequest::setMethodDefaults()` applies the registered defaults; `GenerationHistory` remembers per-generator options while carrying shared settings between modes. The historical descriptor is isolated in `compatibility/`. - -Both `CustomGameScreen` and `NewMapScreen` render these definitions. The command-line generation stress tool samples the same valid ranges and steps for all 12 procedural modes. The lobby keeps its Terrain / Resources / Layout sections and scrolling; the editor uses the existing native Number widgets. The lobby's starting-worker rules control reads the same shared definition. A shared terrain-weight check rejects an all-zero terrain recipe in both screens. - -The study runner requests the compiled presets. The plotting tool requests `MapGeneratorStudy --catalog` and resolves names from the English translation table; the documentation and charts therefore do not maintain another copy of the current presets or names. Checked-in JSON/CSV is a historical measurement snapshot, not runtime configuration. Previous-setting cohorts remain explicit frozen configurations so later default changes cannot alter the baseline. - -All 33 language tables have the renamed generators and new labels. Existing translation keys, enum values and descriptor byte layout remain stable. Translation structure, placeholders and English-fallback regression checks pass; the new translations have not received native-speaker review. - -## What changed - -- Tune all eight procedural generators; Uniform retains its terrain selector. -- Add Crater Lakes lake size (10–40 in steps of 5), Concrete Islands channel width (5–8) and extra islands (0–6), and Isles island size (45–65 in steps of 5) and land bridge width (3–6). -- Terrain weights now allow 0–100, so the measured grass default of 75 is selectable. River width spans the explored 20–65 range in steps of 5; lake density spans 10–50 in steps of 5. Rugged Archipelago island size spans 50–70 and includes its default of 65. Existing smoothing, beach, fruit and repeat controls retain their established bounds. -- Remove the wheat, wood, stone and algae ratio widgets, whose serialized fields are not read by resource placement. Fruit remains configurable in the four modern height-map modes. Existing resource-placement algorithms are preserved. -- Restore the original legacy resource functions removed as unused code on newer master, then call them after placing starting bases/workers. Fix Rugged Archipelago base validation to use the current team instead of indexing team -1. -- Parameterize existing channel, bridge, island and crater constants without replacing the generation algorithms. -- Add Contested Commons, Lattice, Maze and Fjord Continent as registered modules. Their layout controls come from the same metadata as both UIs and the study catalog. -- Fix Lattice and Maze shoreline generation, enlarge their playable home areas, and use clumped resources. Maze uses coarse 20/24/32-tile cells and stone seams inside its water walls. Fjord guarantees clumped wheat and wood on both sides of every fjord. - -## Why these bounds and defaults - -The initial search and shortlists used 23,936 attempts, followed by 768 range-corner and 1,024 size/team checks. Configuration manifests and aggregate results are in [study/search-summary.csv](study/search-summary.csv). A separate original 16,000-attempt holdout supported the initial choices. This rebased implementation was then checked with a fresh **24,000-attempt comparison**: 1,000 fixed seeds for each of eight generators in three cohorts (new defaults, PR #237 lobby settings, and previous editor settings). - -The same seeds appear in multiple configurations; attempt counts are not counts of globally unique seeds. Selection balanced building space, starting resource access, generation reliability and recognizable terrain rather than maximizing grass alone. - -- Concrete widths 3–4 removed most or all water; widths 5–8 keep separate territories. Extra-island counts through 8 were explored; the UI stops at 6 to limit crowding. -- Isles sizes 35–65 and bridge widths 3–7 were explored. The selected ranges retain visible islands and narrow connecting bridges. The default size is 60, with the original bridge setting of 4. -- Crater size 25 and density 25 retain many round lakes with nearby resources. Large sparse lakes can leave resources farther from starts despite having plenty of grass. -- Rugged Archipelago size 70 approached merging islands in inspected samples. The default 65 increases interiors while retaining water separation. -- Shattered Coast keeps its rough patchwork coastlines with smoothing 3. Its improved space and resource access carry a small reliability tradeoff, shown in RESULTS.md. - -The conventional 0–100 terrain-weight domain is not a guarantee that every combination is usable. The measured recipes and new shape-control corners received the strongest coverage. - -## Visual review - -All eight final presets were inspected on fixed seeds 22001, 22002 and 22003, without replacing seeds. These tile-class views come from production maps; they show terrain, wheat, wood, stone and buildings. Algae, fruit and units are not separately colored. Wrapped edges can split a river or island across opposite sides of an image. - -![Same-seed map gallery](maps.png) - -[Swamp](generator-1.png) · [River](generator-2.png) · [Islands](generator-3.png) · [Crater Lakes](generator-4.png) · [Concrete Islands](generator-5.png) · [Isles](generator-6.png) · [Shattered Coast](generator-7.png) · [Rugged Archipelago](generator-8.png) - -The native lobby captures below exercise edited values, including five-unit increments and remembered per-mode settings. They show an update pending immediately after editing; automatic generation and snapshot launching are exercised separately by the same harness. - -| Concrete Islands | Isles | Crater Lakes | -|---|---|---| -| ![Channels](concrete-controls.png) | ![Island and bridge size](isles-controls.png) | ![Lake size](crater-controls.png) | - -| Rugged Archipelago | Shattered Coast | -|---|---| -| ![Rugged Archipelago](rugged-archipelago-controls.png) | ![Shattered Coast](shattered-coast-controls.png) | - -## Measurements and limitations - -Grass uses `Map::isGrass`; immediately free building tiles use `Map::isFreeForBuilding`. Clear 4×4 positions are valid top-left anchors, which overlap and must not be interpreted as independent buildings. Percentages exclude failed maps; every attempted seed and failure is retained in the raw data. - -The all-team start proxy requires each team to have at least 16 valid 4×4 anchors within 24 walking steps of its starting workers, reachable wheat within 24 steps, and wood within 32 steps. The flood follows the hard-space predicate with eight-neighbor movement and toroidal edges. This is a comparative diagnostic, not a complete pathfinding/economy simulation or a guarantee of fairness. - -Both prior-setting cohorts include the same legacy correctness fixes to isolate tuning. Without those fixes, the legacy modes lack their intended resources, and Rugged Archipelago can assert during base placement. The original study's main chart measured constructor settings, not the editor's usual timer-applied zero sand/desert settings; this PR records both relevant baselines explicitly. - -Placement failures still occur. Against the repaired PR #237 lobby baseline, River failures rise from 4 to 18 per 1,000 and Shattered Coast from 0 to 14, while their all-team start-proxy passes rise from 177 to 800 and 281 to 823. Against the previous editor baseline, River failures fall from 145 to 18. These are deliberate building-space/resource-access tradeoffs, not a claim of universally improved generation reliability. The earlier size/team checks expose small crowded maps: at 64×64/four teams, River generated 16/32 and Shattered Coast 17/32; at 128×128/eight teams, 21/32 and 22/32 respectively. These defaults target 128×128/four teams, not every dimension/roster combination. No full-match balance study was performed. - -Old saved descriptors retain their byte layout, but regenerated terrain intentionally changes. The previously unused Concrete Islands extra-island value zero now requests zero fixed neutral islands; a negative sentinel preserves the old random count for the frozen baseline. Legacy unused diameter 50 still selects original channel/bridge/crater constants. Mixed-version multiplayer generation was not tested. - -## Reproduction and checks - -From the repository root: - -```sh -scons release=1 -j12 map-generator-study map-generator-defaults-test custom-setup-test build/src/glob2 -mkdir -p artifacts/map-generator-validation/lobby -build/src/MapGeneratorDefaultsTest glob2-map-defaults-test artifacts/map-generator-validation -build/src/CustomGameSetupHarness -build/src/CustomGameSetupHarness artifacts/map-generator-validation/lobby -python3 data/check_translations.py --strict -python3 test/test_translations.py -python3 tools/map_generator_study.py --configs docs/map-generators/study/validation-configs.json --count 1000 --start 20001 --label validation --workers 8 -python3 tools/plot_map_generator_study.py artifacts/map-generator-validation/validation.csv -``` - -The plotting script requires NumPy and Matplotlib. Each sample runs in a fresh process; the dedicated executable overrides its clock to `1700000000 + seed` because the production generators reseed internally. It seeds Perlin noise and `srand`, and supplies the optional synchronized-RNG seed to `MapGenerator::generateMap` so PR #237’s `random_device` reseeding cannot override the study. Production UI calls omit that argument and retain random seeding. Before sampling, the runner checks three seed positions per configuration in independent repeated processes and aborts on any mismatch. Runner profiles are disposable and cleaned up automatically. UI harness profiles use explicit test names. - -The shared-control regression visits every selectable editor value, checks defaults and step normalization, compares lobby/editor method memory, and verifies serialization round trips. The native lobby harness exercises new dropdown/stepper controls, automatic preview debounce, snapshot ownership, generation recovery, teams, rules, saves and replays. The shared-control test is added to Linux CI alongside the existing lobby tests. Physical macOS mouse delivery is not covered by SDL event injection. +Build the study executable with `scons release=1 map-generator-study`. +`build/src/MapGeneratorStudy --catalog` exports the current control definitions. +Use `python3 tools/map_generator_study.py --help` for seeded generation studies. +Study results and screenshots belong in ignored `artifacts/`, not in this directory. +Record investigation findings and validation results in the relevant pull request. -See [the rectangular-map review](RECTANGULAR_MAP_REVIEW.md) for preview/start-coordinate fixes, rectangular regression coverage, and remaining seeded placement failures. +See [Adding a generator](ADDING_A_GENERATOR.md) for the registry and module interfaces. diff --git a/docs/map-generators/RECTANGULAR_MAP_REVIEW.md b/docs/map-generators/RECTANGULAR_MAP_REVIEW.md deleted file mode 100644 index bbf0883e3..000000000 --- a/docs/map-generators/RECTANGULAR_MAP_REVIEW.md +++ /dev/null @@ -1,78 +0,0 @@ -# Rectangular map review - -The lobby screenshot showing colony numbers in black space exposed a preview -coordinate bug. Map thumbnails store rectangular terrain centered in a square -128×128 image. The lobby stretched that entire thumbnail but placed colony -markers as if the terrain filled the square. - -## Fixed - -- Crop thumbnail padding and fit the actual map into the available preview area. - Wide maps use the available width; tall maps use the available height. The - frame and colony markers use those same dimensions. -- Wrap marker coordinates for existing maps whose start metadata crosses a torus - seam, rather than pinning those markers to an unrelated edge. -- Normalize generated start coordinates in the shared legacy placement paths. - Shattered Coast seed 31001 at 512×128 previously reported colony 2 at `(474,-6)` - even though the building wraps onto the map. Its start is now `(474,122)`. - Shattered Coast's generator revision is incremented to 2. -- Validate that generated colony start metadata lies inside the map bounds. - -The changes do not alter save/replay encodings or stretch the generated terrain. - -## Validation - -- All twelve playable generators successfully generate the 512×128 and 128×512 - regression cases (seed 31001, four colonies), with starts inside map bounds. -- Native lobby captures at 640×480 and 1000×700 cover 512×128, 128×512, 512×64, - and 64×512 Contested Commons maps. Tests inspect rendered terrain and colored - marker pixels, including equivalent start coordinates outside the base torus - rectangle. They also check the frame's aspect ratio. -- Existing generator contracts, request validation, editor/lobby control tests, - preview ownership, failure recovery and team-preservation checks pass. - -| Wide map | Tall map | -| --- | --- | -| ![512×128 preview](preview-wide.png) | ![128×512 preview](preview-tall.png) | - -## Remaining placement limitations - -A deterministic sample exercised all sixteen combinations of 64, 128, 256 and -512 tiles per axis, with seeds 31001–31005 and four colonies: 960 attempts. There -were 51 reported placement failures both before and after these fixes. These are -failed generation attempts, not missing strips of terrain in successful maps. -This sample does not establish balance or a universal success rate. - -Each generator has 60 rectangular and 20 square attempts: - -| Generator | Rectangular failures | Square failures | Observed failure | -| --- | ---: | ---: | --- | -| River | 3 | 1 | Cannot place starting locations on some small maps | -| Separate Islands | 8 | 1 | Resource/starting-region subdivision cannot place every colony | -| Shattered Coast | 11 | 3 | Cannot space every colony on grass | -| Contested Commons | 1 | 6 | Small home regions; all five 512×512 attempts exhaust the point-search budget | -| Fjord Continent | 15 | 2 | No swarm footprint fits in a home region, mostly with a 64-tile short axis | -| Swamp, Islands, Crater Lakes, Isles, Rugged Archipelago, Lattice, Maze | 0 | 0 | No failures in this sample | - -Fjord Continent sizes its continent from the shorter map axis, so making a -64×64 map longer does not automatically give each home region more room. Its -narrow-map failures need layout tuning; removing the preview padding does not -solve them. Likewise, the large Contested Commons budget failure is a separate -scaling issue, not a rectangular-coordinate error. - -Reproduce representative cases from the repository root: - -```sh -scons release=1 -j8 map-generator-study map-generator-defaults-test custom-setup-test -build/src/MapGeneratorDefaultsTest glob2-rectangular-test -mkdir -p artifacts/rectangular-maps -build/src/CustomGameSetupHarness artifacts/rectangular-maps -build/src/MapGeneratorStudy 9 31001 glob2-rectangular-study width=9 height=7 teams=4 -build/src/MapGeneratorStudy 12 31003 glob2-rectangular-study width=6 height=9 teams=4 -build/src/MapGeneratorStudy 9 31001 glob2-rectangular-study width=9 height=9 teams=4 -``` - -`MapGeneratorStudy` reports generation status in its `STUDY` row and prints a -diagnostic for a failed attempt; its process exit status alone is not a -success check. The full sample iterates method IDs 1–12, width/height exponents -6–9, and seeds 31001–31005 using the same command. diff --git a/docs/map-generators/preview-tall.png b/docs/map-generators/preview-tall.png deleted file mode 100644 index aa844504b517de9cc0a3d5cbf2b5cea04e41c9b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93146 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≶) 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 n5|0O~Wy*H>`rj!d(C7aH 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

&&helNw98p=Mj@ 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 percentage points; failure-rate increase >1 point; all-team proxy decrease >3 points. Flags require investigation, not automatic retuning. The start proxy measures nearby building anchors and reachable wheat/wood; it is not a fairness guarantee. - -[Investigation and visual assessment](REVIEW.md) · [Raw data](validation.csv.gz) · [Catalog](catalog.json) - -## Map previews - -Each row shows three fixed seeds and a weak successful start. Failed fixed seeds, if any, are labeled rather than replaced. - -### Swamp - -![Swamp](generator-1.png) - -### River - -![River](generator-2.png) - -### Islands - -![Islands](generator-3.png) - -### Crater lakes - -![Crater lakes](generator-4.png) - -### Concrete islands - -![Concrete islands](generator-5.png) - -### Isles - -![Isles](generator-6.png) - -### Shattered Coast - -![Shattered Coast](generator-7.png) - -### Rugged Archipelago - -![Rugged Archipelago](generator-8.png) - -### Contested commons - -![Contested commons](generator-9.png) - -### Lattice - -![Lattice](generator-10.png) - -### Maze - -![Maze](generator-11.png) - -### Fjord continent - -![Fjord continent](generator-12.png) diff --git a/docs/map-generators/refactor/REVIEW.md b/docs/map-generators/refactor/REVIEW.md deleted file mode 100644 index 80874fe65..000000000 --- a/docs/map-generators/refactor/REVIEW.md +++ /dev/null @@ -1,54 +0,0 @@ -# Refactor review - -The control ranges and defaults from PR #238 are unchanged. The primary comparison uses 1,000 attempts per playable generator, seeds 20001–21000. Every generator's mean immediately buildable area stayed within 0.15 percentage points of the baseline. All default-generation start-proxy changes stayed within the three-point review threshold. - -## Shattered Coast investigation - -The primary cohort produced 26 placement failures rather than the baseline's 14: a 1.2-percentage-point increase, above the one-point review threshold. The failure was inability to space all colonies on grass, not an exception, timeout or incomplete world reported as successful. The original flag remains visible in `RESULTS.md`. - -A separate 1,000-seed pilot (30001–31000) produced 33 failures. To investigate rather than retune around these seeds, the original PR #238 commit (`139e439f45f7cc4a616a968720549f9347b4a330`) was built in an isolated checkout. Both versions then ran 5,000 confirmation seeds, 30001–35000, with their compiled tuned presets: - -| Measure | PR #238 | Modular generators | Difference | -|---|---:|---:|---:| -| Failures | 99 / 5,000 (1.98%) | 122 / 5,000 (2.44%) | +0.46 percentage points | -| All-team start proxy | 4,050 / 5,000 (81.0%) | 4,000 / 5,000 (80.0%) | −1.0 point | -| Mean immediately buildable area | 36.41% | 36.31% | −0.10 points | - -The larger comparison did not reproduce the threshold crossing. In the first 1,000 confirmation seeds, the original implementation itself had 35 failures, compared with the refactor's 33, illustrating variation between finite seed cohorts. The algorithms and defaults were not changed to improve these results. - -A two-sided Fisher exact test for the 5,000-seed failure comparison gives p=0.134. The approximate 95% interval for the failure-rate difference is −0.12 to +1.04 percentage points. This is not proof of identical distributions or a strict non-inferiority bound below one point. It supports accepting this structural refactor alongside the unchanged controls, close terrain measurements, code inspection and visual review, while retaining failure-rate monitoring for future generator changes. - -The refactor intentionally changes seed-to-map outputs. This compares distributions, not matching historical map hashes. The baseline executable retains its historical fake-clock study harness; the new executable uses explicit seeds and no clock interposition. - -[Confirmation statistics](confirmation.json) · [Baseline confirmation rows](baseline-confirmation.csv.gz) · [Refactor confirmation rows](modular-confirmation.csv.gz) - -## Visual assessment - -All three fixed seeds (22001–22003) and one weak successful start per generator were inspected against the earlier gallery. No fixed preview seed needed replacement. - -| Generator | Preserved visual identity | -|---|---| -| Swamp | Broad, gently curved wetland channels and irregular connected grassy areas. | -| River | A dominant winding river, broad banks and occasional ponds. Existing straight bands at wrapping boundaries can still appear. | -| Islands | Rounded, separated landmasses with coastal resource belts and open interiors. | -| Crater Lakes | Connected grass punctured by round lakes, sometimes overlapping into larger pools. | -| Concrete Islands | Angular regions separated by narrow channels, with interior building space and larger resource patches. | -| Isles | Rounded islands joined by narrow land bridges, surrounded by more open water. | -| Shattered Coast | Fragmented, finely textured coastline and scattered inland building clearings. Weak seeds can remain predominantly water and fragmented grass. | -| Rugged Archipelago | Clearly separated islands with rough shores and chunky resource patches near the initial colonies. | - -The weak examples remain useful diagnostics. A successful map can still have poor local expansion or resource access; the service's structural checks deliberately do not enforce the offline start proxy or repair the terrain. - -## Edge settings and integration - -The edge suite covers 208 configurations with four seeds each: individual control endpoints, all-minimum/all-maximum combinations, small crowded maps, rectangular maps and 512×512 maps with twelve colonies. The initial run completed 832 attempts with 717 successes and 115 clean failures, with no crashes or timeouts. Every configuration also passed independent repeated-process checks. Impossible all-zero terrain recipes fail request validation; difficult placement combinations may fail after partial generation, and their candidates are discarded. - -The automated contracts exercise registered defaults/ranges/steps, both UIs' shared settings and mode memory, byte-codec roundtrips, legacy sentinels, a test-only nonconsecutive registration, immutable requests, interleaved map and full-state checksums, RNG restoration on success and failure, and structured errors. The custom-game harness exercises full save/load, real match launch, replay playback and native SDL interaction. - -Raw rows, configuration manifests and reproducibility checks are retained alongside this report. `source-hashes.json` records the final generator and study sources used for the published gallery. - -## Experimental-concept framework upgrades - -The subsequent geometry, topology, discrete-control, validation and constrained-settlement upgrades are documented in [the concept review](../FRAMEWORK_UPGRADES.md). No experimental generator was imported. A repeat of all 8,000 default attempts matched the preceding modular implementation's map hashes and quality outcomes exactly. The new optional helpers are exercised by synthetic test registrations, and existing generators keep local dispersion and their original generation sequence. - -The final edge rerun retained all 832 success/failure classifications (717 successes, 115 clean failures) and passed 624 repeated seed/configuration pairs. Fourteen hashes changed in small Concrete Islands or single-colony Isles configurations after the region-capacity and finite single-site-spacing fixes. [Framework regression counts](framework-regression.json). diff --git a/docs/map-generators/refactor/baseline-confirmation-configs.json b/docs/map-generators/refactor/baseline-confirmation-configs.json deleted file mode 100644 index cb6c5a86c..000000000 --- a/docs/map-generators/refactor/baseline-confirmation-configs.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": "shattered-holdout", - "method": 7, - "preset": true - } -] \ No newline at end of file diff --git a/docs/map-generators/refactor/baseline-confirmation-reproducibility.csv b/docs/map-generators/refactor/baseline-confirmation-reproducibility.csv deleted file mode 100644 index 53f7d321e..000000000 --- a/docs/map-generators/refactor/baseline-confirmation-reproducibility.csv +++ /dev/null @@ -1,7 +0,0 @@ -config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status -shattered-holdout,7,30001,1,16384,5791,1634,3259,5700,5092,1889,7194,4520,4670,0.009705,2712395013894739790,112,19,17,4,277,237,105,108,ok -shattered-holdout,7,30001,1,16384,5791,1634,3259,5700,5092,1889,7194,4520,4670,0.010698,2712395013894739790,112,19,17,4,277,237,105,108,ok -shattered-holdout,7,32501,1,16384,6269,1498,3458,5159,5390,1808,7686,4104,4594,0.009830,545327954787896904,148,9,13,4,403,214,182,162,ok -shattered-holdout,7,32501,1,16384,6269,1498,3458,5159,5390,1808,7686,4104,4594,0.010109,545327954787896904,148,9,13,4,403,214,182,162,ok -shattered-holdout,7,35000,1,16384,7375,1070,3000,4939,6436,2763,8711,3559,4114,0.009394,17586944396754086295,252,8,12,4,465,186,208,126,ok -shattered-holdout,7,35000,1,16384,7375,1070,3000,4939,6436,2763,8711,3559,4114,0.009964,17586944396754086295,252,8,12,4,465,186,208,126,ok diff --git a/docs/map-generators/refactor/baseline-confirmation.csv.gz b/docs/map-generators/refactor/baseline-confirmation.csv.gz deleted file mode 100644 index a41b45bc97410964e959ac80ea654517ee9c8156..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255191 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

)X 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 c#Iv!O$rf%KANG@Mtoz#s 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 6(!KMAo<)C>l*Hmd$- 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 diff --git a/docs/map-generators/refactor/coverage.svg b/docs/map-generators/refactor/coverage.svg deleted file mode 100644 index 02ce3efba..000000000 --- a/docs/map-generators/refactor/coverage.svg +++ /dev/null @@ -1,2832 +0,0 @@ - - - - - - - - 2026-09-09T22:47:23.526513 - image/svg+xml - - - Matplotlib v3.11.1, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/map-generators/refactor/edges-configs.json b/docs/map-generators/refactor/edges-configs.json deleted file mode 100644 index ced8ce4ab..000000000 --- a/docs/map-generators/refactor/edges-configs.json +++ /dev/null @@ -1,1890 +0,0 @@ -[ - { - "method": 1, - "preset": true, - "id": "edge-1-width-min", - "params": { - "width": 6 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-width-max", - "params": { - "width": 9 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-height-min", - "params": { - "height": 6 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-height-max", - "params": { - "height": 9 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-water-min", - "params": { - "water": 0 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-water-max", - "params": { - "water": 100 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-grass-min", - "params": { - "grass": 0 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-grass-max", - "params": { - "grass": 100 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-smoothing-min", - "params": { - "smoothing": 1 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-smoothing-max", - "params": { - "smoothing": 8 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-fruit-min", - "params": { - "fruit": 0 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-fruit-max", - "params": { - "fruit": 64 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-repeat-min", - "params": { - "repeat": 0 - } - }, - { - "method": 1, - "preset": true, - "id": "edge-1-repeat-max", - "params": { - "repeat": 5 - } - }, - { - "method": 1, - "preset": true, - "id": "size-1-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 1, - "preset": true, - "id": "size-1-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 1, - "preset": true, - "id": "size-1-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 1, - "preset": true, - "id": "size-1-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 1, - "preset": true, - "id": "size-1-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 1, - "preset": true, - "id": "size-1-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 1, - "preset": true, - "id": "corner-1-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "water": 0, - "grass": 0, - "smoothing": 1, - "fruit": 0, - "repeat": 0 - } - }, - { - "method": 1, - "preset": true, - "id": "corner-1-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "water": 100, - "grass": 100, - "smoothing": 8, - "fruit": 64, - "repeat": 5 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-width-min", - "params": { - "width": 6 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-width-max", - "params": { - "width": 9 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-height-min", - "params": { - "height": 6 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-height-max", - "params": { - "height": 9 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-water-min", - "params": { - "water": 0 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-water-max", - "params": { - "water": 100 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-sand-min", - "params": { - "sand": 0 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-sand-max", - "params": { - "sand": 100 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-grass-min", - "params": { - "grass": 0 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-grass-max", - "params": { - "grass": 100 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-desert-min", - "params": { - "desert": 0 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-desert-max", - "params": { - "desert": 100 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-smoothing-min", - "params": { - "smoothing": 1 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-smoothing-max", - "params": { - "smoothing": 8 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-river-width-min", - "params": { - "river-width": 20 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-river-width-max", - "params": { - "river-width": 65 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-fruit-min", - "params": { - "fruit": 0 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-fruit-max", - "params": { - "fruit": 64 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-repeat-min", - "params": { - "repeat": 0 - } - }, - { - "method": 2, - "preset": true, - "id": "edge-2-repeat-max", - "params": { - "repeat": 5 - } - }, - { - "method": 2, - "preset": true, - "id": "size-2-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 2, - "preset": true, - "id": "size-2-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 2, - "preset": true, - "id": "size-2-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 2, - "preset": true, - "id": "size-2-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 2, - "preset": true, - "id": "size-2-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 2, - "preset": true, - "id": "size-2-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 2, - "preset": true, - "id": "corner-2-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "water": 0, - "sand": 0, - "grass": 0, - "desert": 0, - "smoothing": 1, - "river-width": 20, - "fruit": 0, - "repeat": 0 - } - }, - { - "method": 2, - "preset": true, - "id": "corner-2-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "water": 100, - "sand": 100, - "grass": 100, - "desert": 100, - "smoothing": 8, - "river-width": 65, - "fruit": 64, - "repeat": 5 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-width-min", - "params": { - "width": 6 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-width-max", - "params": { - "width": 9 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-height-min", - "params": { - "height": 6 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-height-max", - "params": { - "height": 9 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-water-min", - "params": { - "water": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-water-max", - "params": { - "water": 100 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-sand-min", - "params": { - "sand": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-sand-max", - "params": { - "sand": 100 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-grass-min", - "params": { - "grass": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-grass-max", - "params": { - "grass": 100 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-desert-min", - "params": { - "desert": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-desert-max", - "params": { - "desert": 100 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-smoothing-min", - "params": { - "smoothing": 1 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-smoothing-max", - "params": { - "smoothing": 8 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-extra-islands-min", - "params": { - "extra-islands": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-extra-islands-max", - "params": { - "extra-islands": 8 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-fruit-min", - "params": { - "fruit": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-fruit-max", - "params": { - "fruit": 64 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-repeat-min", - "params": { - "repeat": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "edge-3-repeat-max", - "params": { - "repeat": 5 - } - }, - { - "method": 3, - "preset": true, - "id": "size-3-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 3, - "preset": true, - "id": "size-3-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 3, - "preset": true, - "id": "size-3-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 3, - "preset": true, - "id": "size-3-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 3, - "preset": true, - "id": "size-3-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 3, - "preset": true, - "id": "size-3-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 3, - "preset": true, - "id": "corner-3-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "water": 0, - "sand": 0, - "grass": 0, - "desert": 0, - "smoothing": 1, - "extra-islands": 0, - "fruit": 0, - "repeat": 0 - } - }, - { - "method": 3, - "preset": true, - "id": "corner-3-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "water": 100, - "sand": 100, - "grass": 100, - "desert": 100, - "smoothing": 8, - "extra-islands": 8, - "fruit": 64, - "repeat": 5 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-width-min", - "params": { - "width": 6 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-width-max", - "params": { - "width": 9 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-height-min", - "params": { - "height": 6 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-height-max", - "params": { - "height": 9 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-water-min", - "params": { - "water": 0 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-water-max", - "params": { - "water": 100 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-sand-min", - "params": { - "sand": 0 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-sand-max", - "params": { - "sand": 100 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-grass-min", - "params": { - "grass": 0 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-grass-max", - "params": { - "grass": 100 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-desert-min", - "params": { - "desert": 0 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-desert-max", - "params": { - "desert": 100 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-smoothing-min", - "params": { - "smoothing": 1 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-smoothing-max", - "params": { - "smoothing": 8 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-lake-density-min", - "params": { - "lake-density": 10 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-lake-density-max", - "params": { - "lake-density": 50 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-lake-size-min", - "params": { - "lake-size": 10 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-lake-size-max", - "params": { - "lake-size": 40 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-fruit-min", - "params": { - "fruit": 0 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-fruit-max", - "params": { - "fruit": 64 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-repeat-min", - "params": { - "repeat": 0 - } - }, - { - "method": 4, - "preset": true, - "id": "edge-4-repeat-max", - "params": { - "repeat": 5 - } - }, - { - "method": 4, - "preset": true, - "id": "size-4-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 4, - "preset": true, - "id": "size-4-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 4, - "preset": true, - "id": "size-4-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 4, - "preset": true, - "id": "size-4-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 4, - "preset": true, - "id": "size-4-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 4, - "preset": true, - "id": "size-4-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 4, - "preset": true, - "id": "corner-4-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "water": 0, - "sand": 0, - "grass": 0, - "desert": 0, - "smoothing": 1, - "lake-density": 10, - "lake-size": 10, - "fruit": 0, - "repeat": 0 - } - }, - { - "method": 4, - "preset": true, - "id": "corner-4-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "water": 100, - "sand": 100, - "grass": 100, - "desert": 100, - "smoothing": 8, - "lake-density": 50, - "lake-size": 40, - "fruit": 64, - "repeat": 5 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-width-min", - "params": { - "width": 6 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-width-max", - "params": { - "width": 9 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-height-min", - "params": { - "height": 6 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-height-max", - "params": { - "height": 9 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-channel-width-min", - "params": { - "channel-width": 5 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-channel-width-max", - "params": { - "channel-width": 8 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-extra-islands-min", - "params": { - "extra-islands": 0 - } - }, - { - "method": 5, - "preset": true, - "id": "edge-5-extra-islands-max", - "params": { - "extra-islands": 6 - } - }, - { - "method": 5, - "preset": true, - "id": "size-5-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 5, - "preset": true, - "id": "size-5-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 5, - "preset": true, - "id": "size-5-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 5, - "preset": true, - "id": "size-5-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 5, - "preset": true, - "id": "size-5-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 5, - "preset": true, - "id": "size-5-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 5, - "preset": true, - "id": "corner-5-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "channel-width": 5, - "extra-islands": 0 - } - }, - { - "method": 5, - "preset": true, - "id": "corner-5-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "channel-width": 8, - "extra-islands": 6 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-width-min", - "params": { - "width": 6 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-width-max", - "params": { - "width": 9 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-height-min", - "params": { - "height": 6 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-height-max", - "params": { - "height": 9 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-island-size-min", - "params": { - "island-size": 45 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-island-size-max", - "params": { - "island-size": 65 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-bridge-width-min", - "params": { - "bridge-width": 3 - } - }, - { - "method": 6, - "preset": true, - "id": "edge-6-bridge-width-max", - "params": { - "bridge-width": 6 - } - }, - { - "method": 6, - "preset": true, - "id": "size-6-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 6, - "preset": true, - "id": "size-6-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 6, - "preset": true, - "id": "size-6-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 6, - "preset": true, - "id": "size-6-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 6, - "preset": true, - "id": "size-6-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 6, - "preset": true, - "id": "size-6-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 6, - "preset": true, - "id": "corner-6-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "island-size": 45, - "bridge-width": 3 - } - }, - { - "method": 6, - "preset": true, - "id": "corner-6-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "island-size": 65, - "bridge-width": 6 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-width-min", - "params": { - "width": 6 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-width-max", - "params": { - "width": 9 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-height-min", - "params": { - "height": 6 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-height-max", - "params": { - "height": 9 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-water-min", - "params": { - "water": 0 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-water-max", - "params": { - "water": 100 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-sand-min", - "params": { - "sand": 0 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-sand-max", - "params": { - "sand": 100 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-grass-min", - "params": { - "grass": 0 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-grass-max", - "params": { - "grass": 100 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-smoothing-min", - "params": { - "smoothing": 1 - } - }, - { - "method": 7, - "preset": true, - "id": "edge-7-smoothing-max", - "params": { - "smoothing": 8 - } - }, - { - "method": 7, - "preset": true, - "id": "size-7-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 7, - "preset": true, - "id": "size-7-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 7, - "preset": true, - "id": "size-7-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 7, - "preset": true, - "id": "size-7-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 7, - "preset": true, - "id": "size-7-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 7, - "preset": true, - "id": "size-7-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 7, - "preset": true, - "id": "corner-7-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "water": 0, - "sand": 0, - "grass": 0, - "smoothing": 1 - } - }, - { - "method": 7, - "preset": true, - "id": "corner-7-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "water": 100, - "sand": 100, - "grass": 100, - "smoothing": 8 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-width-min", - "params": { - "width": 6 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-width-max", - "params": { - "width": 9 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-height-min", - "params": { - "height": 6 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-height-max", - "params": { - "height": 9 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-teams-min", - "params": { - "teams": 1 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-teams-max", - "params": { - "teams": 12 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-workers-min", - "params": { - "workers": 1 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-workers-max", - "params": { - "workers": 8 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-island-size-min", - "params": { - "island-size": 50 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-island-size-max", - "params": { - "island-size": 70 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-beach-size-min", - "params": { - "beach-size": 0 - } - }, - { - "method": 8, - "preset": true, - "id": "edge-8-beach-size-max", - "params": { - "beach-size": 4 - } - }, - { - "method": 8, - "preset": true, - "id": "size-8-6-6-4", - "params": { - "width": 6, - "height": 6, - "teams": 4 - } - }, - { - "method": 8, - "preset": true, - "id": "size-8-6-6-12", - "params": { - "width": 6, - "height": 6, - "teams": 12 - } - }, - { - "method": 8, - "preset": true, - "id": "size-8-7-7-8", - "params": { - "width": 7, - "height": 7, - "teams": 8 - } - }, - { - "method": 8, - "preset": true, - "id": "size-8-6-9-12", - "params": { - "width": 6, - "height": 9, - "teams": 12 - } - }, - { - "method": 8, - "preset": true, - "id": "size-8-9-6-12", - "params": { - "width": 9, - "height": 6, - "teams": 12 - } - }, - { - "method": 8, - "preset": true, - "id": "size-8-9-9-12", - "params": { - "width": 9, - "height": 9, - "teams": 12 - } - }, - { - "method": 8, - "preset": true, - "id": "corner-8-min", - "params": { - "width": 6, - "height": 6, - "teams": 1, - "workers": 1, - "island-size": 50, - "beach-size": 0 - } - }, - { - "method": 8, - "preset": true, - "id": "corner-8-max", - "params": { - "width": 9, - "height": 9, - "teams": 12, - "workers": 8, - "island-size": 70, - "beach-size": 4 - } - } -] \ No newline at end of file diff --git a/docs/map-generators/refactor/edges-reproducibility.csv b/docs/map-generators/refactor/edges-reproducibility.csv deleted file mode 100644 index 46ff37ed4..000000000 --- a/docs/map-generators/refactor/edges-reproducibility.csv +++ /dev/null @@ -1,1249 +0,0 @@ -config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status -edge-1-width-min,1,24001,1,8192,4506,192,2332,1162,3604,2622,4811,772,2609,0.000740,354655092834897733,787,8,15,4,264,396,150,496,ok -edge-1-width-min,1,24001,1,8192,4506,192,2332,1162,3604,2622,4811,772,2609,0.000750,354655092834897733,787,8,15,4,264,396,150,496,ok -edge-1-width-min,1,24003,1,8192,4613,193,2403,983,3624,2562,4869,684,2639,0.000747,7387189732466504953,304,11,10,4,326,428,135,493,ok -edge-1-width-min,1,24003,1,8192,4613,193,2403,983,3624,2562,4869,684,2639,0.000832,7387189732466504953,304,11,10,4,326,428,135,493,ok -edge-1-width-min,1,24004,1,8192,4620,158,2378,1036,3614,2727,4887,676,2629,0.000962,2283787201577684569,617,9,15,4,365,369,165,496,ok -edge-1-width-min,1,24004,1,8192,4620,158,2378,1036,3614,2727,4887,676,2629,0.000756,2283787201577684569,617,9,15,4,365,369,165,496,ok -edge-1-width-max,1,24001,1,65536,37323,1159,19630,7424,29612,23502,39237,4871,21428,0.005554,1510421835335318050,0,10,22,3,3212,3101,1305,3962,ok -edge-1-width-max,1,24001,1,65536,37323,1159,19630,7424,29612,23502,39237,4871,21428,0.005554,1510421835335318050,0,10,22,3,3212,3101,1305,3962,ok -edge-1-width-max,1,24003,1,65536,37237,1253,19644,7402,29543,23365,39129,4952,21455,0.008551,14655133220378056420,1113,13,12,4,3214,3093,1287,3970,ok -edge-1-width-max,1,24003,1,65536,37237,1253,19644,7402,29543,23365,39129,4952,21455,0.005900,14655133220378056420,1113,13,12,4,3214,3093,1287,3970,ok -edge-1-width-max,1,24004,1,65536,37291,1033,19678,7534,29538,23442,39224,4800,21512,0.005538,2696248690097268267,968,7,17,4,3572,2687,1398,3966,ok -edge-1-width-max,1,24004,1,65536,37291,1033,19678,7534,29538,23442,39224,4800,21512,0.008577,2696248690097268267,968,7,17,4,3572,2687,1398,3966,ok -edge-1-height-min,1,24001,1,8192,4611,153,2419,1009,3618,2677,4875,656,2661,0.000746,5993380641348072149,921,12,16,4,390,360,146,496,ok -edge-1-height-min,1,24001,1,8192,4611,153,2419,1009,3618,2677,4875,656,2661,0.000782,5993380641348072149,921,12,16,4,390,360,146,496,ok -edge-1-height-min,1,24003,1,8192,4757,102,2531,802,3628,2731,4960,502,2730,0.000738,1808543194305134926,750,14,12,4,430,394,219,495,ok -edge-1-height-min,1,24003,1,8192,4757,102,2531,802,3628,2731,4960,502,2730,0.000738,1808543194305134926,750,14,12,4,430,394,219,495,ok -edge-1-height-min,1,24004,1,8192,4651,130,2452,959,3631,2635,4906,609,2677,0.000739,1517223058165164336,596,14,13,4,440,297,191,495,ok -edge-1-height-min,1,24004,1,8192,4651,130,2452,959,3631,2635,4906,609,2677,0.000754,1517223058165164336,596,14,13,4,440,297,191,495,ok -edge-1-height-max,1,24001,1,65536,36887,1444,19417,7788,29444,23038,38891,5338,21307,0.125080,18349716135080740857,604,20,12,4,3366,2798,1183,3961,ok -edge-1-height-max,1,24001,1,65536,36887,1444,19417,7788,29444,23038,38891,5338,21307,0.005767,18349716135080740857,604,20,12,4,3366,2798,1183,3961,ok -edge-1-height-max,1,24003,1,65536,37321,1304,19671,7240,29663,23616,39187,4923,21426,0.128583,18016351330854298622,420,11,6,4,3160,3109,1291,3979,ok -edge-1-height-max,1,24003,1,65536,37321,1304,19671,7240,29663,23616,39187,4923,21426,0.023905,18016351330854298622,420,11,6,4,3160,3109,1291,3979,ok -edge-1-height-max,1,24004,1,65536,37162,1310,19428,7636,29562,23232,39119,5127,21290,0.127327,14147695598255769863,425,10,13,4,3343,2920,1244,3976,ok -edge-1-height-max,1,24004,1,65536,37162,1310,19428,7636,29562,23232,39119,5127,21290,0.010093,14147695598255769863,425,10,13,4,3343,2920,1244,3976,ok -edge-1-teams-min,1,24001,1,16384,9332,282,4946,1824,7377,5675,9806,1193,5385,0.001638,3691568818373152596,688,1,6,1,969,641,315,994,ok -edge-1-teams-min,1,24001,1,16384,9332,282,4946,1824,7377,5675,9806,1193,5385,0.001236,3691568818373152596,688,1,6,1,969,641,315,994,ok -edge-1-teams-min,1,24003,1,16384,9403,278,4989,1714,7363,5792,9842,1134,5408,0.001348,17111279724926110250,987,18,4,1,842,755,400,991,ok -edge-1-teams-min,1,24003,1,16384,9403,278,4989,1714,7363,5792,9842,1134,5408,0.001783,17111279724926110250,987,18,4,1,842,755,400,991,ok -edge-1-teams-min,1,24004,1,16384,9349,285,4978,1772,7375,5879,9811,1170,5403,0.002998,17259471453116953259,1217,5,4,1,593,1058,290,994,ok -edge-1-teams-min,1,24004,1,16384,9349,285,4978,1772,7375,5879,9811,1170,5403,0.001237,17259471453116953259,1217,5,4,1,593,1058,290,994,ok -edge-1-teams-max,1,24001,1,16384,9336,282,4946,1820,7176,5164,9807,1192,5385,0.007912,8377485932594510683,330,13,13,12,954,641,315,994,ok -edge-1-teams-max,1,24001,1,16384,9336,282,4946,1820,7176,5164,9807,1192,5385,0.002530,8377485932594510683,330,13,13,12,954,641,315,994,ok -edge-1-teams-max,1,24003,0,16384,0,0,16384,0,0,0,9842,1134,5408,0.001259,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-1-teams-max,1,24003,0,16384,0,0,16384,0,0,0,9842,1134,5408,0.002513,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-1-teams-max,1,24004,1,16384,9378,279,4953,1774,7185,5387,9840,1166,5378,0.004729,13667748426926015759,0,20,18,11,593,1057,290,994,ok -edge-1-teams-max,1,24004,1,16384,9378,279,4953,1774,7185,5387,9840,1166,5378,0.001920,13667748426926015759,0,20,18,11,593,1057,290,994,ok -edge-1-workers-min,1,24001,1,16384,9332,282,4946,1824,7344,5573,9806,1193,5385,0.001378,13890498528614818315,526,10,10,4,954,641,315,994,ok -edge-1-workers-min,1,24001,1,16384,9332,282,4946,1824,7344,5573,9806,1193,5385,0.001404,13890498528614818315,526,10,10,4,954,641,315,994,ok -edge-1-workers-min,1,24003,1,16384,9411,274,4987,1712,7341,5689,9850,1129,5405,0.005096,1743860520622184812,397,18,17,4,824,755,400,991,ok -edge-1-workers-min,1,24003,1,16384,9411,274,4987,1712,7341,5689,9850,1129,5405,0.001440,1743860520622184812,397,18,17,4,824,755,400,991,ok -edge-1-workers-min,1,24004,1,16384,9349,285,4978,1772,7328,5741,9811,1170,5403,0.001333,17965142136197182547,780,22,8,4,593,1057,290,994,ok -edge-1-workers-min,1,24004,1,16384,9349,285,4978,1772,7328,5741,9811,1170,5403,0.046888,17965142136197182547,780,22,8,4,593,1057,290,994,ok -edge-1-workers-max,1,24001,1,16384,9332,282,4946,1824,7316,5554,9806,1193,5385,0.001339,13890498528614818315,598,9,9,4,954,641,315,994,ok -edge-1-workers-max,1,24001,1,16384,9332,282,4946,1824,7316,5554,9806,1193,5385,0.001743,13890498528614818315,598,9,9,4,954,641,315,994,ok -edge-1-workers-max,1,24003,1,16384,9411,274,4987,1712,7313,5659,9850,1129,5405,0.001378,1743860520622184812,551,18,17,4,824,755,400,991,ok -edge-1-workers-max,1,24003,1,16384,9411,274,4987,1712,7313,5659,9850,1129,5405,0.001358,1743860520622184812,551,18,17,4,824,755,400,991,ok -edge-1-workers-max,1,24004,1,16384,9349,285,4978,1772,7300,5711,9811,1170,5403,0.004252,17965142136197182547,846,20,5,4,593,1057,290,994,ok -edge-1-workers-max,1,24004,1,16384,9349,285,4978,1772,7300,5711,9811,1170,5403,0.001410,17965142136197182547,846,20,5,4,593,1057,290,994,ok -edge-1-water-min,1,24001,0,16384,0,0,16384,0,0,0,16383,1,0,0.001091,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-1-water-min,1,24001,0,16384,0,0,16384,0,0,0,16383,1,0,0.001120,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-1-water-min,1,24003,0,16384,0,0,16384,0,0,0,16383,1,0,0.004801,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-1-water-min,1,24003,0,16384,0,0,16384,0,0,0,16383,1,0,0.001045,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-1-water-min,1,24004,0,16384,0,0,16384,0,0,0,16383,1,0,0.001035,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-1-water-min,1,24004,0,16384,0,0,16384,0,0,0,16383,1,0,0.001033,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-1-water-max,1,24001,1,16384,4979,323,8934,2148,3041,1720,5510,1397,9477,0.001381,14123798832588003375,148,15,7,4,689,842,313,1690,ok -edge-1-water-max,1,24001,1,16384,4979,323,8934,2148,3041,1720,5510,1397,9477,0.001366,14123798832588003375,148,15,7,4,689,842,313,1690,ok -edge-1-water-max,1,24003,1,16384,5107,339,9034,1904,3025,1892,5571,1291,9522,0.004533,6682157493061246418,129,9,7,4,662,955,375,1695,ok -edge-1-water-max,1,24003,1,16384,5107,339,9034,1904,3025,1892,5571,1291,9522,0.001416,6682157493061246418,129,9,7,4,662,955,375,1695,ok -edge-1-water-max,1,24004,1,16384,5182,281,9181,1740,3043,1977,5624,1150,9610,0.001383,10747540473145447402,39,21,14,4,1002,708,321,1695,ok -edge-1-water-max,1,24004,1,16384,5182,281,9181,1740,3043,1977,5624,1150,9610,0.001502,10747540473145447402,39,21,14,4,1002,708,321,1695,ok -edge-1-grass-min,1,24001,0,16384,0,0,16384,0,0,0,330,257,15797,0.001202,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-1-grass-min,1,24001,0,16384,0,0,16384,0,0,0,330,257,15797,0.003721,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-1-grass-min,1,24003,0,16384,0,0,16384,0,0,0,345,226,15813,0.002813,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-1-grass-min,1,24003,0,16384,0,0,16384,0,0,0,345,226,15813,0.001068,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-1-grass-min,1,24004,0,16384,0,0,16384,0,0,0,326,270,15788,0.001081,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-1-grass-min,1,24004,0,16384,0,0,16384,0,0,0,326,270,15788,0.003218,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-1-grass-max,1,24001,1,16384,11285,204,3407,1488,9885,8176,11680,947,3757,0.003325,3609620929673755109,1068,10,13,4,541,475,279,696,ok -edge-1-grass-max,1,24001,1,16384,11285,204,3407,1488,9885,8176,11680,947,3757,0.003975,3609620929673755109,1068,10,13,4,541,475,279,696,ok -edge-1-grass-max,1,24003,1,16384,11324,230,3384,1446,9942,8397,11687,952,3745,0.001341,4936522091075774384,971,23,11,4,558,514,213,698,ok -edge-1-grass-max,1,24003,1,16384,11324,230,3384,1446,9942,8397,11687,952,3745,0.003343,4936522091075774384,971,23,11,4,558,514,213,698,ok -edge-1-grass-max,1,24004,1,16384,11264,237,3389,1494,9918,8343,11660,983,3741,0.004670,16153151779798774698,1132,6,12,4,404,626,223,701,ok -edge-1-grass-max,1,24004,1,16384,11264,237,3389,1494,9918,8343,11660,983,3741,0.003137,16153151779798774698,1132,6,12,4,404,626,223,701,ok -edge-1-smoothing-min,1,24001,1,16384,8114,658,3825,3787,6783,3797,9138,2552,4694,0.003433,5021615561480563420,466,13,6,4,435,556,250,990,ok -edge-1-smoothing-min,1,24001,1,16384,8114,658,3825,3787,6783,3797,9138,2552,4694,0.001494,5021615561480563420,466,13,6,4,435,556,250,990,ok -edge-1-smoothing-min,1,24003,1,16384,8243,651,3920,3570,6875,3845,9180,2437,4767,0.002187,12728242639966732679,376,7,9,4,492,545,227,983,ok -edge-1-smoothing-min,1,24003,1,16384,8243,651,3920,3570,6875,3845,9180,2437,4767,0.004730,12728242639966732679,376,7,9,4,492,545,227,983,ok -edge-1-smoothing-min,1,24004,1,16384,8229,645,3964,3546,6848,3913,9169,2420,4795,0.001407,7515091598127115981,416,9,3,4,543,511,240,994,ok -edge-1-smoothing-min,1,24004,1,16384,8229,645,3964,3546,6848,3913,9169,2420,4795,0.001414,7515091598127115981,416,9,3,4,543,511,240,994,ok -edge-1-smoothing-max,1,24001,1,16384,9465,233,5012,1674,7381,5845,9902,1069,5413,0.001902,7108776383409922387,728,16,15,4,666,949,364,991,ok -edge-1-smoothing-max,1,24001,1,16384,9465,233,5012,1674,7381,5845,9902,1069,5413,0.001357,7108776383409922387,728,16,15,4,666,949,364,991,ok -edge-1-smoothing-max,1,24003,1,16384,9470,260,5040,1614,7295,5826,9880,1066,5438,0.005065,6057965752441029688,795,14,14,4,702,967,398,988,ok -edge-1-smoothing-max,1,24003,1,16384,9470,260,5040,1614,7295,5826,9880,1066,5438,0.002917,6057965752441029688,795,14,14,4,702,967,398,988,ok -edge-1-smoothing-max,1,24004,1,16384,9350,307,4949,1778,7329,5697,9808,1196,5380,0.003134,12055984761683483090,519,12,11,4,827,781,307,993,ok -edge-1-smoothing-max,1,24004,1,16384,9350,307,4949,1778,7329,5697,9808,1196,5380,0.001379,12055984761683483090,519,12,11,4,827,781,307,993,ok -edge-1-fruit-min,1,24001,1,16384,9332,282,4946,1824,7342,5640,9806,1193,5385,0.032317,11947252659821411454,579,10,10,4,954,641,315,994,ok -edge-1-fruit-min,1,24001,1,16384,9332,282,4946,1824,7342,5640,9806,1193,5385,0.001444,11947252659821411454,579,10,10,4,954,641,315,994,ok -edge-1-fruit-min,1,24003,1,16384,9411,274,4987,1712,7352,5779,9850,1129,5405,0.001347,5813234162792613140,556,18,17,4,824,755,400,991,ok -edge-1-fruit-min,1,24003,1,16384,9411,274,4987,1712,7352,5779,9850,1129,5405,0.001906,5813234162792613140,556,18,17,4,824,755,400,991,ok -edge-1-fruit-min,1,24004,1,16384,9349,285,4978,1772,7329,5810,9811,1170,5403,0.001419,5815179870271813462,888,20,6,4,593,1057,290,994,ok -edge-1-fruit-min,1,24004,1,16384,9349,285,4978,1772,7329,5810,9811,1170,5403,0.001336,5815179870271813462,888,20,6,4,593,1057,290,994,ok -edge-1-fruit-max,1,24001,1,16384,9332,282,4946,1824,7044,4165,9806,1193,5385,0.001389,6867792694369419104,327,10,10,4,954,641,315,994,ok -edge-1-fruit-max,1,24001,1,16384,9332,282,4946,1824,7044,4165,9806,1193,5385,0.005230,6867792694369419104,327,10,10,4,954,641,315,994,ok -edge-1-fruit-max,1,24003,1,16384,9411,274,4987,1712,7017,4404,9850,1129,5405,0.001407,4327981281064200431,328,18,17,4,824,755,400,991,ok -edge-1-fruit-max,1,24003,1,16384,9411,274,4987,1712,7017,4404,9850,1129,5405,0.002306,4327981281064200431,328,18,17,4,824,755,400,991,ok -edge-1-fruit-max,1,24004,1,16384,9349,285,4978,1772,7026,4313,9811,1170,5403,0.001422,4390929319407353961,651,20,6,4,593,1057,290,994,ok -edge-1-fruit-max,1,24004,1,16384,9349,285,4978,1772,7026,4313,9811,1170,5403,0.001380,4390929319407353961,651,20,6,4,593,1057,290,994,ok -edge-1-repeat-min,1,24001,1,16384,9332,282,4946,1824,7332,5567,9806,1193,5385,0.001382,13890498528614818315,575,10,10,4,954,641,315,994,ok -edge-1-repeat-min,1,24001,1,16384,9332,282,4946,1824,7332,5567,9806,1193,5385,0.002214,13890498528614818315,575,10,10,4,954,641,315,994,ok -edge-1-repeat-min,1,24003,1,16384,9411,274,4987,1712,7329,5680,9850,1129,5405,0.001691,1743860520622184812,551,18,17,4,824,755,400,991,ok -edge-1-repeat-min,1,24003,1,16384,9411,274,4987,1712,7329,5680,9850,1129,5405,0.009181,1743860520622184812,551,18,17,4,824,755,400,991,ok -edge-1-repeat-min,1,24004,1,16384,9349,285,4978,1772,7316,5732,9811,1170,5403,0.002014,17965142136197182547,851,20,6,4,593,1057,290,994,ok -edge-1-repeat-min,1,24004,1,16384,9349,285,4978,1772,7316,5732,9811,1170,5403,0.004363,17965142136197182547,851,20,6,4,593,1057,290,994,ok -edge-1-repeat-max,1,24001,1,16384,8192,728,4008,3456,6496,2060,9124,2424,4836,0.000693,3221758993762238880,184,2,10,4,384,544,32,960,ok -edge-1-repeat-max,1,24001,1,16384,8192,728,4008,3456,6496,2060,9124,2424,4836,0.000712,3221758993762238880,184,2,10,4,384,544,32,960,ok -edge-1-repeat-max,1,24003,1,16384,7556,660,3232,4936,5732,836,8872,3096,4416,0.001259,15318678605301309947,108,6,4,4,540,416,156,960,ok -edge-1-repeat-max,1,24003,1,16384,7556,660,3232,4936,5732,836,8872,3096,4416,0.000595,15318678605301309947,108,6,4,4,540,416,156,960,ok -edge-1-repeat-max,1,24004,1,16384,8180,728,3756,3720,6464,1564,9172,2556,4656,0.001808,2270769969064918328,138,5,6,4,244,500,124,960,ok -edge-1-repeat-max,1,24004,1,16384,8180,728,3756,3720,6464,1564,9172,2556,4656,0.000576,2270769969064918328,138,5,6,4,244,500,124,960,ok -size-1-6-6-4,1,24001,1,4096,2318,73,1233,472,1764,1089,2443,308,1345,0.000442,17079717480519420910,319,11,6,4,104,269,85,247,ok -size-1-6-6-4,1,24001,1,4096,2318,73,1233,472,1764,1089,2443,308,1345,0.001205,17079717480519420910,319,11,6,4,104,269,85,247,ok -size-1-6-6-4,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000303,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-1-6-6-4,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000738,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-1-6-6-4,1,24004,1,4096,2220,136,1120,620,1746,1089,2383,446,1267,0.001094,11627436278644698582,310,7,18,4,121,194,66,247,ok -size-1-6-6-4,1,24004,1,4096,2220,136,1120,620,1746,1089,2383,446,1267,0.000431,11627436278644698582,310,7,18,4,121,194,66,247,ok -size-1-6-6-12,1,24001,0,4096,0,0,4096,0,0,0,2443,308,1345,0.000325,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-1-6-6-12,1,24001,0,4096,0,0,4096,0,0,0,2443,308,1345,0.000323,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-1-6-6-12,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000967,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-1-6-6-12,1,24003,0,4096,0,0,4096,0,0,0,2431,330,1335,0.000304,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-1-6-6-12,1,24004,0,4096,0,0,4096,0,0,0,2382,447,1267,0.000394,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-1-6-6-12,1,24004,0,4096,0,0,4096,0,0,0,2382,447,1267,0.001097,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-1-7-7-8,1,24001,1,16384,9332,282,4946,1824,7252,5357,9806,1193,5385,0.001508,2571324380836695905,490,13,13,8,954,641,315,994,ok -size-1-7-7-8,1,24001,1,16384,9332,282,4946,1824,7252,5357,9806,1193,5385,0.004267,2571324380836695905,490,13,13,8,954,641,315,994,ok -size-1-7-7-8,1,24003,1,16384,9415,274,4987,1708,7303,5583,9851,1128,5405,0.009835,9280150380393299210,0,18,17,7,789,743,397,991,ok -size-1-7-7-8,1,24003,1,16384,9415,274,4987,1708,7303,5583,9851,1128,5405,0.002735,9280150380393299210,0,18,17,7,789,743,397,991,ok -size-1-7-7-8,1,24004,1,16384,9353,285,4978,1768,7240,5547,9812,1169,5403,0.001501,11396205303651550806,814,20,18,8,593,1057,290,994,ok -size-1-7-7-8,1,24004,1,16384,9353,285,4978,1768,7240,5547,9812,1169,5403,0.002575,11396205303651550806,814,20,18,8,593,1057,290,994,ok -size-1-6-9-12,1,24001,1,32768,18450,768,9592,3958,14638,11066,19484,2747,10537,0.003206,205956527766588996,524,10,15,12,1260,1711,586,1990,ok -size-1-6-9-12,1,24001,1,32768,18450,768,9592,3958,14638,11066,19484,2747,10537,0.048581,205956527766588996,524,10,15,12,1260,1711,586,1990,ok -size-1-6-9-12,1,24003,1,32768,18547,735,9666,3820,14623,11114,19544,2644,10580,0.008060,4488402287655512689,213,12,19,12,1536,1454,677,1988,ok -size-1-6-9-12,1,24003,1,32768,18547,735,9666,3820,14623,11114,19544,2644,10580,0.007410,4488402287655512689,213,12,19,12,1536,1454,677,1988,ok -size-1-6-9-12,1,24004,1,32768,18464,725,9613,3966,14601,11185,19483,2707,10578,0.005707,9377437999984426408,268,11,17,12,1628,1403,567,1990,ok -size-1-6-9-12,1,24004,1,32768,18464,725,9613,3966,14601,11185,19483,2707,10578,0.020534,9377437999984426408,268,11,17,12,1628,1403,567,1990,ok -size-1-9-6-12,1,24001,1,32768,18428,650,9648,4042,14596,10861,19461,2670,10637,0.003411,16480885287169114817,632,13,21,12,1586,1355,637,1981,ok -size-1-9-6-12,1,24001,1,32768,18428,650,9648,4042,14596,10861,19461,2670,10637,0.031473,16480885287169114817,632,13,21,12,1586,1355,637,1981,ok -size-1-9-6-12,1,24003,1,32768,18407,682,9623,4056,14488,10953,19462,2708,10598,0.003339,5197832662345982046,549,17,19,12,1585,1429,650,1980,ok -size-1-9-6-12,1,24003,1,32768,18407,682,9623,4056,14488,10953,19462,2708,10598,0.004480,5197832662345982046,549,17,19,12,1585,1429,650,1980,ok -size-1-9-6-12,1,24004,1,32768,18651,551,9813,3753,14547,11197,19611,2428,10729,0.007475,15292689523055164251,490,13,19,12,1644,1494,707,1978,ok -size-1-9-6-12,1,24004,1,32768,18651,551,9813,3753,14547,11197,19611,2428,10729,0.003726,15292689523055164251,490,13,19,12,1644,1494,707,1978,ok -size-1-9-9-12,1,24001,1,262144,149149,5198,78183,29614,118418,94574,156739,20005,85400,0.140929,16415405346331145548,0,100000,16,11,12901,12409,5160,15840,ok -size-1-9-9-12,1,24001,1,262144,149149,5198,78183,29614,118418,94574,156739,20005,85400,0.207999,16415405346331145548,0,100000,16,11,12901,12409,5160,15840,ok -size-1-9-9-12,1,24003,1,262144,148916,5302,78338,29588,118280,94638,156537,20096,85511,0.302889,17473792559893687585,378,19,16,12,12411,12609,5360,15855,ok -size-1-9-9-12,1,24003,1,262144,148916,5302,78338,29588,118280,94638,156537,20096,85511,0.298061,17473792559893687585,378,19,16,12,12411,12609,5360,15855,ok -size-1-9-9-12,1,24004,1,262144,149399,4538,78867,29340,118261,94428,156953,19209,85982,0.187686,14278055095835393210,637,16,16,12,12710,12805,5373,15920,ok -size-1-9-9-12,1,24004,1,262144,149399,4538,78867,29340,118261,94428,156953,19209,85982,0.226154,14278055095835393210,637,16,16,12,12710,12805,5373,15920,ok -corner-1-min,1,24001,0,0,0,0,0,0,0,0,0,0,0,0.041574,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-1-min,1,24001,0,0,0,0,0,0,0,0,0,0,0,0.000003,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-1-min,1,24003,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-1-min,1,24003,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-1-min,1,24004,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-1-min,1,24004,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-1-max,1,24001,1,262144,113996,5220,113136,29792,56340,17376,121524,20084,120536,0.128402,11153872659230279882,281,1,2,12,25640,13892,7568,21696,ok -corner-1-max,1,24001,1,262144,113996,5220,113136,29792,56340,17376,121524,20084,120536,0.095332,11153872659230279882,281,1,2,12,25640,13892,7568,21696,ok -corner-1-max,1,24003,1,262144,111552,5024,110128,35440,56996,16952,120524,22744,118876,0.064198,13304278174034654577,181,15,1,12,18940,17776,8092,21504,ok -corner-1-max,1,24003,1,262144,111552,5024,110128,35440,56996,16952,120524,22744,118876,0.082056,13304278174034654577,181,15,1,12,18940,17776,8092,21504,ok -corner-1-max,1,24004,1,262144,112668,6588,110440,32448,55832,17184,120676,22812,118656,0.074396,765892347145801071,106,1,1,12,23484,14736,7608,21664,ok -corner-1-max,1,24004,1,262144,112668,6588,110440,32448,55832,17184,120676,22812,118656,0.075969,765892347145801071,106,1,1,12,23484,14736,7608,21664,ok -edge-2-width-min,2,24001,1,8192,4472,313,2617,790,3197,2353,4705,705,2782,0.001032,12503860844371180222,0,22,7,3,359,564,254,472,ok -edge-2-width-min,2,24001,1,8192,4472,313,2617,790,3197,2353,4705,705,2782,0.001542,12503860844371180222,0,22,7,3,359,564,254,472,ok -edge-2-width-min,2,24003,1,8192,4383,285,2580,944,3163,2287,4669,753,2770,0.001019,2289046138649135511,650,13,13,4,527,425,174,482,ok -edge-2-width-min,2,24003,1,8192,4383,285,2580,944,3163,2287,4669,753,2770,0.002871,2289046138649135511,650,13,13,4,527,425,174,482,ok -edge-2-width-min,2,24004,1,8192,4460,248,2581,903,3245,2383,4719,696,2777,0.003043,12008136029917599670,469,5,16,4,420,508,193,478,ok -edge-2-width-min,2,24004,1,8192,4460,248,2581,903,3245,2383,4719,696,2777,0.001045,12008136029917599670,469,5,16,4,420,508,193,478,ok -edge-2-width-max,2,24001,1,65536,37588,1941,22369,3638,26676,23371,38639,3757,23140,0.179237,898222588874111289,1231,23,24,4,3715,4034,3058,3843,ok -edge-2-width-max,2,24001,1,65536,37588,1941,22369,3638,26676,23371,38639,3757,23140,0.155979,898222588874111289,1231,23,24,4,3715,4034,3058,3843,ok -edge-2-width-max,2,24003,1,65536,37493,1887,22178,3978,26774,23275,38591,3874,23071,0.133704,7657302466715763635,1260,20,26,4,4216,3547,2853,3813,ok -edge-2-width-max,2,24003,1,65536,37493,1887,22178,3978,26774,23275,38591,3874,23071,0.085862,7657302466715763635,1260,20,26,4,4216,3547,2853,3813,ok -edge-2-width-max,2,24004,1,65536,37637,2411,22604,2884,26389,23282,38567,3843,23126,0.059894,384484046444111817,1346,28,28,1,4031,3714,3402,3783,ok -edge-2-width-max,2,24004,1,65536,37637,2411,22604,2884,26389,23282,38567,3843,23126,0.021655,384484046444111817,1346,28,28,1,4031,3714,3402,3783,ok -edge-2-height-min,2,24001,1,8192,4520,253,2725,694,3273,2398,4740,596,2856,0.002338,21500989661319052,662,15,15,4,328,495,323,472,ok -edge-2-height-min,2,24001,1,8192,4520,253,2725,694,3273,2398,4740,596,2856,0.002602,21500989661319052,662,15,15,4,328,495,323,472,ok -edge-2-height-min,2,24003,1,8192,4618,216,2688,670,3365,2477,4821,548,2823,0.002526,9714228658532047050,0,100000,14,2,490,332,335,459,ok -edge-2-height-min,2,24003,1,8192,4618,216,2688,670,3365,2477,4821,548,2823,0.002060,9714228658532047050,0,100000,14,2,490,332,335,459,ok -edge-2-height-min,2,24004,1,8192,4599,226,2717,650,3215,2238,4796,548,2848,0.001803,3068368816682946139,719,18,7,4,422,535,318,473,ok -edge-2-height-min,2,24004,1,8192,4599,226,2717,650,3215,2238,4796,548,2848,0.002870,3068368816682946139,719,18,7,4,422,535,318,473,ok -edge-2-height-max,2,24001,1,65536,37439,1851,22285,3961,26656,22969,38603,3823,23110,0.025513,3088623821463467421,1053,23,40,3,4024,3721,2938,3837,ok -edge-2-height-max,2,24001,1,65536,37439,1851,22285,3961,26656,22969,38603,3823,23110,0.044465,3088623821463467421,1053,23,40,3,4024,3721,2938,3837,ok -edge-2-height-max,2,24003,1,65536,37331,1747,22148,4310,26658,22598,38577,3899,23060,0.050557,14885947647247422686,1256,29,34,0,3959,3797,2816,3609,ok -edge-2-height-max,2,24003,1,65536,37331,1747,22148,4310,26658,22598,38577,3899,23060,0.106717,14885947647247422686,1256,29,34,0,3959,3797,2816,3609,ok -edge-2-height-max,2,24004,1,65536,37334,2033,22487,3682,26353,22681,38493,3866,23177,0.039131,6600342002624190149,0,21,100000,1,3880,3854,3150,3764,ok -edge-2-height-max,2,24004,1,65536,37334,2033,22487,3682,26353,22681,38493,3866,23177,0.245534,6600342002624190149,0,21,100000,1,3880,3854,3150,3764,ok -edge-2-teams-min,2,24001,1,16384,8946,923,5518,997,6276,4247,9294,1417,5673,0.001784,18091492188118644277,902,1,9,1,1247,899,485,959,ok -edge-2-teams-min,2,24001,1,16384,8946,923,5518,997,6276,4247,9294,1417,5673,0.002718,18091492188118644277,902,1,9,1,1247,899,485,959,ok -edge-2-teams-min,2,24003,1,16384,9552,607,5280,945,7338,5547,9860,1077,5447,0.004448,9744020794824514254,1742,11,13,1,828,883,465,960,ok -edge-2-teams-min,2,24003,1,16384,9552,607,5280,945,7338,5547,9860,1077,5447,0.001560,9744020794824514254,1742,11,13,1,828,883,465,960,ok -edge-2-teams-min,2,24004,1,16384,9018,622,5520,1224,6595,5307,9397,1230,5757,0.001751,11258575627840650753,1106,9,10,1,954,740,700,964,ok -edge-2-teams-min,2,24004,1,16384,9018,622,5520,1224,6595,5307,9397,1230,5757,0.002096,11258575627840650753,1106,9,10,1,954,740,700,964,ok -edge-2-teams-max,2,24001,1,16384,8953,919,5515,997,6176,3965,9301,1413,5670,0.002302,18386146786474578188,0,100000,17,11,1202,869,447,959,ok -edge-2-teams-max,2,24001,1,16384,8953,919,5515,997,6176,3965,9301,1413,5670,0.002343,18386146786474578188,0,100000,17,11,1202,869,447,959,ok -edge-2-teams-max,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.004491,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-2-teams-max,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.004771,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-2-teams-max,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.003091,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-teams-max,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.001614,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-workers-min,2,24001,1,16384,8949,920,5518,997,6268,4190,9297,1414,5673,0.004927,5660897849027081340,320,15,20,4,1210,899,485,959,ok -edge-2-workers-min,2,24001,1,16384,8949,920,5518,997,6268,4190,9297,1414,5673,0.001813,5660897849027081340,320,15,20,4,1210,899,485,959,ok -edge-2-workers-min,2,24003,1,16384,9552,607,5280,945,7362,5545,9860,1077,5447,0.001713,6519285490690037608,602,11,13,4,800,839,465,960,ok -edge-2-workers-min,2,24003,1,16384,9552,607,5280,945,7362,5545,9860,1077,5447,0.023920,6519285490690037608,602,11,13,4,800,839,465,960,ok -edge-2-workers-min,2,24004,1,16384,9030,610,5520,1224,6562,5234,9409,1218,5757,0.001741,14833552175046425086,388,9,10,4,954,737,700,964,ok -edge-2-workers-min,2,24004,1,16384,9030,610,5520,1224,6562,5234,9409,1218,5757,0.001745,14833552175046425086,388,9,10,4,954,737,700,964,ok -edge-2-workers-max,2,24001,1,16384,8949,920,5518,997,6240,4189,9297,1414,5673,0.001947,5660897849027081340,418,15,17,4,1210,899,485,959,ok -edge-2-workers-max,2,24001,1,16384,8949,920,5518,997,6240,4189,9297,1414,5673,0.008564,5660897849027081340,418,15,17,4,1210,899,485,959,ok -edge-2-workers-max,2,24003,1,16384,9552,607,5280,945,7334,5521,9860,1077,5447,0.001743,6519285490690037608,737,10,12,4,800,839,465,960,ok -edge-2-workers-max,2,24003,1,16384,9552,607,5280,945,7334,5521,9860,1077,5447,0.002654,6519285490690037608,737,10,12,4,800,839,465,960,ok -edge-2-workers-max,2,24004,1,16384,9030,610,5520,1224,6534,5207,9409,1218,5757,0.001885,14833552175046425086,488,8,9,4,954,737,700,964,ok -edge-2-workers-max,2,24004,1,16384,9030,610,5520,1224,6534,5207,9409,1218,5757,0.001807,14833552175046425086,488,8,9,4,954,737,700,964,ok -edge-2-water-min,2,24001,1,16384,14609,1028,0,747,14514,13149,14991,1393,0,0.001878,15818806745139821749,1659,100000,100000,0,0,0,6,0,ok -edge-2-water-min,2,24001,1,16384,14609,1028,0,747,14514,13149,14991,1393,0,0.002063,15818806745139821749,1659,100000,100000,0,0,0,6,0,ok -edge-2-water-min,2,24003,1,16384,14653,1155,0,576,14533,13340,14948,1436,0,0.001564,17836359041140578458,1565,100000,100000,0,0,0,15,0,ok -edge-2-water-min,2,24003,1,16384,14653,1155,0,576,14533,13340,14948,1436,0,0.004168,17836359041140578458,1565,100000,100000,0,0,0,15,0,ok -edge-2-water-min,2,24004,1,16384,14196,1348,0,840,14102,12533,14624,1760,0,0.001831,7469588657728267115,1789,100000,100000,0,0,0,0,0,ok -edge-2-water-min,2,24004,1,16384,14196,1348,0,840,14102,12533,14624,1760,0,0.003183,7469588657728267115,1789,100000,100000,0,0,0,0,0,ok -edge-2-water-max,2,24001,1,16384,6173,831,7375,2005,3535,1864,6760,1833,7791,0.002096,17006424890009917153,331,8,14,4,991,928,626,1495,ok -edge-2-water-max,2,24001,1,16384,6173,831,7375,2005,3535,1864,6760,1833,7791,0.001947,17006424890009917153,331,8,14,4,991,928,626,1495,ok -edge-2-water-max,2,24003,1,16384,6199,590,7573,2022,3133,1707,6764,1598,8022,0.001729,1703270134573847676,0,34,6,3,854,1395,724,1490,ok -edge-2-water-max,2,24003,1,16384,6199,590,7573,2022,3133,1707,6764,1598,8022,0.001654,1703270134573847676,0,34,6,3,854,1395,724,1490,ok -edge-2-water-max,2,24004,1,16384,6125,557,8336,1366,3463,1818,6540,1236,8608,0.003470,5844940350594474362,214,14,8,4,619,1167,781,1487,ok -edge-2-water-max,2,24004,1,16384,6125,557,8336,1366,3463,1818,6540,1236,8608,0.001874,5844940350594474362,214,14,8,4,619,1167,781,1487,ok -edge-2-sand-min,2,24001,1,16384,9027,884,5523,950,7164,5282,9351,1353,5680,0.003961,1287794769456463877,918,17,20,4,764,391,614,987,ok -edge-2-sand-min,2,24001,1,16384,9027,884,5523,950,7164,5282,9351,1353,5680,0.004739,1287794769456463877,918,17,20,4,764,391,614,987,ok -edge-2-sand-min,2,24003,1,16384,8866,778,5504,1236,7309,5475,9282,1391,5711,0.001672,15593615391680384401,912,7,13,4,453,396,610,981,ok -edge-2-sand-min,2,24003,1,16384,8866,778,5504,1236,7309,5475,9282,1391,5711,0.001632,15593615391680384401,912,7,13,4,453,396,610,981,ok -edge-2-sand-min,2,24004,1,16384,9022,739,5413,1210,6553,5232,9402,1340,5642,0.001837,1511840691034133524,485,8,9,4,1089,837,454,988,ok -edge-2-sand-min,2,24004,1,16384,9022,739,5413,1210,6553,5232,9402,1340,5642,0.001761,1511840691034133524,485,8,9,4,1089,837,454,988,ok -edge-2-sand-max,2,24001,1,16384,5334,6360,3120,1570,3649,1915,5953,7143,3288,0.001899,13415170006420454930,293,6,10,4,497,478,620,546,ok -edge-2-sand-max,2,24001,1,16384,5334,6360,3120,1570,3649,1915,5953,7143,3288,0.001834,13415170006420454930,293,6,10,4,497,478,620,546,ok -edge-2-sand-max,2,24003,1,16384,5162,6659,3141,1422,4293,2611,5742,7368,3274,0.005413,7498629016206827803,344,100000,100000,0,0,0,776,540,ok -edge-2-sand-max,2,24003,1,16384,5162,6659,3141,1422,4293,2611,5742,7368,3274,0.001899,7498629016206827803,344,100000,100000,0,0,0,776,540,ok -edge-2-sand-max,2,24004,1,16384,5283,6530,3055,1516,3582,1899,5807,7289,3288,0.001881,8936974655475243293,278,13,14,4,429,879,298,534,ok -edge-2-sand-max,2,24004,1,16384,5283,6530,3055,1516,3582,1899,5807,7289,3288,0.001776,8936974655475243293,278,13,14,4,429,879,298,534,ok -edge-2-grass-min,2,24001,0,16384,0,0,16384,0,0,0,602,1671,14111,0.001525,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-2-grass-min,2,24001,0,16384,0,0,16384,0,0,0,602,1671,14111,0.001764,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-2-grass-min,2,24003,1,16384,329,975,13785,1295,21,0,639,1647,14098,0.002534,17907722784531174403,0,100000,100000,0,0,0,228,2336,ok -edge-2-grass-min,2,24003,1,16384,329,975,13785,1295,21,0,639,1647,14098,0.002342,17907722784531174403,0,100000,100000,0,0,0,228,2336,ok -edge-2-grass-min,2,24004,0,16384,0,0,16384,0,0,0,594,2055,13735,0.001598,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-grass-min,2,24004,0,16384,0,0,16384,0,0,0,594,2055,13735,0.091162,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-grass-max,2,24001,1,16384,10045,658,4676,1005,7993,6246,10397,1152,4835,0.003787,15747200618786921993,1153,19,20,4,842,474,642,801,ok -edge-2-grass-max,2,24001,1,16384,10045,658,4676,1005,7993,6246,10397,1152,4835,0.001965,15747200618786921993,1153,19,20,4,842,474,642,801,ok -edge-2-grass-max,2,24003,1,16384,10419,541,4704,720,8382,6634,10649,898,4837,0.013078,4750185470642720124,771,15,13,4,698,693,548,806,ok -edge-2-grass-max,2,24003,1,16384,10419,541,4704,720,8382,6634,10649,898,4837,0.001639,4750185470642720124,771,15,13,4,698,693,548,806,ok -edge-2-grass-max,2,24004,1,16384,10329,362,4495,1198,8179,7021,10683,958,4743,0.001822,2892416573280956093,1021,11,20,4,1004,585,472,785,ok -edge-2-grass-max,2,24004,1,16384,10329,362,4495,1198,8179,7021,10683,958,4743,0.001850,2892416573280956093,1021,11,20,4,1004,585,472,785,ok -edge-2-desert-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.002526,5660897849027081340,418,15,17,4,1210,899,485,959,ok -edge-2-desert-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.003701,5660897849027081340,418,15,17,4,1210,899,485,959,ok -edge-2-desert-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.001695,6519285490690037608,723,11,13,4,800,839,465,960,ok -edge-2-desert-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.001638,6519285490690037608,723,11,13,4,800,839,465,960,ok -edge-2-desert-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.001811,14833552175046425086,492,9,10,4,954,737,700,964,ok -edge-2-desert-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.001832,14833552175046425086,492,9,10,4,954,737,700,964,ok -edge-2-desert-max,2,24001,1,16384,3921,7965,3006,1492,2507,982,4500,8716,3168,0.003974,5497211519881356870,86,11,39,3,765,267,289,536,ok -edge-2-desert-max,2,24001,1,16384,3921,7965,3006,1492,2507,982,4500,8716,3168,0.002214,5497211519881356870,86,11,39,3,765,267,289,536,ok -edge-2-desert-max,2,24003,1,16384,4122,7759,3085,1418,2558,1182,4698,8471,3215,0.001557,3480572482632351213,202,42,42,2,504,566,390,540,ok -edge-2-desert-max,2,24003,1,16384,4122,7759,3085,1418,2558,1182,4698,8471,3215,0.004231,3480572482632351213,202,42,42,2,504,566,390,540,ok -edge-2-desert-max,2,24004,1,16384,4737,7281,2882,1484,3435,1841,5247,8026,3111,0.001872,1099917036119030571,197,2,12,4,577,412,210,534,ok -edge-2-desert-max,2,24004,1,16384,4737,7281,2882,1484,3435,1841,5247,8026,3111,0.002180,1099917036119030571,197,2,12,4,577,412,210,534,ok -edge-2-smoothing-min,2,24001,1,16384,9048,666,5536,1134,6861,4387,9468,1220,5696,0.001960,2613473709282920172,630,12,12,4,728,696,667,958,ok -edge-2-smoothing-min,2,24001,1,16384,9048,666,5536,1134,6861,4387,9468,1220,5696,0.001791,2613473709282920172,630,12,12,4,728,696,667,958,ok -edge-2-smoothing-min,2,24003,1,16384,9245,449,5296,1394,6634,3921,9726,1134,5524,0.001587,66553570091746125,511,4,4,4,841,975,690,958,ok -edge-2-smoothing-min,2,24003,1,16384,9245,449,5296,1394,6634,3921,9726,1134,5524,0.001617,66553570091746125,511,4,4,4,841,975,690,958,ok -edge-2-smoothing-min,2,24004,1,16384,9166,422,5510,1286,6625,5123,9579,1056,5749,0.028921,14894153078089829296,804,8,9,4,1029,723,700,962,ok -edge-2-smoothing-min,2,24004,1,16384,9166,422,5510,1286,6625,5123,9579,1056,5749,0.004390,14894153078089829296,804,8,9,4,1029,723,700,962,ok -edge-2-smoothing-max,2,24001,1,16384,9387,615,5534,848,6629,5251,9657,1036,5691,0.052779,16332416003315470351,0,23,100000,3,933,1036,689,959,ok -edge-2-smoothing-max,2,24001,1,16384,9387,615,5534,848,6629,5251,9657,1036,5691,0.001801,16332416003315470351,0,23,100000,3,933,1036,689,959,ok -edge-2-smoothing-max,2,24003,1,16384,9051,873,5496,964,6949,5555,9361,1352,5671,0.001729,10575936385512013596,434,13,14,4,849,777,378,954,ok -edge-2-smoothing-max,2,24003,1,16384,9051,873,5496,964,6949,5555,9361,1352,5671,0.001665,10575936385512013596,434,13,14,4,849,777,378,954,ok -edge-2-smoothing-max,2,24004,0,16384,0,0,16384,0,0,0,9489,1140,5755,0.001620,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-smoothing-max,2,24004,0,16384,0,0,16384,0,0,0,9489,1140,5755,0.097096,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-river-width-min,2,24001,1,16384,9047,976,4395,1966,7052,4809,9643,1957,4784,0.001543,995488406803712075,0,7,100000,3,672,470,755,961,ok -edge-2-river-width-min,2,24001,1,16384,9047,976,4395,1966,7052,4809,9643,1957,4784,0.001637,995488406803712075,0,7,100000,3,672,470,755,961,ok -edge-2-river-width-min,2,24003,1,16384,8950,938,4380,2116,6847,4524,9574,1993,4817,0.001492,14309378745742228429,597,9,16,4,548,762,697,961,ok -edge-2-river-width-min,2,24003,1,16384,8950,938,4380,2116,6847,4524,9574,1993,4817,0.001798,14309378745742228429,597,9,16,4,548,762,697,961,ok -edge-2-river-width-min,2,24004,1,16384,8871,743,4938,1832,7021,4914,9428,1653,5303,0.004875,17138623470901157076,407,13,8,4,761,643,346,965,ok -edge-2-river-width-min,2,24004,1,16384,8871,743,4938,1832,7021,4914,9428,1653,5303,0.001867,17138623470901157076,407,13,8,4,761,643,346,965,ok -edge-2-river-width-max,2,24001,1,16384,9270,595,5649,870,6418,5302,9554,1026,5804,0.002624,13040961475971353362,1330,19,38,3,969,967,815,951,ok -edge-2-river-width-max,2,24001,1,16384,9270,595,5649,870,6418,5302,9554,1026,5804,0.004005,13040961475971353362,1330,19,38,3,969,967,815,951,ok -edge-2-river-width-max,2,24003,1,16384,9304,653,5671,756,6441,5446,9555,1026,5803,0.002171,4763919030790921477,496,28,28,1,881,1058,819,961,ok -edge-2-river-width-max,2,24003,1,16384,9304,653,5671,756,6441,5446,9555,1026,5803,0.002442,4763919030790921477,496,28,28,1,881,1058,819,961,ok -edge-2-river-width-max,2,24004,0,16384,0,0,16384,0,0,0,9501,1120,5763,0.002452,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-river-width-max,2,24004,0,16384,0,0,16384,0,0,0,9501,1120,5763,0.003508,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-fruit-min,2,24001,1,16384,9605,382,5629,768,7199,5630,9817,765,5802,0.001847,7176297995739189086,667,16,12,4,815,758,753,997,ok -edge-2-fruit-min,2,24001,1,16384,9605,382,5629,768,7199,5630,9817,765,5802,0.001914,7176297995739189086,667,16,12,4,815,758,753,997,ok -edge-2-fruit-min,2,24003,0,16384,0,0,16384,0,0,0,9815,858,5711,0.001389,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-2-fruit-min,2,24003,0,16384,0,0,16384,0,0,0,9815,858,5711,0.002189,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-2-fruit-min,2,24004,0,16384,0,0,16384,0,0,0,9918,513,5953,0.029737,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-fruit-min,2,24004,0,16384,0,0,16384,0,0,0,9918,513,5953,0.002514,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-2-fruit-max,2,24001,1,16384,5156,5803,3755,1670,3094,1029,5835,6636,3913,0.002021,3788484734914861314,80,24,38,3,803,411,467,651,ok -edge-2-fruit-max,2,24001,1,16384,5156,5803,3755,1670,3094,1029,5835,6636,3913,0.002181,3788484734914861314,80,24,38,3,803,411,467,651,ok -edge-2-fruit-max,2,24003,1,16384,5437,5607,3794,1546,3664,1417,6079,6379,3926,0.001584,5260591269051681174,299,20,30,4,489,416,507,654,ok -edge-2-fruit-max,2,24003,1,16384,5437,5607,3794,1546,3664,1417,6079,6379,3926,0.004311,5260591269051681174,299,20,30,4,489,416,507,654,ok -edge-2-fruit-max,2,24004,1,16384,5728,5261,3557,1838,3774,1339,6412,6180,3792,0.001967,210623947230701153,141,26,28,3,781,486,299,651,ok -edge-2-fruit-max,2,24004,1,16384,5728,5261,3557,1838,3774,1339,6412,6180,3792,0.002174,210623947230701153,141,26,28,3,781,486,299,651,ok -edge-2-repeat-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.001920,5660897849027081340,418,15,17,4,1210,899,485,959,ok -edge-2-repeat-min,2,24001,1,16384,8949,920,5518,997,6256,4190,9297,1414,5673,0.001904,5660897849027081340,418,15,17,4,1210,899,485,959,ok -edge-2-repeat-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.003039,6519285490690037608,723,11,13,4,800,839,465,960,ok -edge-2-repeat-min,2,24003,1,16384,9552,607,5280,945,7350,5537,9860,1077,5447,0.013182,6519285490690037608,723,11,13,4,800,839,465,960,ok -edge-2-repeat-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.004103,14833552175046425086,492,9,10,4,954,737,700,964,ok -edge-2-repeat-min,2,24004,1,16384,9030,610,5520,1224,6550,5225,9409,1218,5757,0.001836,14833552175046425086,492,9,10,4,954,737,700,964,ok -edge-2-repeat-max,2,24001,1,16384,7104,480,3680,5120,4860,1120,8608,3008,4768,0.000539,15024589055358181125,70,1,2,4,448,828,416,800,ok -edge-2-repeat-max,2,24001,1,16384,7104,480,3680,5120,4860,1120,8608,3008,4768,0.000517,15024589055358181125,70,1,2,4,448,828,416,800,ok -edge-2-repeat-max,2,24003,1,16384,7104,416,3552,5312,4904,600,8640,3040,4704,0.000494,5590689445227312896,30,2,2,4,596,600,512,928,ok -edge-2-repeat-max,2,24003,1,16384,7104,416,3552,5312,4904,600,8640,3040,4704,0.001229,5590689445227312896,30,2,2,4,596,600,512,928,ok -edge-2-repeat-max,2,24004,1,16384,7520,576,3616,4672,4596,608,8768,2912,4704,0.000511,17324586385127991603,37,1,1,4,576,1308,352,896,ok -edge-2-repeat-max,2,24004,1,16384,7520,576,3616,4672,4596,608,8768,2912,4704,0.001039,17324586385127991603,37,1,1,4,576,1308,352,896,ok -size-2-6-6-4,2,24001,1,4096,2214,141,1319,422,1504,786,2340,350,1406,0.000527,12702816318780975272,171,5,14,4,396,110,114,238,ok -size-2-6-6-4,2,24001,1,4096,2214,141,1319,422,1504,786,2340,350,1406,0.000517,12702816318780975272,171,5,14,4,396,110,114,238,ok -size-2-6-6-4,2,24003,1,4096,2208,132,1288,468,1559,927,2344,366,1386,0.001279,3938647859703722544,247,8,8,4,183,251,115,234,ok -size-2-6-6-4,2,24003,1,4096,2208,132,1288,468,1559,927,2344,366,1386,0.000519,3938647859703722544,247,8,8,4,183,251,115,234,ok -size-2-6-6-4,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.001112,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-2-6-6-4,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.000359,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-2-6-6-12,2,24001,0,4096,0,0,4096,0,0,0,2340,350,1406,0.000469,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-2-6-6-12,2,24001,0,4096,0,0,4096,0,0,0,2340,350,1406,0.000448,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-2-6-6-12,2,24003,0,4096,0,0,4096,0,0,0,2344,366,1386,0.000368,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-2-6-6-12,2,24003,0,4096,0,0,4096,0,0,0,2344,366,1386,0.000380,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-2-6-6-12,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.001593,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-2-6-6-12,2,24004,0,4096,0,0,4096,0,0,0,2393,373,1330,0.001113,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-2-7-7-8,2,24001,1,16384,8949,920,5518,997,6208,4023,9297,1414,5673,0.003702,16617434980050527102,0,100000,17,7,1213,897,452,959,ok -size-2-7-7-8,2,24001,1,16384,8949,920,5518,997,6208,4023,9297,1414,5673,0.005853,16617434980050527102,0,100000,17,7,1213,897,452,959,ok -size-2-7-7-8,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.001526,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -size-2-7-7-8,2,24003,0,16384,0,0,16384,0,0,0,9860,1077,5447,0.026580,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -size-2-7-7-8,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.001863,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -size-2-7-7-8,2,24004,0,16384,0,0,16384,0,0,0,9397,1230,5757,0.006247,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -size-2-6-9-12,2,24001,1,32768,17769,892,10374,3733,13014,9371,18891,2748,11129,0.070280,11686570519299650906,628,25,24,11,1846,1898,752,1930,ok -size-2-6-9-12,2,24001,1,32768,17769,892,10374,3733,13014,9371,18891,2748,11129,0.050174,11686570519299650906,628,25,24,11,1846,1898,752,1930,ok -size-2-6-9-12,2,24003,1,32768,17580,1137,9993,4058,13138,9747,18693,3161,10914,0.033997,3187380565725862322,511,22,18,12,1913,1783,485,1913,ok -size-2-6-9-12,2,24003,1,32768,17580,1137,9993,4058,13138,9747,18693,3161,10914,0.068471,3187380565725862322,511,22,18,12,1913,1783,485,1913,ok -size-2-6-9-12,2,24004,1,32768,17767,1012,10224,3765,13087,9517,18840,2887,11041,0.197296,4914701710707839179,760,20,20,12,1895,1868,661,1807,ok -size-2-6-9-12,2,24004,1,32768,17767,1012,10224,3765,13087,9517,18840,2887,11041,0.022514,4914701710707839179,760,20,20,12,1895,1868,661,1807,ok -size-2-9-6-12,2,24001,1,32768,18175,731,10642,3220,12980,9656,19142,2339,11287,0.044046,14530006048841308419,250,14,16,12,1925,1919,1087,1800,ok -size-2-9-6-12,2,24001,1,32768,18175,731,10642,3220,12980,9656,19142,2339,11287,0.021462,14530006048841308419,250,14,16,12,1925,1919,1087,1800,ok -size-2-9-6-12,2,24003,1,32768,17666,1085,10025,3992,13142,9649,18745,3081,10942,0.084821,15235139514388628623,596,15,12,12,1830,1875,567,1779,ok -size-2-9-6-12,2,24003,1,32768,17666,1085,10025,3992,13142,9649,18745,3081,10942,0.076352,15235139514388628623,596,15,12,12,1830,1875,567,1779,ok -size-2-9-6-12,2,24004,1,32768,17915,956,10413,3484,13062,9824,18905,2695,11168,0.076457,17904064177986095803,611,15,18,12,1879,1960,765,1866,ok -size-2-9-6-12,2,24004,1,32768,17915,956,10413,3484,13062,9824,18905,2695,11168,0.070326,17904064177986095803,611,15,18,12,1879,1960,765,1866,ok -size-2-9-9-12,2,24001,1,262144,149100,12955,91905,8184,109726,85886,152601,16958,92585,0.848434,9572728202868954291,0,100000,100000,7,14454,13999,10668,15420,ok -size-2-9-9-12,2,24001,1,262144,149100,12955,91905,8184,109726,85886,152601,16958,92585,0.701838,9572728202868954291,0,100000,100000,7,14454,13999,10668,15420,ok -size-2-9-9-12,2,24003,1,262144,147869,13206,89841,11228,103729,78628,152140,18741,91263,0.598385,10176986978518237311,522,14,13,12,16701,16929,10248,15436,ok -size-2-9-9-12,2,24003,1,262144,147869,13206,89841,11228,103729,78628,152140,18741,91263,0.744190,10176986978518237311,522,14,13,12,16701,16929,10248,15436,ok -size-2-9-9-12,2,24004,1,262144,150768,12248,91836,7292,106434,95707,153413,15855,92876,0.844746,6462093021749933095,1443,51,52,2,14398,15267,14415,15377,ok -size-2-9-9-12,2,24004,1,262144,150768,12248,91836,7292,106434,95707,153413,15855,92876,0.854000,6462093021749933095,1443,51,52,2,14398,15267,14415,15377,ok -corner-2-min,2,24001,0,0,0,0,0,0,0,0,0,0,0,0.000009,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-2-min,2,24001,0,0,0,0,0,0,0,0,0,0,0,0.000011,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-2-min,2,24003,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-2-min,2,24003,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-2-min,2,24004,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-2-min,2,24004,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-2-max,2,24001,1,262144,47952,136696,52192,25304,17240,224,56348,149348,56448,0.011546,11845766406100531494,0,1,1,0,6924,11520,4992,9280,ok -corner-2-max,2,24001,1,262144,47952,136696,52192,25304,17240,224,56348,149348,56448,0.076648,11845766406100531494,0,1,1,0,6924,11520,4992,9280,ok -corner-2-max,2,24003,1,262144,48208,137304,51392,25240,15364,96,56604,149924,55616,0.100834,6995744940389614058,0,2,1,0,7984,10620,5600,8992,ok -corner-2-max,2,24003,1,262144,48208,137304,51392,25240,15364,96,56604,149924,55616,0.070179,6995744940389614058,0,2,1,0,7984,10620,5600,8992,ok -corner-2-max,2,24004,1,262144,48112,137336,51520,25176,15408,224,56476,149924,55744,0.245977,6866782697663556605,5,1,2,0,10300,9036,4480,8896,ok -corner-2-max,2,24004,1,262144,48112,137336,51520,25176,15408,224,56476,149924,55744,0.033469,6866782697663556605,5,1,2,0,10300,9036,4480,8896,ok -edge-3-width-min,3,24001,1,8192,3779,316,2579,1518,2694,1583,4175,1072,2945,0.002238,5528040763038411418,317,9,17,4,425,411,150,546,ok -edge-3-width-min,3,24001,1,8192,3779,316,2579,1518,2694,1583,4175,1072,2945,0.000858,5528040763038411418,317,9,17,4,425,411,150,546,ok -edge-3-width-min,3,24003,1,8192,3777,357,2576,1482,2693,1601,4156,1096,2940,0.000876,16057670206668850732,83,8,19,4,474,325,189,546,ok -edge-3-width-min,3,24003,1,8192,3777,357,2576,1482,2693,1601,4156,1096,2940,0.000895,16057670206668850732,83,8,19,4,474,325,189,546,ok -edge-3-width-min,3,24004,1,8192,3781,319,2604,1488,2707,1578,4170,1060,2962,0.001447,10194287565770016619,221,12,11,4,433,399,151,545,ok -edge-3-width-min,3,24004,1,8192,3781,319,2604,1488,2707,1578,4170,1060,2962,0.002381,10194287565770016619,221,12,11,4,433,399,151,545,ok -edge-3-width-max,3,24001,1,65536,34383,1840,24878,4435,22401,19171,35580,4055,25901,0.144345,1121796287204890133,1175,26,27,3,4614,4122,3144,4371,ok -edge-3-width-max,3,24001,1,65536,34383,1840,24878,4435,22401,19171,35580,4055,25901,0.077997,1121796287204890133,1175,26,27,3,4614,4122,3144,4371,ok -edge-3-width-max,3,24003,1,65536,34350,1826,24873,4487,22446,19094,35550,4065,25921,0.006762,15420516851104356162,1205,30,31,3,4581,4180,3048,4376,ok -edge-3-width-max,3,24003,1,65536,34350,1826,24873,4487,22446,19094,35550,4065,25921,0.006519,15420516851104356162,1205,30,31,3,4581,4180,3048,4376,ok -edge-3-width-max,3,24004,1,65536,34467,1809,24982,4278,22472,19480,35608,3946,25982,0.078431,9783622262180087610,1119,37,38,2,4933,3799,3157,4337,ok -edge-3-width-max,3,24004,1,65536,34467,1809,24982,4278,22472,19480,35608,3946,25982,0.015036,9783622262180087610,1119,37,38,2,4933,3799,3157,4337,ok -edge-3-height-min,3,24001,1,8192,3781,330,2595,1486,2692,1543,4171,1069,2952,0.002316,8296798997807980933,211,15,8,4,378,459,155,545,ok -edge-3-height-min,3,24001,1,8192,3781,330,2595,1486,2692,1543,4171,1069,2952,0.000926,8296798997807980933,211,15,8,4,378,459,155,545,ok -edge-3-height-min,3,24003,1,8192,3777,340,2615,1460,2674,1588,4159,1068,2965,0.002542,3287260318131999003,201,18,10,4,361,496,146,543,ok -edge-3-height-min,3,24003,1,8192,3777,340,2615,1460,2674,1588,4159,1068,2965,0.002197,3287260318131999003,201,18,10,4,361,496,146,543,ok -edge-3-height-min,3,24004,1,8192,3747,304,2585,1556,2674,1470,4161,1077,2954,0.000893,8306510028419146177,130,11,8,4,362,437,182,545,ok -edge-3-height-min,3,24004,1,8192,3747,304,2585,1556,2674,1470,4161,1077,2954,0.003821,8306510028419146177,130,11,8,4,362,437,182,545,ok -edge-3-height-max,3,24001,1,65536,34189,2110,24709,4528,22420,19210,35400,4370,25766,0.007522,14451902555196200460,1362,31,49,1,3801,4953,2913,4377,ok -edge-3-height-max,3,24001,1,65536,34189,2110,24709,4528,22420,19210,35400,4370,25766,0.011496,14451902555196200460,1362,31,49,1,3801,4953,2913,4377,ok -edge-3-height-max,3,24003,1,65536,34397,1997,24946,4196,22518,19529,35501,4093,25942,0.009812,4567928969128129527,1060,46,20,3,4720,3986,3075,4345,ok -edge-3-height-max,3,24003,1,65536,34397,1997,24946,4196,22518,19529,35501,4093,25942,0.007646,4567928969128129527,1060,46,20,3,4720,3986,3075,4345,ok -edge-3-height-max,3,24004,1,65536,34403,1989,24918,4226,22434,19318,35539,4099,25898,0.005880,16325195449582695868,1130,52,29,3,4891,3848,3136,4354,ok -edge-3-height-max,3,24004,1,65536,34403,1989,24918,4226,22434,19318,35539,4099,25898,0.006299,16325195449582695868,1130,52,29,3,4891,3848,3136,4354,ok -edge-3-teams-min,3,24001,1,16384,8637,466,6301,980,5592,4748,8908,955,6521,0.001594,15467076962016452729,1415,14,39,0,1146,1053,810,1094,ok -edge-3-teams-min,3,24001,1,16384,8637,466,6301,980,5592,4748,8908,955,6521,0.001584,15467076962016452729,1415,14,39,0,1146,1053,810,1094,ok -edge-3-teams-min,3,24003,1,16384,8642,446,6268,1028,5596,4769,8922,959,6503,0.001588,16213537115684893476,1725,21,37,0,1149,1050,811,1081,ok -edge-3-teams-min,3,24003,1,16384,8642,446,6268,1028,5596,4769,8922,959,6503,0.003589,16213537115684893476,1725,21,37,0,1149,1050,811,1081,ok -edge-3-teams-min,3,24004,1,16384,8636,448,6262,1038,5595,4741,8919,966,6499,0.001615,15389565672406902104,1193,31,8,0,1167,1017,817,1085,ok -edge-3-teams-min,3,24004,1,16384,8636,448,6262,1038,5595,4741,8919,966,6499,0.001595,15389565672406902104,1193,31,8,0,1167,1017,817,1085,ok -edge-3-teams-max,3,24001,1,16384,7305,663,4863,3553,5349,2758,8215,2433,5736,0.001878,15526420115868331245,54,9,13,12,676,784,246,1093,ok -edge-3-teams-max,3,24001,1,16384,7305,663,4863,3553,5349,2758,8215,2433,5736,0.001959,15526420115868331245,54,9,13,12,676,784,246,1093,ok -edge-3-teams-max,3,24003,1,16384,7240,720,4857,3567,5297,2654,8166,2495,5723,0.001934,9678267356762982999,113,8,13,12,674,767,246,1087,ok -edge-3-teams-max,3,24003,1,16384,7240,720,4857,3567,5297,2654,8166,2495,5723,0.001852,9678267356762982999,113,8,13,12,674,767,246,1087,ok -edge-3-teams-max,3,24004,1,16384,7189,643,4847,3705,5236,2467,8154,2487,5743,0.001940,17741115356312039064,62,10,11,12,644,728,318,1079,ok -edge-3-teams-max,3,24004,1,16384,7189,643,4847,3705,5236,2467,8154,2487,5743,0.026656,17741115356312039064,62,10,11,12,644,728,318,1079,ok -edge-3-workers-min,3,24001,1,16384,7907,530,5612,2335,5485,3698,8532,1694,6158,0.001681,12754827918508373822,536,13,13,4,1123,743,471,1084,ok -edge-3-workers-min,3,24001,1,16384,7907,530,5612,2335,5485,3698,8532,1694,6158,0.001573,12754827918508373822,536,13,13,4,1123,743,471,1084,ok -edge-3-workers-min,3,24003,1,16384,8072,494,5698,2120,5526,3950,8630,1551,6203,0.001615,13384879564688048696,682,20,16,4,1080,957,422,1089,ok -edge-3-workers-min,3,24003,1,16384,8072,494,5698,2120,5526,3950,8630,1551,6203,0.004522,13384879564688048696,682,20,16,4,1080,957,422,1089,ok -edge-3-workers-min,3,24004,1,16384,8056,534,5672,2122,5550,4015,8607,1593,6184,0.001584,7198112912547603041,629,13,19,4,1071,918,427,1084,ok -edge-3-workers-min,3,24004,1,16384,8056,534,5672,2122,5550,4015,8607,1593,6184,0.003620,7198112912547603041,629,13,19,4,1071,918,427,1084,ok -edge-3-workers-max,3,24001,1,16384,7907,530,5612,2335,5457,3658,8532,1694,6158,0.001616,12754827918508373822,526,12,12,4,1123,743,471,1084,ok -edge-3-workers-max,3,24001,1,16384,7907,530,5612,2335,5457,3658,8532,1694,6158,0.001508,12754827918508373822,526,12,12,4,1123,743,471,1084,ok -edge-3-workers-max,3,24003,1,16384,8072,494,5698,2120,5498,3919,8630,1551,6203,0.002459,13384879564688048696,786,20,16,4,1080,957,422,1089,ok -edge-3-workers-max,3,24003,1,16384,8072,494,5698,2120,5498,3919,8630,1551,6203,0.001559,13384879564688048696,786,20,16,4,1080,957,422,1089,ok -edge-3-workers-max,3,24004,1,16384,8056,534,5672,2122,5522,3987,8607,1593,6184,0.004305,7198112912547603041,778,12,18,4,1071,918,427,1084,ok -edge-3-workers-max,3,24004,1,16384,8056,534,5672,2122,5522,3987,8607,1593,6184,0.002560,7198112912547603041,778,12,18,4,1071,918,427,1084,ok -edge-3-water-min,3,24001,1,16384,14576,1038,0,770,14469,12851,14986,1398,0,0.004253,7011207854252150283,1735,100000,100000,0,0,0,2,0,ok -edge-3-water-min,3,24001,1,16384,14576,1038,0,770,14469,12851,14986,1398,0,0.001609,7011207854252150283,1735,100000,100000,0,0,0,2,0,ok -edge-3-water-min,3,24003,1,16384,14496,966,0,922,14387,12555,14989,1395,0,0.001492,17813000635001715956,1796,100000,100000,0,0,0,4,0,ok -edge-3-water-min,3,24003,1,16384,14496,966,0,922,14387,12555,14989,1395,0,0.001513,17813000635001715956,1796,100000,100000,0,0,0,4,0,ok -edge-3-water-min,3,24004,1,16384,14600,1066,0,718,14503,12983,14985,1399,0,0.003047,12600054127630809182,1888,100000,100000,0,0,0,3,0,ok -edge-3-water-min,3,24004,1,16384,14600,1066,0,718,14503,12983,14985,1399,0,0.005787,12600054127630809182,1888,100000,100000,0,0,0,3,0,ok -edge-3-water-max,3,24001,1,16384,5858,399,8156,1971,3175,1730,6382,1380,8622,0.003365,3028610027892560003,246,8,12,4,1157,891,527,1485,ok -edge-3-water-max,3,24001,1,16384,5858,399,8156,1971,3175,1730,6382,1380,8622,0.001580,3028610027892560003,246,8,12,4,1157,891,527,1485,ok -edge-3-water-max,3,24003,1,16384,5943,404,8209,1828,3199,1924,6425,1315,8644,0.004482,16231263052623910756,392,16,7,4,1151,1013,477,1496,ok -edge-3-water-max,3,24003,1,16384,5943,404,8209,1828,3199,1924,6425,1315,8644,0.005360,16231263052623910756,392,16,7,4,1151,1013,477,1496,ok -edge-3-water-max,3,24004,1,16384,5945,436,8183,1820,3262,2071,6413,1344,8627,0.016115,4402223064569854256,276,11,16,4,1265,865,461,1481,ok -edge-3-water-max,3,24004,1,16384,5945,436,8183,1820,3262,2071,6413,1344,8627,0.002601,4402223064569854256,276,11,16,4,1265,865,461,1481,ok -edge-3-sand-min,3,24001,1,16384,7860,728,5467,2329,5608,3830,8480,1889,6015,0.001659,2946918700507912519,378,6,14,4,1061,716,378,1114,ok -edge-3-sand-min,3,24001,1,16384,7860,728,5467,2329,5608,3830,8480,1889,6015,0.001558,2946918700507912519,378,6,14,4,1061,716,378,1114,ok -edge-3-sand-min,3,24003,1,16384,7979,708,5569,2128,5629,4034,8540,1769,6075,0.006882,17593408615499011249,766,20,16,4,1039,901,311,1114,ok -edge-3-sand-min,3,24003,1,16384,7979,708,5569,2128,5629,4034,8540,1769,6075,0.001581,17593408615499011249,766,20,16,4,1039,901,311,1114,ok -edge-3-sand-min,3,24004,1,16384,7988,730,5526,2140,5682,4139,8542,1798,6044,0.002828,7014644291778475253,896,13,20,4,1042,848,314,1110,ok -edge-3-sand-min,3,24004,1,16384,7988,730,5526,2140,5682,4139,8542,1798,6044,0.004365,7014644291778475253,896,13,20,4,1042,848,314,1110,ok -edge-3-sand-max,3,24001,1,16384,4786,6211,3229,2158,3165,1827,5253,7287,3844,0.001445,2135500165035206320,243,6,11,4,652,555,324,636,ok -edge-3-sand-max,3,24001,1,16384,4786,6211,3229,2158,3165,1827,5253,7287,3844,0.002860,2135500165035206320,243,6,11,4,652,555,324,636,ok -edge-3-sand-max,3,24003,1,16384,4838,6285,3261,2000,3158,1939,5262,7282,3840,0.005026,1391947087025844220,315,14,15,4,668,594,309,640,ok -edge-3-sand-max,3,24003,1,16384,4838,6285,3261,2000,3158,1939,5262,7282,3840,0.001508,1391947087025844220,315,14,15,4,668,594,309,640,ok -edge-3-sand-max,3,24004,1,16384,4864,6272,3266,1982,3232,2123,5273,7262,3849,0.001480,14692715695912506646,246,13,18,4,765,468,316,632,ok -edge-3-sand-max,3,24004,1,16384,4864,6272,3266,1982,3232,2123,5273,7262,3849,0.001542,14692715695912506646,246,13,18,4,765,468,316,632,ok -edge-3-grass-min,3,24001,0,16384,0,0,16384,0,0,0,8,1847,14529,0.001211,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-3-grass-min,3,24001,0,16384,0,0,16384,0,0,0,8,1847,14529,0.003670,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-3-grass-min,3,24003,0,16384,0,0,16384,0,0,0,10,1844,14530,0.003749,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-3-grass-min,3,24003,0,16384,0,0,16384,0,0,0,10,1844,14530,0.001201,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-3-grass-min,3,24004,0,16384,0,0,16384,0,0,0,5,1849,14530,0.001299,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-3-grass-min,3,24004,0,16384,0,0,16384,0,0,0,5,1849,14530,0.001169,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-3-grass-max,3,24001,1,16384,8963,512,4485,2424,7148,5252,9604,1721,5059,0.001550,3006635018881676965,822,14,14,4,764,623,331,926,ok -edge-3-grass-max,3,24001,1,16384,8963,512,4485,2424,7148,5252,9604,1721,5059,0.004568,3006635018881676965,822,14,14,4,764,623,331,926,ok -edge-3-grass-max,3,24003,1,16384,9100,481,4575,2228,7153,5436,9683,1592,5109,0.001615,16155656616633487546,658,15,17,4,829,701,313,920,ok -edge-3-grass-max,3,24003,1,16384,9100,481,4575,2228,7153,5436,9683,1592,5109,0.001576,16155656616633487546,658,15,17,4,829,701,313,920,ok -edge-3-grass-max,3,24004,1,16384,9109,509,4548,2218,7201,5579,9679,1616,5089,0.002959,3721709725734800813,1081,14,22,4,874,637,306,914,ok -edge-3-grass-max,3,24004,1,16384,9109,509,4548,2218,7201,5579,9679,1616,5089,0.001577,3721709725734800813,1081,14,22,4,874,637,306,914,ok -edge-3-desert-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.039450,12754827918508373822,533,13,13,4,1123,743,471,1084,ok -edge-3-desert-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.002335,12754827918508373822,533,13,13,4,1123,743,471,1084,ok -edge-3-desert-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.004556,13384879564688048696,793,20,16,4,1080,957,422,1089,ok -edge-3-desert-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.001608,13384879564688048696,793,20,16,4,1080,957,422,1089,ok -edge-3-desert-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001627,7198112912547603041,779,13,19,4,1071,918,427,1084,ok -edge-3-desert-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.002638,7198112912547603041,779,13,19,4,1071,918,427,1084,ok -edge-3-desert-max,3,24001,1,16384,3516,7008,2560,3300,2544,330,4563,8656,3165,0.001557,17078476210265677871,18,14,17,4,363,322,191,625,ok -edge-3-desert-max,3,24001,1,16384,3516,7008,2560,3300,2544,330,4563,8656,3165,0.004225,17078476210265677871,18,14,17,4,363,322,191,625,ok -edge-3-desert-max,3,24003,1,16384,3663,6983,2619,3119,2738,292,4645,8543,3196,0.005704,6493544119451687889,26,23,22,4,351,337,140,620,ok -edge-3-desert-max,3,24003,1,16384,3663,6983,2619,3119,2738,292,4645,8543,3196,0.001433,6493544119451687889,26,23,22,4,351,337,140,620,ok -edge-3-desert-max,3,24004,1,16384,3665,6985,2566,3168,2709,382,4659,8566,3159,0.001549,18152073624814054768,5,10,14,3,463,247,144,620,ok -edge-3-desert-max,3,24004,1,16384,3665,6985,2566,3168,2709,382,4659,8566,3159,0.001518,18152073624814054768,5,10,14,3,463,247,144,620,ok -edge-3-smoothing-min,3,24001,1,16384,7907,542,5579,2356,5471,3674,8540,1717,6127,0.001556,2522226670394524074,547,13,10,4,1241,666,427,1087,ok -edge-3-smoothing-min,3,24001,1,16384,7907,542,5579,2356,5471,3674,8540,1717,6127,0.002024,2522226670394524074,547,13,10,4,1241,666,427,1087,ok -edge-3-smoothing-min,3,24003,1,16384,7989,518,5671,2206,5482,3837,8579,1616,6189,0.002766,5300193696107674525,752,13,13,4,1018,979,407,1093,ok -edge-3-smoothing-min,3,24003,1,16384,7989,518,5671,2206,5482,3837,8579,1616,6189,0.001552,5300193696107674525,752,13,13,4,1018,979,407,1093,ok -edge-3-smoothing-min,3,24004,1,16384,8063,522,5637,2162,5529,3912,8632,1600,6152,0.001673,17631944061623576957,765,12,19,4,1050,947,435,1080,ok -edge-3-smoothing-min,3,24004,1,16384,8063,522,5637,2162,5529,3912,8632,1600,6152,0.001889,17631944061623576957,765,12,19,4,1050,947,435,1080,ok -edge-3-smoothing-max,3,24001,1,16384,7920,539,5610,2315,5478,3686,8550,1692,6142,0.003520,11141673957128682515,426,8,11,4,1187,698,460,1093,ok -edge-3-smoothing-max,3,24001,1,16384,7920,539,5610,2315,5478,3686,8550,1692,6142,0.001992,11141673957128682515,426,8,11,4,1187,698,460,1093,ok -edge-3-smoothing-max,3,24003,1,16384,8048,501,5697,2138,5478,3868,8620,1567,6197,0.005673,2630897386024857052,604,20,12,4,1030,1024,418,1086,ok -edge-3-smoothing-max,3,24003,1,16384,8048,501,5697,2138,5478,3868,8620,1567,6197,0.118373,2630897386024857052,604,20,12,4,1030,1024,418,1086,ok -edge-3-smoothing-max,3,24004,1,16384,8082,487,5727,2088,5517,3957,8638,1528,6218,0.001557,9305954735862488224,768,12,23,4,1178,854,438,1085,ok -edge-3-smoothing-max,3,24004,1,16384,8082,487,5727,2088,5517,3957,8638,1528,6218,0.001572,9305954735862488224,768,12,23,4,1178,854,438,1085,ok -edge-3-extra-islands-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.002429,12754827918508373822,533,13,13,4,1123,743,471,1084,ok -edge-3-extra-islands-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.003526,12754827918508373822,533,13,13,4,1123,743,471,1084,ok -edge-3-extra-islands-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.001586,13384879564688048696,793,20,16,4,1080,957,422,1089,ok -edge-3-extra-islands-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.003008,13384879564688048696,793,20,16,4,1080,957,422,1089,ok -edge-3-extra-islands-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001709,7198112912547603041,779,13,19,4,1071,918,427,1084,ok -edge-3-extra-islands-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.005363,7198112912547603041,779,13,19,4,1071,918,427,1084,ok -edge-3-extra-islands-max,3,24001,1,16384,7264,700,4863,3557,5466,3020,8176,2472,5736,0.003253,6464814558566477888,194,9,10,4,678,784,246,1093,ok -edge-3-extra-islands-max,3,24001,1,16384,7264,700,4863,3557,5466,3020,8176,2472,5736,0.001793,6464814558566477888,194,9,10,4,678,784,246,1093,ok -edge-3-extra-islands-max,3,24003,1,16384,7219,739,4857,3569,5436,3003,8146,2515,5723,0.002916,18057301383644286829,176,8,8,4,674,767,246,1087,ok -edge-3-extra-islands-max,3,24003,1,16384,7219,739,4857,3569,5436,3003,8146,2515,5723,0.006614,18057301383644286829,176,8,8,4,674,767,246,1087,ok -edge-3-extra-islands-max,3,24004,1,16384,7171,649,4849,3715,5373,2707,8141,2498,5745,0.072596,4649899318218875381,197,9,11,4,644,728,318,1079,ok -edge-3-extra-islands-max,3,24004,1,16384,7171,649,4849,3715,5373,2707,8141,2498,5745,0.001553,4649899318218875381,197,9,11,4,644,728,318,1079,ok -edge-3-fruit-min,3,24001,1,16384,8311,132,5829,2112,5771,4432,8826,1186,6372,0.001649,9074598716795909270,685,14,14,4,1184,779,497,1124,ok -edge-3-fruit-min,3,24001,1,16384,8311,132,5829,2112,5771,4432,8826,1186,6372,0.001563,9074598716795909270,685,14,14,4,1184,779,497,1124,ok -edge-3-fruit-min,3,24003,1,16384,8430,96,5910,1948,5768,4519,8906,1069,6409,0.001703,4550702485884274400,797,20,18,4,1109,995,478,1123,ok -edge-3-fruit-min,3,24003,1,16384,8430,96,5910,1948,5768,4519,8906,1069,6409,0.001558,4550702485884274400,797,20,18,4,1109,995,478,1123,ok -edge-3-fruit-min,3,24004,1,16384,8398,119,5887,1980,5773,4508,8881,1108,6395,0.004665,4367030019395138836,808,16,20,4,1120,961,464,1124,ok -edge-3-fruit-min,3,24004,1,16384,8398,119,5887,1980,5773,4508,8881,1108,6395,0.002655,4367030019395138836,808,16,20,4,1120,961,464,1124,ok -edge-3-fruit-max,3,24001,1,16384,4751,5143,3386,3104,3225,666,5713,6692,3979,0.002006,11118651149639723521,14,14,9,3,491,464,254,756,ok -edge-3-fruit-max,3,24001,1,16384,4751,5143,3386,3104,3225,666,5713,6692,3979,0.001616,11118651149639723521,14,14,9,3,491,464,254,756,ok -edge-3-fruit-max,3,24003,1,16384,4864,5139,3472,2909,3284,651,5755,6590,4039,0.001795,13841240366447628489,46,21,8,4,560,454,214,748,ok -edge-3-fruit-max,3,24003,1,16384,4864,5139,3472,2909,3284,651,5755,6590,4039,0.001680,13841240366447628489,46,21,8,4,560,454,214,748,ok -edge-3-fruit-max,3,24004,1,16384,4856,5144,3430,2954,3254,695,5764,6617,4003,0.002682,11238189415334656871,32,12,21,4,662,381,196,754,ok -edge-3-fruit-max,3,24004,1,16384,4856,5144,3430,2954,3254,695,5764,6617,4003,0.001622,11238189415334656871,32,12,21,4,662,381,196,754,ok -edge-3-repeat-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.001564,12754827918508373822,533,13,13,4,1123,743,471,1084,ok -edge-3-repeat-min,3,24001,1,16384,7907,530,5612,2335,5473,3686,8532,1694,6158,0.001939,12754827918508373822,533,13,13,4,1123,743,471,1084,ok -edge-3-repeat-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.001558,13384879564688048696,793,20,16,4,1080,957,422,1089,ok -edge-3-repeat-min,3,24003,1,16384,8072,494,5698,2120,5514,3941,8630,1551,6203,0.002084,13384879564688048696,793,20,16,4,1080,957,422,1089,ok -edge-3-repeat-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001561,7198112912547603041,779,13,19,4,1071,918,427,1084,ok -edge-3-repeat-min,3,24004,1,16384,8056,534,5672,2122,5538,4009,8607,1593,6184,0.001615,7198112912547603041,779,13,19,4,1071,918,427,1084,ok -edge-3-repeat-max,3,24001,1,16384,6180,672,3644,5888,4608,748,7716,3584,5084,0.000586,11485152635733424568,4,2,4,0,428,488,224,896,ok -edge-3-repeat-max,3,24001,1,16384,6180,672,3644,5888,4608,748,7716,3584,5084,0.000562,11485152635733424568,4,2,4,0,428,488,224,896,ok -edge-3-repeat-max,3,24003,1,16384,6112,640,3616,6016,4344,416,7680,3616,5088,0.000556,9445496948617613408,6,2,9,0,384,480,384,896,ok -edge-3-repeat-max,3,24003,1,16384,6112,640,3616,6016,4344,416,7680,3616,5088,0.000548,9445496948617613408,6,2,9,0,384,480,384,896,ok -edge-3-repeat-max,3,24004,1,16384,6072,632,3784,5896,4624,752,7644,3548,5192,0.000558,4872015995867199970,6,2,6,0,508,448,188,896,ok -edge-3-repeat-max,3,24004,1,16384,6072,632,3784,5896,4624,752,7644,3548,5192,0.001155,4872015995867199970,6,2,6,0,508,448,188,896,ok -size-3-6-6-4,3,24001,1,4096,1757,196,1139,1004,1298,550,2010,695,1391,0.001480,2408001620924930647,101,7,11,4,179,135,50,269,ok -size-3-6-6-4,3,24001,1,4096,1757,196,1139,1004,1298,550,2010,695,1391,0.000528,2408001620924930647,101,7,11,4,179,135,50,269,ok -size-3-6-6-4,3,24003,1,4096,1717,259,1072,1048,1263,584,1982,781,1333,0.001305,7780550642838424467,0,10,8,3,123,193,44,247,ok -size-3-6-6-4,3,24003,1,4096,1717,259,1072,1048,1263,584,1982,781,1333,0.001225,7780550642838424467,0,10,8,3,123,193,44,247,ok -size-3-6-6-4,3,24004,1,4096,1723,172,1120,1081,1271,466,2003,708,1385,0.000506,14041330535239152130,60,7,9,4,170,141,54,269,ok -size-3-6-6-4,3,24004,1,4096,1723,172,1120,1081,1271,466,2003,708,1385,0.000519,14041330535239152130,60,7,9,4,170,141,54,269,ok -size-3-6-6-12,3,24001,1,4096,1399,269,673,1755,922,57,1823,1154,1119,0.000666,1489856931770057666,6,8,7,8,75,99,55,135,ok -size-3-6-6-12,3,24001,1,4096,1399,269,673,1755,922,57,1823,1154,1119,0.001186,1489856931770057666,6,8,7,8,75,99,55,135,ok -size-3-6-6-12,3,24003,1,4096,1420,300,797,1579,940,87,1845,1106,1145,0.000701,2284419457830081742,13,9,7,11,96,86,49,208,ok -size-3-6-6-12,3,24003,1,4096,1420,300,797,1579,940,87,1845,1106,1145,0.000903,2284419457830081742,13,9,7,11,96,86,49,208,ok -size-3-6-6-12,3,24004,1,4096,1420,259,712,1705,932,78,1843,1120,1133,0.000672,13781994263373817762,6,13,6,10,99,88,41,179,ok -size-3-6-6-12,3,24004,1,4096,1420,259,712,1705,932,78,1843,1120,1133,0.000719,13781994263373817762,6,13,6,10,99,88,41,179,ok -size-3-7-7-8,3,24001,1,16384,7632,605,5192,2955,5447,3362,8401,2077,5906,0.001719,3134242000182279350,211,13,12,8,872,830,315,1079,ok -size-3-7-7-8,3,24001,1,16384,7632,605,5192,2955,5447,3362,8401,2077,5906,0.001729,3134242000182279350,211,13,12,8,872,830,315,1079,ok -size-3-7-7-8,3,24003,1,16384,7523,582,5165,3114,5407,3207,8346,2133,5905,0.007214,9477542308708889540,259,12,14,8,744,826,364,1091,ok -size-3-7-7-8,3,24003,1,16384,7523,582,5165,3114,5407,3207,8346,2133,5905,0.001786,9477542308708889540,259,12,14,8,744,826,364,1091,ok -size-3-7-7-8,3,24004,1,16384,7533,615,5147,3089,5407,3156,8335,2154,5895,0.001971,3885494216138766292,80,10,11,8,917,683,349,1087,ok -size-3-7-7-8,3,24004,1,16384,7533,615,5147,3089,5407,3156,8335,2154,5895,0.004681,3885494216138766292,80,10,11,8,917,683,349,1087,ok -size-3-6-9-12,3,24001,1,32768,15532,1255,10797,5184,10922,7265,16862,3843,12063,0.004440,4292325463971593553,333,25,26,11,1788,1831,738,2168,ok -size-3-6-9-12,3,24001,1,32768,15532,1255,10797,5184,10922,7265,16862,3843,12063,0.003479,4292325463971593553,333,25,26,11,1788,1831,738,2168,ok -size-3-6-9-12,3,24003,1,32768,15617,1211,10890,5050,10904,7336,16944,3726,12098,0.003421,9077211435807253382,268,24,22,12,1739,1869,842,2177,ok -size-3-6-9-12,3,24003,1,32768,15617,1211,10890,5050,10904,7336,16944,3726,12098,0.003402,9077211435807253382,268,24,22,12,1739,1869,842,2177,ok -size-3-6-9-12,3,24004,1,32768,15671,1114,10899,5084,10929,7385,16998,3649,12121,0.066922,16684913468144428077,441,16,17,12,1733,1961,784,2184,ok -size-3-6-9-12,3,24004,1,32768,15671,1114,10899,5084,10929,7385,16998,3649,12121,0.003441,16684913468144428077,441,16,17,12,1733,1961,784,2184,ok -size-3-9-6-12,3,24001,1,32768,15587,1146,10912,5123,10901,7225,16933,3701,12134,0.004738,11860156535094621637,13,100000,22,10,1751,1899,778,2183,ok -size-3-9-6-12,3,24001,1,32768,15587,1146,10912,5123,10901,7225,16933,3701,12134,0.005198,11860156535094621637,13,100000,22,10,1751,1899,778,2183,ok -size-3-9-6-12,3,24003,1,32768,15659,1207,10862,5040,10978,7434,16960,3723,12085,0.003789,14734992354316944878,45,20,100000,11,1833,1809,783,2170,ok -size-3-9-6-12,3,24003,1,32768,15659,1207,10862,5040,10978,7434,16960,3723,12085,0.105734,14734992354316944878,45,20,100000,11,1833,1809,783,2170,ok -size-3-9-6-12,3,24004,1,32768,15662,1185,10905,5016,10944,7338,16951,3688,12129,0.003726,13574052253604112191,89,14,18,12,2107,1564,787,2185,ok -size-3-9-6-12,3,24004,1,32768,15662,1185,10905,5016,10944,7338,16951,3688,12129,0.005941,13574052253604112191,89,14,18,12,2107,1564,787,2185,ok -size-3-9-9-12,3,24001,1,262144,137934,7746,100142,16322,90050,78820,142345,15896,103903,0.324298,17791796161650287991,1279,29,45,6,18801,16092,12744,17386,ok -size-3-9-9-12,3,24001,1,262144,137934,7746,100142,16322,90050,78820,142345,15896,103903,0.313554,17791796161650287991,1279,29,45,6,18801,16092,12744,17386,ok -size-3-9-9-12,3,24003,1,262144,138248,7774,100139,15983,90246,79624,142476,15759,103909,0.255989,2802800432745166429,1189,32,48,7,17575,17347,12834,17477,ok -size-3-9-9-12,3,24003,1,262144,138248,7774,100139,15983,90246,79624,142476,15759,103909,0.058551,2802800432745166429,1189,32,48,7,17575,17347,12834,17477,ok -size-3-9-9-12,3,24004,1,262144,138018,7475,100529,16122,89932,78537,142386,15522,104236,0.339608,2986413288923889907,1170,51,42,3,17800,17223,12811,17390,ok -size-3-9-9-12,3,24004,1,262144,138018,7475,100529,16122,89932,78537,142386,15522,104236,0.262262,2986413288923889907,1170,51,42,3,17800,17223,12811,17390,ok -corner-3-min,3,24001,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-3-min,3,24001,0,0,0,0,0,0,0,0,0,0,0,0.000022,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-3-min,3,24003,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-3-min,3,24003,0,0,0,0,0,0,0,0,0,0,0,0.000008,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-3-min,3,24004,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-3-min,3,24004,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-3-max,3,24001,1,262144,47052,132844,49888,32360,16268,32,56752,148976,56416,0.015632,3882014515311933110,1,12,4,0,7968,10304,4704,9280,ok -corner-3-max,3,24001,1,262144,47052,132844,49888,32360,16268,32,56752,148976,56416,0.014599,3882014515311933110,1,12,4,0,7968,10304,4704,9280,ok -corner-3-max,3,24003,1,262144,47068,133132,50016,31928,16156,64,56808,149048,56288,0.038924,12350383196907311314,2,5,5,0,9088,9280,4608,9248,ok -corner-3-max,3,24003,1,262144,47068,133132,50016,31928,16156,64,56808,149048,56288,0.124088,12350383196907311314,2,5,5,0,9088,9280,4608,9248,ok -corner-3-max,3,24004,1,262144,46656,133312,49920,32256,15256,224,56448,149408,56288,0.070951,9589820212205955278,1,1,3,0,8032,10240,4480,9280,ok -corner-3-max,3,24004,1,262144,46656,133312,49920,32256,15256,224,56448,149408,56288,0.020159,9589820212205955278,1,1,3,0,8032,10240,4480,9280,ok -edge-4-width-min,4,24001,1,8192,5378,285,1593,936,4631,3576,5644,753,1795,0.000853,4541659758168081294,799,11,13,4,244,275,125,317,ok -edge-4-width-min,4,24001,1,8192,5378,285,1593,936,4631,3576,5644,753,1795,0.001823,4541659758168081294,799,11,13,4,244,275,125,317,ok -edge-4-width-min,4,24003,1,8192,5318,297,1581,996,4642,3494,5606,794,1792,0.001666,18160520391721934799,724,9,7,4,213,250,115,316,ok -edge-4-width-min,4,24003,1,8192,5318,297,1581,996,4642,3494,5606,794,1792,0.002143,18160520391721934799,724,9,7,4,213,250,115,316,ok -edge-4-width-min,4,24004,1,8192,5390,268,1600,934,4668,3631,5667,734,1791,0.000812,11804285690529431025,295,13,20,4,229,298,91,317,ok -edge-4-width-min,4,24004,1,8192,5390,268,1600,934,4668,3631,5667,734,1791,0.000794,11804285690529431025,295,13,20,4,229,298,91,317,ok -edge-4-width-max,4,24001,1,65536,42919,2255,12891,7471,37697,30747,45080,5982,14474,0.006483,12734081638166914099,1354,14,20,4,2126,2061,923,2537,ok -edge-4-width-max,4,24001,1,65536,42919,2255,12891,7471,37697,30747,45080,5982,14474,0.009254,12734081638166914099,1354,14,20,4,2126,2061,923,2537,ok -edge-4-width-max,4,24003,1,65536,42855,2285,12828,7568,37743,30584,45044,6061,14431,0.012220,2107868326165549141,712,18,15,4,2034,2130,845,2535,ok -edge-4-width-max,4,24003,1,65536,42855,2285,12828,7568,37743,30584,45044,6061,14431,0.006295,2107868326165549141,712,18,15,4,2034,2130,845,2535,ok -edge-4-width-max,4,24004,1,65536,42909,2313,12782,7532,37796,30922,45068,6076,14392,0.052076,6059020898415111911,735,15,25,4,2219,1977,832,2529,ok -edge-4-width-max,4,24004,1,65536,42909,2313,12782,7532,37796,30922,45068,6076,14392,0.006980,6059020898415111911,735,15,25,4,2219,1977,832,2529,ok -edge-4-height-min,4,24001,1,8192,5325,259,1583,1025,4598,3411,5632,770,1790,0.000814,9093900865957847469,922,26,10,3,215,302,106,316,ok -edge-4-height-min,4,24001,1,8192,5325,259,1583,1025,4598,3411,5632,770,1790,0.000810,9093900865957847469,922,26,10,3,215,302,106,316,ok -edge-4-height-min,4,24003,1,8192,5308,279,1579,1026,4629,3519,5616,791,1785,0.000836,6046727951330402993,839,16,16,4,250,230,97,315,ok -edge-4-height-min,4,24003,1,8192,5308,279,1579,1026,4629,3519,5616,791,1785,0.000940,6046727951330402993,839,16,16,4,250,230,97,315,ok -edge-4-height-min,4,24004,1,8192,5338,274,1604,976,4630,3453,5630,760,1802,0.000862,2739917840607138342,756,8,13,4,261,257,99,318,ok -edge-4-height-min,4,24004,1,8192,5338,274,1604,976,4630,3453,5630,760,1802,0.000870,2739917840607138342,756,8,13,4,261,257,99,318,ok -edge-4-height-max,4,24001,1,65536,42907,2242,12858,7529,37753,30832,45078,5994,14464,0.008825,8351122231019620984,862,19,19,4,2162,2004,885,2551,ok -edge-4-height-max,4,24001,1,65536,42907,2242,12858,7529,37753,30832,45078,5994,14464,0.005740,8351122231019620984,862,19,19,4,2162,2004,885,2551,ok -edge-4-height-max,4,24003,1,65536,43086,2213,13071,7166,37684,31105,45179,5796,14561,0.037019,16747919569644837282,1025,20,27,4,2261,2132,910,2550,ok -edge-4-height-max,4,24003,1,65536,43086,2213,13071,7166,37684,31105,45179,5796,14561,0.005916,16747919569644837282,1025,20,27,4,2261,2132,910,2550,ok -edge-4-height-max,4,24004,1,65536,42840,2269,12711,7716,37781,30509,45075,6112,14349,0.047404,4070000572400454117,678,13,16,4,2075,2000,890,2549,ok -edge-4-height-max,4,24004,1,65536,42840,2269,12711,7716,37781,30509,45075,6112,14349,0.011417,4070000572400454117,678,13,16,4,2075,2000,890,2549,ok -edge-4-teams-min,4,24001,1,16384,10687,563,3186,1948,9392,7468,11261,1533,3590,0.001680,14607272382324076510,2187,15,23,1,535,506,213,632,ok -edge-4-teams-min,4,24001,1,16384,10687,563,3186,1948,9392,7468,11261,1533,3590,0.001362,14607272382324076510,2187,15,23,1,535,506,213,632,ok -edge-4-teams-min,4,24003,1,16384,10682,556,3153,1993,9432,7515,11261,1550,3573,0.002074,3459117622114281977,1171,8,12,1,488,514,213,632,ok -edge-4-teams-min,4,24003,1,16384,10682,556,3153,1993,9432,7515,11261,1550,3573,0.001369,3459117622114281977,1171,8,12,1,488,514,213,632,ok -edge-4-teams-min,4,24004,1,16384,10625,598,3118,2043,9432,7583,11208,1620,3556,0.002790,5151524803494005197,1311,9,8,1,453,500,200,635,ok -edge-4-teams-min,4,24004,1,16384,10625,598,3118,2043,9432,7583,11208,1620,3556,0.001979,5151524803494005197,1311,9,8,1,453,500,200,635,ok -edge-4-teams-max,4,24001,1,16384,10712,562,3158,1952,9201,6972,11288,1534,3562,0.026853,2362421247749281498,439,25,23,11,534,503,213,632,ok -edge-4-teams-max,4,24001,1,16384,10712,562,3158,1952,9201,6972,11288,1534,3562,0.001812,2362421247749281498,439,25,23,11,534,503,213,632,ok -edge-4-teams-max,4,24003,1,16384,10682,556,3153,1993,9238,7035,11261,1550,3573,0.001896,1927621497452940970,743,19,22,12,483,493,213,632,ok -edge-4-teams-max,4,24003,1,16384,10682,556,3153,1993,9238,7035,11261,1550,3573,0.001906,1927621497452940970,743,19,22,12,483,493,213,632,ok -edge-4-teams-max,4,24004,1,16384,10645,583,3113,2043,9235,7102,11228,1605,3551,0.003720,3573620643882315263,663,16,21,12,450,500,200,635,ok -edge-4-teams-max,4,24004,1,16384,10645,583,3113,2043,9235,7102,11228,1605,3551,0.001902,3573620643882315263,663,16,21,12,450,500,200,635,ok -edge-4-workers-min,4,24001,1,16384,10712,562,3158,1952,9370,7365,11288,1534,3562,0.001459,8254136538010680180,518,18,23,4,534,506,213,632,ok -edge-4-workers-min,4,24001,1,16384,10712,562,3158,1952,9370,7365,11288,1534,3562,0.001987,8254136538010680180,518,18,23,4,534,506,213,632,ok -edge-4-workers-min,4,24003,1,16384,10682,556,3153,1993,9395,7405,11261,1550,3573,0.006068,1062923027973093604,729,8,12,4,485,506,213,632,ok -edge-4-workers-min,4,24003,1,16384,10682,556,3153,1993,9395,7405,11261,1550,3573,0.001524,1062923027973093604,729,8,12,4,485,506,213,632,ok -edge-4-workers-min,4,24004,1,16384,10625,598,3118,2043,9384,7450,11208,1620,3556,0.001524,3707328080328529486,802,9,8,4,453,500,200,635,ok -edge-4-workers-min,4,24004,1,16384,10625,598,3118,2043,9384,7450,11208,1620,3556,0.002563,3707328080328529486,802,9,8,4,453,500,200,635,ok -edge-4-workers-max,4,24001,1,16384,10712,562,3158,1952,9342,7335,11288,1534,3562,0.001546,8254136538010680180,539,15,23,4,534,506,213,632,ok -edge-4-workers-max,4,24001,1,16384,10712,562,3158,1952,9342,7335,11288,1534,3562,0.001747,8254136538010680180,539,15,23,4,534,506,213,632,ok -edge-4-workers-max,4,24003,1,16384,10682,556,3153,1993,9367,7397,11261,1550,3573,0.001507,1062923027973093604,802,8,11,4,485,506,213,632,ok -edge-4-workers-max,4,24003,1,16384,10682,556,3153,1993,9367,7397,11261,1550,3573,0.001528,1062923027973093604,802,8,11,4,485,506,213,632,ok -edge-4-workers-max,4,24004,1,16384,10625,598,3118,2043,9356,7417,11208,1620,3556,0.001507,3707328080328529486,865,9,7,4,453,500,200,635,ok -edge-4-workers-max,4,24004,1,16384,10625,598,3118,2043,9356,7417,11208,1620,3556,0.084143,3707328080328529486,865,9,7,4,453,500,200,635,ok -edge-4-water-min,4,24001,1,16384,14642,1102,0,640,14534,13094,14979,1405,0,0.003265,4794024178719344639,2103,100000,100000,0,0,0,3,0,ok -edge-4-water-min,4,24001,1,16384,14642,1102,0,640,14534,13094,14979,1405,0,0.001436,4794024178719344639,2103,100000,100000,0,0,0,3,0,ok -edge-4-water-min,4,24003,1,16384,14668,1110,0,606,14564,13209,14987,1397,0,0.001432,18136764116766803282,1599,100000,100000,0,0,0,2,0,ok -edge-4-water-min,4,24003,1,16384,14668,1110,0,606,14564,13209,14987,1397,0,0.003431,18136764116766803282,1599,100000,100000,0,0,0,2,0,ok -edge-4-water-min,4,24004,1,16384,14707,1145,0,532,14613,13471,14984,1400,0,0.001409,307839220994827965,1851,100000,100000,0,0,0,0,0,ok -edge-4-water-min,4,24004,1,16384,14707,1145,0,532,14613,13471,14984,1400,0,0.001458,307839220994827965,1851,100000,100000,0,0,0,0,0,ok -edge-4-water-max,4,24001,1,16384,5964,477,8269,1674,3289,1952,6416,1313,8655,0.001498,1543614798758202213,0,12,100000,2,924,1113,546,1497,ok -edge-4-water-max,4,24001,1,16384,5964,477,8269,1674,3289,1952,6416,1313,8655,0.002464,1543614798758202213,0,12,100000,2,924,1113,546,1497,ok -edge-4-water-max,4,24003,1,16384,5814,429,8089,2052,3230,1718,6366,1452,8566,0.001482,11021664634660526930,124,12,9,4,1066,961,460,1496,ok -edge-4-water-max,4,24003,1,16384,5814,429,8089,2052,3230,1718,6366,1452,8566,0.001655,11021664634660526930,124,12,9,4,1066,961,460,1496,ok -edge-4-water-max,4,24004,1,16384,5751,469,8106,2058,3298,1980,6308,1497,8579,0.001494,16619325114269491904,82,10,11,4,851,1114,390,1498,ok -edge-4-water-max,4,24004,1,16384,5751,469,8106,2058,3298,1980,6308,1497,8579,0.004484,16619325114269491904,82,10,11,4,851,1114,390,1498,ok -edge-4-sand-min,4,24001,1,16384,10683,786,2951,1964,9623,7596,11268,1765,3351,0.001491,13410321720515600371,560,16,24,4,383,421,155,654,ok -edge-4-sand-min,4,24001,1,16384,10683,786,2951,1964,9623,7596,11268,1765,3351,0.001459,13410321720515600371,560,16,24,4,383,421,155,654,ok -edge-4-sand-min,4,24003,1,16384,10643,822,2932,1987,9641,7650,11226,1814,3344,0.002153,14477966123237028695,826,10,14,4,355,395,151,651,ok -edge-4-sand-min,4,24003,1,16384,10643,822,2932,1987,9641,7650,11226,1814,3344,0.003203,14477966123237028695,826,10,14,4,355,395,151,651,ok -edge-4-sand-min,4,24004,1,16384,10579,886,2912,2007,9602,7685,11160,1891,3333,0.001476,9589238647282499469,1053,9,14,4,332,414,131,648,ok -edge-4-sand-min,4,24004,1,16384,10579,886,2912,2007,9602,7685,11160,1891,3333,0.001496,9589238647282499469,1053,9,14,4,332,414,131,648,ok -edge-4-sand-max,4,24001,1,16384,5577,7541,1678,1588,4836,3299,6038,8343,2003,0.001450,14189173806338207264,459,13,15,4,223,295,128,333,ok -edge-4-sand-max,4,24001,1,16384,5577,7541,1678,1588,4836,3299,6038,8343,2003,0.004784,14189173806338207264,459,13,15,4,223,295,128,333,ok -edge-4-sand-max,4,24003,1,16384,5524,7472,1682,1706,4825,3192,6047,8332,2005,0.001617,15696502119339532185,213,18,18,4,254,208,136,332,ok -edge-4-sand-max,4,24003,1,16384,5524,7472,1682,1706,4825,3192,6047,8332,2005,0.001451,15696502119339532185,213,18,18,4,254,208,136,332,ok -edge-4-sand-max,4,24004,1,16384,5512,7459,1674,1739,4854,3370,6038,8341,2005,0.043162,4849518523568563639,232,14,10,4,155,288,107,331,ok -edge-4-sand-max,4,24004,1,16384,5512,7459,1674,1739,4854,3370,6038,8341,2005,0.003715,4849518523568563639,232,14,10,4,155,288,107,331,ok -edge-4-grass-min,4,24001,0,16384,0,0,16384,0,0,0,21,3564,12799,0.001299,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-4-grass-min,4,24001,0,16384,0,0,16384,0,0,0,21,3564,12799,0.001272,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-4-grass-min,4,24003,0,16384,0,0,16384,0,0,0,19,3576,12789,0.001155,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-4-grass-min,4,24003,0,16384,0,0,16384,0,0,0,19,3576,12789,0.001153,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-4-grass-min,4,24004,0,16384,0,0,16384,0,0,0,16,3576,12792,0.001179,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-4-grass-min,4,24004,0,16384,0,0,16384,0,0,0,16,3576,12792,0.001216,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-4-grass-max,4,24001,1,16384,11660,473,2497,1754,10666,8734,12174,1347,2863,0.003041,3081479129659193038,697,17,25,4,365,360,164,514,ok -edge-4-grass-max,4,24001,1,16384,11660,473,2497,1754,10666,8734,12174,1347,2863,0.001578,3081479129659193038,697,17,25,4,365,360,164,514,ok -edge-4-grass-max,4,24003,1,16384,11613,484,2450,1837,10639,8706,12157,1401,2826,0.001499,16220650475573237379,958,10,13,4,331,369,169,516,ok -edge-4-grass-max,4,24003,1,16384,11613,484,2450,1837,10639,8706,12157,1401,2826,0.001618,16220650475573237379,958,10,13,4,331,369,169,516,ok -edge-4-grass-max,4,24004,1,16384,11573,500,2445,1866,10635,8780,12120,1430,2834,0.001487,1097820657693125625,1055,11,23,4,310,369,167,516,ok -edge-4-grass-max,4,24004,1,16384,11573,500,2445,1866,10635,8780,12120,1430,2834,0.001531,1097820657693125625,1055,11,23,4,310,369,167,516,ok -edge-4-desert-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.001414,8254136538010680180,518,15,23,4,534,506,213,632,ok -edge-4-desert-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.001970,8254136538010680180,518,15,23,4,534,506,213,632,ok -edge-4-desert-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.004261,1062923027973093604,770,8,12,4,485,506,213,632,ok -edge-4-desert-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.001904,1062923027973093604,770,8,12,4,485,506,213,632,ok -edge-4-desert-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.097196,3707328080328529486,841,9,8,4,453,500,200,635,ok -edge-4-desert-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.003716,3707328080328529486,841,9,8,4,453,500,200,635,ok -edge-4-desert-max,4,24001,1,16384,4943,7917,1383,2141,4408,1932,5719,8988,1677,0.001510,4426490326196461153,331,10,18,4,153,184,103,325,ok -edge-4-desert-max,4,24001,1,16384,4943,7917,1383,2141,4408,1932,5719,8988,1677,0.001509,4426490326196461153,331,10,18,4,153,184,103,325,ok -edge-4-desert-max,4,24003,1,16384,4853,7837,1383,2311,4328,1578,5705,8995,1684,0.002314,12785947869004050330,244,12,11,4,192,140,98,324,ok -edge-4-desert-max,4,24003,1,16384,4853,7837,1383,2311,4328,1578,5705,8995,1684,0.001495,12785947869004050330,244,12,11,4,192,140,98,324,ok -edge-4-desert-max,4,24004,1,16384,4797,7851,1376,2360,4276,1502,5665,9038,1681,0.001514,10830302627865370351,152,21,19,4,152,183,89,328,ok -edge-4-desert-max,4,24004,1,16384,4797,7851,1376,2360,4276,1502,5665,9038,1681,0.001478,10830302627865370351,152,21,19,4,152,183,89,328,ok -edge-4-smoothing-min,4,24001,1,16384,10618,497,3184,2085,9255,6983,11262,1530,3592,0.001955,11493225337334270162,919,15,23,4,512,523,227,634,ok -edge-4-smoothing-min,4,24001,1,16384,10618,497,3184,2085,9255,6983,11262,1530,3592,0.001499,11493225337334270162,919,15,23,4,512,523,227,634,ok -edge-4-smoothing-min,4,24003,1,16384,10620,499,3141,2124,9304,7010,11274,1552,3558,0.002631,5034150749963636557,482,22,20,4,432,556,215,636,ok -edge-4-smoothing-min,4,24003,1,16384,10620,499,3141,2124,9304,7010,11274,1552,3558,0.003138,5034150749963636557,482,22,20,4,432,556,215,636,ok -edge-4-smoothing-min,4,24004,1,16384,10500,571,3056,2257,9308,7006,11171,1697,3516,0.001606,17443006140792250911,1011,8,6,4,399,499,200,636,ok -edge-4-smoothing-min,4,24004,1,16384,10500,571,3056,2257,9308,7006,11171,1697,3516,0.001544,17443006140792250911,1011,8,6,4,399,499,200,636,ok -edge-4-smoothing-max,4,24001,1,16384,10658,510,3189,2027,9305,7218,11272,1519,3593,0.004813,3760489223540803542,686,24,22,4,465,565,228,633,ok -edge-4-smoothing-max,4,24001,1,16384,10658,510,3189,2027,9305,7218,11272,1519,3593,0.001484,3760489223540803542,686,24,22,4,465,565,228,633,ok -edge-4-smoothing-max,4,24003,1,16384,10730,584,3174,1896,9391,7381,11264,1530,3590,0.001580,3114500145959872434,1162,29,17,3,562,437,233,635,ok -edge-4-smoothing-max,4,24003,1,16384,10730,584,3174,1896,9391,7381,11264,1530,3590,0.001513,3114500145959872434,1162,29,17,3,562,437,233,635,ok -edge-4-smoothing-max,4,24004,1,16384,10660,560,3113,2051,9406,7473,11243,1583,3558,0.001501,731412055027956474,901,12,19,4,425,523,214,634,ok -edge-4-smoothing-max,4,24004,1,16384,10660,560,3113,2051,9406,7473,11243,1583,3558,0.001974,731412055027956474,901,12,19,4,425,523,214,634,ok -edge-4-lake-density-min,4,24001,1,16384,11044,514,3530,1296,9357,7548,11450,1157,3777,0.001494,8629925748382678687,1000,21,19,4,567,588,434,637,ok -edge-4-lake-density-min,4,24001,1,16384,11044,514,3530,1296,9357,7548,11450,1157,3777,0.004653,8629925748382678687,1000,21,19,4,567,588,434,637,ok -edge-4-lake-density-min,4,24003,1,16384,10877,496,3411,1600,9299,7467,11375,1289,3720,0.005198,3261879646515407256,660,21,17,4,626,572,293,636,ok -edge-4-lake-density-min,4,24003,1,16384,10877,496,3411,1600,9299,7467,11375,1289,3720,0.001453,3261879646515407256,660,21,17,4,626,572,293,636,ok -edge-4-lake-density-min,4,24004,1,16384,11026,576,3522,1260,9382,7756,11412,1202,3770,0.001473,3513928367786325251,523,13,19,4,430,730,391,636,ok -edge-4-lake-density-min,4,24004,1,16384,11026,576,3522,1260,9382,7756,11412,1202,3770,0.001471,3513928367786325251,523,13,19,4,430,730,391,636,ok -edge-4-lake-density-max,4,24001,1,16384,10285,657,2844,2598,9289,6716,11046,1956,3382,0.001576,5093769824615020375,892,13,10,4,372,357,171,635,ok -edge-4-lake-density-max,4,24001,1,16384,10285,657,2844,2598,9289,6716,11046,1956,3382,0.003514,5093769824615020375,892,13,10,4,372,357,171,635,ok -edge-4-lake-density-max,4,24003,1,16384,10355,584,2887,2558,9294,6652,11127,1857,3400,0.001545,3048521784300642060,829,15,18,4,411,381,175,636,ok -edge-4-lake-density-max,4,24003,1,16384,10355,584,2887,2558,9294,6652,11127,1857,3400,0.001550,3048521784300642060,829,15,18,4,411,381,175,636,ok -edge-4-lake-density-max,4,24004,1,16384,10269,623,2900,2592,9238,6626,11042,1916,3426,0.001521,14214230773756149775,722,9,5,4,387,373,177,636,ok -edge-4-lake-density-max,4,24004,1,16384,10269,623,2900,2592,9238,6626,11042,1916,3426,0.001538,14214230773756149775,722,9,5,4,387,373,177,636,ok -edge-4-lake-size-min,4,24001,1,16384,10382,783,2942,2277,8925,6631,11058,1919,3407,0.003082,356576884332847302,667,22,16,4,569,465,318,637,ok -edge-4-lake-size-min,4,24001,1,16384,10382,783,2942,2277,8925,6631,11058,1919,3407,0.112929,356576884332847302,667,22,16,4,569,465,318,637,ok -edge-4-lake-size-min,4,24003,1,16384,10342,787,2872,2383,8839,6746,11043,1978,3363,0.001512,11976132177559827711,26,18,16,4,485,599,326,637,ok -edge-4-lake-size-min,4,24003,1,16384,10342,787,2872,2383,8839,6746,11043,1978,3363,0.001443,11976132177559827711,26,18,16,4,485,599,326,637,ok -edge-4-lake-size-min,4,24004,1,16384,10480,728,3036,2140,9019,6817,11109,1796,3479,0.001476,12510093574884878762,882,9,13,4,465,548,350,637,ok -edge-4-lake-size-min,4,24004,1,16384,10480,728,3036,2140,9019,6817,11109,1796,3479,0.001486,12510093574884878762,882,9,13,4,465,548,350,637,ok -edge-4-lake-size-max,4,24001,1,16384,10768,580,3202,1834,9397,7630,11297,1494,3593,0.002013,4917776454980176967,664,21,24,4,482,540,248,630,ok -edge-4-lake-size-max,4,24001,1,16384,10768,580,3202,1834,9397,7630,11297,1494,3593,0.002851,4917776454980176967,664,21,24,4,482,540,248,630,ok -edge-4-lake-size-max,4,24003,1,16384,10749,589,3192,1854,9406,7498,11269,1515,3600,0.001652,192118136470395207,956,11,14,4,530,493,226,635,ok -edge-4-lake-size-max,4,24003,1,16384,10749,589,3192,1854,9406,7498,11269,1515,3600,0.002128,192118136470395207,956,11,14,4,530,493,226,635,ok -edge-4-lake-size-max,4,24004,1,16384,10712,612,3188,1872,9343,7454,11246,1549,3589,0.001618,13579775533962361372,805,13,9,4,518,469,276,633,ok -edge-4-lake-size-max,4,24004,1,16384,10712,612,3188,1872,9343,7454,11246,1549,3589,0.008064,13579775533962361372,805,13,9,4,518,469,276,633,ok -edge-4-fruit-min,4,24001,1,16384,11265,46,3343,1730,9862,8410,11718,910,3756,0.073504,13727028702911961066,705,20,23,4,552,540,231,654,ok -edge-4-fruit-min,4,24001,1,16384,11265,46,3343,1730,9862,8410,11718,910,3756,0.003111,13727028702911961066,705,20,23,4,552,540,231,654,ok -edge-4-fruit-min,4,24003,1,16384,11225,59,3291,1809,9865,8278,11699,965,3720,0.001501,13210986106375009244,726,16,13,4,503,564,213,659,ok -edge-4-fruit-min,4,24003,1,16384,11225,59,3291,1809,9865,8278,11699,965,3720,0.001605,13210986106375009244,726,16,13,4,503,564,213,659,ok -edge-4-fruit-min,4,24004,1,16384,11164,81,3271,1868,9856,8281,11650,1020,3714,0.001488,7241652660036938659,1018,19,14,4,488,532,208,660,ok -edge-4-fruit-min,4,24004,1,16384,11164,81,3271,1868,9856,8281,11650,1020,3714,0.004024,7241652660036938659,1018,19,14,4,488,532,208,660,ok -edge-4-fruit-max,4,24001,1,16384,6325,5984,1820,2255,5345,2188,7114,7113,2157,0.003653,4362957826146166621,302,5,16,4,217,258,129,407,ok -edge-4-fruit-max,4,24001,1,16384,6325,5984,1820,2255,5345,2188,7114,7113,2157,0.001529,4362957826146166621,302,5,16,4,217,258,129,407,ok -edge-4-fruit-max,4,24003,1,16384,6281,5932,1833,2338,5340,2156,7119,7100,2165,0.001547,9401746125361811691,230,10,11,4,244,223,116,407,ok -edge-4-fruit-max,4,24003,1,16384,6281,5932,1833,2338,5340,2156,7119,7100,2165,0.001494,9401746125361811691,230,10,11,4,244,223,116,407,ok -edge-4-fruit-max,4,24004,1,16384,6203,5948,1785,2448,5250,1947,7076,7171,2137,0.001585,16759074768503135645,271,13,7,4,229,254,117,406,ok -edge-4-fruit-max,4,24004,1,16384,6203,5948,1785,2448,5250,1947,7076,7171,2137,0.001581,16759074768503135645,271,13,7,4,229,254,117,406,ok -edge-4-repeat-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.004474,8254136538010680180,518,15,23,4,534,506,213,632,ok -edge-4-repeat-min,4,24001,1,16384,10712,562,3158,1952,9358,7356,11288,1534,3562,0.001495,8254136538010680180,518,15,23,4,534,506,213,632,ok -edge-4-repeat-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.001511,1062923027973093604,770,8,12,4,485,506,213,632,ok -edge-4-repeat-min,4,24003,1,16384,10682,556,3153,1993,9383,7405,11261,1550,3573,0.001594,1062923027973093604,770,8,12,4,485,506,213,632,ok -edge-4-repeat-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.001533,3707328080328529486,841,9,8,4,453,500,200,635,ok -edge-4-repeat-min,4,24004,1,16384,10625,598,3118,2043,9372,7439,11208,1620,3556,0.001629,3707328080328529486,841,9,8,4,453,500,200,635,ok -edge-4-repeat-max,4,24001,1,16384,9272,792,2096,4224,8236,2900,10584,2904,2896,0.000678,2300712310789260919,392,4,6,4,64,360,56,576,ok -edge-4-repeat-max,4,24001,1,16384,9272,792,2096,4224,8236,2900,10584,2904,2896,0.000747,2300712310789260919,392,4,6,4,64,360,56,576,ok -edge-4-repeat-max,4,24003,1,16384,9308,584,2404,4088,7556,1824,10736,2568,3080,0.001151,15676489074034912807,294,4,1,4,220,664,244,576,ok -edge-4-repeat-max,4,24003,1,16384,9308,584,2404,4088,7556,1824,10736,2568,3080,0.001113,15676489074034912807,294,4,1,4,220,664,244,576,ok -edge-4-repeat-max,4,24004,1,16384,9476,608,2396,3904,7632,1984,10788,2528,3068,0.000609,10650943619486687672,347,7,3,4,256,352,224,576,ok -edge-4-repeat-max,4,24004,1,16384,9476,608,2396,3904,7632,1984,10788,2528,3068,0.000593,10650943619486687672,347,7,3,4,256,352,224,576,ok -size-4-6-6-4,4,24001,1,4096,2645,130,789,532,2240,1571,2807,394,895,0.000685,6522132994922700061,886,16,17,4,117,137,44,158,ok -size-4-6-6-4,4,24001,1,4096,2645,130,789,532,2240,1571,2807,394,895,0.000466,6522132994922700061,886,16,17,4,117,137,44,158,ok -size-4-6-6-4,4,24003,1,4096,2613,131,766,586,2247,1495,2792,423,881,0.000501,3893475650893073251,590,9,15,4,101,113,54,158,ok -size-4-6-6-4,4,24003,1,4096,2613,131,766,586,2247,1495,2792,423,881,0.000491,3893475650893073251,590,9,15,4,101,113,54,158,ok -size-4-6-6-4,4,24004,1,4096,2688,140,810,458,2271,1659,2820,369,907,0.001783,16598518496421687426,657,19,19,4,115,158,52,158,ok -size-4-6-6-4,4,24004,1,4096,2688,140,810,458,2271,1659,2820,369,907,0.001887,16598518496421687426,657,19,19,4,115,158,52,158,ok -size-4-6-6-12,4,24001,0,4096,0,0,4096,0,0,0,2807,394,895,0.000366,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-4-6-6-12,4,24001,0,4096,0,0,4096,0,0,0,2807,394,895,0.000962,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-4-6-6-12,4,24003,0,4096,0,0,4096,0,0,0,2790,424,882,0.000424,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-4-6-6-12,4,24003,0,4096,0,0,4096,0,0,0,2790,424,882,0.000505,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-4-6-6-12,4,24004,0,4096,0,0,4096,0,0,0,2820,369,907,0.001127,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-4-6-6-12,4,24004,0,4096,0,0,4096,0,0,0,2820,369,907,0.000386,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-4-7-7-8,4,24001,1,16384,10712,562,3158,1952,9281,7171,11288,1534,3562,0.001894,15583763586484531551,518,25,23,7,534,503,213,632,ok -size-4-7-7-8,4,24001,1,16384,10712,562,3158,1952,9281,7171,11288,1534,3562,0.001633,15583763586484531551,518,25,23,7,534,503,213,632,ok -size-4-7-7-8,4,24003,1,16384,10682,556,3153,1993,9317,7244,11261,1550,3573,0.002103,5482014022574351707,770,11,12,8,485,492,213,632,ok -size-4-7-7-8,4,24003,1,16384,10682,556,3153,1993,9317,7244,11261,1550,3573,0.001760,5482014022574351707,770,11,12,8,485,492,213,632,ok -size-4-7-7-8,4,24004,1,16384,10631,597,3113,2043,9301,7281,11214,1619,3551,0.004585,7813010578140552413,841,16,21,8,450,500,200,635,ok -size-4-7-7-8,4,24004,1,16384,10631,597,3113,2043,9301,7281,11214,1619,3551,0.001704,7813010578140552413,841,16,21,8,450,500,200,635,ok -size-4-6-9-12,4,24001,1,32768,21547,1105,6540,3576,18628,14887,22595,2886,7287,0.003564,14873471884697784680,685,37,38,10,1164,1045,453,1264,ok -size-4-6-9-12,4,24001,1,32768,21547,1105,6540,3576,18628,14887,22595,2886,7287,0.004295,14873471884697784680,685,37,38,10,1164,1045,453,1264,ok -size-4-6-9-12,4,24003,1,32768,21447,1109,6422,3790,18715,15021,22559,2997,7212,0.079111,3903544181060677136,412,19,21,12,973,1049,463,1272,ok -size-4-6-9-12,4,24003,1,32768,21447,1109,6422,3790,18715,15021,22559,2997,7212,0.005647,3903544181060677136,412,19,21,12,973,1049,463,1272,ok -size-4-6-9-12,4,24004,1,32768,21571,1103,6525,3569,18716,14860,22606,2883,7279,0.007159,4155787913330145562,674,19,20,12,1010,1108,479,1269,ok -size-4-6-9-12,4,24004,1,32768,21571,1103,6525,3569,18716,14860,22606,2883,7279,0.010276,4155787913330145562,674,19,20,12,1010,1108,479,1269,ok -size-4-9-6-12,4,24001,1,32768,21136,1097,6235,4300,18564,14126,22413,3241,7114,0.003605,16525546010019320907,998,22,19,12,836,1048,423,1271,ok -size-4-9-6-12,4,24001,1,32768,21136,1097,6235,4300,18564,14126,22413,3241,7114,0.003836,16525546010019320907,998,22,19,12,836,1048,423,1271,ok -size-4-9-6-12,4,24003,1,32768,21401,1185,6291,3891,18748,14854,22511,3133,7124,0.006538,4891460588746945516,641,24,16,12,924,1070,401,1274,ok -size-4-9-6-12,4,24003,1,32768,21401,1185,6291,3891,18748,14854,22511,3133,7124,0.003958,4891460588746945516,641,24,16,12,924,1070,401,1274,ok -size-4-9-6-12,4,24004,1,32768,21514,1155,6484,3615,18699,14990,22539,2962,7267,0.004790,7109692120240274476,831,25,27,11,1109,973,465,1270,ok -size-4-9-6-12,4,24004,1,32768,21514,1155,6484,3615,18699,14990,22539,2962,7267,0.006008,7109692120240274476,831,25,27,11,1109,973,465,1270,ok -size-4-9-9-12,4,24001,1,262144,171342,9373,50976,30453,151343,124193,180016,24580,57548,0.386439,9883531783368080857,689,29,32,11,8197,8064,3479,10146,ok -size-4-9-9-12,4,24001,1,262144,171342,9373,50976,30453,151343,124193,180016,24580,57548,0.163084,9883531783368080857,689,29,32,11,8197,8064,3479,10146,ok -size-4-9-9-12,4,24003,1,262144,171683,9069,51518,29874,151181,123641,180299,23974,57871,0.143692,14463795164623604611,889,22,24,12,8277,8389,3576,10148,ok -size-4-9-9-12,4,24003,1,262144,171683,9069,51518,29874,151181,123641,180299,23974,57871,0.176869,14463795164623604611,889,22,24,12,8277,8389,3576,10148,ok -size-4-9-9-12,4,24004,1,262144,171322,9023,51209,30590,151167,122582,180171,24281,57692,0.331625,3652722481845561522,827,33,41,11,7695,8693,3491,10158,ok -size-4-9-9-12,4,24004,1,262144,171322,9023,51209,30590,151167,122582,180171,24281,57692,0.138683,3652722481845561522,827,33,41,11,7695,8693,3491,10158,ok -corner-4-min,4,24001,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-4-min,4,24001,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-4-min,4,24003,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-4-min,4,24003,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-4-min,4,24004,0,0,0,0,0,0,0,0,0,0,0,0.000007,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-4-min,4,24004,0,0,0,0,0,0,0,0,0,0,0,0.000012,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-4-max,4,24001,1,262144,35892,120480,48348,57424,15088,0,56940,149176,56028,0.011404,11431102598582502079,0,7,7,0,7080,6856,3104,9376,ok -corner-4-max,4,24001,1,262144,35892,120480,48348,57424,15088,0,56940,149176,56028,0.011668,11431102598582502079,0,7,7,0,7080,6856,3104,9376,ok -corner-4-max,4,24003,1,262144,32812,117332,48224,63776,12072,0,56680,149304,56160,0.131737,10343313809265980641,0,4,1,0,7376,6496,2580,9376,ok -corner-4-max,4,24003,1,262144,32812,117332,48224,63776,12072,0,56680,149304,56160,0.043587,10343313809265980641,0,4,1,0,7376,6496,2580,9376,ok -corner-4-max,4,24004,1,262144,33936,116304,47616,64288,13388,0,57040,148656,56448,0.084651,3610559417604788182,0,3,1,0,6304,7008,3304,9376,ok -corner-4-max,4,24004,1,262144,33936,116304,47616,64288,13388,0,57040,148656,56448,0.012865,3610559417604788182,0,3,1,0,6304,7008,3304,9376,ok -edge-5-width-min,5,24001,1,8192,4244,61,1954,1933,2178,957,4679,1028,2485,0.006794,17171761383611781295,156,2,9,4,1042,914,24,2,ok -edge-5-width-min,5,24001,1,8192,4244,61,1954,1933,2178,957,4679,1028,2485,0.057199,17171761383611781295,156,2,9,4,1042,914,24,2,ok -edge-5-width-min,5,24003,1,8192,4171,60,2022,1939,2113,847,4590,1022,2580,0.005190,5703813900008587670,131,1,8,4,1091,854,24,2,ok -edge-5-width-min,5,24003,1,8192,4171,60,2022,1939,2113,847,4590,1022,2580,0.052252,5703813900008587670,131,1,8,4,1091,854,24,2,ok -edge-5-width-min,5,24004,1,8192,4664,46,1654,1828,2348,1038,5089,960,2143,0.005554,7841045921695058678,162,1,7,4,1213,987,24,2,ok -edge-5-width-min,5,24004,1,8192,4664,46,1654,1828,2348,1038,5089,960,2143,0.005486,7841045921695058678,162,1,7,4,1213,987,24,2,ok -edge-5-width-max,5,24001,1,65536,53666,136,5327,6407,39819,34855,55233,3337,6966,0.356904,6551436702106436975,353,4,36,3,8300,5437,24,2,ok -edge-5-width-max,5,24001,1,65536,53666,136,5327,6407,39819,34855,55233,3337,6966,0.484098,6551436702106436975,353,4,36,3,8300,5437,24,2,ok -edge-5-width-max,5,24003,1,65536,53379,173,5199,6785,37943,32469,55071,3562,6903,0.502737,18123336872848219798,91,2,100000,2,10113,5210,24,0,ok -edge-5-width-max,5,24003,1,65536,53379,173,5199,6785,37943,32469,55071,3562,6903,0.457281,18123336872848219798,91,2,100000,2,10113,5210,24,0,ok -edge-5-width-max,5,24004,1,65536,55344,134,4349,5709,41200,36288,56767,2985,5784,0.572890,14238173093679142971,1070,1,58,3,8876,5152,24,0,ok -edge-5-width-max,5,24004,1,65536,55344,134,4349,5709,41200,36288,56767,2985,5784,0.527236,14238173093679142971,1070,1,58,3,8876,5152,24,0,ok -edge-5-height-min,5,24001,1,8192,4042,55,2024,2071,2128,830,4510,1091,2591,0.020844,18181004065509020172,118,3,5,4,907,897,24,3,ok -edge-5-height-min,5,24001,1,8192,4042,55,2024,2071,2128,830,4510,1091,2591,0.008257,18181004065509020172,118,3,5,4,907,897,24,3,ok -edge-5-height-min,5,24003,1,8192,4498,44,1731,1919,2250,982,4934,1004,2254,0.015101,13641719035445064182,141,1,11,4,1180,955,24,1,ok -edge-5-height-min,5,24003,1,8192,4498,44,1731,1919,2250,982,4934,1004,2254,0.005411,13641719035445064182,141,1,11,4,1180,955,24,1,ok -edge-5-height-min,5,24004,1,8192,4375,60,1728,2029,2222,1038,4850,1073,2269,0.005258,8915583425988203371,229,5,7,4,1047,990,24,2,ok -edge-5-height-min,5,24004,1,8192,4375,60,1728,2029,2222,1038,4850,1073,2269,0.046863,8915583425988203371,229,5,7,4,1047,990,24,2,ok -edge-5-height-max,5,24001,1,65536,56580,112,3915,4929,43156,38625,57790,2576,5170,0.467537,2606329050636720493,968,2,73,2,8597,4717,24,0,ok -edge-5-height-max,5,24001,1,65536,56580,112,3915,4929,43156,38625,57790,2576,5170,0.410093,2606329050636720493,968,2,73,2,8597,4717,24,0,ok -edge-5-height-max,5,24003,1,65536,53244,113,5319,6860,37061,31890,54942,3543,7051,0.557101,7521527562398943057,59,3,100000,2,10876,5194,24,1,ok -edge-5-height-max,5,24003,1,65536,53244,113,5319,6860,37061,31890,54942,3543,7051,0.522673,7521527562398943057,59,3,100000,2,10876,5194,24,1,ok -edge-5-height-max,5,24004,1,65536,52480,139,5628,7289,38311,32618,54266,3787,7483,0.563887,13289230739172764216,236,2,55,2,8893,5160,24,1,ok -edge-5-height-max,5,24004,1,65536,52480,139,5628,7289,38311,32618,54266,3787,7483,0.681050,13289230739172764216,236,2,55,2,8893,5160,24,1,ok -edge-5-teams-min,5,24001,1,16384,12077,72,1889,2346,7754,6362,12655,1246,2483,0.010668,12139880955903287514,512,3,27,1,3273,1018,6,0,ok -edge-5-teams-min,5,24001,1,16384,12077,72,1889,2346,7754,6362,12655,1246,2483,0.049256,12139880955903287514,512,3,27,1,3273,1018,6,0,ok -edge-5-teams-min,5,24003,1,16384,12083,75,1872,2354,7369,5967,12657,1253,2474,0.021280,3350966425812963857,1184,1,9,1,3590,1089,6,0,ok -edge-5-teams-min,5,24003,1,16384,12083,75,1872,2354,7369,5967,12657,1253,2474,0.010580,3350966425812963857,1184,1,9,1,3590,1089,6,0,ok -edge-5-teams-min,5,24004,1,16384,11895,53,1968,2468,7299,5781,12504,1287,2593,0.073430,5773595217240884218,1063,1,46,0,3409,1149,6,0,ok -edge-5-teams-min,5,24004,1,16384,11895,53,1968,2468,7299,5781,12504,1287,2593,0.016160,5773595217240884218,1063,1,46,0,3409,1149,6,0,ok -edge-5-teams-max,5,24001,1,16384,8600,136,3685,3963,4271,1573,9495,2117,4772,0.041424,1242877349242468839,73,3,10,12,1974,2037,72,0,ok -edge-5-teams-max,5,24001,1,16384,8600,136,3685,3963,4271,1573,9495,2117,4772,0.109248,1242877349242468839,73,3,10,12,1974,2037,72,0,ok -edge-5-teams-max,5,24003,1,16384,8690,119,3701,3874,4226,1439,9593,2054,4737,0.029636,2931377641924482398,65,3,14,12,2119,2024,72,0,ok -edge-5-teams-max,5,24003,1,16384,8690,119,3701,3874,4226,1439,9593,2054,4737,0.039162,2931377641924482398,65,3,14,12,2119,2024,72,0,ok -edge-5-teams-max,5,24004,1,16384,8749,104,3693,3838,4280,1477,9623,2017,4744,0.094048,15092112594054926975,77,4,8,12,2019,2126,72,0,ok -edge-5-teams-max,5,24004,1,16384,8749,104,3693,3838,4280,1477,9623,2017,4744,0.041540,15092112594054926975,77,4,8,12,2019,2126,72,0,ok -edge-5-workers-min,5,24001,1,16384,11254,76,2321,2733,6638,4665,11914,1441,3029,0.098683,7123999391715415104,0,6,100000,3,2607,1911,24,0,ok -edge-5-workers-min,5,24001,1,16384,11254,76,2321,2733,6638,4665,11914,1441,3029,0.073933,7123999391715415104,0,6,100000,3,2607,1911,24,0,ok -edge-5-workers-min,5,24003,1,16384,10432,89,2681,3182,5881,3814,11191,1680,3513,0.064071,1175273401662215666,0,6,100000,3,2573,1877,24,0,ok -edge-5-workers-min,5,24003,1,16384,10432,89,2681,3182,5881,3814,11191,1680,3513,0.108449,1175273401662215666,0,6,100000,3,2573,1877,24,0,ok -edge-5-workers-min,5,24004,1,16384,10593,71,2710,3010,6258,4069,11298,1572,3514,0.034747,5190250561350529432,462,5,21,4,2432,1799,24,0,ok -edge-5-workers-min,5,24004,1,16384,10593,71,2710,3010,6258,4069,11298,1572,3514,0.084410,5190250561350529432,462,5,21,4,2432,1799,24,0,ok -edge-5-workers-max,5,24001,1,16384,11254,76,2321,2733,6578,4579,11914,1441,3029,0.087516,14655486661103581760,524,1,13,4,2596,1954,24,0,ok -edge-5-workers-max,5,24001,1,16384,11254,76,2321,2733,6578,4579,11914,1441,3029,0.085124,14655486661103581760,524,1,13,4,2596,1954,24,0,ok -edge-5-workers-max,5,24003,1,16384,10432,89,2681,3182,5869,3832,11191,1680,3513,0.050959,5061961116353793522,382,1,12,4,2521,1913,24,0,ok -edge-5-workers-max,5,24003,1,16384,10432,89,2681,3182,5869,3832,11191,1680,3513,0.093912,5061961116353793522,382,1,12,4,2521,1913,24,0,ok -edge-5-workers-max,5,24004,1,16384,10593,71,2710,3010,6062,3952,11298,1572,3514,0.013060,12822396717584548248,371,2,21,4,2511,1888,24,0,ok -edge-5-workers-max,5,24004,1,16384,10593,71,2710,3010,6062,3952,11298,1572,3514,0.011030,12822396717584548248,371,2,21,4,2511,1888,24,0,ok -edge-5-channel-width-min,5,24001,1,16384,11254,76,2321,2733,6532,4472,11914,1441,3029,0.059151,16198348791202468928,520,2,23,4,2649,1963,24,0,ok -edge-5-channel-width-min,5,24001,1,16384,11254,76,2321,2733,6532,4472,11914,1441,3029,0.021339,16198348791202468928,520,2,23,4,2649,1963,24,0,ok -edge-5-channel-width-min,5,24003,1,16384,10432,89,2681,3182,5810,3616,11191,1680,3513,0.081161,9817715955724006130,371,1,27,4,2597,1912,24,0,ok -edge-5-channel-width-min,5,24003,1,16384,10432,89,2681,3182,5810,3616,11191,1680,3513,0.050435,9817715955724006130,371,1,27,4,2597,1912,24,0,ok -edge-5-channel-width-min,5,24004,1,16384,10593,71,2710,3010,6117,3988,11298,1572,3514,0.076508,13565529459509754520,502,3,15,4,2428,1932,24,0,ok -edge-5-channel-width-min,5,24004,1,16384,10593,71,2710,3010,6117,3988,11298,1572,3514,0.089174,13565529459509754520,502,3,15,4,2428,1932,24,0,ok -edge-5-channel-width-max,5,24001,1,16384,8097,60,6048,2179,4518,2917,8615,1147,6622,0.087184,10995638509345458802,575,2,9,4,1891,1578,24,3759,ok -edge-5-channel-width-max,5,24001,1,16384,8097,60,6048,2179,4518,2917,8615,1147,6622,0.100170,10995638509345458802,575,2,9,4,1891,1578,24,3759,ok -edge-5-channel-width-max,5,24003,1,16384,6852,57,7040,2435,3653,1935,7428,1272,7684,0.087515,12403104663351606330,351,2,12,4,1692,1393,24,4431,ok -edge-5-channel-width-max,5,24003,1,16384,6852,57,7040,2435,3653,1935,7428,1272,7684,0.113171,12403104663351606330,351,2,12,4,1692,1393,24,4431,ok -edge-5-channel-width-max,5,24004,1,16384,7228,56,6833,2267,3822,2008,7770,1190,7424,0.034384,15586038024190125750,236,3,8,4,1771,1519,24,4311,ok -edge-5-channel-width-max,5,24004,1,16384,7228,56,6833,2267,3822,2008,7770,1190,7424,0.034391,15586038024190125750,236,3,8,4,1771,1519,24,4311,ok -edge-5-extra-islands-min,5,24001,1,16384,12315,35,1850,2184,7554,5742,12847,1127,2410,0.054103,15319057177535303165,755,2,22,4,2497,2160,24,0,ok -edge-5-extra-islands-min,5,24001,1,16384,12315,35,1850,2184,7554,5742,12847,1127,2410,0.010895,15319057177535303165,755,2,22,4,2497,2160,24,0,ok -edge-5-extra-islands-min,5,24003,1,16384,12276,73,1811,2224,7898,6001,12813,1184,2387,0.070369,816497216597935629,849,3,14,4,2289,1985,24,1,ok -edge-5-extra-islands-min,5,24003,1,16384,12276,73,1811,2224,7898,6001,12813,1184,2387,0.019834,816497216597935629,849,3,14,4,2289,1985,24,1,ok -edge-5-extra-islands-min,5,24004,1,16384,12419,42,1821,2102,8050,6148,12931,1093,2360,0.014440,4024635329982450899,838,2,34,3,2248,2017,24,1,ok -edge-5-extra-islands-min,5,24004,1,16384,12419,42,1821,2102,8050,6148,12931,1093,2360,0.124215,4024635329982450899,838,2,34,3,2248,2017,24,1,ok -edge-5-extra-islands-max,5,24001,1,16384,9382,109,3265,3628,5115,3089,10240,1920,4224,0.013927,3677509818522375674,27,2,21,4,2411,1739,24,0,ok -edge-5-extra-islands-max,5,24001,1,16384,9382,109,3265,3628,5115,3089,10240,1920,4224,0.084618,3677509818522375674,27,2,21,4,2411,1739,24,0,ok -edge-5-extra-islands-max,5,24003,1,16384,9036,114,3424,3810,4740,2576,9935,2019,4430,0.108538,4650947493334634350,363,2,16,4,2521,1653,24,0,ok -edge-5-extra-islands-max,5,24003,1,16384,9036,114,3424,3810,4740,2576,9935,2019,4430,0.015736,4650947493334634350,363,2,16,4,2521,1653,24,0,ok -edge-5-extra-islands-max,5,24004,1,16384,10012,76,2948,3348,5484,3047,10796,1748,3840,0.016219,1822594680024405239,582,3,10,4,2800,1601,24,1,ok -edge-5-extra-islands-max,5,24004,1,16384,10012,76,2948,3348,5484,3047,10796,1748,3840,0.166408,1822594680024405239,582,3,10,4,2800,1601,24,1,ok -size-5-6-6-4,5,24001,1,4096,1501,47,1265,1283,677,89,1783,687,1626,0.002810,4651726352976272702,8,3,5,2,360,355,24,0,ok -size-5-6-6-4,5,24001,1,4096,1501,47,1265,1283,677,89,1783,687,1626,0.003919,4651726352976272702,8,3,5,2,360,355,24,0,ok -size-5-6-6-4,5,24003,1,4096,1734,42,1139,1181,783,90,2004,632,1460,0.004965,4106057933311672777,8,3,4,3,439,403,24,0,ok -size-5-6-6-4,5,24003,1,4096,1734,42,1139,1181,783,90,2004,632,1460,0.002865,4106057933311672777,8,3,4,3,439,403,24,0,ok -size-5-6-6-4,5,24004,1,4096,1638,38,1189,1231,736,152,1917,652,1527,0.004491,7623062973509903136,0,3,4,3,394,392,24,3,ok -size-5-6-6-4,5,24004,1,4096,1638,38,1189,1231,736,152,1917,652,1527,0.002673,7623062973509903136,0,3,4,3,394,392,24,3,ok -size-5-6-6-12,5,24001,0,4096,923,43,1709,1421,876,280,1220,753,2123,0.006725,6375674900583504980,4096,100000,100000,0,24,17,6,0,failed -size-5-6-6-12,5,24001,0,4096,923,43,1709,1421,876,280,1220,753,2123,0.005554,6375674900583504980,4096,100000,100000,0,24,17,6,0,failed -size-5-6-6-12,5,24003,0,4096,950,28,1717,1401,908,311,1243,727,2126,0.002221,10650788504315581361,4096,100000,100000,0,22,12,6,2,failed -size-5-6-6-12,5,24003,0,4096,950,28,1717,1401,908,311,1243,727,2126,0.002094,10650788504315581361,4096,100000,100000,0,22,12,6,2,failed -size-5-6-6-12,5,24004,0,4096,857,38,1825,1376,817,253,1139,723,2234,0.002856,11680217874443233530,4096,100000,100000,0,19,15,6,2,failed -size-5-6-6-12,5,24004,0,4096,857,38,1825,1376,817,253,1139,723,2234,0.002932,11680217874443233530,4096,100000,100000,0,19,15,6,2,failed -size-5-7-7-8,5,24001,1,16384,9267,106,3419,3592,4717,2317,10108,1897,4379,0.184038,801385284587197134,201,4,13,8,2197,2139,48,0,ok -size-5-7-7-8,5,24001,1,16384,9267,106,3419,3592,4717,2317,10108,1897,4379,0.082424,801385284587197134,201,4,13,8,2197,2139,48,0,ok -size-5-7-7-8,5,24003,1,16384,9302,120,3370,3592,4733,2215,10135,1910,4339,0.066871,8610794216540169989,201,5,9,8,2298,2054,48,0,ok -size-5-7-7-8,5,24003,1,16384,9302,120,3370,3592,4733,2215,10135,1910,4339,0.107539,8610794216540169989,201,5,9,8,2298,2054,48,0,ok -size-5-7-7-8,5,24004,1,16384,9418,97,3303,3566,4837,2461,10236,1876,4272,0.014448,12042202373557920104,208,4,14,8,2262,2099,48,1,ok -size-5-7-7-8,5,24004,1,16384,9418,97,3303,3566,4837,2461,10236,1876,4272,0.042163,12042202373557920104,208,4,14,8,2262,2099,48,1,ok -size-5-6-9-12,5,24001,1,32768,20327,179,5644,6618,11064,6319,21893,3480,7395,0.249690,5418256031852975315,290,4,26,12,4634,4311,72,0,ok -size-5-6-9-12,5,24001,1,32768,20327,179,5644,6618,11064,6319,21893,3480,7395,0.296518,5418256031852975315,290,4,26,12,4634,4311,72,0,ok -size-5-6-9-12,5,24003,1,32768,20829,136,5353,6450,11173,6552,22386,3356,7026,0.282039,16836414780086224395,330,2,16,12,4935,4400,72,0,ok -size-5-6-9-12,5,24003,1,32768,20829,136,5353,6450,11173,6552,22386,3356,7026,0.273700,16836414780086224395,330,2,16,12,4935,4400,72,0,ok -size-5-6-9-12,5,24004,1,32768,19996,173,5816,6783,11077,6452,21615,3564,7589,0.288426,11284631810791223717,159,4,15,12,4414,4181,72,0,ok -size-5-6-9-12,5,24004,1,32768,19996,173,5816,6783,11077,6452,21615,3564,7589,0.351615,11284631810791223717,159,4,15,12,4414,4181,72,0,ok -size-5-9-6-12,5,24001,1,32768,19833,207,5904,6824,10503,6101,21463,3618,7687,0.444717,9914795222840391452,333,2,16,12,4713,4299,72,4,ok -size-5-9-6-12,5,24001,1,32768,19833,207,5904,6824,10503,6101,21463,3618,7687,0.277273,9914795222840391452,333,2,16,12,4713,4299,72,4,ok -size-5-9-6-12,5,24003,1,32768,20018,129,5759,6862,10908,6336,21649,3563,7556,0.264060,10626352557760732287,323,5,18,12,4566,4223,72,1,ok -size-5-9-6-12,5,24003,1,32768,20018,129,5759,6862,10908,6336,21649,3563,7556,0.191762,10626352557760732287,323,5,18,12,4566,4223,72,1,ok -size-5-9-6-12,5,24004,1,32768,20170,153,5939,6506,10916,6357,21714,3400,7654,0.241783,2930393585886118444,323,5,23,12,4609,4321,72,0,ok -size-5-9-6-12,5,24004,1,32768,20170,153,5939,6506,10916,6357,21714,3400,7654,0.456094,2930393585886118444,323,5,23,12,4609,4321,72,0,ok -size-5-9-9-12,5,24001,1,262144,222605,484,17401,21654,179792,162618,227887,11308,22949,4.060011,18016697878824307604,687,3,92,9,25058,17437,72,0,ok -size-5-9-9-12,5,24001,1,262144,222605,484,17401,21654,179792,162618,227887,11308,22949,4.333187,18016697878824307604,687,3,92,9,25058,17437,72,0,ok -size-5-9-9-12,5,24003,1,262144,222341,511,17745,21547,177847,159953,227618,11284,23242,4.594076,13204712083959918866,0,5,100000,7,27034,17139,72,0,ok -size-5-9-9-12,5,24003,1,262144,222341,511,17745,21547,177847,159953,227618,11284,23242,4.365123,13204712083959918866,0,5,100000,7,27034,17139,72,0,ok -size-5-9-9-12,5,24004,1,262144,225930,351,16056,19807,181496,165172,230811,10240,21093,4.264293,13394875853363115778,0,3,100000,6,26369,17741,72,0,ok -size-5-9-9-12,5,24004,1,262144,225930,351,16056,19807,181496,165172,230811,10240,21093,4.021998,13394875853363115778,0,3,100000,6,26369,17741,72,0,ok -corner-5-min,5,24001,0,4096,0,0,4096,0,0,0,0,0,4096,0.001281,15070008172932738451,4096,100000,100000,0,0,0,0,2596,failed -corner-5-min,5,24001,0,4096,0,0,4096,0,0,0,0,0,4096,0.001310,15070008172932738451,4096,100000,100000,0,0,0,0,2596,failed -corner-5-min,5,24003,0,4096,0,0,4096,0,0,0,0,0,4096,0.001297,3473231813497914905,4096,100000,100000,0,0,0,0,2649,failed -corner-5-min,5,24003,0,4096,0,0,4096,0,0,0,0,0,4096,0.003182,3473231813497914905,4096,100000,100000,0,0,0,0,2649,failed -corner-5-min,5,24004,0,4096,0,0,4096,0,0,0,0,0,4096,0.001413,18126423395565846657,4096,100000,100000,0,0,0,0,2901,failed -corner-5-min,5,24004,0,4096,0,0,4096,0,0,0,0,0,4096,0.002548,18126423395565846657,4096,100000,100000,0,0,0,0,2901,failed -corner-5-max,5,24001,1,262144,189760,411,51837,20136,146952,131029,194706,10476,56962,3.919425,8267887055777218505,144,2,100000,7,26242,16193,72,31719,ok -corner-5-max,5,24001,1,262144,189760,411,51837,20136,146952,131029,194706,10476,56962,4.016038,8267887055777218505,144,2,100000,7,26242,16193,72,31719,ok -corner-5-max,5,24003,1,262144,186756,427,52992,21969,143202,126250,192167,11412,58565,4.240843,15587302819288139138,280,2,67,7,26102,17074,72,32031,ok -corner-5-max,5,24003,1,262144,186756,427,52992,21969,143202,126250,192167,11412,58565,3.981434,15587302819288139138,280,2,67,7,26102,17074,72,32031,ok -corner-5-max,5,24004,1,262144,189203,418,52102,20421,143164,126522,194185,10622,57337,4.158956,8924188191017556042,819,1,63,5,29311,16345,72,32098,ok -corner-5-max,5,24004,1,262144,189203,418,52102,20421,143164,126522,194185,10622,57337,3.665281,8924188191017556042,819,1,63,5,29311,16345,72,32098,ok -edge-6-width-min,6,24001,1,8192,3876,322,2089,1905,2086,901,4414,1293,2485,0.007337,8301481773998981155,215,4,10,4,841,845,24,74,ok -edge-6-width-min,6,24001,1,8192,3876,322,2089,1905,2086,901,4414,1293,2485,0.004646,8301481773998981155,215,4,10,4,841,845,24,74,ok -edge-6-width-min,6,24003,1,8192,3400,330,2761,1701,1809,562,3857,1200,3135,0.007951,11260754162497162439,149,1,5,4,747,740,24,46,ok -edge-6-width-min,6,24003,1,8192,3400,330,2761,1701,1809,562,3857,1200,3135,0.010023,11260754162497162439,149,1,5,4,747,740,24,46,ok -edge-6-width-min,6,24004,1,8192,3312,314,2753,1813,1773,546,3807,1228,3157,0.032714,11622027674796834941,131,3,4,4,712,723,24,98,ok -edge-6-width-min,6,24004,1,8192,3312,314,2753,1813,1773,546,3807,1228,3157,0.006903,11622027674796834941,131,3,4,4,712,723,24,98,ok -edge-6-width-max,6,24001,1,65536,26058,1471,31659,6348,18362,14960,27762,4673,33101,0.292315,15831918896906361328,1090,2,25,4,3896,3696,24,94,ok -edge-6-width-max,6,24001,1,65536,26058,1471,31659,6348,18362,14960,27762,4673,33101,0.263758,15831918896906361328,1090,2,25,4,3896,3696,24,94,ok -edge-6-width-max,6,24003,1,65536,26796,1685,30000,7055,19392,15893,28653,5230,31653,0.205091,7572749383616265500,1172,4,28,4,3748,3552,24,100,ok -edge-6-width-max,6,24003,1,65536,26796,1685,30000,7055,19392,15893,28653,5230,31653,0.236993,7572749383616265500,1172,4,28,4,3748,3552,24,100,ok -edge-6-width-max,6,24004,1,65536,24443,1364,33803,5926,17244,13924,25994,4355,35187,0.280962,4840076621543531009,948,2,29,4,3733,3362,24,87,ok -edge-6-width-max,6,24004,1,65536,24443,1364,33803,5926,17244,13924,25994,4355,35187,0.253937,4840076621543531009,948,2,29,4,3733,3362,24,87,ok -edge-6-height-min,6,24001,1,8192,3675,369,2214,1934,1957,754,4183,1347,2662,0.004422,602148175110294740,177,2,9,4,784,830,24,56,ok -edge-6-height-min,6,24001,1,8192,3675,369,2214,1934,1957,754,4183,1347,2662,0.005902,602148175110294740,177,2,9,4,784,830,24,56,ok -edge-6-height-min,6,24003,1,8192,3424,434,2560,1774,1825,598,3916,1341,2935,0.034891,10724234975970144101,108,3,8,4,728,767,24,73,ok -edge-6-height-min,6,24003,1,8192,3424,434,2560,1774,1825,598,3916,1341,2935,0.004659,10724234975970144101,108,3,8,4,728,767,24,73,ok -edge-6-height-min,6,24004,1,8192,3836,443,2048,1865,2042,783,4319,1392,2481,0.005227,16560501378303616241,192,4,9,4,817,873,24,75,ok -edge-6-height-min,6,24004,1,8192,3836,443,2048,1865,2042,783,4319,1392,2481,0.004520,16560501378303616241,192,4,9,4,817,873,24,75,ok -edge-6-height-max,6,24001,1,65536,27059,1123,31121,6233,19455,15821,28676,4270,32590,0.213385,14239867738790271377,1169,1,39,3,3902,3598,24,97,ok -edge-6-height-max,6,24001,1,65536,27059,1123,31121,6233,19455,15821,28676,4270,32590,0.273713,14239867738790271377,1169,1,39,3,3902,3598,24,97,ok -edge-6-height-max,6,24003,1,65536,24646,936,34597,5357,17463,14225,26029,3639,35868,0.302143,5829103401409267935,1005,4,26,4,3723,3356,24,99,ok -edge-6-height-max,6,24003,1,65536,24646,936,34597,5357,17463,14225,26029,3639,35868,0.256553,5829103401409267935,1005,4,26,4,3723,3356,24,99,ok -edge-6-height-max,6,24004,1,65536,24685,1027,34316,5508,17292,13948,26121,3825,35590,0.402356,14424515157103611352,947,1,43,3,3703,3586,24,100,ok -edge-6-height-max,6,24004,1,65536,24685,1027,34316,5508,17292,13948,26121,3825,35590,0.277400,14424515157103611352,947,1,43,3,3703,3586,24,100,ok -edge-6-teams-min,6,24001,0,16384,1670,146,13956,612,1670,1353,1819,455,14110,0.002701,13724714730050812290,16384,100000,100000,0,0,0,0,0,failed -edge-6-teams-min,6,24001,0,16384,1670,146,13956,612,1670,1353,1819,455,14110,0.002511,13724714730050812290,16384,100000,100000,0,0,0,0,0,failed -edge-6-teams-min,6,24003,0,16384,1602,129,14015,638,1602,1292,1770,450,14164,0.015378,5555895874860407129,16384,100000,100000,0,0,0,0,0,failed -edge-6-teams-min,6,24003,0,16384,1602,129,14015,638,1602,1292,1770,450,14164,0.002591,5555895874860407129,16384,100000,100000,0,0,0,0,0,failed -edge-6-teams-min,6,24004,0,16384,1645,141,13942,656,1645,1327,1811,473,14100,0.002517,16883743711992608893,16384,100000,100000,0,0,0,0,0,failed -edge-6-teams-min,6,24004,0,16384,1645,141,13942,656,1645,1327,1811,473,14100,0.004241,16883743711992608893,16384,100000,100000,0,0,0,0,0,failed -edge-6-teams-max,6,24001,1,16384,10616,701,1141,3926,6177,1756,11936,2656,1792,0.042114,1078194504538917648,104,3,11,12,2066,2061,72,92,ok -edge-6-teams-max,6,24001,1,16384,10616,701,1141,3926,6177,1756,11936,2656,1792,0.053325,1078194504538917648,104,3,11,12,2066,2061,72,92,ok -edge-6-teams-max,6,24003,1,16384,10158,791,1312,4123,6263,1692,11603,2828,1953,0.174375,10246265899692024718,100,5,7,12,1737,1846,72,115,ok -edge-6-teams-max,6,24003,1,16384,10158,791,1312,4123,6263,1692,11603,2828,1953,0.049400,10246265899692024718,100,5,7,12,1737,1846,72,115,ok -edge-6-teams-max,6,24004,1,16384,10483,668,1386,3847,6530,1837,11815,2564,2005,0.083996,9955619800240440577,71,6,7,12,1827,1814,72,164,ok -edge-6-teams-max,6,24004,1,16384,10483,668,1386,3847,6530,1837,11815,2564,2005,0.069513,9955619800240440577,71,6,7,12,1827,1814,72,164,ok -edge-6-workers-min,6,24001,1,16384,6997,592,6141,2654,4047,2304,7700,1921,6763,0.074498,10478685938506820208,505,4,14,4,1454,1404,24,99,ok -edge-6-workers-min,6,24001,1,16384,6997,592,6141,2654,4047,2304,7700,1921,6763,0.008573,10478685938506820208,505,4,14,4,1454,1404,24,99,ok -edge-6-workers-min,6,24003,1,16384,6893,597,5962,2932,3919,2164,7668,2070,6646,0.008415,16638262727558778513,320,4,10,4,1459,1423,24,92,ok -edge-6-workers-min,6,24003,1,16384,6893,597,5962,2932,3919,2164,7668,2070,6646,0.050374,16638262727558778513,320,4,10,4,1459,1423,24,92,ok -edge-6-workers-min,6,24004,1,16384,7089,539,5668,3088,4002,2187,7906,2092,6386,0.012881,8225055719630622565,403,5,15,4,1497,1498,24,89,ok -edge-6-workers-min,6,24004,1,16384,7089,539,5668,3088,4002,2187,7906,2092,6386,0.022610,8225055719630622565,403,5,15,4,1497,1498,24,89,ok -edge-6-workers-max,6,24001,1,16384,6997,592,6141,2654,4018,2266,7700,1921,6763,0.013143,17181742508508505200,514,2,5,4,1438,1421,24,99,ok -edge-6-workers-max,6,24001,1,16384,6997,592,6141,2654,4018,2266,7700,1921,6763,0.009956,17181742508508505200,514,2,5,4,1438,1421,24,99,ok -edge-6-workers-max,6,24003,1,16384,6893,597,5962,2932,3872,2135,7668,2070,6646,0.032319,3330212283408972177,454,1,8,4,1497,1404,24,92,ok -edge-6-workers-max,6,24003,1,16384,6893,597,5962,2932,3872,2135,7668,2070,6646,0.040489,3330212283408972177,454,1,8,4,1497,1404,24,92,ok -edge-6-workers-max,6,24004,1,16384,7089,539,5668,3088,4081,2174,7906,2092,6386,0.029831,1518336612106003301,395,2,6,4,1466,1422,24,89,ok -edge-6-workers-max,6,24004,1,16384,7089,539,5668,3088,4081,2174,7906,2092,6386,0.008412,1518336612106003301,395,2,6,4,1466,1422,24,89,ok -edge-6-island-size-min,6,24001,1,16384,4428,448,8951,2557,2435,1019,5105,1729,9550,0.048474,2337629969544153032,241,2,9,4,920,969,24,98,ok -edge-6-island-size-min,6,24001,1,16384,4428,448,8951,2557,2435,1019,5105,1729,9550,0.008013,2337629969544153032,241,2,9,4,920,969,24,98,ok -edge-6-island-size-min,6,24003,1,16384,4286,482,9017,2599,2278,895,4948,1799,9637,0.007839,9941356866946445158,185,5,7,4,963,941,24,87,ok -edge-6-island-size-min,6,24003,1,16384,4286,482,9017,2599,2278,895,4948,1799,9637,0.012050,9941356866946445158,185,5,7,4,963,941,24,87,ok -edge-6-island-size-min,6,24004,1,16384,4449,588,8518,2829,2441,937,5182,2019,9183,0.051960,16934352449055104569,213,4,9,4,950,954,24,81,ok -edge-6-island-size-min,6,24004,1,16384,4449,588,8518,2829,2441,937,5182,2019,9183,0.009031,16934352449055104569,213,4,9,4,950,954,24,81,ok -edge-6-island-size-max,6,24001,1,16384,7474,535,5436,2939,4230,2384,8253,2022,6109,0.014569,9523367320184550060,434,1,10,4,1601,1539,24,95,ok -edge-6-island-size-max,6,24001,1,16384,7474,535,5436,2939,4230,2384,8253,2022,6109,0.009447,9523367320184550060,434,1,10,4,1601,1539,24,95,ok -edge-6-island-size-max,6,24003,1,16384,7411,563,5413,2997,4178,2414,8193,2064,6127,0.054111,15266498219304234197,500,2,17,4,1586,1543,24,95,ok -edge-6-island-size-max,6,24003,1,16384,7411,563,5413,2997,4178,2414,8193,2064,6127,0.009252,15266498219304234197,500,2,17,4,1586,1543,24,95,ok -edge-6-island-size-max,6,24004,1,16384,7668,574,5058,3084,4472,2525,8461,2132,5791,0.060344,17668093791446278362,437,3,9,4,1587,1505,24,100,ok -edge-6-island-size-max,6,24004,1,16384,7668,574,5058,3084,4472,2525,8461,2132,5791,0.041982,17668093791446278362,437,3,9,4,1587,1505,24,100,ok -edge-6-bridge-width-min,6,24001,1,16384,6752,614,6275,2743,3775,2233,7478,1990,6916,0.017442,5556296913352800080,484,1,6,4,1459,1414,24,100,ok -edge-6-bridge-width-min,6,24001,1,16384,6752,614,6275,2743,3775,2233,7478,1990,6916,0.009452,5556296913352800080,484,1,6,4,1459,1414,24,100,ok -edge-6-bridge-width-min,6,24003,1,16384,6628,601,6157,2998,3657,1990,7398,2115,6871,0.027132,102958655870675986,437,3,13,4,1459,1408,24,100,ok -edge-6-bridge-width-min,6,24003,1,16384,6628,601,6157,2998,3657,1990,7398,2115,6871,0.056861,102958655870675986,437,3,13,4,1459,1408,24,100,ok -edge-6-bridge-width-min,6,24004,1,16384,6815,555,5878,3136,3834,2191,7625,2143,6616,0.008786,399138138934018572,493,2,13,4,1496,1381,24,100,ok -edge-6-bridge-width-min,6,24004,1,16384,6815,555,5878,3136,3834,2191,7625,2143,6616,0.068053,399138138934018572,493,2,13,4,1496,1381,24,100,ok -edge-6-bridge-width-max,6,24001,1,16384,7510,555,5769,2550,4283,2364,8182,1834,6368,0.140124,11436370948444010525,495,2,11,4,1621,1502,24,100,ok -edge-6-bridge-width-max,6,24001,1,16384,7510,555,5769,2550,4283,2364,8182,1834,6368,0.068692,11436370948444010525,495,2,11,4,1621,1502,24,100,ok -edge-6-bridge-width-max,6,24003,1,16384,7484,541,5558,2801,4338,2282,8225,1946,6213,0.010345,16058708852202796820,494,5,6,4,1503,1539,24,98,ok -edge-6-bridge-width-max,6,24003,1,16384,7484,541,5558,2801,4338,2282,8225,1946,6213,0.010996,16058708852202796820,494,5,6,4,1503,1539,24,98,ok -edge-6-bridge-width-max,6,24004,1,16384,7710,540,5172,2962,4523,2493,8502,2029,5853,0.018877,6983343505154156422,500,1,15,4,1591,1492,24,100,ok -edge-6-bridge-width-max,6,24004,1,16384,7710,540,5172,2962,4523,2493,8502,2029,5853,0.010890,6983343505154156422,500,1,15,4,1591,1492,24,100,ok -size-6-6-6-4,6,24001,1,4096,2272,237,328,1259,1210,213,2623,871,602,0.003098,4297036404364719968,69,1,5,4,472,486,24,14,ok -size-6-6-6-4,6,24001,1,4096,2272,237,328,1259,1210,213,2623,871,602,0.002529,4297036404364719968,69,1,5,4,472,486,24,14,ok -size-6-6-6-4,6,24003,1,4096,2234,228,437,1197,1145,307,2555,838,703,0.019897,4448878305702187952,72,2,3,4,473,512,24,51,ok -size-6-6-6-4,6,24003,1,4096,2234,228,437,1197,1145,307,2555,838,703,0.002531,4448878305702187952,72,2,3,4,473,512,24,51,ok -size-6-6-6-4,6,24004,1,4096,2221,216,409,1250,1156,213,2565,846,685,0.002576,2339653472317238558,54,3,3,4,501,460,24,45,ok -size-6-6-6-4,6,24004,1,4096,2221,216,409,1250,1156,213,2565,846,685,0.002497,2339653472317238558,54,3,3,4,501,460,24,45,ok -size-6-6-6-12,6,24001,0,4096,3852,21,0,223,3852,3283,3982,112,2,0.004930,9275156586024609708,4096,100000,100000,0,0,0,0,0,failed -size-6-6-6-12,6,24001,0,4096,3852,21,0,223,3852,3283,3982,112,2,0.002065,9275156586024609708,4096,100000,100000,0,0,0,0,0,failed -size-6-6-6-12,6,24003,0,4096,3820,37,0,239,3820,3275,3949,140,7,0.001936,18090506768242481536,4096,100000,100000,0,0,0,0,0,failed -size-6-6-6-12,6,24003,0,4096,3820,37,0,239,3820,3275,3949,140,7,0.002883,18090506768242481536,4096,100000,100000,0,0,0,0,0,failed -size-6-6-6-12,6,24004,0,4096,3418,91,52,535,3418,2601,3641,344,111,0.001839,17445749914128481966,4096,100000,100000,0,0,0,0,18,failed -size-6-6-6-12,6,24004,0,4096,3418,91,52,535,3418,2601,3641,344,111,0.003876,17445749914128481966,4096,100000,100000,0,0,0,0,18,failed -size-6-7-7-8,6,24001,1,16384,7102,813,3831,4638,4157,1047,8432,3140,4812,0.059656,1544098799779975629,124,3,7,8,1382,1355,48,120,ok -size-6-7-7-8,6,24001,1,16384,7102,813,3831,4638,4157,1047,8432,3140,4812,0.020510,1544098799779975629,124,3,7,8,1382,1355,48,120,ok -size-6-7-7-8,6,24003,1,16384,8184,754,3349,4097,4605,1365,9376,2796,4212,0.161835,5206711405975171718,142,4,11,8,1661,1710,48,131,ok -size-6-7-7-8,6,24003,1,16384,8184,754,3349,4097,4605,1365,9376,2796,4212,0.057456,5206711405975171718,142,4,11,8,1661,1710,48,131,ok -size-6-7-7-8,6,24004,1,16384,8523,758,3139,3964,4863,1805,9647,2761,3976,0.127345,9930504172083004857,179,3,8,8,1724,1728,48,140,ok -size-6-7-7-8,6,24004,1,16384,8523,758,3139,3964,4863,1805,9647,2761,3976,0.056197,9930504172083004857,179,3,8,8,1724,1728,48,140,ok -size-6-6-9-12,6,24001,1,32768,15663,1333,7412,8360,9034,3817,17998,5537,9233,0.295250,9744454154937155772,270,3,12,12,3164,3153,72,213,ok -size-6-6-9-12,6,24001,1,32768,15663,1333,7412,8360,9034,3817,17998,5537,9233,0.248852,9744454154937155772,270,3,12,12,3164,3153,72,213,ok -size-6-6-9-12,6,24003,1,32768,15764,1139,7675,8190,9091,3567,18075,5275,9418,0.195161,3945711515277836980,260,4,14,12,3241,3120,72,172,ok -size-6-6-9-12,6,24003,1,32768,15764,1139,7675,8190,9091,3567,18075,5275,9418,0.186376,3945711515277836980,260,4,14,12,3241,3120,72,172,ok -size-6-6-9-12,6,24004,1,32768,15416,1151,8118,8083,8872,3617,17643,5219,9906,0.222475,1130691957958131145,242,5,12,12,3152,3080,72,222,ok -size-6-6-9-12,6,24004,1,32768,15416,1151,8118,8083,8872,3617,17643,5219,9906,0.159700,1130691957958131145,242,5,12,12,3152,3080,72,222,ok -size-6-9-6-12,6,24001,1,32768,14684,2078,7268,8738,8231,3564,17085,6468,9215,0.197666,11857700093439170408,274,3,18,12,3150,2991,72,248,ok -size-6-9-6-12,6,24001,1,32768,14684,2078,7268,8738,8231,3564,17085,6468,9215,0.158530,11857700093439170408,274,3,18,12,3150,2991,72,248,ok -size-6-9-6-12,6,24003,1,32768,15472,1813,7527,7956,8974,3896,17641,5818,9309,0.238045,2610189826550231322,285,4,10,12,3140,3046,72,185,ok -size-6-9-6-12,6,24003,1,32768,15472,1813,7527,7956,8974,3896,17641,5818,9309,0.296042,2610189826550231322,285,4,10,12,3140,3046,72,185,ok -size-6-9-6-12,6,24004,1,32768,14973,1858,7737,8200,8473,3515,17249,5972,9547,0.238810,8162734438950355997,223,4,15,12,3090,3098,72,244,ok -size-6-9-6-12,6,24004,1,32768,14973,1858,7737,8200,8473,3515,17249,5972,9547,0.298055,8162734438950355997,223,4,15,12,3090,3098,72,244,ok -size-6-9-9-12,6,24001,1,262144,94799,7136,122592,37617,71106,54946,104894,26009,131241,3.791361,14985575880983416012,1018,2,49,9,12308,11073,72,273,ok -size-6-9-9-12,6,24001,1,262144,94799,7136,122592,37617,71106,54946,104894,26009,131241,3.800057,14985575880983416012,1018,2,49,9,12308,11073,72,273,ok -size-6-9-9-12,6,24003,1,262144,93848,6430,127677,34189,70435,54384,103023,23608,135513,3.426301,14775211458720681956,819,3,48,6,12070,11031,72,281,ok -size-6-9-9-12,6,24003,1,262144,93848,6430,127677,34189,70435,54384,103023,23608,135513,3.702816,14775211458720681956,819,3,48,6,12070,11031,72,281,ok -size-6-9-9-12,6,24004,1,262144,96866,6257,124848,34173,72649,57187,106027,23423,132694,3.626516,10644597901623392893,890,3,51,7,12315,11590,72,288,ok -size-6-9-9-12,6,24004,1,262144,96866,6257,124848,34173,72649,57187,106027,23423,132694,3.707816,10644597901623392893,890,3,51,7,12315,11590,72,288,ok -corner-6-min,6,24001,0,4096,364,37,3413,282,364,225,431,179,3486,0.000680,13032831175469049256,4096,100000,100000,0,0,0,0,0,failed -corner-6-min,6,24001,0,4096,364,37,3413,282,364,225,431,179,3486,0.000708,13032831175469049256,4096,100000,100000,0,0,0,0,0,failed -corner-6-min,6,24003,0,4096,346,67,3381,302,346,205,418,219,3459,0.001828,4538073216810535782,4096,100000,100000,0,0,0,0,0,failed -corner-6-min,6,24003,0,4096,346,67,3381,302,346,205,418,219,3459,0.000677,4538073216810535782,4096,100000,100000,0,0,0,0,0,failed -corner-6-min,6,24004,0,4096,321,48,3399,328,321,186,402,213,3481,0.001331,6504701441893939564,4096,100000,100000,0,0,0,0,0,failed -corner-6-min,6,24004,0,4096,321,48,3399,328,321,186,402,213,3481,0.001437,6504701441893939564,4096,100000,100000,0,0,0,0,0,failed -corner-6-max,6,24001,1,262144,120167,6188,102948,32841,93768,70201,129117,22679,110348,3.929968,6449499192887501291,885,1,40,10,13739,12300,72,298,ok -corner-6-max,6,24001,1,262144,120167,6188,102948,32841,93768,70201,129117,22679,110348,4.096833,6449499192887501291,885,1,40,10,13739,12300,72,298,ok -corner-6-max,6,24003,1,262144,123597,6580,96143,35824,96452,69703,133362,24589,104193,3.753294,12680308190373788541,942,3,48,6,13940,12845,72,297,ok -corner-6-max,6,24003,1,262144,123597,6580,96143,35824,96452,69703,133362,24589,104193,3.665227,12680308190373788541,942,3,48,6,13940,12845,72,297,ok -corner-6-max,6,24004,1,262144,119881,6052,103104,33107,91750,68214,128814,22657,110673,3.485251,3158686122395171908,895,1,50,9,14656,13115,72,294,ok -corner-6-max,6,24004,1,262144,119881,6052,103104,33107,91750,68214,128814,22657,110673,3.755580,3158686122395171908,895,1,50,9,14656,13115,72,294,ok -edge-7-width-min,7,24001,0,8192,0,0,8192,0,0,0,3891,2244,2057,0.019886,7749714202859875114,8192,100000,100000,0,0,0,0,0,failed -edge-7-width-min,7,24001,0,8192,0,0,8192,0,0,0,3891,2244,2057,0.058049,7749714202859875114,8192,100000,100000,0,0,0,0,0,failed -edge-7-width-min,7,24003,0,8192,0,0,8192,0,0,0,4320,2078,1794,0.097895,4139461070367048631,8192,100000,100000,0,0,0,0,0,failed -edge-7-width-min,7,24003,0,8192,0,0,8192,0,0,0,4320,2078,1794,0.042093,4139461070367048631,8192,100000,100000,0,0,0,0,0,failed -edge-7-width-min,7,24004,1,8192,4013,811,710,2658,3119,849,4936,2127,1129,0.053420,1932614858527718601,161,14,15,4,388,169,257,114,ok -edge-7-width-min,7,24004,1,8192,4013,811,710,2658,3119,849,4936,2127,1129,0.091390,1932614858527718601,161,14,15,4,388,169,257,114,ok -edge-7-width-max,7,24001,1,65536,29290,4778,12068,19400,28288,14750,34528,14549,16459,0.184120,4289464862370130736,295,11,18,4,461,191,270,116,ok -edge-7-width-max,7,24001,1,65536,29290,4778,12068,19400,28288,14750,34528,14549,16459,0.260332,4289464862370130736,295,11,18,4,461,191,270,116,ok -edge-7-width-max,7,24003,1,65536,28922,4642,14159,17813,27868,14568,33718,13584,18234,0.357274,5039114077258876817,580,8,22,4,490,192,292,184,ok -edge-7-width-max,7,24003,1,65536,28922,4642,14159,17813,27868,14568,33718,13584,18234,0.213452,5039114077258876817,580,8,22,4,490,192,292,184,ok -edge-7-width-max,7,24004,1,65536,29612,7023,5639,23262,28592,10392,37684,18630,9222,0.386971,2919346984229201714,500,6,14,4,521,229,190,87,ok -edge-7-width-max,7,24004,1,65536,29612,7023,5639,23262,28592,10392,37684,18630,9222,0.247708,2919346984229201714,500,6,14,4,521,229,190,87,ok -edge-7-height-min,7,24001,1,8192,3401,590,1612,2589,2654,733,4093,1904,2195,0.073513,9639025586956667039,130,16,23,4,306,123,238,175,ok -edge-7-height-min,7,24001,1,8192,3401,590,1612,2589,2654,733,4093,1904,2195,0.085679,9639025586956667039,130,16,23,4,306,123,238,175,ok -edge-7-height-min,7,24003,0,8192,0,0,8192,0,0,0,4278,1925,1989,0.080120,16889309551585618671,8192,100000,100000,0,0,0,0,0,failed -edge-7-height-min,7,24003,0,8192,0,0,8192,0,0,0,4278,1925,1989,0.021210,16889309551585618671,8192,100000,100000,0,0,0,0,0,failed -edge-7-height-min,7,24004,0,8192,0,0,8192,0,0,0,4861,2534,797,0.068787,11722665298193346431,8192,100000,100000,0,0,0,0,0,failed -edge-7-height-min,7,24004,0,8192,0,0,8192,0,0,0,4861,2534,797,0.179168,11722665298193346431,8192,100000,100000,0,0,0,0,0,failed -edge-7-height-max,7,24001,1,65536,28770,5154,11868,19744,27745,13904,34182,15115,16239,0.190713,8462703386345682414,439,8,14,4,468,185,292,144,ok -edge-7-height-max,7,24001,1,65536,28770,5154,11868,19744,27745,13904,34182,15115,16239,0.200248,8462703386345682414,439,8,14,4,468,185,292,144,ok -edge-7-height-max,7,24003,1,65536,28274,5226,13150,18886,27294,13827,33367,14755,17414,0.196687,17965852414514832769,389,10,17,4,466,174,260,108,ok -edge-7-height-max,7,24003,1,65536,28274,5226,13150,18886,27294,13827,33367,14755,17414,0.242180,17965852414514832769,389,10,17,4,466,174,260,108,ok -edge-7-height-max,7,24004,1,65536,30239,6899,5731,22667,29214,11368,37957,18238,9341,0.278769,16426413119338499249,417,9,12,4,485,240,220,121,ok -edge-7-height-max,7,24004,1,65536,30239,6899,5731,22667,29214,11368,37957,18238,9341,0.199088,16426413119338499249,417,9,12,4,485,240,220,121,ok -edge-7-teams-min,7,24001,1,16384,6727,1513,2961,5183,6504,3026,8186,4144,4054,0.024204,2996695497079447710,565,5,11,1,117,37,49,34,ok -edge-7-teams-min,7,24001,1,16384,6727,1513,2961,5183,6504,3026,8186,4144,4054,0.030498,2996695497079447710,565,5,11,1,117,37,49,34,ok -edge-7-teams-min,7,24003,1,16384,6971,1448,2991,4974,6747,3047,8377,3959,4048,0.195630,10850214427827422687,778,7,19,1,113,49,42,33,ok -edge-7-teams-min,7,24003,1,16384,6971,1448,2991,4974,6747,3047,8377,3959,4048,0.054465,10850214427827422687,778,7,19,1,113,49,42,33,ok -edge-7-teams-min,7,24004,1,16384,7338,1720,1599,5727,7084,2650,9315,4587,2482,0.068931,16120643585322759056,723,5,9,1,140,45,49,22,ok -edge-7-teams-min,7,24004,1,16384,7338,1720,1599,5727,7084,2650,9315,4587,2482,0.126231,16120643585322759056,723,5,9,1,140,45,49,22,ok -edge-7-teams-max,7,24001,0,16384,0,0,16384,0,0,0,7774,4313,4297,0.066800,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-teams-max,7,24001,0,16384,0,0,16384,0,0,0,7774,4313,4297,0.205120,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-teams-max,7,24003,0,16384,0,0,16384,0,0,0,8268,4021,4095,0.093587,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-7-teams-max,7,24003,0,16384,0,0,16384,0,0,0,8268,4021,4095,0.055442,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-7-teams-max,7,24004,0,16384,0,0,16384,0,0,0,9186,4638,2560,0.127962,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-7-teams-max,7,24004,0,16384,0,0,16384,0,0,0,9186,4638,2560,0.027212,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-7-workers-min,7,24001,1,16384,6469,1542,3046,5327,5538,1915,7958,4248,4178,0.077322,4204626813648185965,173,8,23,4,428,218,217,69,ok -edge-7-workers-min,7,24001,1,16384,6469,1542,3046,5327,5538,1915,7958,4248,4178,0.144408,4204626813648185965,173,8,23,4,428,218,217,69,ok -edge-7-workers-min,7,24003,1,16384,7197,1387,2877,4923,6359,2453,8599,3869,3916,0.107967,3252641365549671066,107,11,35,3,361,173,236,151,ok -edge-7-workers-min,7,24003,1,16384,7197,1387,2877,4923,6359,2453,8599,3869,3916,0.082422,3252641365549671066,107,11,35,3,361,173,236,151,ok -edge-7-workers-min,7,24004,1,16384,7434,1672,1633,5645,6587,2213,9362,4500,2522,0.115490,5596711191859025707,204,8,13,4,372,164,243,35,ok -edge-7-workers-min,7,24004,1,16384,7434,1672,1633,5645,6587,2213,9362,4500,2522,0.076133,5596711191859025707,204,8,13,4,372,164,243,35,ok -edge-7-workers-max,7,24001,1,16384,6469,1542,3046,5327,5512,1892,7958,4248,4178,0.167392,14634870893842776685,217,8,22,4,428,218,215,69,ok -edge-7-workers-max,7,24001,1,16384,6469,1542,3046,5327,5512,1892,7958,4248,4178,0.161514,14634870893842776685,217,8,22,4,428,218,215,69,ok -edge-7-workers-max,7,24003,1,16384,7197,1387,2877,4923,6331,2430,8599,3869,3916,0.215144,3252641365549671066,108,11,35,3,361,173,236,151,ok -edge-7-workers-max,7,24003,1,16384,7197,1387,2877,4923,6331,2430,8599,3869,3916,0.132042,3252641365549671066,108,11,35,3,361,173,236,151,ok -edge-7-workers-max,7,24004,1,16384,7434,1672,1633,5645,6559,2180,9362,4500,2522,0.103474,5596711191859025707,210,6,13,4,372,164,243,35,ok -edge-7-workers-max,7,24004,1,16384,7434,1672,1633,5645,6559,2180,9362,4500,2522,0.086251,5596711191859025707,210,6,13,4,372,164,243,35,ok -edge-7-water-min,7,24001,0,16384,0,0,16384,0,0,0,15616,768,0,0.018816,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-water-min,7,24001,0,16384,0,0,16384,0,0,0,15616,768,0,0.071860,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-water-min,7,24003,0,16384,0,0,16384,0,0,0,13971,2413,0,0.035242,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-7-water-min,7,24003,0,16384,0,0,16384,0,0,0,13971,2413,0,0.019439,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-7-water-min,7,24004,0,16384,0,0,16384,0,0,0,16133,251,0,0.033833,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-7-water-min,7,24004,0,16384,0,0,16384,0,0,0,16133,251,0,0.021039,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-7-water-max,7,24001,1,16384,1705,1166,8863,4650,1280,125,2512,3494,10378,0.076079,9060107067306753658,9,100000,45,0,121,162,62,179,ok -edge-7-water-max,7,24001,1,16384,1705,1166,8863,4650,1280,125,2512,3494,10378,0.050275,9060107067306753658,9,100000,45,0,121,162,62,179,ok -edge-7-water-max,7,24003,1,16384,3110,1863,5477,5934,2703,480,4520,4873,6991,0.125294,14641659834926888497,40,56,113,1,160,100,67,139,ok -edge-7-water-max,7,24003,1,16384,3110,1863,5477,5934,2703,480,4520,4873,6991,0.054056,14641659834926888497,40,56,113,1,160,100,67,139,ok -edge-7-water-max,7,24004,1,16384,2783,1286,7166,5149,2217,327,3893,3883,8608,0.116582,8546910261617826056,40,100000,52,2,216,156,114,169,ok -edge-7-water-max,7,24004,1,16384,2783,1286,7166,5149,2217,327,3893,3883,8608,0.128315,8546910261617826056,40,100000,52,2,216,156,114,169,ok -edge-7-sand-min,7,24001,1,16384,6422,1350,3486,5126,5590,2220,7742,3944,4698,0.092266,18420736929713685340,170,15,52,2,366,173,213,117,ok -edge-7-sand-min,7,24001,1,16384,6422,1350,3486,5126,5590,2220,7742,3944,4698,0.074208,18420736929713685340,170,15,52,2,366,173,213,117,ok -edge-7-sand-min,7,24003,1,16384,4962,1320,5353,4749,4222,1531,6062,3691,6631,0.086618,12794643825776072709,85,15,59,3,377,160,123,177,ok -edge-7-sand-min,7,24003,1,16384,4962,1320,5353,4749,4222,1531,6062,3691,6631,0.135348,12794643825776072709,85,15,59,3,377,160,123,177,ok -edge-7-sand-min,7,24004,1,16384,7898,1043,3285,4158,6941,3288,9047,3130,4207,0.033000,12672208926715367398,365,9,17,4,437,221,219,134,ok -edge-7-sand-min,7,24004,1,16384,7898,1043,3285,4158,6941,3288,9047,3130,4207,0.132046,12672208926715367398,365,9,17,4,437,221,219,134,ok -edge-7-sand-max,7,24001,1,16384,3344,6561,1353,5126,2836,538,4784,9291,2309,0.135224,14567139433359851325,83,13,31,4,294,55,79,101,ok -edge-7-sand-max,7,24001,1,16384,3344,6561,1353,5126,2836,538,4784,9291,2309,0.025340,14567139433359851325,83,13,31,4,294,55,79,101,ok -edge-7-sand-max,7,24003,1,16384,2834,8036,925,4589,2349,458,4059,10514,1811,0.086162,14334491603640591819,49,24,32,4,222,145,38,71,ok -edge-7-sand-max,7,24003,1,16384,2834,8036,925,4589,2349,458,4059,10514,1811,0.031681,14334491603640591819,49,24,32,4,222,145,38,71,ok -edge-7-sand-max,7,24004,1,16384,2575,7158,1073,5578,2141,322,4061,10187,2136,0.065618,10342097500297702324,44,34,31,3,141,105,108,57,ok -edge-7-sand-max,7,24004,1,16384,2575,7158,1073,5578,2141,322,4061,10187,2136,0.157132,10342097500297702324,44,34,31,3,141,105,108,57,ok -edge-7-grass-min,7,24001,0,16384,0,0,16384,0,0,0,0,1167,15217,0.062430,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-grass-min,7,24001,0,16384,0,0,16384,0,0,0,0,1167,15217,0.046082,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-grass-min,7,24003,0,16384,0,0,16384,0,0,0,0,523,15861,0.022064,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-7-grass-min,7,24003,0,16384,0,0,16384,0,0,0,0,523,15861,0.152995,17614258571223508803,16384,100000,100000,0,0,0,0,0,failed -edge-7-grass-min,7,24004,0,16384,0,0,16384,0,0,0,0,456,15928,0.081969,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-7-grass-min,7,24004,0,16384,0,0,16384,0,0,0,0,456,15928,0.047870,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -edge-7-grass-max,7,24001,1,16384,11093,783,1312,3196,10095,6485,12063,2385,1936,0.074527,9127560645823346963,453,5,36,3,555,176,187,103,ok -edge-7-grass-max,7,24001,1,16384,11093,783,1312,3196,10095,6485,12063,2385,1936,0.158525,9127560645823346963,453,5,36,3,555,176,187,103,ok -edge-7-grass-max,7,24003,1,16384,9270,1150,1771,4193,8327,4639,10471,3254,2659,0.034744,4133842926619578305,433,14,17,4,417,192,254,101,ok -edge-7-grass-max,7,24003,1,16384,9270,1150,1771,4193,8327,4639,10471,3254,2659,0.233804,4133842926619578305,433,14,17,4,417,192,254,101,ok -edge-7-grass-max,7,24004,1,16384,9250,1166,1820,4148,8334,4209,10515,3253,2616,0.022217,14889756881492068813,457,7,18,4,387,186,263,140,ok -edge-7-grass-max,7,24004,1,16384,9250,1166,1820,4148,8334,4209,10515,3253,2616,0.114765,14889756881492068813,457,7,18,4,387,186,263,140,ok -edge-7-smoothing-min,7,24001,1,16384,4637,3391,855,7501,3864,537,6907,7336,2141,0.011141,7854435468174684219,72,6,7,4,312,163,218,50,ok -edge-7-smoothing-min,7,24001,1,16384,4637,3391,855,7501,3864,537,6907,7336,2141,0.006329,7854435468174684219,72,6,7,4,312,163,218,50,ok -edge-7-smoothing-min,7,24003,1,16384,5166,3168,982,7068,4426,830,7368,6894,2122,0.010020,14522476709359291924,85,15,16,4,291,163,206,56,ok -edge-7-smoothing-min,7,24003,1,16384,5166,3168,982,7068,4426,830,7368,6894,2122,0.007431,14522476709359291924,85,15,16,4,291,163,206,56,ok -edge-7-smoothing-min,7,24004,1,16384,5275,2855,1246,7008,4542,941,7435,6489,2460,0.006783,4167283095269788562,131,8,16,4,299,203,151,66,ok -edge-7-smoothing-min,7,24004,1,16384,5275,2855,1246,7008,4542,941,7435,6489,2460,0.006278,4167283095269788562,131,8,16,4,299,203,151,66,ok -edge-7-smoothing-max,7,24001,0,16384,0,0,16384,0,0,0,8780,2386,5218,3.301861,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-smoothing-max,7,24001,0,16384,0,0,16384,0,0,0,8780,2386,5218,3.377591,253967105468988286,16384,100000,100000,0,0,0,0,0,failed -edge-7-smoothing-max,7,24003,1,16384,7956,814,4334,3280,7025,4090,8815,2463,5106,3.445955,2945018518815158476,232,8,17,4,496,206,149,158,ok -edge-7-smoothing-max,7,24003,1,16384,7956,814,4334,3280,7025,4090,8815,2463,5106,3.478460,2945018518815158476,232,8,17,4,496,206,149,158,ok -edge-7-smoothing-max,7,24004,1,16384,8319,709,4140,3216,7333,4189,9162,2322,4900,3.135704,926688446117414204,268,10,26,4,493,184,229,136,ok -edge-7-smoothing-max,7,24004,1,16384,8319,709,4140,3216,7333,4189,9162,2322,4900,3.287979,926688446117414204,268,10,26,4,493,184,229,136,ok -size-7-6-6-4,7,24001,1,4096,1581,375,621,1519,985,139,1962,1138,996,0.019082,4942677217394702267,41,9,13,4,294,103,119,76,ok -size-7-6-6-4,7,24001,1,4096,1581,375,621,1519,985,139,1962,1138,996,0.114679,4942677217394702267,41,9,13,4,294,103,119,76,ok -size-7-6-6-4,7,24003,1,4096,1926,295,692,1183,1224,247,2244,892,960,0.148391,9993748889653469060,92,10,12,4,307,166,149,70,ok -size-7-6-6-4,7,24003,1,4096,1926,295,692,1183,1224,247,2244,892,960,0.055659,9993748889653469060,92,10,12,4,307,166,149,70,ok -size-7-6-6-4,7,24004,0,4096,0,0,4096,0,0,0,2460,1206,430,0.033299,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-7-6-6-4,7,24004,0,4096,0,0,4096,0,0,0,2460,1206,430,0.113059,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-7-6-6-12,7,24001,0,4096,0,0,4096,0,0,0,1868,1165,1063,0.058378,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-7-6-6-12,7,24001,0,4096,0,0,4096,0,0,0,1868,1165,1063,0.025972,3724996725508102354,4096,100000,100000,0,0,0,0,0,failed -size-7-6-6-12,7,24003,0,4096,0,0,4096,0,0,0,2200,899,997,0.094144,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-7-6-6-12,7,24003,0,4096,0,0,4096,0,0,0,2200,899,997,0.017872,3748852957731584147,4096,100000,100000,0,0,0,0,0,failed -size-7-6-6-12,7,24004,0,4096,0,0,4096,0,0,0,2487,1180,429,0.021959,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-7-6-6-12,7,24004,0,4096,0,0,4096,0,0,0,2487,1180,429,0.022632,5764149895046820619,4096,100000,100000,0,0,0,0,0,failed -size-7-7-7-8,7,24001,1,16384,6567,1517,3009,5291,5025,1371,8049,4203,4132,0.129933,1760404624663908432,132,10,12,8,746,343,293,161,ok -size-7-7-7-8,7,24001,1,16384,6567,1517,3009,5291,5025,1371,8049,4203,4132,0.046047,1760404624663908432,132,10,12,8,746,343,293,161,ok -size-7-7-7-8,7,24003,1,16384,7095,1390,2948,4951,5554,1797,8500,3890,3994,0.093135,3541327904259981104,71,14,13,8,681,330,370,199,ok -size-7-7-7-8,7,24003,1,16384,7095,1390,2948,4951,5554,1797,8500,3890,3994,0.072566,3541327904259981104,71,14,13,8,681,330,370,199,ok -size-7-7-7-8,7,24004,0,16384,0,0,16384,0,0,0,9150,4668,2566,0.240752,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -size-7-7-7-8,7,24004,0,16384,0,0,16384,0,0,0,9150,4668,2566,0.119946,17810711605240484000,16384,100000,100000,0,0,0,0,0,failed -size-7-6-9-12,7,24001,0,32768,0,0,32768,0,0,0,16046,7940,8782,0.267628,17663834589658576963,32768,100000,100000,0,0,0,0,0,failed -size-7-6-9-12,7,24001,0,32768,0,0,32768,0,0,0,16046,7940,8782,0.119421,17663834589658576963,32768,100000,100000,0,0,0,0,0,failed -size-7-6-9-12,7,24003,0,32768,0,0,32768,0,0,0,16123,8205,8440,0.121072,2126817964852287323,32768,100000,100000,0,0,0,0,0,failed -size-7-6-9-12,7,24003,0,32768,0,0,32768,0,0,0,16123,8205,8440,0.143997,2126817964852287323,32768,100000,100000,0,0,0,0,0,failed -size-7-6-9-12,7,24004,0,32768,0,0,32768,0,0,0,17991,9354,5423,0.113465,14399021283580285130,32768,100000,100000,0,0,0,0,0,failed -size-7-6-9-12,7,24004,0,32768,0,0,32768,0,0,0,17991,9354,5423,0.037693,14399021283580285130,32768,100000,100000,0,0,0,0,0,failed -size-7-9-6-12,7,24001,1,32768,13001,2473,7384,9910,10314,3257,15703,7469,9596,0.123001,697173060607085614,115,12,38,11,1346,573,528,378,ok -size-7-9-6-12,7,24001,1,32768,13001,2473,7384,9910,10314,3257,15703,7469,9596,0.052905,697173060607085614,115,12,38,11,1346,573,528,378,ok -size-7-9-6-12,7,24003,0,32768,0,0,32768,0,0,0,17583,7586,7599,0.109105,4324331741806674723,32768,100000,100000,0,0,0,0,0,failed -size-7-9-6-12,7,24003,0,32768,0,0,32768,0,0,0,17583,7586,7599,0.116770,4324331741806674723,32768,100000,100000,0,0,0,0,0,failed -size-7-9-6-12,7,24004,0,32768,0,0,32768,0,0,0,18658,9739,4371,0.271079,1060199958884461770,32768,100000,100000,0,0,0,0,0,failed -size-7-9-6-12,7,24004,0,32768,0,0,32768,0,0,0,18658,9739,4371,0.115559,1060199958884461770,32768,100000,100000,0,0,0,0,0,failed -size-7-9-9-12,7,24001,1,262144,115973,20461,48225,77485,112961,59742,136825,59539,65780,0.894000,16861156850021574435,549,11,22,12,1421,690,661,363,ok -size-7-9-9-12,7,24001,1,262144,115973,20461,48225,77485,112961,59742,136825,59539,65780,0.865847,16861156850021574435,549,11,22,12,1421,690,661,363,ok -size-7-9-9-12,7,24003,1,262144,123593,20709,43844,73998,120485,63773,144618,57984,59542,0.975009,17276785293891729727,666,10,22,12,1580,609,679,387,ok -size-7-9-9-12,7,24003,1,262144,123593,20709,43844,73998,120485,63773,144618,57984,59542,0.991754,17276785293891729727,666,10,22,12,1580,609,679,387,ok -size-7-9-9-12,7,24004,1,262144,118599,28700,23874,90971,115508,44908,149999,74099,38046,1.230714,10847581075193611586,467,11,14,12,1679,549,623,311,ok -size-7-9-9-12,7,24004,1,262144,118599,28700,23874,90971,115508,44908,149999,74099,38046,1.097365,10847581075193611586,467,11,14,12,1679,549,623,311,ok -corner-7-min,7,24001,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-7-min,7,24001,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-7-min,7,24003,0,0,0,0,0,0,0,0,0,0,0,0.000006,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-7-min,7,24003,0,0,0,0,0,0,0,0,0,0,0,0.000005,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-7-min,7,24004,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-7-min,7,24004,0,0,0,0,0,0,0,0,0,0,0,0.000004,14695981039346656037,0,100000,100000,0,0,0,0,0,failed -corner-7-max,7,24001,1,262144,61736,80069,58973,61366,58624,23551,76619,111541,73984,5.101618,3928696166872537512,199,7,13,12,1663,553,608,395,ok -corner-7-max,7,24001,1,262144,61736,80069,58973,61366,58624,23551,76619,111541,73984,5.273087,3928696166872537512,199,7,13,12,1663,553,608,395,ok -corner-7-max,7,24003,1,262144,62664,78735,60194,60551,59721,24893,77566,109731,74847,5.953248,15068249694491358920,230,11,22,12,1501,473,681,502,ok -corner-7-max,7,24003,1,262144,62664,78735,60194,60551,59721,24893,77566,109731,74847,5.364685,15068249694491358920,230,11,22,12,1501,473,681,502,ok -corner-7-max,7,24004,1,262144,64821,77421,60608,59294,61712,26156,79518,107701,74925,6.030400,16857853472906737771,258,11,16,12,1485,616,720,391,ok -corner-7-max,7,24004,1,262144,64821,77421,60608,59294,61712,26156,79518,107701,74925,3.885962,16857853472906737771,258,11,16,12,1485,616,720,391,ok -edge-8-width-min,8,24001,1,8192,4167,475,2012,1538,3068,1679,4511,1256,2425,0.023390,11168460705556684115,478,3,4,4,487,528,4,27,ok -edge-8-width-min,8,24001,1,8192,4167,475,2012,1538,3068,1679,4511,1256,2425,0.006128,11168460705556684115,478,3,4,4,487,528,4,27,ok -edge-8-width-min,8,24003,1,8192,3715,486,2424,1567,2725,1432,4027,1281,2884,0.006104,17785469579485744303,374,3,4,4,403,503,4,15,ok -edge-8-width-min,8,24003,1,8192,3715,486,2424,1567,2725,1432,4027,1281,2884,0.007637,17785469579485744303,374,3,4,4,403,503,4,15,ok -edge-8-width-min,8,24004,1,8192,4036,590,1864,1702,2875,1576,4400,1447,2345,0.007919,13688052440775195898,361,3,3,4,523,554,4,31,ok -edge-8-width-min,8,24004,1,8192,4036,590,1864,1702,2875,1576,4400,1447,2345,0.005369,13688052440775195898,361,3,3,4,523,554,4,31,ok -edge-8-width-max,8,24001,1,65536,47703,1431,11807,4595,33063,28267,48732,3756,13048,0.221746,16732010231130978669,1331,13,14,4,5516,9040,4,239,ok -edge-8-width-max,8,24001,1,65536,47703,1431,11807,4595,33063,28267,48732,3756,13048,0.343654,16732010231130978669,1331,13,14,4,5516,9040,4,239,ok -edge-8-width-max,8,24003,1,65536,45385,1406,14360,4385,30988,26301,46356,3628,15552,0.213345,13337121346526928145,1369,13,14,4,5896,8417,4,277,ok -edge-8-width-max,8,24003,1,65536,45385,1406,14360,4385,30988,26301,46356,3628,15552,0.362500,13337121346526928145,1369,13,14,4,5896,8417,4,277,ok -edge-8-width-max,8,24004,1,65536,45740,1633,12992,5171,30749,25697,46855,4240,14441,0.371658,1230238689060249801,1369,13,14,4,5328,9579,4,543,ok -edge-8-width-max,8,24004,1,65536,45740,1633,12992,5171,30749,25697,46855,4240,14441,0.254556,1230238689060249801,1369,13,14,4,5328,9579,4,543,ok -edge-8-height-min,8,24001,1,8192,3811,488,2340,1553,2816,1535,4157,1273,2762,0.023963,8092515821314941492,398,2,4,4,507,404,4,16,ok -edge-8-height-min,8,24001,1,8192,3811,488,2340,1553,2816,1535,4157,1273,2762,0.004608,8092515821314941492,398,2,4,4,507,404,4,16,ok -edge-8-height-min,8,24003,1,8192,3415,446,3016,1315,2377,1261,3684,1106,3402,0.011287,4553775760903998981,295,3,4,4,496,458,4,28,ok -edge-8-height-min,8,24003,1,8192,3415,446,3016,1315,2377,1261,3684,1106,3402,0.005565,4553775760903998981,295,3,4,4,496,458,4,28,ok -edge-8-height-min,8,24004,1,8192,3069,454,3261,1408,1950,869,3353,1165,3674,0.026750,10086129522298792280,182,3,4,4,489,546,4,21,ok -edge-8-height-min,8,24004,1,8192,3069,454,3261,1408,1950,869,3353,1165,3674,0.004667,10086129522298792280,182,3,4,4,489,546,4,21,ok -edge-8-height-max,8,24001,1,65536,47458,1401,12177,4500,32066,27256,48448,3669,13419,0.417064,16737113895491891884,1340,13,14,4,7213,8095,4,0,ok -edge-8-height-max,8,24001,1,65536,47458,1401,12177,4500,32066,27256,48448,3669,13419,0.284789,16737113895491891884,1340,13,14,4,7213,8095,4,0,ok -edge-8-height-max,8,24003,1,65536,46049,1751,12205,5531,30842,25849,47241,4537,13758,0.282010,12613659818067807716,1369,13,14,4,6451,8672,4,0,ok -edge-8-height-max,8,24003,1,65536,46049,1751,12205,5531,30842,25849,47241,4537,13758,0.249848,12613659818067807716,1369,13,14,4,6451,8672,4,0,ok -edge-8-height-max,8,24004,1,65536,42637,1729,15649,5521,29066,24337,43811,4528,17197,0.352061,6552194030669331597,1331,13,14,4,5362,8125,4,26,ok -edge-8-height-max,8,24004,1,65536,42637,1729,15649,5521,29066,24337,43811,4528,17197,0.324213,6552194030669331597,1331,13,14,4,5362,8125,4,26,ok -edge-8-teams-min,8,24001,1,16384,6966,459,7568,1391,4976,3907,7229,1157,7998,0.031341,8731846698091770699,1205,9,10,1,622,1347,1,121,ok -edge-8-teams-min,8,24001,1,16384,6966,459,7568,1391,4976,3907,7229,1157,7998,0.043410,8731846698091770699,1205,9,10,1,622,1347,1,121,ok -edge-8-teams-min,8,24003,1,16384,7483,477,6934,1490,5469,4389,7766,1231,7387,0.064573,8603429038783025515,1247,10,10,1,585,1408,1,117,ok -edge-8-teams-min,8,24003,1,16384,7483,477,6934,1490,5469,4389,7766,1231,7387,0.011875,8603429038783025515,1247,10,10,1,585,1408,1,117,ok -edge-8-teams-min,8,24004,1,16384,7065,414,7575,1330,4576,3586,7331,1076,7977,0.057349,1187664599781887709,1140,10,10,1,949,1519,1,121,ok -edge-8-teams-min,8,24004,1,16384,7065,414,7575,1330,4576,3586,7331,1076,7977,0.013850,1187664599781887709,1140,10,10,1,949,1519,1,121,ok -edge-8-teams-max,8,24001,1,16384,7758,1189,3871,3566,5789,2581,8526,3005,4853,0.008123,16900393535412682397,128,3,4,12,827,890,12,24,ok -edge-8-teams-max,8,24001,1,16384,7758,1189,3871,3566,5789,2581,8526,3005,4853,0.015489,16900393535412682397,128,3,4,12,827,890,12,24,ok -edge-8-teams-max,8,24003,1,16384,7419,1075,4516,3374,5537,2474,8143,2770,5471,0.008760,2282274312235013453,178,3,4,12,848,783,11,55,ok -edge-8-teams-max,8,24003,1,16384,7419,1075,4516,3374,5537,2474,8143,2770,5471,0.007175,2282274312235013453,178,3,4,12,848,783,11,55,ok -edge-8-teams-max,8,24004,1,16384,6957,1345,4019,4063,5295,2190,7772,3397,5215,0.009615,2586129154889094525,141,3,4,12,677,734,11,82,ok -edge-8-teams-max,8,24004,1,16384,6957,1345,4019,4063,5295,2190,7772,3397,5215,0.009106,2586129154889094525,141,3,4,12,677,734,11,82,ok -edge-8-workers-min,8,24001,1,16384,6350,789,6670,2575,4506,2615,6844,2083,7457,0.005736,6515502212627969163,440,5,6,4,765,1007,4,99,ok -edge-8-workers-min,8,24001,1,16384,6350,789,6670,2575,4506,2615,6844,2083,7457,0.004918,6515502212627969163,440,5,6,4,765,1007,4,99,ok -edge-8-workers-min,8,24003,1,16384,6052,820,6891,2621,4350,2440,6573,2131,7680,0.006827,5808590115991954283,460,5,6,4,882,748,4,98,ok -edge-8-workers-min,8,24003,1,16384,6052,820,6891,2621,4350,2440,6573,2131,7680,0.087149,5808590115991954283,460,5,6,4,882,748,4,98,ok -edge-8-workers-min,8,24004,1,16384,6036,808,7012,2528,4257,2396,6508,2077,7799,0.008937,8755176265363253992,513,5,6,4,880,827,4,100,ok -edge-8-workers-min,8,24004,1,16384,6036,808,7012,2528,4257,2396,6508,2077,7799,0.006001,8755176265363253992,513,5,6,4,880,827,4,100,ok -edge-8-workers-max,8,24001,1,16384,6350,789,6670,2575,4478,2575,6844,2083,7457,0.081936,6515502212627969163,430,5,5,4,765,1007,4,99,ok -edge-8-workers-max,8,24001,1,16384,6350,789,6670,2575,4478,2575,6844,2083,7457,0.005097,6515502212627969163,430,5,5,4,765,1007,4,99,ok -edge-8-workers-max,8,24003,1,16384,6052,820,6891,2621,4322,2400,6573,2131,7680,0.006804,5808590115991954283,450,5,5,4,882,748,4,98,ok -edge-8-workers-max,8,24003,1,16384,6052,820,6891,2621,4322,2400,6573,2131,7680,0.005200,5808590115991954283,450,5,5,4,882,748,4,98,ok -edge-8-workers-max,8,24004,1,16384,6036,808,7012,2528,4229,2356,6508,2077,7799,0.005224,8755176265363253992,503,5,5,4,880,827,4,100,ok -edge-8-workers-max,8,24004,1,16384,6036,808,7012,2528,4229,2356,6508,2077,7799,0.115159,8755176265363253992,503,5,5,4,880,827,4,100,ok -edge-8-island-size-min,8,24001,1,16384,4135,553,9707,1989,2991,1552,4505,1545,10334,0.004854,10172128676173860437,187,3,5,4,531,529,4,99,ok -edge-8-island-size-min,8,24001,1,16384,4135,553,9707,1989,2991,1552,4505,1545,10334,0.005815,10172128676173860437,187,3,5,4,531,529,4,99,ok -edge-8-island-size-min,8,24003,1,16384,3812,620,9763,2189,2801,1368,4225,1713,10446,0.080019,11538144604986265008,268,4,4,4,470,457,4,99,ok -edge-8-island-size-min,8,24003,1,16384,3812,620,9763,2189,2801,1368,4225,1713,10446,0.014929,11538144604986265008,268,4,4,4,470,457,4,99,ok -edge-8-island-size-min,8,24004,1,16384,3718,673,9965,2028,2673,1287,4105,1687,10592,0.004762,10959576368931806186,263,4,5,4,530,431,4,100,ok -edge-8-island-size-min,8,24004,1,16384,3718,673,9965,2028,2673,1287,4105,1687,10592,0.007813,10959576368931806186,263,4,5,4,530,431,4,100,ok -edge-8-island-size-max,8,24001,1,16384,7747,832,4999,2806,5396,3247,8301,2242,5841,0.005476,16066042146348431062,517,5,6,4,960,1307,4,91,ok -edge-8-island-size-max,8,24001,1,16384,7747,832,4999,2806,5396,3247,8301,2242,5841,0.025421,16066042146348431062,517,5,6,4,960,1307,4,91,ok -edge-8-island-size-max,8,24003,1,16384,7465,930,5151,2838,5018,2963,8001,2350,6033,0.007803,2882025045471101485,553,5,6,4,1281,1082,4,82,ok -edge-8-island-size-max,8,24003,1,16384,7465,930,5151,2838,5018,2963,8001,2350,6033,0.104742,2882025045471101485,553,5,6,4,1281,1082,4,82,ok -edge-8-island-size-max,8,24004,1,16384,7386,874,5304,2820,5009,2938,7917,2293,6174,0.010600,17272371466343954211,600,5,6,4,975,1318,4,98,ok -edge-8-island-size-max,8,24004,1,16384,7386,874,5304,2820,5009,2938,7917,2293,6174,0.005044,17272371466343954211,600,5,6,4,975,1318,4,98,ok -edge-8-beach-size-min,8,24001,1,16384,6357,475,7288,2264,4501,2605,6857,1610,7917,0.008022,642294031756670187,437,5,6,4,765,1007,4,100,ok -edge-8-beach-size-min,8,24001,1,16384,6357,475,7288,2264,4501,2605,6857,1610,7917,0.004196,642294031756670187,437,5,6,4,765,1007,4,100,ok -edge-8-beach-size-min,8,24003,1,16384,6057,497,7512,2318,4343,2430,6590,1662,8132,0.004405,11476863113224605731,457,5,6,4,882,748,4,100,ok -edge-8-beach-size-min,8,24003,1,16384,6057,497,7512,2318,4343,2430,6590,1662,8132,0.007247,11476863113224605731,457,5,6,4,882,748,4,100,ok -edge-8-beach-size-min,8,24004,1,16384,6040,503,7650,2191,4249,2385,6519,1602,8263,0.004310,18276758955520330632,510,5,6,4,880,827,4,100,ok -edge-8-beach-size-min,8,24004,1,16384,6040,503,7650,2191,4249,2385,6519,1602,8263,0.007436,18276758955520330632,510,5,6,4,880,827,4,100,ok -edge-8-beach-size-max,8,24001,1,16384,6336,2170,4829,3049,4480,2597,6815,3753,5816,0.030607,5803748850750772787,437,5,6,4,765,1007,4,78,ok -edge-8-beach-size-max,8,24001,1,16384,6336,2170,4829,3049,4480,2597,6815,3753,5816,0.017551,5803748850750772787,437,5,6,4,765,1007,4,78,ok -edge-8-beach-size-max,8,24003,1,16384,6038,2257,4995,3094,4324,2425,6537,3859,5988,0.007158,195699143552790923,456,5,6,4,882,748,4,80,ok -edge-8-beach-size-max,8,24003,1,16384,6038,2257,4995,3094,4324,2425,6537,3859,5988,0.065655,195699143552790923,456,5,6,4,882,748,4,80,ok -edge-8-beach-size-max,8,24004,1,16384,6030,2141,5127,3086,4239,2384,6491,3732,6161,0.008905,6657545027504875160,510,5,6,4,880,827,4,100,ok -edge-8-beach-size-max,8,24004,1,16384,6030,2141,5127,3086,4239,2384,6491,3732,6161,0.009795,6657545027504875160,510,5,6,4,880,827,4,100,ok -size-8-6-6-4,8,24001,1,4096,1582,388,942,1184,1087,323,1834,996,1266,0.001110,6499355411517282126,64,2,3,4,240,172,3,22,ok -size-8-6-6-4,8,24001,1,4096,1582,388,942,1184,1087,323,1834,996,1266,0.001630,6499355411517282126,64,2,3,4,240,172,3,22,ok -size-8-6-6-4,8,24003,1,4096,1790,418,668,1220,1299,473,2050,1045,1001,0.001221,6987438381204511612,180,2,3,4,202,205,4,16,ok -size-8-6-6-4,8,24003,1,4096,1790,418,668,1220,1299,473,2050,1045,1001,0.001387,6987438381204511612,180,2,3,4,202,205,4,16,ok -size-8-6-6-4,8,24004,1,4096,1864,409,525,1298,1300,425,2138,1078,880,0.000860,5781171558839341672,143,2,3,4,244,236,4,25,ok -size-8-6-6-4,8,24004,1,4096,1864,409,525,1298,1300,425,2138,1078,880,0.000860,5781171558839341672,143,2,3,4,244,236,4,25,ok -size-8-6-6-12,8,24001,1,4096,3031,247,82,736,2000,711,3266,629,201,0.006894,8654506366848622993,277,2,3,12,470,311,10,0,ok -size-8-6-6-12,8,24001,1,4096,3031,247,82,736,2000,711,3266,629,201,0.005010,8654506366848622993,277,2,3,12,470,311,10,0,ok -size-8-6-6-12,8,24003,1,4096,3093,295,55,653,2051,688,3304,633,159,0.004199,18078120065523699720,258,2,3,12,481,311,10,2,ok -size-8-6-6-12,8,24003,1,4096,3093,295,55,653,2051,688,3304,633,159,0.004389,18078120065523699720,258,2,3,12,481,311,10,2,ok -size-8-6-6-12,8,24004,1,4096,3090,205,128,673,2020,659,3315,545,236,0.006114,17738325349339576213,294,2,2,12,493,327,10,5,ok -size-8-6-6-12,8,24004,1,4096,3090,205,128,673,2020,659,3315,545,236,0.004699,17738325349339576213,294,2,2,12,493,327,10,5,ok -size-8-7-7-8,8,24001,1,16384,5702,964,6678,3040,4074,1777,6299,2490,7595,0.044214,7948674464223908679,93,3,4,8,666,794,8,63,ok -size-8-7-7-8,8,24001,1,16384,5702,964,6678,3040,4074,1777,6299,2490,7595,0.055451,7948674464223908679,93,3,4,8,666,794,8,63,ok -size-8-7-7-8,8,24003,1,16384,6457,895,6096,2936,4665,2157,7057,2371,6956,0.009191,5605007882511156480,198,3,4,8,720,904,8,41,ok -size-8-7-7-8,8,24003,1,16384,6457,895,6096,2936,4665,2157,7057,2371,6956,0.011074,5605007882511156480,198,3,4,8,720,904,8,41,ok -size-8-7-7-8,8,24004,1,16384,6208,976,6116,3084,4443,2087,6811,2533,7040,0.007935,2557215965569984820,233,3,4,8,715,882,8,54,ok -size-8-7-7-8,8,24004,1,16384,6208,976,6116,3084,4443,2087,6811,2533,7040,0.020009,2557215965569984820,233,3,4,8,715,882,8,54,ok -size-8-6-9-12,8,24001,1,32768,29759,414,1357,1238,17594,13078,30100,1040,1628,0.031754,1146600372999166395,725,6,7,12,5196,6718,11,0,ok -size-8-6-9-12,8,24001,1,32768,29759,414,1357,1238,17594,13078,30100,1040,1628,0.134063,1146600372999166395,725,6,7,12,5196,6718,11,0,ok -size-8-6-9-12,8,24003,1,32768,29903,541,832,1492,18105,13671,30308,1293,1167,0.106047,439073163953902621,796,6,7,12,5184,6362,12,0,ok -size-8-6-9-12,8,24003,1,32768,29903,541,832,1492,18105,13671,30308,1293,1167,0.108150,439073163953902621,796,6,7,12,5184,6362,12,0,ok -size-8-6-9-12,8,24004,1,32768,29513,552,980,1723,17263,12406,29983,1433,1352,0.114159,1615816978887300187,732,6,7,12,5073,6925,12,0,ok -size-8-6-9-12,8,24004,1,32768,29513,552,980,1723,17263,12406,29983,1433,1352,0.015979,1615816978887300187,732,6,7,12,5073,6925,12,0,ok -size-8-9-6-12,8,24001,1,32768,30314,572,212,1670,18051,13234,30807,1433,528,0.083175,10246488510024421728,664,6,7,12,4881,7130,12,11,ok -size-8-9-6-12,8,24001,1,32768,30314,572,212,1670,18051,13234,30807,1433,528,0.036305,10246488510024421728,664,6,7,12,4881,7130,12,11,ok -size-8-9-6-12,8,24003,1,32768,28972,708,932,2156,16986,12213,29547,1811,1410,0.103459,10334973683976933424,591,6,7,12,4854,6880,12,2,ok -size-8-9-6-12,8,24003,1,32768,28972,708,932,2156,16986,12213,29547,1811,1410,0.105073,10334973683976933424,591,6,7,12,4854,6880,12,2,ok -size-8-9-6-12,8,24004,1,32768,29356,664,716,2032,17411,12858,29870,1708,1190,0.118923,592925104582868716,586,6,7,12,4350,7343,12,5,ok -size-8-9-6-12,8,24004,1,32768,29356,664,716,2032,17411,12858,29870,1708,1190,0.043719,592925104582868716,586,6,7,12,4350,7343,12,5,ok -size-8-9-9-12,8,24001,1,262144,122523,5699,115974,17948,85439,70549,126210,14732,121202,1.380596,639902297847682829,1216,12,13,12,17030,19802,12,1756,ok -size-8-9-9-12,8,24001,1,262144,122523,5699,115974,17948,85439,70549,126210,14732,121202,1.213039,639902297847682829,1216,12,13,12,17030,19802,12,1756,ok -size-8-9-9-12,8,24003,1,262144,116913,5448,122562,17221,81595,66803,120458,14107,127579,1.400221,13366311633393428937,1245,12,13,12,14847,20219,12,1505,ok -size-8-9-9-12,8,24003,1,262144,116913,5448,122562,17221,81595,66803,120458,14107,127579,1.301910,13366311633393428937,1245,12,13,12,14847,20219,12,1505,ok -size-8-9-9-12,8,24004,1,262144,119369,5846,118969,17960,82617,68027,123048,14883,124213,1.291885,876562435972935345,1245,12,13,12,14249,22251,12,2026,ok -size-8-9-9-12,8,24004,1,262144,119369,5846,118969,17960,82617,68027,123048,14883,124213,1.182049,876562435972935345,1245,12,13,12,14249,22251,12,2026,ok -corner-8-min,8,24001,1,4096,754,88,2858,396,509,230,836,287,2973,0.001091,2094329235498495192,230,3,4,1,156,71,1,25,ok -corner-8-min,8,24001,1,4096,754,88,2858,396,509,230,836,287,2973,0.001493,2094329235498495192,230,3,4,1,156,71,1,25,ok -corner-8-min,8,24003,1,4096,738,89,2841,428,522,226,833,305,2958,0.000964,11439071887512958536,226,3,5,1,114,84,1,25,ok -corner-8-min,8,24003,1,4096,738,89,2841,428,522,226,833,305,2958,0.000970,11439071887512958536,226,3,5,1,114,84,1,25,ok -corner-8-min,8,24004,1,4096,823,81,2754,438,677,347,920,301,2875,0.000989,17713212695282631944,341,3,4,1,49,79,1,25,ok -corner-8-min,8,24004,1,4096,823,81,2754,438,677,347,920,301,2875,0.000961,17713212695282631944,341,3,4,1,49,79,1,25,ok -corner-8-max,8,24001,1,262144,137991,15548,88089,20516,93443,77856,141558,26250,94336,1.640130,6975546691930020269,1236,12,12,12,18311,25937,12,1370,ok -corner-8-max,8,24001,1,262144,137991,15548,88089,20516,93443,77856,141558,26250,94336,1.640624,6975546691930020269,1236,12,12,12,18311,25937,12,1370,ok -corner-8-max,8,24003,1,262144,131834,14505,96383,19422,88851,73699,135281,24557,102306,1.549813,13500037929696646449,1236,12,12,12,16689,25994,12,1224,ok -corner-8-max,8,24003,1,262144,131834,14505,96383,19422,88851,73699,135281,24557,102306,1.594883,13500037929696646449,1236,12,12,12,16689,25994,12,1224,ok -corner-8-max,8,24004,1,262144,134906,15168,91778,20292,90927,75562,138482,25724,97938,1.523318,8163465322631622208,1228,12,12,12,17357,26322,12,1936,ok -corner-8-max,8,24004,1,262144,134906,15168,91778,20292,90927,75562,138482,25724,97938,1.487342,8163465322631622208,1228,12,12,12,17357,26322,12,1936,ok diff --git a/docs/map-generators/refactor/edges.csv.gz b/docs/map-generators/refactor/edges.csv.gz deleted file mode 100644 index ea3756b0dea8ae89e082025dd887105266ee0e72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40284 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

M)i=&Ll{*C+!ealWGjmv2|| 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 gxwlE0Z3e}ml;oA< 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
5qBt1U)?w}|lQhsq;) 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 diff --git a/docs/map-generators/refactor/generator-10.png b/docs/map-generators/refactor/generator-10.png deleted file mode 100644 index e359bbac791bcca5430252c4d72f68ff74431b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42471 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 diff --git a/docs/map-generators/refactor/generator-12.png b/docs/map-generators/refactor/generator-12.png deleted file mode 100644 index 66d5bba328e0c3268f3cec9f58d5c2ba55c4041f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35401 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 z 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 diff --git a/docs/map-generators/refactor/generator-2.png b/docs/map-generators/refactor/generator-2.png deleted file mode 100644 index af6835fde57948a8674c0079c329c7276ef1dd9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38733 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 diff --git a/docs/map-generators/refactor/generator-3.png b/docs/map-generators/refactor/generator-3.png deleted file mode 100644 index d2ccaf29661a1a8197fde48a551c23414fcf5f34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44637 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 diff --git a/docs/map-generators/refactor/generator-4.png b/docs/map-generators/refactor/generator-4.png deleted file mode 100644 index aa4e6f131c8e23be6afbf95c76bea1f6509994a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42806 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

P-WH(z9Ib5)S`3}>5rNJGiCJBi>Y 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 diff --git a/docs/map-generators/refactor/generator-5.png b/docs/map-generators/refactor/generator-5.png deleted file mode 100644 index 2fab88732c5768f4a37a60992d22632dae1aee9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44413 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

6@_U1!yGNF)Z#vmIu6;VT-N)pQXMXjRX@jr7 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'&W!D(NtbZ`t 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 diff --git a/docs/map-generators/refactor/generator-6.png b/docs/map-generators/refactor/generator-6.png deleted file mode 100644 index 5a4b562a271daa1cbee565f7d8dfc256e05da711..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44818 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

80^QQVHm#&zY!I_mESe=u8%U4Xm0(~7wMT#s*2 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 Jc}wC?{{ntxVk-au diff --git a/docs/map-generators/refactor/generator-9.png b/docs/map-generators/refactor/generator-9.png deleted file mode 100644 index 4c52c1cb346cc603e8f10959898e5ae20bae8e46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38828 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 R(`@Mf=pgjrMc3~?`5)(fMEL*! diff --git a/docs/map-generators/refactor/modular-confirmation-configs.json b/docs/map-generators/refactor/modular-confirmation-configs.json deleted file mode 100644 index cb6c5a86c..000000000 --- a/docs/map-generators/refactor/modular-confirmation-configs.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": "shattered-holdout", - "method": 7, - "preset": true - } -] \ No newline at end of file diff --git a/docs/map-generators/refactor/modular-confirmation-reproducibility.csv b/docs/map-generators/refactor/modular-confirmation-reproducibility.csv deleted file mode 100644 index 3717d6068..000000000 --- a/docs/map-generators/refactor/modular-confirmation-reproducibility.csv +++ /dev/null @@ -1,7 +0,0 @@ -config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status -shattered-holdout,7,30001,1,16384,7046,1453,2169,5716,6188,1967,8810,4315,3259,0.018893,4967080206951358053,163,9,13,4,387,233,158,31,ok -shattered-holdout,7,30001,1,16384,7046,1453,2169,5716,6188,1967,8810,4315,3259,0.018362,4967080206951358053,163,9,13,4,387,233,158,31,ok -shattered-holdout,7,32501,1,16384,6100,1500,3549,5235,5315,1836,7507,4130,4747,0.017830,18383014318267758100,168,7,23,4,404,135,166,144,ok -shattered-holdout,7,32501,1,16384,6100,1500,3549,5235,5315,1836,7507,4130,4747,0.018262,18383014318267758100,168,7,23,4,404,135,166,144,ok -shattered-holdout,7,35000,1,16384,6471,1599,3166,5148,5639,1999,7894,4197,4293,0.018125,10314852731698056258,253,4,17,4,416,175,161,82,ok -shattered-holdout,7,35000,1,16384,6471,1599,3166,5148,5639,1999,7894,4197,4293,0.018156,10314852731698056258,253,4,17,4,416,175,161,82,ok diff --git a/docs/map-generators/refactor/modular-confirmation.csv.gz b/docs/map-generators/refactor/modular-confirmation.csv.gz deleted file mode 100644 index 22a88a88b28dd96692ace652ef41460678efa73e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255104 zcmV((K;XY0iwFP!00002|D>J$u4K89-2XS=Hz^zZN`5?zFqURlg0&hzGxEN^^B2ie zm$w(gGCV7L)avQ3%Q}ZlW=2Lt{`r@G{M+CD{q-L||Mu5k{_^_u=b!x_|N8TvfByB? z*KdFO`_I3={{5dn{rc;l;@7|a^pC&nU;gJ$zy18r=ePg*%Rhg9{o6l(_J92CxAywi zfBZ>*i(l|p_;3Ar|NKAy@{hm#`uginzy9_0AAkGDpZ@;KpMU!MpY*r>^Ot}A`rDuW z^RGYu^xL2Q^0!}q`{^Hl{`vXMUw-+^*YEwGzy0*T{{Hiye*5{S|L}F_&+j$y$M%wb z`{f@$?_dA)_kaKC=lAdb^xMCF{qO(n*T4Ssn=k0+zx2$H%BzppoH@N(&GxFr)+_zbRC;UvTmCQ8OeOVYx!O$r z_xy_)Pz3LpVQvJ7VzG{B`^1pw7CFNL2O|M)=^`Eu;M~szUJvINNp4V&oKVP-4 z{%WlZt;BjW|GWNsbNFA*y`<4fw!x>cl;KOUD#Z8|M1jb zb@lf0&u{#2^Uw5W`p|c@pzAdGHEv5M8@|Wx%pJ#npt-h2t{2TIh27ARf$k$R@>TFKm z+^w4bqs+AOT)Frcnp#U`EqZjxKcxC+T$eK5OELdx{L+6v`)}JE{fh=JcQMzw#eu=KFc1oL_@I>Yv2>iIeEhiETWuE16$sM<>>N3zt5NZ+idG zUi~AA|B%=0#TS;EfBNu$!AfTLQ#|~|WIso~c~h=y`9t)5^YSNaJQ7Zqt$01ft}9u` zu5JFbn;&I=J^qFKaQLpRlNZlH>S^!VQgo1fk2HS(Z4G}hqothk2{25LU11!aZ zNa2U@Y|Hiac)WTqW%<|6=6}AJA6BcAfHJjH%4_IZE-%Al2yssQ8w#{f^9^{NOY0FnU)J>XR6jk{zXR8TKP}%!ynUj6^H5!g#cY^+l*?zt z9ej)*Ab&sp#khuy*jAc<>Z~54n!@gk&{BL~RYLsm*q8hCyJlG*gYfVBoCu|e%R})E zTnm5px|XkqI|yHKq8l%1`PTGZM5yV$c_$z3N7uK2ZxpR%@C>ZjKh-x zwXE%1`BZ;9c@1sh+hUHGYkH~LzP|B1rhAO>@97_Het4Is5Ur3F_^qSGEWJYiEx(eg zpML+PUmo0;z7KbN(Wc^A>zXUqrF_L)zhiZw`9bLZX4>#==NpF0rjoCC2=;rp@{gkM z?-`8$-Qh3KLsRnb@0m;U6YLMfFD(Boy(3;3C%|)_b$=@Jd__E155pe1{0ZzVAF%`&E_M|9*zq8!W}2h~M14bA2nM zYaUn95nlYF$yE=S8*s1|KR0nTO@CSbSo|#1wU4y_k4y-wo;DWpx#Sm?Um?1Tdt8ex zqWFs(bzEOiIdfaZO5Y)6k}9xDzW}&gc|nbBxE3ohWt6pWW%?E}->ZI#{Nx(Zj%2th zwiGSJpPX+q64G_uwxt|*k+JzbEcuV{TW#@sTq|Yz$Is$7Q0=4-%_5mIk8S?gJm@xm z6dvDJs=k2>Ss2LzF~R?e|C+5!4(|{Aww5F27G2RCAACkkSvtQ zwtX$6lIyh=5ykH(PcXZGPU7AnMaX;K>Mp9fr#~#E9eE4EvHG_!BnoY161+vu?`fR} z)Wqo-Tt$)d4?kf3%qUL$syEMqoBv;;%-{;#`<8g&o2&5O^&0)l8FBdrlSt=@hzEY( zXZ}U~UoYQ0%ZN6P2*^B8O6RG#5n)g%lwyhR_^FVZbSCM<*XdH5|asrA4(w# ztlukzdgiBl*l!$Lr3ao$c|GEBp8dLf6M52^JPiGzN~p{;N$|h#-;$V5W#~WMFBsnl zlw@;}Q+fvcBC186J(I3rKk;UUKDw`UAJug3bme2 zlgSZA$ZP${FT$@s4&Ji|4ZjCF+bBf=;$f>2oBg@?N%LBgM>PjWxsubieU#Zv%Odkn z*AQ2a9Cz@RxIX-UG23jc>>Gu7g>28+P)r_eOng~Ze$D`*$fTa_-w&+8J87%X#7C$Bja@vtOwQJ+ur}_@r|s;GW7cX@0V8 zc5NHK_WVU>0V;brQ9ZLhKbZIQsUDbuwpioKkj_zadCF}Rets&4Crf@RIEN7q%HHcrj(D56M_EZB>$VE=#E1In@H6eXw6BB<(dy0< z$fbO%c)oQuhQ16tzIf~wRH^wO=& zOCO^|)}M@k;cwJWkY_8NP{NKBSJY(f%^Dn7Y=`QAG zUgUbdef_5zk4Z`KAGw2CIkefHbp}vM(4W^c+{zCnkTwa4bnBIHA}FC!zWZ+TALAds;x_cB z;^Aryzj?T}r$0cCxl7Q_1ntQJo=EzKU@)M|+fwTBP{6~CEXh3SF~X`@S2zH4tK7*; z-&02?PcqI$!R9x)pDz!#emO5n6kW?Imlf>&bIJQYyq_b;RxVD@q5Ygrf3M)49TY?P zHuo)AE&phduXrj+$j!j%{%k5Yl|Mi#s7wW9Zy>uX&iZ|MUCLLG^`FgM?ti|5Yjn!F zMu5zu37w+dh<--?z)~`aw^sx}DjxbhrOMOuD1T;?82{2)2HDL1TiiSS{(`=utgbgd z{QhqIbb6Ha$ffM@T<5NrIZdvf{eb;_P z*{-??Afe2~F7cyd9>4wj`IR(=Kb}Ui%p1{CD!|y9bCOVcaX38yEmHxMiT8pKM&B76 zC;GmWd0ssvWYqj5ujXHnx1vYf{TvRJsD3JDj;G=|0a;d0*-Mqx7hMmJcLJ%{7{GBa z06x~vDf$38wcii3%Xa}-4|qh12VpY~8CbF3-_#{)BzFu{^hg)9qH^$AySsgV+RV7~5HJF?lq z_VxOoY?K?_A9d$S^bPEn2_So2WV76!$_ttb0gtZ3|Ivgj`cw47Q`U2MfOj(W9f^Ow zCf0V~wRPx00*d-UG>(y(iWmQugWIQZBpbPqp2~=8dhxLQHt>s)lssW)g(yOQzg%tJUj9` z$di`AEk=&?K%^8LByfFv!ZZ32Z#kVFAwz_P^!}6R^$c*)5NVv#Rj!_fJ3KEde2uQ&jpljf33*X^WS_{#ca6G=Q6E9eVlf zA!GWPsh&&ty==f<05WhDfE1O%sg@GTbFY$nIdBzVyTJ|Wp9z!!u2RTXC^MAQ36lUq zP>iEr9ymnscMIbVxJJUC-}4pY(#=cW+lyYbeQ%8i*adGpBoI+mDDl!Q?r7{{4OblOxzt zPUh>FJmhLB5qlJ5i)Ll@kl<|)d;Cv4E;S`1qbpR7dSdFq&g+v&;DM>p3H2~FidLq^_lHw74}gal z4a=kQiVXl)39f%5J(dE3t(zqP9wVT8dFB5*@@u0FcQsP3sZ=Q9OcK^sPa-9r>_#&h!Jc{f*9jzT%_!kKFvxRsl( zT@TVICryGpEW>ONdBo2PnIqp*Gzpr!kVk0vDqUEL(Y;YWHvcM~m(~FKSy04XWr^$s zD(CwpJ-!0(q_doQ z>fS-kxvu1d6O>za>Ie!v^7YP^C{Urqk!ajrwQZDtLC=~LX5BA#zX3fm7aGAk>MpIM zymTq3e|J67;|RKI2_*tK0s>|0_C(i@JbO~RDc;t+Bb}ugy++SoJk}V5G*84VTK{Wv zYJ^b$Z<8)uv;XlGb*K8Prs8E#B=UynofO?_iAJeE5xS+p*`qZ@$HkvTAIl>h=mThS zJcI}GA%d}i1a)55a)e#FxEW;Aw+Db4$t7K7u@V;RtMsAk*zH|4idbWVKC%wBtPlKVton1fyI+b z(V)stsN)-Rkm^0UtV{P0I^$?9YQ&ALbWi;}0@Hr?nBqTs@Du(JqA*Y7mQ&5%Dq^!v z(l4W$Az|@kpa-TdLX$uaT^#-|d+!t`fV-2X1MS$?+{qll4(0|KL%z19=7HRT#Q95S zpMZf0-=mJy$(~@aLN`>2s5Db2lQPwFU={(@aZ*!K)a@HFb+JX*`}2BwW;x+~4$f2# zNL@v?55!Ul-M~sDGwj$FMdCD}4?u?gbgMtSZx+(c0_KzAZXTr1+3Ozs64CQ9tjHGUz;b*1u>>B@eF)gpiD_Q?C}CV#;yb6oC7t zJ@5obOqoV0#l$9;&MexY*ICpi4g3ZAL6nFvt30{(6)gJCo?}q|aschgw3QyiqGX{wM?Y`EUh%IVonS(o z@)fUki&FoOY{7LxxGY`paaKRbH*5zOE@U;(&0t(j+N(REEcA|&XXnt6*|bo_Gk-wz z2-~2XntMXWuBp>H0&eCgs_DkIZBSM_sa#>(Qjq!wcTukbteoJ@5EuROp=$P&#aAvm zvOks)@CL12vJ2AFe#Ly&5pa_-bIKhW2cB8-RDghHBSR3wXDUP3Xfg~ak@5`2pnrx( z-36R6l~6|T&ru+%NW3Sv>nq?kT4H`EsE#Q|;yG~r0||H)KwHRPVy7wy1{m)b8tC*$ z2g{^$MI_Qs^*@TZrSVuITe*gpW#=oRj;DD7gEJCsC0* zs6;$nsH9EBx3ul>&o)u>Y<-eBtOHXRk~#RL2>M4~-T$$g*fgkN#OT*{nXz3wv{SS} z`1EX)x63uDCYR8OSd6MpUYv5G`-VBwTMCDkd(dZ$=0&H?Tu3gLaX=RQ5Bg+!Fg!I< zDE~WtF;4c4uUoAC^{9lD-U22;ZMYw!lXOcuMWO6P1VSXjRp*Hh^a7Li!|WqKA?+9S z>?@Lj3}RJ;5w1;~tUFogftwcb$nR@e=fy~-4UR^`p@F;isc=GJwuF434%rtYfF;~4 zzlQuP`ECP(>LT!Tf!jsBs=V=zd4DSEdA@fGWe{*zX=sH!Wcb;UgrFdG@Q?ylOEZ4A zs!0B`f-Zo!XL5f0XppG4+oS#vTO)dVAJDLTD#~c=X3?qmT8)hy{9n@VDXwtmkjHAfSD?F@erup z;_3br+*CdL@(j$&AkTjhx#?a+X~bH{F=>F2T9*5BIcZ?P+vpu=vJ+jXx|$ldegS~# zHWGnK_c$8h{?Jy2?KDOZYdooj5}TC!kB90?hZ616{i&RQ!W4Ac_Zve0($(KtwAWD! zRr&z_h9dXxigTov3qV*@7C*=;P3*lCn%`xUNb3Em*x7&BSyV0R;c@?(Za)87o*GEG z)Zwy3a_QM0(hTH>-5(6KnJ0M&dZLA=;w)|SiI96uT=JnT8@e$nS2AC%dG6}h4zSCB zO(s$m_mM)K3TH@seyqIOAMk+v!wL-&FO!Or%kYiUEE~RlT;Q-L4|Od^petj@#6)rXr-1Vp$P} zXi1Vf00x0nC@wZ3n7Ra!-7d4_&r0wYkEOPaw2Qhvu1h)cT=i74fHve(qHiqj z5u)1!HC2dfwDT1G=}}}DVFdy(&8q2tK!-?_{%8^j_NVO&$Lm%X7Xd$)+~@PrVc8ZL)(3DQyu3&16f(>zE%(N{=2kUTYnRhh5XRM4tB_OG3^v1iocvg zw(1Gvq(Xx?PWrEQFER_iBsN$nt%1KBzk!g&dD7gxv=JV_O%$@#7W9qiKVJjgp)Kn= zj8*B0A6^3>Fp~(?|J2}`M*0G9p_n^81J9%~p8BWh&vN>sZF(W2KY+5-ro1Q4(6zmp zPh}Z!e#qSX&IeG2y2lguEs0dtLMe)hH{#s#pW%Vf5Sn(6MMe*gz)go>vnI&M(q2lr zhTMFz!lu+Uo$%dt#n2AtV}N8Q9T^A(k3^?~Jn=K2Vj1LUm>gN8(<27n>S3o^|#{wL+1m$m5K;C#gy@@sHP@$Mx*TC|y>MZeVB1i(_V zFoHM1HoKP4`H+@%Wp=!!3glzbHIgnBaGxReAdUilE6-gKiPYzR+BiTS#a79x!&|U{ z$Ac77DsGDOZ7nCVKz|5?pHz`O0ke~ogI3IxYM+L#r%+plj=A}n47(4pE9UJTIR6<&J1E^k^0GK48X@jEfjL@Ze2%xQ=zV~_g#C0TXAe)AE9f6g=}R1v;+xp29BdQ0ufDHMFo2|sL7oc$UrMZ zimZyG#by!p+TuE3)7%y_&z8LWEg)(7`Bm!gTa+%KAMCN9O>kqY#E-sAVLPo+v)|_f zY763vstvkJR>(mTmwoJpUvav<;LZQ78_P(i)sk zQG&t7dL<c&f|vYW3~;l|gwl1LB~^Ge#H|%&#yvHW2AIH%&aR zE_yIH>MZjiad)zN&!Q&C zuK7tU!UrTP+52TZa^CWRW|I$jSr?qzgun5ANQA6}k}87?t;posrXP9wUnGu&1?pFK zAqV!4qN0nCTNHK0&97n%6L)TlIXSt+0@4hx={MqTYWMTL24JdJa9E5)g9h#hiKpM$ z*>5h-Q3mLH)^NFjV%;}BhQbhnl716aZ*64DCp^XFZ1u90F*2aU1 z5j2oe`6&i>fg8`AOu*ZWA0zJ0pu13yT-I``&A4o`D!Vb`;Zxm%Bs-oWjA(OG zlX!(dsqMSH(cgY-uGNpu7S$jkfBjRwW*QFdGx9FJ&P!C!=&qS@21guOZfszn!Q8ZwgHc2o3^pVlZuWgLFo4sHRR{pIfUB zg2zdwQQ(itQ<&15F8SB|QZlJEFJaX+Z)s<=66(p$`wGp9Uo2!L>a|Afpv1 zjlM_dcgnk9ie`ikKOYI(6S(&KRx3yfDVe&?qYE*4#us3z+))YvJd}&fT3BCCLr-o%a zsbSP07!j#^*rr-ld>2TH&!17Z^8yvwI3KFU&B3ciJJ(D;xx>a|L zb3Vmp0!{%}3Or;ax-Yoc^kkrQQOD8}6Tx0fnWP9fo6&9bs-^pvfxjKsP5NreXbX(` zL5Y!1xmi?nh5Q!McJM3ZB1mn9sYAoz0OrVcPvCCpu=nU%O=|hIBq5Sj0swLiH4_W* z1zyGZ`P7>UUr^&zMp&d$U91!o&r>L-0&WY=kDnAGk0)-$g9&_iPcFOKbwt+9RT!a! zgGhd7+2m7i2E{as2nb8lWcr7+x^T-6c~8j&v2Wd|kwbWzoyEmnm;iCgh6I6cORlFS zFSh(ObF$N|W=2Ip?|G9syQzJMdBo`U5d9yQB`gCncSZ8|C&{Q^isHVJLOwnu^8u9S zr&rW}|19G}Qb$-?l&25FWdbdzPe`AhB}`%Sf9xzD9EBlq3b!bH5lc$nuto`%npl;j z2Xzxk7HBno7QMsu!_dt zTWpikh4%r*O-~I7N*?YK8rBl5WE0k3BAd(7C|}pIPAU3MYAZy#*5y&g?E&0Av7Ca9 zKC4DyB% zD>$;Isw8~vX2jv5E8Cq> zsm7UB!vqQm1SzURzg&f;qy$zEjP4gv5C@!)0SDlCvD#5N`7QE(HX81-7vPceOF-}DQcK?Epx z8*H^`|3s%b)MYi!Jz^Ca4>4x{4ZKnhu;`GHKqW1)*R zqS-)5zB-uZ@y%Sk?$nKa8&o~vuTt)+{Z5VEED*SYMul;9TU$fjs9pA66y97bvCdj1_Mt4Inv@)i^gX^b69yzxDuWoZ- zX-=(AJgaLLsK?V(C*o>1t%I4zyc<&_9b5~ z1s?en8zMb}aQWSrkuCY-i)MfS%4FlXE8=mr-k!(lbSuwDrT7!o1+4Kdf;B$fbT>SA zLvFoWby@A>j@!im8O>pCAvsRR(MaUQya7O)pHlo&R2C5-JVQb3ecN@m8g_<|Iw3nj zYu!Dt)eFGDPz2N3AsRsQsnV8+ zb`+Ok>uLxlN=Z-yx(|MV8{1!rwQRkk`Vt}J-g+NLxRrXaN#70bCUvT1)ps7@(4#k{ zaARMFVxX>sW;WxFNWollc+an`0LjsC#|h#ZQVv~N*cT#+N5O!Lga2c3s%75?2e6@w zsB6o3xrOOV`W~_AFB0PflVr(Qh`x)cK&9QLlMR>5>|v)~S8^(Nxj1+Lm8_Z~leIhqw^q<72jDU}CIfKESoD&5gTm*+W*=<$WrbrlC_T0f;(D*e>Ga zEYPj4N5BhkjFTKDPqdkR$*Bo$g5p%gp&^=xQMo=Ci|2W^#l+?EHykqyFo~A`RR(by zUHJ!1IT(dXZcUbGwX$W?AlbEA(+2M0P|5;Qy4hgR++7^ACDzG>FZ9SYWqe(dD%VHGoLaXDCCehvYxHzP_r3FL%_{(CxNz3GMkING4FhqOMK>)BJ zi{lwUx0#yfS&UlhexyCEAg%ZM9>cW8z_C5Y7T8~w z9KF>}vF$<<#gK+2kj)wED122RbI}9SbKZCTexAr(9gRf)p0dnB?b7RO%2`@5D zW|8t+nVFxNbzjFgr;o6^#e4)oz*|Y(xeQ}|k##q0qD3;>;{jBwdB)wCp^&Q@{=o)+ z_6OT})2-Syv>j5iPF9`aLVIYGl&r#@k+%g5G^~Zlnxrxa17ed9vB4Pk8hKWcX~L6# z>~|wZb_w<29pE{YWK!er@h))QlIZL7`_TYZIfep(?I~PHfn=^?B`|eQ-*9Hnf#^J{ zHjo40I9y?GX>B>AWJN@E`jUoGQ`ePY8V$C#_2Pib({{VR%wPyDpvexo(i8r~^17Ic z>Ya*f2a>ivFb~QpB-1o4*{DK?F&szyTk7oSp#Z?qQO(yGO%Uoo3 z&Das8`j5};k{tKCma`+Kv0}urF@ils+^CW4%Ye}X7p>E@jTWHmoEG>2QUVJOaZEEEhfbhsna-N!h>FptB6B}FS! z9IHs0S`Pudin?-LN;~cFSJ(#?8Se5EyO25B*i%5^5?eh#vxMZl1e0{)Q{(B=s*E)# zqPK2brNpc!Ni5QRzkJ2onZuqihXYJgu_;ak$;~8^L zsxpffgUR!1M4?*lujuq(8$ti6wrGM~)DW{Z-j&X)@UyyMdQNpIiU@aaR9%(4DTMoWgD-oeFUaDA{(mV9D zyT+`kzVm(6vVrY?+PPM$Ns8oZ5GzW{YCAUFY8F2~KzPsFA?sfdh83<;f z2`|wUi_`>p6K(BIg)XO*OyfQJd5~IUm3dw86 zv>a~c!Yvma{5vizB1ANGl?a3oMA`WY79MP~mRw*>9w5vY20-_=y@%Zo#tdU+})ur^7F zDF=!Ic0WOO_I^ZX~-JK|znC;}74DtkWS*k|D?@sq+ zdl)F()vci%>ao6s=_HH@2(Xej5e$crDC^<6+qS8z?du61Lv6-X+NG0pB$L-a%}Z1t+j1SgtfzZ^68+%&`Sy&s%+11hYUXVn zkppqMt)-t%^pZc%?az;?g~?$LhS!i$K56@-WvfZ~z~=fesIwkPk-pSTg_ z5Rl0nH0E$YDrN=^gg8!--zfu|2F-kJc|1)rZXADfzGsnzjw07+wfF2v_?2eMlQ?}q z)yt_Mlcmu}C2|8ekeiClVIEyuNLEHM_mJiS0dV=%&{5nr>67FUl^*0t{^DI|{hgIp z>NlTT$i52F z&mNfsTa;utacZ1HC(AD_UQpSZy7i1uP@djt0|5}hp+Rh}7o;I+A!p!8Kb??#-z_I{ zD?0(8Vg?~fPi;i33{7;lRAl97WfrwZO8_rd>0>$6 zV>pqr>Bz@4@rgMjHN3x&bI?}gYqt_PF0Vwo5cnhhBhy>}m4Zi*G6l#Y3fS1focmHv zquVZu2hBsgBf#sr+&Y2+U1X-(!UkU1f@#c1!-hoDqdjRrjKA&t9s}Sag^>c?9ZfjJ zW6>-j<3a9XfIiF_VJ!%@Ba(4H++JY82B=Tl!|Uv8!rjg$ znVd$**pQtV_=Qm$;$)6Sj7&a>rQY{6JN*AJU1y|;FCCzzt(Z}>wq{o+sQ*9AgIGpn~Wb@5&~xnj}!S0y73(fNS6_iy0JG1bM+Y%-SFiN%~km_yLq}@-fY0 z3vWJ6y<(s?Zd?>gGeAnY-WKhap2UHp70+N9c`7YWIQ5D}j0|bZ-GCw?+dQLf# z(%0Sv6#0ZiF+5T2N!(JcjXXHqK(i}`OHd@%2*G> zoM+Jg@RwN9l06Ux7BSFHh<{T`Iqt{!l<8_l1kDR|KNR(u{!zrb;8f$n8P;1G^2eJj z35zzmLlg7rC8IaZqzn_)B4&|5i+)WI&KYHLe-rVHx~nN%=vwAwpw1!-0=bQA*x-@+7KB}OcbIRn-8Z&(GW|PPexTpYTOwy?)rJcpFnks3E zKR-meewTZYW%v!}<13kzDKh&{6v*pJ&apYVdSuQig<-MaJ_+AnG6_}K+p4~nVU7-D z_6*(j7>JF3q$YGd!i5q5S<>bH;X<~Ph9EB+h-S0~a4Pppu&I1O%&;^drN6B!ko8Rg z+?kruP@T+~0+*-59OlezOC9S6*P?Q&#xxnZhQxy=4!HqYMufG_mz=!(85XzrzK}2` zNVce{C*YIZph|4m{1}|8ywQxMYhHWm#)d8ojkORxgMkuL7N+>^GIcdX9{srnJ2x-Z z*NjR~qZ#kTeQ+U=L4+=m=?9tfdh;B*v6;f2SEUE}TUQgWYDASM8PI=K3$zW#71>%!qs*<&2cL|yd&Gq%RDUZ?8_5uUNi`N?@n|O}o zKXwTos{^Xx2C`Ab!RwYOGY)y%J4w1M-#Mr*Tcz3B#5+~4k>dK1wWeI6 z*N0m9)Qv@gf>r>4Ejp=LLlrwgB?%L3?|YD0yP}YEB0gQC24$o~6nU%u1ImV~b2SMn zhit4QjrL?eAl5Z;K~2-5z*Y!sVr`Y}_t?Tuv}l?}stZ4lgzGOU^?o}cQQ%&Ip=}>5 zi^`!K+sF#K1nSfhS)p0zKt`mgEZ=A?f=R^&BWs#g>3trfOnoB*GqTAsbh&ZhwMCre6RQsEm`nXNbxmbZKJFy}f&i z$Ye1^%~uJmrglt`K(-3LlspLyZ)%TrnFru6W*E~O7-Qi1Zk5mU`wi)-kREbM?g-^M zrEEoAIAdaI;|A(<9|~J&7DawI5+y%L3;lji`WWOpkVG0xwyJ>gjIfryl}YJ-)#8*x zMmgg0ZCF8BnOMTQV=D1(3a@gMVe3}Ws%4BbuH!e z!jvQwwiC!BA<*;|iu4q+2&Gs~J7poLknb@G1KTfVEaA(n5L-9#3K)ZsMUr^$+8rV? z=);uUOGBJvJ{j~OicASDG$&cWdf$ViXSD2b!~*B0rjGF??Q2>l8OwSGeBXD=ITuo8 z47&`5LZkEFQCevFRXFoNtYNa{NcZR)Drw!w=*Ha8G%|0|vWxo5(N`2DjVKwEDl8vd* zudznChXqexx@Q?8s@b=u*4=o?L9R44oKkMhX*aGDrC?OTrY`z)5^_i+yKY{$`lrj?|8VW#U zP$Px&0K?dbV%TGFfkOlfnh4?b%$X^~xXWFvEtLI}m#bhcn`Fk~K zPz~)vrS4drOi)oZ;alD;qeV*24a)PuKmq|oc(TF3+^r9ZVq(VMRZLLMA!PyklH$WN zmBm=c?y*b_DxfIfLVD}oA-1BC1dqaLmu5_pVNuK}=nq%if@XC0PAZ3*Y{@d6{6v&+ zVmMPcW2{mRe>CyoZM)_%B>^vOa(@o7Ty8zj6 z=$D#@Ks!Z7!&BCl0fQBURI3}QHXw`Ub=DM}f7-3vfGkEtwoi!M{5JNyN&N~gIh3IV zz+Z7(nRsK?CYW!FAj4G^A2e}TWEkWdMxJRJQZx3*gd*&AelT$@^YjwuqcvHIjZ0I7 zLPQy^MkZ(!m$*i68+?ty7`|=6eV8MJYiO`X=2zn*hWE@5#%yJt39j|X6vXdAP>+3e z4;##hRX`JFheBwN$&}NKufa=ayp7=BYTQgvuwqhj4mW_AK2iF=%_&pXA|*hPc9(__ zH)|KU7Al4c>9nn?~+_pk>ui$So8{87$p236pu_mxDbe_cm<@L<0=|?-s#s61MDZ}8X{wN`!aD&>Ft4k*PWMBOeR*RXl|N-q5>KZ zjl%siG)feCRt(-^xDHvikNHgZ={LAv6Uh#K`DmJc9~Sr2ZNOaZOv+*(M9OksQrtF|2H8 zND(GT)1Dz%Eo3nek*cJsiHWncy-BttDCM+mI+(JRyw`J$bn66hg+8)FF)T2emnbRr zF^a7#--Nz78$%((nA@xvENOAyq=>q&=*>0~o3ULJ?sHI?{u&CFT4jA!#Z%r(n+yt& zs;)g6sxB>8Bcbr;%e!I_k;eF@#~xHvw-QX257_o0D;r(dt1O|1{4*rdVP*bxrVMf{ zY#-O-_&Zg#cE*EsX7CM@Ks=J|7=6A$F{_^f-Ki`Sh+ z!aXMeGx$FdTnt9TsG~$h6rIep%`of7jr+|4Oc)f!)fB-8D;CKmB#V)EUX`!PE<*`IbLovY@zLu&13IdH6HflbGs&%>%?*iNFxZe%D78bcEZ#5JVZ)k8DSo6OC(;bI5m zz@GpoGJjdKFq&J?vSvfZG>OMluztwsYC54@A-}K(d9FroY9?R)u7oy+Bt{d3I7N%< z-}m;M8QC3%q85$W5Y!^A4 zdZoG+kOC4f6O5cfUA7)>nmV=hgNK9ql)pR5T9O=2a(aF`A#`|<_4r4RlY2SaAM`k^ zhpMbAVjAg8*s^M% z)zDs)`>UBpPg7+4sln}go)NAYWR;+&L>ieQ_k9NmXEvxF1DzrY8Bi$bS^b%+ZDl97 z^y2GvB`5VZl?b*X_ZUTEH+hfxHGo8xb|$w%2+;jJn`Wxx3y;A99$W@&iF?~K6S!)s zUe{B(VjJ~PmGvf7SskI~^4m~zAvJEZFws*}##v8zj#Q25D$oY~8jPQEc%``WuHuT z>nSXY-6bYH=NM^zIJH3x9y9!4`kU?QH=Y@0b{jJP&NcnRp%&Lwjl~-gV2e=My zsHe2d6scn|nxHgeYJmn~PTWhYhV(*K2!E$V6PKiaH3Vj!FBCD@>~tTl5{X9iy+-{* zTt-tpWKbCzXtWZ|hahOcc7?PAT!$SskA*L~don{@i^{AVzr3pFcOF}qL#2&s5A!L;Jdv-i zeqG8LaHX`CLcc-*VYEh%@t*+x)B(|e*iQv|Hqpj+EM|6KVJJuXmSb+okd9$A!tbQP zdg#lB#>2DoDx4@#0w5@fxPkX{f~1Kod;SSD8>%tiBNSvFex&IF$yf(B4RCvWnwC?~ z>0~OmYjnZYIMbt%vl5of@+%Gb-v+}}Z6OsZu%ZxU_>&<_l4TEfN-f9-tHO`&m z$(ppTG9!{vn#qzm#dP4QS@1Xry)Bs`vY-@azyypjKYmq349Vj&B$-5|ZrAyHZhPp< zrs(MLazT@YMRL&#I9-}iJk{fzd)6%I-N2iPp~j4f^3$Z)Bm<$CWG8__2%N9Yy!U}H^_)(& z1Q-dG%PM$tsGLiSX$jgaBOFZQB3k4RU~yhJE~c?i#V5Uyap;9+4q)zn5uNHX%_Yrq zk6;Wde&%BrnmbbiK5)=$TLIG0Wa6#|bn7o-U|1vBFKxQMv!@x2cqQ6b`FTC3lP#HZ z$g-`$R~X4{-B$Fyv&W&GMu+=1b7gmQlqNi>s@^8=o9k1XE?l%=5I zVJBE(AhM~IKm&0QXsYH2wz_M06!?@t9h?*WK`k1AB)g%pZ8o^bl$2%ls%~o;r;ciF zuyV)-Ms!BRmR)xu^*g&;jv!*-Y96izWL88F9)c>aPneL6=$I)(uE8v6_lGjiGN@^| zBc&-6C_b`ia4A?JTVVBrL1sOtlZBBg zokR9(BVKiF5gD!XLrA8niFV&P2SViOHdDR*{Vv9htJHXyVJ*Z3;j?jlL-jOZ1m7$| z-~c6sCCJqa3}|u|G$*pK|H(wSMC$@W3VPgl+_; zrvYKA`=)$^NEGU0(p)6w=a~6_JQeQ9hBQMhFqM5L3P;GhJ#@_!PI+09LYo;4__Boe zh|FvKEs#f~fRP1f>#@jf5v*e+4YU{@r8BQ59GMRX`o059xu2r`quB=K8PAAGc^>~w zEjV17$#F+a-q>T$XYl1b!$ro5u|NaP^V3$-F`O*MBS!=J3M-uQvN^WaP4NoXf6@|o zf&My{jCmP+>(OU>a>@A+3l%JPSYt=GDN&fTZZY3`ZXMOSs}budFVo;#lI5ZfCV4~0 zu38Y?67GbN1GZ$bloj0y9Kt#_It1OpYQt_L(uE-A2W8?Oc29Yk#)#?NF3l{(UJFd0 zIRqZ*WCe`3>e{|JOc$h7ADH=gurb`H*RET$fkZmIZl0<5+30TBnraW zJ7hI=!trmV9RqW0?PCccQ0J27!$98>@j5qlCmHY%(O9)a zd8qZy+6O%=FFRvn}YMt_%7p;oymsRayC3v5*v5e9qyEb#1_ z+#I;J$WABChZzt849QDBZrO2yK4R^YFm=&|xDz$;u!u2%8Up{NJhyU4MC2rbG;_E; zx}_!g9h<|V3X%Mjb&T{0UO#7(jc7g6y8y0?#9ss^`iNp-iQDl+Tg{5}!cwM`9%>l2 zFokz>4{w%2OIs?6miN9*zG-;gUiD$@ll%$Czvcgh`xg;XA;oj!2h9R|# zh!K@Q`S!z=Q-EVK#dRsC$}CCE0(GUzXahV<(R#@1Tu&Y%z+*>G4^yTFFkk2!ArVnI z)Pg}G$!gR8lu_9oWItqP;;`f}*5hV|_|tmmwz1uyNe|qO3CkWe$R0;&;1}s=kj~qX ze;1eGe9tdUv0JWO#(EAX6Ip??!)R>AZ7`KyfdIaN+{Ni*DAoiM3&^LzRbE(+R78M7 zld$uA8L-~rt7}w;VM zdE0`-t&XPM-;$kULrI2g;}{`dE#Wn$6jz56G2Hg7!J--9Z&5mHkPy_$c-Q6qU}Rb| z{~w0dR69{znLPt866e*cQm0TflJJ$#aMj%kDNNTO&iMnRZ3Q^Kptg~B3iTg*RGI(} zB~br#PYsh z&Y-L9uhbrZAGM4iZ(V=#3#u?j7&aBzc8pg?B!fRB?sxuZg}&DEHaUu+TvHtPD>JMf zlC!y$B{^)yoBD|&EkK&o0nwDbXVPq8+w#y%z=E8c(LRHc>DmVqnUh8gNR^7$1>~ zBCou?vw|H`Qlu|1cf%{7mF)!|nge)pzF#~LFmSn>=rqUZXlUEiu*i_EvF=oy=^V)$ zprg)Vs;o_+YyNSADycH)x{aMi1tkGZCv&~>F@fb~DuDQw+OHGk4&P1@H>fH~qo`N$CayKyt z=}_{aT_Yy}ekKp0RgH<<6WH~XxK4rRyee1)@ed{W`XYPwkYq!Gx|-70Q*YKq`Q`zG zdbSz10X%7sP!p3S&3mmockv`43%DB8A1yPaB_qd54@<3W@}8oNCV*a-ax%lZRE_j6 zn8-*cqpKnUB$!JVG`sS-7#Rx*FpkBHzn8-7+3>Ye(;+~Gn2570*89FaxC%`;Gz=pe zGgkK~&mvV26CCJ-1-`PGVM_OK00BBpUdVgarRh{Dznc(qGyN&F?_ur;xdPE7OV@Gk zf|Zx%-D$qi+GMO66b?pzV3C#R>G*=kOR>BOYcAVqle{2ive3NlA>UsSODZ@c$`Gi@oh3cyvvccquBwCjU2rBh64{wD(GG)) z#c7tY7G-c#{q`PEtcu;A8k+FQloWpkiLRsg(Rp+I*S7r=dGES)Psy34p%h6(H9lIb zg{cKV4avqgJVu<4pg}+`kMto*~TYq?wns%<%lIqYRzDuqT0?~ zE_TfqNP?FGuD>z3Y1_&XVeRuzh*?Xbgyk0&uiv z(PW>hfni(W%eI-z_fcmkV`d87ke|QaPo%kHehoCRYsx)N)A77jn`&9XZF(7*e}St7 z#puBZ*B(QtaNs+I8HBz_xpQUBMy4tG=w(WfQ2p{<_w#&OGZIcqKyQ+`5&Y>0;-)h- zBv}HzhsGxf4RCxxl`yCViiN~QeLU@26==IHU#^M5sW%glB(o)GfkyctwgMbzTqbH5 z<+rg}Hd8ht7@lKdATI;|I#>gi7+V=iNe$8oQ_&Aih*NPU(=aN-D%B=xCym5FZq3|1 zjB_JKcM)g7< zgITkgP_T0cj+qSp#_zbPR1pA{pBz{IYl8moJ*#=nc~Cu128GB=W5qj0V45N}r~Gu# zhpuhpIhOdcW{4A)7YMXDljJ;mxuFi}CL%6ZYy&(5`g5TQ7eP_V>%A0iMd2Lwap&4;R4-wRFk#{f2$)pB>^xPC0C41a zJBI?j(^%f&+X&H_r^Gd+NG}8d@)_E8-{q57rp}YE5DL;1mPbY+81wFwwQJ^f5|QB< znml5HH6wN8@6Z@_D!ieF2VsBsEn~xU=^AwB>4!@N0cTniF-#6wU=AE}PIX}89FC+xDVo z8HQHoR-#TG>e97?^c5LKi;Sdk<(ehqP7U%@a ze}T3$HGlWY@>HDF0L3y;lk9!^!}EvSq%(yjRI?}iOW>F!az_r> z_k@rQYV@E8moSaWvT}GtY!AX~C2~s6T8x+j`$2=*O1!R?B@}YxwHQmMggI^) z?>$rwnT7=!=tK5@XwGOPu*Im~vnHLYHX>Jq35>HBDylHy6EjP65xg@sAAU0V)HfZB zT>4f@qoUNZAPwJZ=u>m1L!m2da`#T=V90G~YMcg;WtU_9zM>7745+^2T?IbFT&TXO z9M5BE`!16F@xEWic`wRujJlh9F`SPgeP>{BOe5q1m|J@gbCT#5`FfUbaFkxE2pFar zt=9CIFmZ`pS2EAGy!zF8s(te~rnolv9>K7wQq1&dMO3w50Tj7J7&0ekK+H`>dYmuh zBgi{19~s<-=B&xBlxCqU?M#Fg`Ul8`des;S5(Qb0Q`on<4VY$3b1rn4mheFyVqf8g_(_U|n3#1O|aVF)#zh&5RjIY60 zyVmk8eRCkUhf&BGGEEeq;mc=CvFOMwY2H0r`K(cv3y(RbbWj@+Mf#T{aPIT7J~iiC z`exLqgd3U$DHJI3NJD^!<|OWhNZrMJ!5WJKj>)UEIGI1Ol;FXH-x(AMz=(|QI%c7r zsxyny9F{3Z8iYk{qCycnh-U${&$&fSLdyn%c}fxm{vwOEIv!rTlrRzr_WPu)c8boz zHn}O48`~y}L3@Rh$V7PhLpiWQg*YOQzYs#Kr<7PaGr2<=@kjRzwLAv6l)F1`I|rF* zPOGW6sx$$RL%J1c^`1P@mJyXara~bN2e_sm!<0T+t@w9QZObomJPjgl0(Efiujj~b za~PT!#WOY&A{Z=n6yXTq-;I!@S$pgw)#{dOvs$cPzWax)cq;iZ)S*Tjf&1UD!}e{K zS*)GASmrSUutoSK8f>TnRgrF$$HqqYuX#y}9aZW~)~rjOSfbzZ&z6*lQoh4>&pqhOH?7CGslPw)rQ@wX9;>|wUIJ#bs1@GIKVzKt{! zcq%U_Dc~_DVa7MP8X{~9Ct80Id*V-$Ngl-d>A8h(ih&$rX@bmSqg*4ii}8{7rJPna zIu>#lsvK!y?Ne7Aq&j+7;;XIQIUBf%WrTzy?czK0*?adO>%vvo2i1 z)}A?<@9BH8uqX?O45Z|VgRK2Ao8DHw4UXham3gy#_dsp9SOK@fIJgdN+wHgA=t$r{ z$W#MBQ@Gz9a#abFcsi^i1uA8+PtW7qM6<|lOH4G|?}pw3YG|%s`uyVy!ZqzIuN@&< z7Mge-`x`bGasbQQGBWQWRerj!gDl&jIqUcoWyH$0%S#RK7#Z>LYKRw<@A48}k2P+W zHTgojND0F0QrR=!E!Msvxt=~imF<+A(M8Q6h@`bU12(1E0}TqoTm>9!RIX}lPMKRGL18oD$5@>Kdn5juc-=(l`0$GCRFLYq=sLuo9g#{F#Kuhvq~&pCgQuDcMc)V# z2V<5obM*8ARjZKf$EMN3K3}Z4t3_}7Hqor_TR3{pMHQ>&k-^?zs7|DE^+|7O{LV&I zYZ~F`i;E_c(*3RT+Ul8AJ}%`{oT(v8UZT%p9BdcQ43gFy8fHlep98>cMw;m9T$*!Y zU%BAr3Vnd^x^sRWa@kap`xR8q;pifba%{z4RBaaKlkkqXt?Duf+S)nLEFtRY6iFJG zBCn++RUmhWES;6!j&>hO-VV{3hTCR&4noy1%>e{l#)sCZAR6Vnrvut6ST4~L2M$|I zr-D%E4=BIXC*c246a`o)bnr^8Z>Qo+p7mKkCBib2cS$*`tO=mZR}FAa0ryv$Q`~Z^ zFtnud-yq7UD*;Gj{zMqHl-;7gW|ZeRGa4+p>Pl73EDE0p&&Z7>=vjel>kXOwT>R!E z^QR$G7(UlUl(g|4f0tq%y#D)UIV5M8VDN%4Qt_MK!i3xZ+F zUhzZSfY+>nCu4p&(MqclE`V1}ZL23mA(fz)79{EhOS-G&hIiTw(~CKxo3<&+GE*5C zID%;S8|%g#nmr36ly9;wDatYL(e~7uX~q>1v;s~vVm6%(x@T+ad#h$DZ#j4Y1oCK` zJe!M-i;*WsZx>mvD5Cs*n)06z%#d?`UJ@UBv5up{ty6@tLX zo~^*fg-D2NeE75E9r`s&We-2KdtfjA-#hiQ~OS_=q71i7-Yjis6o zO~z8g!LRR}O53wAqF+W5C#ifD;(u96OFtt0s5YhLVzfgSC7F;n0scM2Dz%JhOjFu4 zB!IqZSON(&QywG$c#p2OhT+9Wc6~dCnL+=`2(1VdF)Grgyg(rF@u=$1zlaVvAUGim zdV*h1UNj>h6<5qeQPVx7d>++H5AgK1mU6HNRa&UjgcpI0(qLQ(EuR-SI?X$3tnjgE z>bx@%!`F@=kkmoudD59b`q5d!{hNSue=Pr{VV2}Ak+#*Ti2;a33exKa*S@8;ttN8# z;Gxn4ne*EU6WHw!{0MjW067QPrxN~Do2i7)5?#=WQu!ylE4Cm0WDsxio6_Q`j84Yv z=vM~F1`fGHL+J9C-grftfJ^Tc)^10;%%Y{kg~L^%>7rO(K%l{sl}_1EqPT|>B734- zK#=H}rTQQmsu_Gn(fl_XSQ*rZr_z77R2_7f3}edi61}zSgm@F zGsCzPb$QrPX?UH{2a(~$fR|i{hDL=}zlkR9FRuY0pr}H>B74ty@+3_6pcu3F_QyHU zOtW&5Mjgeh7_x+De31>%SCzeL52R;LoQDksT~?fWl1<)Bo$v8B>hDNk{>g8z9cr_1 zP}Yn@1WOIXCW4E_WaFChVN;<-_xF3!(=BMHcfYaI*8e})3`rK>`eYCeF<+hUG( zS&Z6gYK20TkZYyvXdq&A#d2=N?T}*2!mMl}!NA?eA7{hyE=B*Z@0AEqo$b7W?B9UgngwT@Vjf$wxx|m-HiSAc!woC#sb<@PTttaHuS*%H z-6~-KynV58xt1f)A&Hz*Sugo-3aMn1x~ic=2XwSxl_!*FnQVg?Z~5rcv8wq~!0qQ0 z$vQWJODPo$ADkK^VS$Jyt--?{@y9~yg%`Rpt-Ucdn26hg&AH9C>)NwzK6Nw?q1lj8 zSNNHb8qnpyA}1Q0OE?4O95ruhn%_f9WK-rnfD9r|FS^L$ILA~0_D)RWoA;%ZAFQ^u zF-Zv<3@Zi($jKCS9@%Y?$xwcHuheSn*d%s=G4DFbMC5_B;~L5#v%Ok|wL@kmJVTt2 zDxI+gaEE_K=iw^8Wl6i@1$CMuw57ZZVqB}IY#y2Cis?&bPYsqQvTT;y2e*rM>dZ8e zxV@1|&0AOSCsiJ61=3TE%ltt(U&2x}Ip#)PtSDvP`#SjYa?0bUS&zZf%X}u{@ z@GcAd6n+AD`vAz{Ls42#C59#`nc&IA_MMd=nZsw}J$+8vt`IlfNj~33o5f&rE=A$U z25eeoe9tY#ej!>l3s8>vAZ!KCi5KMI0hMGo4eG0GYFgQPonY2nFQRj@nJC);K^0#c zqx3WaZy{C=LR^#9mLJE&>xpbaK2XW4gN~VP6B;8G!t+>0=l}stuL|sTNX@z?qnj9e zC?si!$Pc|x5s2!d!`#5vSc>VEVOTk)C9=5olA(x;ww=1FbeiG+adpvbhtSM|#|*}T z386#=O&u>BAiBEVu{k!Nu$CrI(%6OBKNCHwPFIyoOAXCeea~6=Gz+7RbDX5au}xPF zt?#9w%<962l&JDs3#S&10N03LV;VDt_5L*RQWlkrJeb6-g!GP0|B0>L4wczZT}r84 z*3je>gEh^*VN#pe+DbXshAJ)Wh>8+jcuyJ)B`;^IZ}->w4B45V#y7S@Wj1t+n(TeC zchEdU5oKu1%o!HFn{K(UM2%kW7~!Eu_dkMTy|UTII{)F`GN$PoR?e|zy2~N9))>%( z5U|7yVQ?^wWSw7^bJH;^>b+xGS)e>>>FnzKxNqdG8XD_^`$iil8?4A>K$%z%Z_$B( z3a4{V9_8Le-^jwGiyEHLhh7L>uRoQ9_CLL%XT0B#W=JmC{H*&6I^7h4Z%CuIqNhSP z6yBob{oDj~=yXIXXN;X-T56YfKVI#AzYE3?SiczoGt{fPnXWRkcFr{u1*2U!=y zB!4nj6G^MdIlhLV$|R!CNCGg90o*p7>KfPcCb8FGI|`T^_qCj;f)EY}vlBi_=ti-B z9dc8cv^3FEr9BZ`ZdFV^T?%g4Z)DjAhA};z-i`Px&HY#ojT6Gv4Kn5IquI*%JAu*B zah^3atQJ&u1s6CsSaCDVcqY6QR!lZTsl1620{n;0)dzfIrr#$@RI&6l<8u>{XHkuo zmSLIf2itG#CIR6e0iRBNnVuj~#seHw_NpzRLKAqMqDNhjyc-~zNJ9RG>~?6%^F&^3 zZ}cf#H=5p!^1=H)Ift5M`BMO01MVw zESNW#&|_A6kZi8E;XF+g0zZRnp>ADzu4O)J0&X?l_!rVI6NQ2`3O1hDr6ORR!Jb)< z=~8-owA?x4O}pZEP|B`!mPE3LkHPi2|fL(viHQWn#|6_tz; z3quoRC_XSsiPi_@37h~}1taV6$l@z$3SX8fqb9sr9HD!Hu)fWY+$1s1Lfdv-Z`CzvTP)kb44~Wu&ls(qJM~l zLjEybLligdc1c=sS+9sJUzAu|QfLQfN#MhS#f_cn6_tCt4XK3TH2LsizP38mcIUZN7ki0zFCT|I9I zM|Ev}>57bdE%a3+=M6xEZQEn2LD0op@JFzkyF!$Qs!T&fW#FkTz|6y!98Untr2}kW ztEv$r;K~x2$$OQcObZhW8}JCs%8GE?5pD&Nrd+jl?VG5K%(#k72`ov-qx-tlQIZ65=l?S9W5yE+QQ^PS# zW3@Lgnz1_YP^k;y3Zs(orDVz3@*cvcOSW08=blWiGp|+tL1ZX$eQKN`8oW>+rbfy` znrn5>vTV^Wq4$oRk74JQ<%ml9_sF?02-7ImPi#s{-~1D1a1&@cg@{1o%4F3N$%1_2 zA8+em)3IaNTTAGASvj0cpK%W(Tq2<*+$|C9ke1m>D>PIA40a_iU9p>jcUhQww!W8A zF&G@vZ-m15mWF1|0H**ocZ}0DR6@vmU&x8v3ey-G9!BT~V*x}aWkZA5Laf@_Ho;vH zz*<4ufGZ;BumLH7cGe1kd})MQ5$>A^bk47 z5Ad3ZrwXD(95<^_r^gCwY?nPE`&Hy-hpw#0bX}SXNt#YvI$IF3N{Aq$CzN+^k>7}V zWq{hJv69cS_t(Xhgv0=jFc+i1kk-Q$(!c`)tH z3pOuPD+*sd+w_*WiS#sLvgK9HWrwb8h^9?vsd9}pBpzKk3e>?VnHTQ-*b>J|@`jy; z3_$!T`mT^dKBhozAxa71lJewqUCU{mz{}8NpDNE}dP16zLWa4r{PLx?TU$KKrHz4{ zr&iOG7AAX^5kkLnM4~37V+VI#$Qf;GiM(H#TKLpM$76C0*3LD^rT*Rfqd$kMhn~!% zO6S9tB2eA~a)Z?=)I+3Gnwht?oX!b~aJb^5=rNW-5>1Lo#sVr3-WzSF{UB1P<#BKJ z&l-4y1uPQU4{_J!($sb%=K6xpab}v89Ye(60+e^}BLVZtp&f6D26`IV(^9#?84@dC zspXJj25mnjt3!mnE2rO1S=mrZiSY^?zDA~EbTx}co`$&vO*@OhkyS%?hHvu8QAW-K zIiZAG!!sYbm(-?8M)Ic5QBC~anYOk%iZz}8JNHJK}uMKV^Q(Sjzq ztKpQ;c3~#-W`O{ARM5_8W@>=aaVSvmjO)+jf&)OwSZgAjOkaZyFqKhZN&>HE#avC` zJk5Np58rje$Ec^S5$@EKL7a)eFZ)g&3X^Z60)UAT);^l|5M>#*0baB+#@$nPkAX5V z*YL1+)GzX3C?E7nb}Gt}qO9BuWCuR5 zr%wv18Up&#+?B1mwhtp|EuXlG~m02A@wyzA? zeger!D)i(lV*|)s?$;ddRFsML)?|@G8&lmtWmU z3IxvWD7{0X49A#PF;ZGpDYQb;~78Xs+m1Wch0>)nxah8!; z!3^)=51>=2CTspMj@C|LnV7NK^ef1E+{~hh!s&$mscz4c+A@AzX|_KFgn^#OfrAW1 z13i`CNc3&c^)aO^=V%?Wa?Nl>h;M|~j@l+TX^eK38o$o*poF9_1q(ci_>@s**1CsV z?_mbA9+oRa`+%3)DJ(;CnXBd%FWjxVD8PzYN^haL#!MS!EPapkT}<~$y*0Q1E9yZ3 zJ~~9Y5H%ume%QIEuuLOdNMW{~xN;xHjFYnU#8pzEXR=NJVSbG9vS#qnz_Cw)dJRdO zkEiL-0ERme%uZREh#(?}1bIDc?Ar)h0uyja`mqImFgcSrmqg$1RrCL2tdx-J5Ll~C z*1ihoE1$;BS!Ol@u(n-*WVy*u+?o1QZTf#tW@GYjwhN*X zfFaX^53+narDc)}a2l(6{=9;)kv7SAl|ONm_RtnyC2c)sk!P;yMf(nbUV{dAS{cCb z+TE%9iT*}Rka`iKAlB~aPfZ3R&`G%aK;X#qVptZEcqCs)FBXG@*YpP(m8L+szP(3c zR#y68_+j6snH3#S)P9%}4Bd(Wb&$5GfvmxeaVyrsgY!Jw7N$@}rB;h*bG^Rt!4cjC z(k@*gA3MjHDLhIFCPPZMkHAsgqyfn=IJXr+%uKn}NA zJ(qlcF7*W2TNp|dLam}mXZS~vdI?*-isa1WMLd_DLq0F74FK*e%O`~s`!Yb^BCn!L zX4oim<)JnVr(zb+K<-w7ow~ z?Xq*4S(1q_s2s?G0jshf!%&+!&cqd3RpC1Bo0Xyngs@8y&usu-A@rI07q@Y4;tw5%vSKTO+&5YjWt(B)=L-Ep76R|q(i{J*)7*I zbgIi_rXXiu&_JbfCgUX{@QhZk1o0O6+=7EOyjbPJw5r)UCV;GuFA5g**@JQPYsfv+ zWd^Uh$lrwsVl?!doGTG`3FDPfL!-F~iLp&tSi%~I{`7xI`?n-9awF>+oe&=)aL4>7 zcCeNsl3@MT$j^>s-6g41BqJkmao@f#%;neLgNBClR7eb3q{sx@^Li&9;xd_5hF*7! z&nkW+=`rxgK;aRD*TGj(coA|sz=*kSsY+xN!zql~y-dm6>?6B^p}N;iL<70Zhszgdsea>{wk=US_?P1&@Vy#AJjMlt;1U6C5zirW9%=K!&Cyu{_3q=Pkq@ARL7%$y=~75 zGc55tcd4gBaWy3IOkn7>=a_P}8v?ae3PuF0w%eau@(eNhRT)#yG_$Feraw_#R}uqY zg0mZ6%)tzFq@xWdUP`|gjE3i!6}P8#_)JZjKY!wwT+D4yd0i>>q$mgxQlP%hG$EP7 zRZ1iW3_T5SK`(pRt~JVtudHTl^&AZn6->qwVw{hI1v36_pT8?ruhxGcT)8!BngQ9f z5UxKFWrAL_dXU7yGT=Tz7g!A%<4uTqn2_&-{~NDLbB;`bJI|*c>ar2Z2aJrB$!bmJ zfCds1gTnCTlJn07dRmZG`Y^qCZKCzJ$UWuc{*jmIJvO(?-W}UM)68U8&_CrWTZGJ0 zImKYPqbm6@Z74`bkd54PTs;;0p3aTgor<;}LZvj4FC_PuhdJ~PSs^6{JdlZ1f&p_l z5)f%T!bQLB>$&Ozw62Ddp-oK3iCr5nSBlVF|1*QNch6p}_4Hz7>Ey8!1a<~T=@Dht zyChTZXRl_MQcWKh*aQ>5(~jYen&|od#7D}~9&zklkGNA@_GnOUJUUNdV@wmEyWWtd z3=@bf!g!RC!-h4bM|us&LJNM{_Ott0wtVaRJ2|ywI#~}rM;MJ79oDp9AZUoU&G7dN z8d^nDSD`zx8(dF<5QUyd)55>eVKYRC`WoHW$*C=SbEv3tq}!1h50Mk|dyvznSdi2c_K;fLp~7ptdikcU?2@MsfQV zU5xCdwF+DkfV!Ny+VmlX__8yDA1P)bmAM)@)Kgt1JCQzYdrD9fz=2H>dxzr+e%8XL zJc}fSAg;DPd0wm!+`g2mhp^r}8%8fdhkmI&~36 zxr3+YI#n9(IqP8)CRA#%=$zxVv^{lpYI3b_4|&;0F#5J()AjQofm{44=ot_KBG6 zbs8cOXU3SEp3w+~Br*uWsYcsu{MP+p&f->RyXzOjh*!ZQ z^~q#U=zZ(?+nYkCN{=AcUajjSZrs<(0pOx*R?wCxpet12DtQLTFER-^$x#ycUK;$` z0f#S*nV?*kRaovaNe1B;UcX(_?x5+JVJ3?}F$z_OhV;$gJgUh-&1YyMvs53Y@agI* zpC%W>Ai5E)TZk+^M>}Z~{r|yLqW+U%R&>GX5yf8l#MKj(PLOo^h$eRqK)o^`kFVnm zr%Re59(8L(;`oZE@5rGe>2<4!BO9 z7f#=+dyJ5DHnS3EN7vn2;f}~1+Op=8SJQq)Z&7~h5=mR+OT(6ii}DR^S&y$#NUzcR zc2!anX5G{((|L?Rg50rr{$k)(4{h0y>}k1!S3q@oK+9*0816|_M8$SI>ZeKA!8+hZ zUkhqoi|eE1tVQ_vu>)50utD}~giY;dP_M305J{LRRw~YlI{~=)TDfly)mv`CPFqXL z^Wwn{I72gqWHZ%FlV!WJ13;a8eH_Bqd+3ywY0t`D&ZfBWR92c{i^+&0gCv8g1UFzh zNkz?B6qO-KYddMFe;&L!XbLn_*L6EiO4y^111A^PGY?jqeA0%PZH$;{mK^a|-Q2sH zX;R2-S}jV6L$X*UY@?0wqj>mbt&9^-h%T$d?5;w^Z_q4X8pfhLuY=O1sn4%rnbR1s$KJhq92!+p{mtTiVm5?XiG8a z7{HqY%1sSS)2>}8pAOAvrj<2%NT{5a#$ZJ+3Kr+@;i76zJNerw+vg=CysSemXMgffr3)m8{L zF2MfqF0?Y*gzX#-2ElE=ua|lTa5%l;QH=Z`Gt$v7lfzMU;x&OOF6ec@84L{XbZz%A zVaG!^`SYi52Cff6h!L;hOZ8Nm{dJM1;I9e!5UGfX#FpN1V>DS|8&EZo!im(3%*gd` z>mInL6CwCX4nndz05t7W?h3JFGBjSwHe>&lOk3@N~75d>x%Szd1I}ci%WMs z5t2B99WgX3U%<2+t>?G)`JW6k1^V>vfTAH9W%`^XFyVewl2&6>V2EMnGaN7#fi zAF_!)3#y&Jm)b8a^eHv7;<2E89nHMP56LWnpr3kV$3S-CQMjAIV?aRw2uW+dnmRWllKfc7^Esz!pC@*qI~?i@j@y!5c=TD7Y*wDnF<0z!0Ly`xP;8-K0; zS~!B(O_%E>a0t$7EP6_P??tTwn#lzis*b#eN2O_Y@iGJ?=mSFVK=KMrE(ucqkjyr{m3mU^JFXXim zc^&D_GdwJ%&Rn15JuNG($25qkaT~2az~D!`l7=(j7LLm6rL1~Ji+%FhwGg0rS|9ff z$H7lS@-Yipe%6BIz^y z*cze{xg_B+-LKDb(!-2*mXrIY&G>HA>a;rNqmv6lo;BC&-RqC!YZ~Zf9(Xx&u zpM$1=lxBi?4}U?>rP(gPJ2HP>{%22qr$%Bv~29Xe?k*Q;`><}1ZUoZAQ zBT{nnz8ceS?tkqRof%rf7RX9tz#W@T%2Qua6!gX3-W|Q#F?*{r(Zpi1O-j{Trp)2n z`&YEj_TWRi>F(?0D0gkLiZSVgrl=B`N*RbrSIPexP%hY=*3l$4r>(YsRp*Vk_O;-Z z&_U7vvhTtY3n;bS_t89QmaG;N5ulcF z%7NhD4pyxE3CQEVr_VsMX?(1SfLWWz7YdthI1o7)iLgSW);pkzvJ_Q1)OWDg7?wPGzcQpqO2%=UuJ}8w zr4J$Y4{x??Y|O>d!>r{B0<}YTRzpbml+dKq;v8Q0d1$C1Ga`z&*fS;=xHb(P#vB(y zA-6E?i|}h2+fG%Rzx77#RG#&|{iviR6xj4(pj6sl(7bH~hlF5&JqIww6wX#{UQ+v6!Gw77o+k8kss@N${ zTu!S^^9Og7E#3*x;JTp?_1VxD4V3a_%2>g1JLy8A->*u~JapPsQS=VOHVGQ#d#)>T zl@J=m3x!Y-j+-UI7%QSPMK7h55w5KKo$=OwL5}kJyrHvw$!qh4_ zt@l!swOmmC!%A(#LYlbW*ULX319dkV+E+Jc4eNE*K{?ZfyD9TZycR@N+Tz)b<*libyTtDI*3PE5bVvJQ< z6o`=e#P12CZ%^Rjxhpu^DL`vOMhtkET+4`hdjfL6K8_2whpdSC6%DMs3&_(L=dZ)B z8SG=`L(CV;MyS?db2a~N|KytKm**Ips|cS6U_d0p((H50^YZ3e!l9^tAjGc1boF>x zdsR(5947t*b41!`w6xbRbmp3AvIFd#IC(3Ax~f=wYQ@1B>cm+Xqwp6soZ8TJ@Z&5>>ga0%(f?qO)zX=- z6CU>t<2z~sa7)k=s-CI&Y*U-303yA%-_U|_J6Pu%-V4KCQ;z6|iRIuaR`$Z^H9xTaY-`)NaA>SY(a^_R(_Gp=ZM!!Majei^FkrVZEUDs_KFd0C9*s3u4umW{ z&S8e*IsRMa7ScU*nK&`)6TGK6K67e&iZy|xWBJS-JoejGo z4tJ*Z0vUCRoxJjtpHL8$rp?@o(rM_4syX#4ZG%1i!~*$|5tUuw{%TFq4%t}?t(TT9 zY-x5>VD7{CJDu|xMU=83RFOvVl##wd8S03!%g$2!;M@o{k3r{^57qw3G%MQpBi%=) z@`{NGBN=e29y=Lt5nsu)J|^=xRJHZeR#ms|?Y!-*_f9G?=uG0U*GD<$Xzht8@V$7O z3K5W4%1Q(%Mz)Z8;o_Cf)*G@j;s*eBV^XdwR@>#1Rts&<^sWo$)SY!x&>qoo#0Qj> zf|Y<2Q!`D&Id;S|GkaQM&RsLFAWzT9ZNul?VlO$|V~YUpW#yqetByLmv|Nrk`}r)D z2}uM^Jpp&9x19eHUvF`-wh!@Y2%r%o}Z2R0*N5XIHUFsz_ zBMYUeCXJghB~6V`iZ{&l=>uS;yEFtXgWwneRpAMBe=X;+%cIj7e(D1$baS6BxL-x) zM3i#tP*h!ltJ(zf=?Eld;;W;c-uUSj?Yx46YKP^>&cSdg%lQl;DLmDfI9QvZpAaxtVer0KKTwSWtuqz{hHn=7ne(o z8yd@g%tP>YUU$Hg5N$BWLtML|FfFnav$WspmQ!>ln%07Or+}B1oz1Rqn6Yzk3-UDc zVf~EV0qyH2;lP71WwwaBrR_^GY$EeYyM}QWY?d#8N4kW}YnVhJ6&a(%d zyuke8R>IhNqS}0+WcF=;5nVju9Wxi7UvQRkI*=C-6wY(RtAYCs!r#US9{&ahvY0`wW%wzYGW+h8zi!s# zpjJa}e^5R5FYPF8ynlykDiOaos^v_32)9SWd04Q`)HyYDBlZuWufB*ND<`c6Cb#m- zC4oPzT%gf^;C?~VWUR-Zc7HFYZBo*!e+hlB{*Gd3G4h`;M?JddJ% z;(yfW7paY@e_p3jv&Jv6jqS`dQ~wIgmQjzQ??$EqH4Fa$w&}Y5*hnJ4fBtYVy+FpUI}>Ro#ib*FE%)>P&Hfy%~Dmp|>0F1;qRd6o6i1n^LtGZbn+XenDC9b8S`JoOw!) zfG7QAS+*UNYiEA#%j#L&YGZ+VCv<`3hwuNsq0|dH`HpvPO%-ux92CKPZ04lLY8`*G zjAKwmGk8Y4OuUll@4zL&*2fGnuAosi}h3=14DMEe0J%6sB#zu*YXSW|& z0#J?K{z`3cr|Jw3bX^rvq_CogVK9Q~mCWK4F?_j&oV6m=)U6Nt7+kkZE-THpr1DSX zJK~t*q`Km9hwiLt%R|q|ZXjI^S9Ir=Y|kdEU7E=zE0gWR7wRwI0)+f^wZEh9{L1fH zMgYqS67H|%?~%LC5`KAAZkNfRLw=FBePc@+%E0c+k!4(?&d3<6te2l5NbzoR7};>_ zP-&;Hq^!Tum0SBKMiQFMt4$VRB^`1*eVMlK&O;dSZH5?RbmnUb9@HQ=9&yW%$y?H2 zJs!z(!HKmL*Xe?2_1E5mGoJ-ton3WgRTDFd@qB83?N% zG`A~-NFTXA$~iRC;sjbxwppi@Ad~e|6XCs@J^$miJeDr#cBsNTesuLQij=-;e4fE= z7yiHaqPH{Ej6q=#%7JPNBdRLx^4)+64LmJ%a2to=A?su$T3}*}K3B(`%%ytT-_+6h zlL&rIHEZoGa`PSy5%mh==CuVx#J1UErG#yQ)T8yDG(cQO3$@E!yJ%2j(!|ssKfsvB zzv1F_wJ&b%bPv<;^fC9b1?E}W@msu3J(a$QQsayFzz37)3mp#LR;H)nq`J@uAe2VsGw6@i35sN54@NJ#2h zL!II?=lHmw!U_by^)DP(YXx)wumX@?ayT&6_0&jQP{6M5IR~wX$A3!whe-l;Hr1d) zz4v>uzyF=~iYOPyci*#g+<8Eh^VGwod|9kf1|Y=i806%7D}F@qrLGBMR15AYyueVK7dgRN-K_z)gA3E#!L9@4)N3tt9m*;y37QnLC# zwA`^(A`GNa!!RZSe$@X$u9^la!T4Oq3J=enq$4a0F@|U@(Cxn_Kh4uWsDN7gxIDQVtq4tQ2t~PEx3Kb?j1CHK~ns>CB=#Ms`L!|lkEa$0P zJfwRZ`IYmnX$HxBF>Zii+qN~?cJweM){{J(ct|&NHq;<(ryURA(XWM^OrOZD+VhH)Q*tWV2TW7_Sk0192qYJ*XD34F-iDs`BD)uDUMZ zM~l-0R4gXd#y&FX`&^Hl7L*+beqAP~@+{Pi`XLfb4pdmHaZ{KP3}|?7$V0w{NT$~? z+-yB?!_IyXR>6#QZX>|*IWW~&> zv^~VXm8y`a1YNr=TFBENfI+cd6E_hM(; zffb0%=Gyt2fXa#&CQS9WlI0Yh8KIP;1*>6tSHTeQhFov$`6%r>0>TcJvURFEdjbAO zcZP&)wC|EdD0rt4-LJz_c=qae?I@6?n~Z>%Uh*m2w>44uOliAm;V%)j5aF5O(a`B! zMg-Y*_WYb+OZtTsd?uWkU}Y4tuJHo7nowZFI>sj$$sFB{;dM%zAXP(k1N)r8i1$An>=wMkGGBAZuqlz-M78TLUza9brnB6>~Y2B5JK@ePv zz1)eo{W%@P)SFNka=llIdi_rlUuYLm=`d3sLNyFRDlY0Uhvf(VwQrdpk}GRx{1Qsh zPt}k^omAg2v$#luO8%@@&;syW)NLmN3x$M_ULmq38PpZ?}BcGzjOqcyw^ub zr>CZjvKf&wI+RqM@N_#t<6|aLsbOm{X1Cp1eUKJy8Th4-$8jt;;rPH7O%2wsZr3L{ z9UDsxFMZAKlp$)In8`DqPB(pJc^g@xF@OfTu0pDX0grExC8$%;=TN2d53Bz-VoU!= zafZUBD)ORkVI311z=6vLDvsHaSckawu^N5}KEdx{?Gnr_O%Kemn*)RbvXm{1ncwwU z>M0_nZh=(;DY-g7R74k6RQM4AG?#aK0=`f53kY&`HCyE#r=@8P)Vach20d$yf49i( zr`}A2A3u$ZieK|tT*{Eom)#ZNGE>UjpmNflqpuQ|&WJo9jnm=ueK5-#*OCSI6=&&( z-mGciBy&i7Fam_Apji4{RWp4^+4vJ%JH7n|Yp@ zA-aedLnl(n#b|{Z#_4Qk_GC7$oV*eV;ssr0hmh5Euy?dC-lOn=<-5aK?+9T(Q_f7D zFN)>iV(AqfH(G3#G6#lQCH^f`S{2i81X_DD!@nZ3LBu#_Q_y&=0v8CScT0 zbJGa93)*zPiVhzDC07F-HIm_<)JMt~3N%Hn;4C#mfaDl2{X}{_uI69PmP2sX9Uf=1 z=c+olH=J$=j)d*cf|)X4!}A*F3(24mf^ijY^d>@r^`}L}6x(Q2AVJX9C zdm2DumVE>_A^en6War{ch3<-2J`Cq*Mrw`0)cDLv0^bOE#`PLHgk}=n-Y^PNqVj48 z%gcqOD8HWqt8UN6^)(c}W;WR>_ZyVMwPv=w`D=(q`0q)2xHgIXkeczqS>^AhHDdX? zr5Ln$98zdvLd_a@u0d=QbHKJrnuhr`gOMN;?&;8GbO55K1Dp+IEjEioFG zDX`Gw{RGh7cMGy=x5m~&pU51#(~ZcfT!+zC_Zs`HT&DkGl!%Yg{|T{&olmtFo5Al? zc)aS5b!rt>8fIA$c=A6$?EOqQa~Q={o7uBS*?YRzn0seG~=F!D5aM+S}7k0gn(Z6z(}&L8H6gfy91lBj>B z&{5j@Bt7gk-x_=Q_9*={>*6TlI-DlE5Dz<@e^=?rDa6l6msdlkxPxFkzE7Np zJWEg%-nf?e$oJiI`iAkFC?mG1(DDx3M2&k#Gi>LTq82?f>2rS+7~my|MbV2TNOy6Z zu|L`()nAb0e(KCLrz?8WS`JN(TG#P8bfZUc{_Sn!xta zaG&r;OksO^dy>;L>=k>MJHMB{x#`48B|c)LI^~zt&+jz+9;{~935z0QFvWopu!osi z;?$s#+!ahcpI+oK<8=p+;zghUdW=ksF$weZG1A;t%FX%Bq}D|W1@CEDCPe?^qXmBc(SJ*-QqZHoo4-k?g<J^Ac3j%B5Pt z(qAkpJ8*0PH7Yv;507hIgdeCBAmKpC`cQ!kqmCJY1DdVVH1ckf8!xI{}dsQT6Ic-Y@Q(W&X>kmQifx@|;^ zi@`MOA*s)H*g}r#x~e6c*jLfQVrm1CL)2jW1WC#+H#hIkRRP}5R5Q!#E^_tmsJC#d zU~1GQj>EqBsQo33zEK1~8wUI;>z!)NZc=)rwsU0mcYA1F-^wX4>$1n>uu#)-D%mP0 zjDdz}D37Q4+zHszSQZp_li5wt7$w6hqGS4ND&*Lk7VVC$90Icv`W|!1!B(DB;!MHg zgi8R+ClqVXF5;fYIqoDo1yT0IZ@WT9H?Lv8mmHof4iK)PKmC-LJ>2As^E0D{3P!1a zJeCAs{tzYA9tmN=D4Ko$k zO50Ef*nAJ|<|6|iwqE$NbGHY(FXo5kdoPB*{T80m51kpYp)?LpkeZs(L?B}Vbuc6Y z53AI>K?LjOz0q~3wjM8`i7!w+i*)zya^S6ve>?n7k=Zn@3%L>ZYK2nN9AnUpBQ=bu zGsPIOZXG;xMPZ~i0O^Zs-iV)%AYCILiqiIe%*|7WZljPRKcGqx*UA`pqybBps)u|; zJ(ncRqd;eMwu33zI_>m>En<_KHMo5Y`uJ_NoXKXv@L}7+wX1NJDt}%=|JN~;#_EY% zIzExk1_?4V)xSN)R=jg^Cs7cOBILsn=x-#kuW>Ts40m_ z-@$Vc-M-G7g#50&Scco;hZj~b80!v0+Kc!0FsC^qwF2KGGka0~S@mK93F|gNPK3VK zQXEfKLaUxtSile(18E5t>t~*pV<;J37xxSLP?;&lf8qIM!1KzVRD)HJDRg}&B?l|l zq-PXYc#;s!2@XQeK_oWs;L-eJMPA}z?mN1kfWq?~x?z|=WYr_<8!S%5P$!v|_DQz| zp=i;KWX*=H`8ECq2}`^V3+-QAw~0;oKdWsfAR6qI?csntUiZj#AG12vN`XP|Na25| zwhxKfEMalLQh!U-CW9f&`m=PWXWbeMG$QJgss#vj9kQz^N9e{dzQ}d?>*m9-_YFNt zy)Tqg)hi{3MT^nss;QsIwM>ySgz=O%I(>dox%z`3TS^>@{B`g#{S41edwaK=kBIQK zX+9-pk`#&nvQ!NQ%_BlsMgULvhK7fNha5-b>fva`h|EH+8RiqQMmEnV?O^5imtcW@ zNX(j06-bz_BNZcdX_bj&gWe2-;4iG+h&J2V8Cis5;M_e&nDst?Y5h7Le{s3ji>RH^ zz4{4q5dtyRCOKNV-P*$j#uTfTmOrHqn+@?Qm?|?(EX>)+Db&sp#Qg>f`28*Zx}Vu* z-d~J4vJD|Sy6K?ADL1;dui8w-f>}*;)-Mj*|HRm%k*EY-^fy~M?_q;m;(iacf5v3W zW+{{{@tHNFwbfH$VCb+42iD7;t9H={YB^4rinAS^cvW`%X9P~EiA~*k5O+xKkeGEx zTA-wvx~^dP(wdo&NKcwRmUTqu>T6M+&|#NihmIbQ*^mUJFhfiqzq4+D{SL05A~Tf? zRf|?jrJ=mbimiqRcww-u`}Yo7<6+i1Sxcyp6taHZOtK2(G3L)xTA5b<^~Kcx45(09 zkj({qWc$Tn1P_VYZXW)0*kX~PstGmLgBTX`9h2RHENPXRW&3UD(sdujoFX%KBCcdk zq@1`k?%KpvB0g9c(td&VqZmv0;){ga>nc9$`p4;JLI!AxW4IiopuJv`qWYmSd*7gs ztaWOhB*vl;DV_cBPPAe-;ZQ~ZJ|J3QA&JM6w}r`tm0a9MPP>Z^diDMoCpAH2oVgf- zcRx&R;oLETt>?K>>WI5B`P>j5SLXrbvOe2L-i-&N#%RExLEV`nr@l-R&N>gYM6f&nNvfta4rFE-J5E`IMJ=%!AVC-(2NL z<2+B+DtRSuWZ*F)jN%1Tl<^{oF{&JT+VGP*YuiwZik;?m{^hM4f?f|M72qHZRfDGp z^=2cP{LIO32v3y#de*G=mOwAG#$1vHw#Rj-FtP@F!#3o8Bc9?ieHz(`G06nhZ^m+v z{x%E~GeAB$jNw_VwoD*Jq4~huO9F@FGGlV2pL)PP{WsI(-v42OaoR%h?ZT;%$cmA`hmMCs@KfMz>`ynms#x-gMJxu-vQfOwL ztW$2#KxgEF5TN6&Xhqhzj3(!tDReEYxYJLUShj02@A5u&_*Ys+sm#7$6PKaG(KELc zu^7~9;=u;y80=C7GfM3wXjP&dO=muQXGLCm1dl%V_i~2!nuYc_>|yXQ48XRjLgprc zol2Jxqx-q&!~!+>L2XyKn5(pM_S$ab>rS;?@Ahh4(9du)Nv1WjV8JsO-5GpjF*) zKGsP~Aouv+^j!UjH47hp$Yx1W5AL4)X%R|0BiAAuzZP|NGD3YY45CyZ>0!QE3($A| znB`|9#kb-2oiaGy|6~ZW-1BR87dBx{SBTLfy#?AaW=k7RP;tH%E{W~yyk9rKQ}g_; zpwJ-d%_1T87t4Zv2Akzje#;7{fk$(p_5qk(%&Ta}%emu{b^Z&%kf=lgKlcDHhIG#c zaQm|ZO8RdM;BiWMwK(z;0+)krfTJukc?PR=Nnvpj)1iVdJe>Z5W6fMe=i2$HL~7(0 zQ#FFRX!_6f3L5`V6Y6I56fJ8Syeni1ko1R<3wZFB8sZOLhttlC-!r^-ER7?HZ2tHT z^6N!We`5n2(z0e$k-cV^Y(N96juVkB;ocD3#S*e5h7eVD^1vE}iD&D!Z>fY~!Q81J zK^L6Ajo=Sy**c`efY~&~Wuk*8=ro4o7zYXtDhC&*bn;5EM^Gzae7$HZz_~KFNu{NA z3uy4pFglfG{;|}4P`@c_H6KK?hDjLcTkfuu5m5hHqh@71jLz`m20494xT*aaU*{2B zv{?Ur27f5ahO3s}KN-!XHLjazlZyIQIn)h+ zLGr%7m4Ad~0Rzm?sp_<3HdmscoR2KMsir9&r9UE}lZ3}kw>+qu3>V|^P1^W23yqVVfQI9u-U z`k^Yzv*OyK2`nR2l>yDzz=2s?>d9;3ihE)-_J1X_eZ8;p#gATrWmVP>OAJ<}Vb= zQ&MIKZ^TQ(^z+omnm=l6;AYi_>o0vbsl+)>`sNopWs!Q0-dC35<^)(gq<~lMbGuVj z7ItE~k@C}XlT@&s^h)S1?WTCp0rZS{j9DaZv(gcs{MP~ zX3;Q$uWDDT%J~4%iebIXPF*S`lW5;ym>cZ&(c;D z7(vU!(Vznr%w$c!;z~@?UBfi9IBD$}15d0I5}kdcV-R@y=)ce3Jg7TY;#8Gs+g$az zVJe2(2A-UZKOoX1C1T))fg}U=KL%@I&IKrf{%<&xDlv-*x0lhZZVohd+Q9bj;x+$xxMsu1(~K0UN})oBQVfn!j{^mU6C@w(UM(!$bp; zBAh1!y+a-W!GZDV2MXI%W-uV7FAgeP(!sKQch=k&lrF?lug`K;xEz%L;~F5;C+5y< zDbqQbFzBjK=I&JO?rD?(`B5i6tlPF@D|Z85LLurFze=sg^N$NIa74C&-vpmVp}z_O zw&GJ`N|nckSiB(C!>=lj%+rD&Zc;wq-;d9)J6I3V_dQp6oH1rSfK7%3usA7N+RzFC ziqjMJVB9cmBTv;v9lu^iL+iF&?{wLUkR4=~ zVD3p^*J+dtGdPcUIlkVFk+=8RTWlpD9;5xeKFTR`sFJ%!nCex4fQIp3;>e#YYfY2W zho{TVn6bxdyZs_;qCn)_Z`3K!-L|^wXHjPja>>W#v zOL6^A5#-Zp$T*AEsf}%bF~Qp-fZrrQFx=d)PjU{+v@7|3?uZYl3Z@yBX$^)9WQ!i4 zO+8X8)``6OAQDqZ4%0!Rm7k$d4sUH9<;qDOho~&fm^C=ZmaLkESdI5V7&+cBscdhU z)zE=Rfy0j%YbqEM37x-F=N$v3P-tvo|9z>P;H{B)z_Zjk1#WR8C=f>|RxJ~SK7*iI!TF|Aem2O&r0(X$4YobE1r5)UZP||SxDBkQd{WVJFPhb^;N|fuPoWN}@ z+l!0uB8zYXw;5J~m$G>}V-GUnl1yO1)cK(}p$r~n!PLKUe1{Ty!h3s^Gc3|iT&zt% zXyGjyaRE(w0KjBDfj_FAmKTV<6Ko?w@9;mDwh+i*|8RbO#@_B$*5eE?3%wd613VcD zNM*AsKsz>YN+7~24jUmbE&dr{Rcyv_NM;%f!cd1aY`*B z+JG#^oxr3g_BvzTl}Fju4M@19bFS8qY%RdBXY5+#{ity{L`(Dig-z+i>;F36U`JEW zN7kt3Tj5K(y0?OJgAoSn{fy^-JKxe-(rPvZMgtX@;ZB6VV#AMtMA|WG@3X5Eyx}TD z5kONcM>yX9Jtgv zYS%Kxc^zqfQk?L!eW+*Et^Q&fIu1Qqh2AP)keNNPJ;QS3#Rjk!nMFxj&%5nQNk2*u z8<9UskAvBdk<*9rd{!e|sn<1<{~3|_O61t}&m>1@KJ1+^bloGeCz4UJ3>@4ySZk}g zUbj27O{nR1+>%0}@^z@!BjwbS`Sf&`X=si~h7ky9!rBS@8A^P9_waY(z->l=cgmB# z(>!JE>Piz;WQhiq=$h+7IU~!$tFXTj5EH4q*(U*p_8wyLOdfnJ6b)E_RM&e$zqJ18 zJ~RjX3#`C4q<&xjew|B=Be1M1j)CTvpWvYpMQxxn6o6?vkIUH;pl#J5a;}n}@$XFJ zr{P}OC1$i>(Nxj2v9nUHSMsST3%*EKf61B!;|bCsWG2e$BE2R;2r*!(Mz3~4%O4!+ z*Iv_n=sf}h!nH6`dhg%oC}^u6ndTJH7#st1G3Kcm53j{-`l$}qqcYTc5=k4AG0d3K zksU46y?=NsW1GC@Yjtqul_h1wpsO725^h5{(OhwT-^O|7P@qWkf>uNX1j`$?k=ACE z1m8W_`VL$wzj%9$Q&Oe^3)~tJZ)KXlpjQjT~|49hW&IbfCNL0zjwVD(5yum5T5n0Sg%Zgy13SFFmc~ zpOWt8%hZCJV1jp|p(IVj%>gmrDvODJboI%WXR7o=&x)&_u7eJYE-V09~}@_nhC z9$lYD^m%r`h}D@abNtGROLX#3YFn=oV9I>tn0O2sVD!7 zW+CgxZEvsHbjlv&8@62=zDkI8I^txdm@o?Rc_P1`I4#$b{?zVaVHjoC*&vv=JuG)G! zC3s=!lu*CygxbVO9y(MxoGrv{;Vqu^66&+pBs>avt(@_=E?ULO{+4eBJoLd7KQl)gKPL4CL zENBo6nNaFf8etEP#OEFw2H?z-XINd8p?%AGMYt83)lj!l@cMaKYt0T8SUgD7`>x= zlkOfh3p%Q`Vae!OmzZ1!gkaPiqeAVnR%@<*L<=_SC*0bZrfjNtf08p9GJ?eDdARTZ z0-ETI@(K{`O~($1X97klw?m*0sc|OwlqK$(=X0wLc6Rs=-=F0qBCyl}vJ>mS((1Oy zbdgvp6eko*PINtsmCe6AoH(?xF%FV#+p$lk@GmliNQb4$09H?2HitzY5Rj zMgG^}AZd;}t94~FXrcj(95FJY1+4kt&^ zJQu`%y&6u;3o9Y@X!wsA-i8tx1a}n{z47zgc!X)4B!nqEc`uv($tV-c)9WF0TRprZ zUi?LdR##|2^l(rq`x;6M9Je+yRV@zWZ+Ij%m-Vcxnlsxt zX6;eY4<|K$8_t+jVJE?A zojxEb{@%u%B68eWm3$@YQ07IBWwW!F@p&?H=g#*RbK7x>$jl-Fe^X0==}qvQG|}s@ zMgp|ma&xFM&WjtuLoAgDtnE%XQY00$kNE|2nD2RimUB90XtSn@#k`LcRKdvmXlg8m zjP)?|GccF!gVhg0?_sROZkXSnknb4)%YEJcBO!C(7*jx`YY=~_dli!mi2y~YCpr&e zdnz0;0=(AO!uI{aJ}6+(9cN9Xmm7KQz59jO|B1}u5^}Win61&!)9E`hK=Hwkk~Q28 zHF`ssy1K#D!oi$b&5!7ifA$lXkmb*po^~x*P7Rsnu{AIzGq^Mf;>ZH}i8o@f-wykj zGHV`^HO&PWsp`6&=KJ?<)=;)aPj+XZ^^=BRmhjx6C)JSe{Dch~d4cz#xLJ@`+Q^r!}YYzCX%|+UnzJjRFvdpGM1}=6pg~2R9K#by(NUN2}_` zV6qnp+=I(h-#HfVcl4K#p75Ej+Tn5LlWFp!Mtw&AF8LO48=D*>R{^&*jSUIDtckTC z)_+{J!s;+c5v^vy{MhJi(%||v{dzl}@npW(W=JR;L1<>dcVkY|@y|hd37EZu7)^=- z{_ZrB9kmo;WlfUg`0&XTIhniGygbXHB&#`+Jpi4mCZUjMw&3Cg|9 zK15^#v{k(!$wVZlnxswDt(i&$6ElaX;aG~hy9zO^b``%=l4GzKf^MxwY)Js1K|mjg0Q#6RkcR~Z!oal?>f{H7~1c^0kXWI*Y&=sv1FarJzJ)9N1Vp# zZ>z}4NNt~ms9ALFDXFMNW1(E?Gc`{&K*5%9HP6`fPmo1{c-D(X4HD0GKG}URb*jj! zss`HEoVKE^x1g!&YH0^wO*4synZV05PtQXOPoVa?L>U-yAj}OR%T?E3lS#}fEP!;DKpJJ+fL^#&))l6f|lab(0 z2~$PVgYILIwMi-|2${t;IYH5wfsK(K^sHSCdt_FnWKtx5{7&>b$(xgXt+h`fnF2TY z;D!%$pHJ^x2X=-+%9tzB#3w-Hj(zyVu55iDveA2>r9V+0)~OnG-i^1$JW00PIM9l? z-2h8TVtiGX5r4(#J^5~Sx@%^?qoZ9V)G8`TN_dj3D(*-73$ZtLT*6&5HO_pprs>6$ zR?FNF(}u`)2keAt%2X~qPiW%x1{Wo|Ejj*SH8{tarM1kn@BculmPv=986pE7%kf zdd?+Q^$<438bA}47k726mEkyeHE!$LgY5*4yzZ(u7U-AmUE3bNx8K^;c^cgcZ6v21 zgmOTCw#^4@UVj)-3HZHkA=wb4YHRO9_9V2Yf-5{?41Ydbf)pzJeVbh$W&D3giypCP zU3!jf1toDUmRAwM{E6S~X3V`bQG6FLjn%WRAHPPt_wH6pio6^ksCi#4Xa25`Ld+b{ zZPChmRWYU?dWh;CIO2jf1)hFxu79n6Bg}#E)Rst<%`tkmoh$$U-kxRtBP6q#<0m(U zQO#_y{16d?)+uT$El&SotC9<&v#DYVja^u{YWiOG{38H`dvuf3~j3LVe3Z$X6qEPiJ4Po6XxGS_c zq>|iVrGK(r-8wm046Nqpfrkd~ZUZ3O~lPL#xNJ<&UWvnv;e+We$KPRNc8L-CG&u>*G)O8K=7t^u_ znTPJ6drVe?q@m$dxqqR5Bn?c2^kC3fYG}}`GSj>Wryu57`-Ntx5D;uBAfPe8s)ntS z+E1u=R@a8Apeby)J!h{4*oNjgVR=Yu zEq@_dp<^&kvERp2|*H6xjUuj z(dR)!V^~HDf7V}_XY^)VMVM~wz3doQ@ zPUjb7>hSwYbe)Hatm?haD>lQmdK+tnf*w;AR#CS4XbSKm=3xjh;YY1Kgghq$`3og~ zVUT9B#!1(&EB8z$^C~BYA_H)hp(z2*Xq@eXqbCm~@NWcPsV!@m{$_=t@MAMuF6y{= zDMg;>pHzO`M2CnhYoG|x8=D#EoapdbCfSNP1@vS#BA=(=E=B^hVDWyGFlWTq_V7gv9rai(d&T? z%1qGl;+KQv5RrAA9wd5CLk1u3p&D-ZlQK*x8-ot_@_>hhm|@+?vlW7b($G>z-Qr%g zx^eG2`ni9hlbR5Qm`IU4R~j=a2}~Rl#y+8@%C7?u%?&{t@F1b)W4bsbc+~de>twCl z; zO#gT!P}<_iV3;air8V%hg3BRVLx{;g^X8$`>Z+m!j1GuCb4WAPO7Qt;dL_P4|4pbg z)0w;*LDS&KUFY>DTxsOqg$WpAnNs%UJj1V_8XM2S2jifwl3~P4nXZ=M;zWK7hvdTfp9l2L>D#cjXg5l^DAnCH)LZU8f#V1hA_ z3Xyp$eDi7&9K*Wt25hRnE_P6Xf8V$b+a&fQ!F>g4G77VXcWQVuzs@1<-ADQd!PVRY zUmD0gMwse{#gqt*R(R*+5sv~#kV6%@H_;mypbo-0nrM37>fyRG57+N@rKHVMNoJ%I z=TJuFSFoUKQfTM;DF+LK^6`iAG^A(DqHv1zi?xKAI|L!&yADBYIQ_K&xo@F^2NuF) z!43VoaW*7@9ROV?*%^fO^gfZO=iIl2%6zIQ!^%sRN%|Y%kix2gPU8N7PR(w1^K*H@ z@}+GDfDF5p34)b@3v-J@u0dA+GpqNXGdEpzh}S3I!Kn>C=80e#}EnR-rt< z0AU&L6AzZ+j8X3yBUxY#XyYJh9l&|rh7bkUe$ze<|izB>rgY%_3&brG#MGKy?4y&6l-}lg|DDxA^HB#P= z*0N%SnBWwVwgtXUGqhcq8$W6_;tv<=Rq{!+*$!l%0#Zsqjku?Ko$yaWnZ^VZ+Vsxg zCc%Zu;Sl|dP+b>Bzd_m{KQGg<63hBBMtt$CQ3K42Ro%7$ZE0`!Pjv7TjBRHAn5_39 zphEHbqo8{x$A@g{vcY~#ayWklaxr?M6Cuqjc(?z?J;5luR&uANETH~K=io+6W+O}A zW8Z3CR4nOt1TbAKl_WNQLUEk_TI!6yWaK<|4od#T>u?^TvL2WD^M>ENd%0yVRvEk| z5v#O|OqSIIspm#$PAVnNvLsk5T(Q)SGxyoS7U8FIeVBGKUh@}NpoRI%fy#Bw#YTkd z_EUXhme}>Kereb+=q1OXau3*nlppeK3J>_S3(VWI9EYyN2u_F8VjE?!?l)G?peM=Z zS+~z=1SD~yZ=hkuZx($_^Pu}NR2tDVbKYEiqGvwIw@z5aYto0`DtLpHwiUcEe4g0Q zO&2jWQP}!`I&(a_3KZ?N-b4e?z82(v`I9zy$l_pcyxdhyD9LmOYPry#E0+ zE&n%eJF^*W-0^KQ7HyYb#IXcKK`>mmIaGxI@gdS_6!b(ijT2NT*L%}|rVLzA@|68M z_D5;f^suvOWnoFwtVJ=RwzfAg%~?<{f3Xptr;@DjBMHyyt}aQh>9nv8HK|Y~8dZZ- z@jo`7lAxn?^-OF&djIKBH}7hMm6Ht3`*B~+s#Y4MouPIsVYfwrO|6fdfbZ~=Zoa-< z(LRtNeL)V1i(N1pbNh)8inOVHH(ss$X8Fh6_Uo6Sw^q>$D2og!QF&9+!dM8ihV}IT zb9-G^abfiW7BZU($?#SYJHmNV1jyYHjZ;fTl^U*sjF2iR zR%3E1joX7de2zWL<^L$bBJ$nka<3-;5tC_2p{oeTI;8fdHx1{;Q2H=Ubu~%oeSNY_ ztZW9Kf``dH6lNi4<1goE!{i_bXD4|h(l_*2v=nd!YeP7x ztXM8hSe;32b}xTT$Q*e;p@aP83;jK1$>9cw6XADiSR*|ME*yvw6RA!j`@RGpbJVcr zE#{-mHrwD?B9+&rh44E897;D7i~GF;(1Qk~q0e@(p#Z_p2llNPPlZutf2In`uOBn{#(tR55eQA#sHvdBfQ=`a7cuWG@tq>8a^ zU!4P45AT%x6~lL}Tt}EbLi3pS_i`jR%oa>8@`DDVg0|fwU{Um<Khs8Opqq0d)IlA}mh>nDU z^g3vOWBjM2j3tV!Wjh#QlG9YWp*Weam&#PDrsR+C<{+$O zY&=O1g?-;6pG}MC@{!k_D{?d5p5!#RO;40>as0zZuIB7@=2PZ;q{t8Q#GOW#QA*;{ zlM0c7x2O31yN*xcA# z+d-I;t@8JC14#P{5GgTl`xh?#JjG>AKrvh_04W((8Q5ah3ET@+;W_t@qtLA#iOH#x z*qn9I)*O`N`Bij1-ir*WYo&XN%WStcP6Htk`| z+SlpV4NXW}QLG`dju7(~CjC6rWnF&*y)g7|pqexiD>1Yge6K8c7%A`s0w8om-nJGB zt}uAo=@t*ycJoa8j>~!H-4J;m(y|g0hU(GsM(k7e18Qd(cLHpv9vPYIB1|6b$RepL z=IW+%q`opi35Hz$mMKf68ih9^2EJmuqEk#C5?=M@Z++*lqv*p!qOFEM zW18?bwL;b4p1zZ20W_lEzFtJ_|8`uQrhoyTrXFFP&EpTF-{PtuJtBoi@Bh-T9;&i# zc5w30PtFP0O=iqWE)`;*;-x{mg737}4vw9qS>WJ}CME0DG z0-Y~VRseCehj+re(SBT7N)B@A99hmYk+^yF`gE+3*c_0~_;dx++0<(c-1S-BGm}!? z3a6K;R|(0gtPkYGyfp2ISG^X`(!{ll?Lpgw_%jbt`e_I8SuFf-%cm{9pD&(4nrmVeg$|TEZ zcOX^LCfALpjd9-i5%^M8wP5hfI_X!nM*-C>bU_b)W-#`uk@9`_dY}KKk_CsD7mEjr z#ONVFPlqVi$$YkV%Q7Rk$kuN4%L&*ES*GkDNYRnrK*53fw!Xh%ll|n}Iw+()O^)AL zFxpJR%g{qw);OeIc-4DOzVNxPR2|R4vP(qtK8E^ zl-YH;cEHL^pItT(#EJ0|-vBlyL{rs1jg$Hsxl6vF`hao#e2(G|>pqRHfEL%=}F zRe8@*9{^VLZsjs`?uc0}6b8y$ea_V_-Lm-U#@yo!vziksGrNXK6gkFWcXyz}l3{#;>t+3;Y{`fty2P;Z;#@>jYnx3im9 zX9V<}k0Bo_zMsCukkoKA?~LmRU0+wC#PG^43N!Kg`I60F9K7c#EYn5aAz6~_%i)iu zD6$T`B+3?+g~P^L?wM-`Q`R?OpXN2U>x!{^Hf#!B z8tDy|oD|jgWQ%NzF0bvoj$H&pxR+8;l|AKtHtXpPnM@d2U#tFk#_wuo)js$M-9v=K zkn=Qn#TiuYb3MddR!nIOwOf?X` zbv;B7ypEMQ==6{lF|mBS9O1uCfS#a?(B_68Gu)6> z0l}UFS1|A?4CD~YVwpgC5X|jF)<%&KQMW2aW&RU2!yWPmNcz9}GFTc#ubLTF$o-X} zNi>3%C(MSXQ;`3yF*`+NPTw6ZdhjTy`J_VWWB*1c>>Jdu8Y|``>iOu1ITA8nF%bf5v7!| zIz>-gUwr5JjdTGk&CFZO0)z|^+c8n?y1%m|f?>4}HbdWc;NgA7d#K5z`@Wp0PB&yZ zp-1{7dTaJdSBtYXD1{ziU>=m=&v5J{eW@Z_nQ2*8r6bdH{t_JbKBt&Wx(J1RHhat9 z&~Qobnu)^C;B0G~&pAw+B;|X3)&W%_qX5ylwElSDr*D?|-V4h2Ib+G}aH-AdYQm4Q z$vwe1nK}d%ZqvY&!)V}E1X%FZXt=EF8a1>O7geY8ebT;;?yn{Nr{*tA?SQ{E&Zs%! z$$2e!N(7X!$!MyRFy6|He~BT25c>pAkxI_t6wb_!;L=_1_daJZnMafTAtUXIti0+@ z3hkr^%PH01SMpj^z7-TFt5KyCW>C-2)A)*g^NcOWq(rz``{8$RVqhF_^qNctD+1zH zGzXaKcSmG_<(?xWZ6Zitx6$e=S5aSx7iX^Kc;eHPMY%lVBFy)ZYD9CGv>F z@sRA>5nTxt)mI!m)j;00j0ReaO5A$V3!nBOj+XExE-^j4p*`<&D#~Q^Q4}lmsjuCp zAQt)dx>?I0=`pfn7G-aQ@3z_Tvb zr#YI-$VNVl-$}M$X|GyoRkoO^`nQH(Wc$9?FwTlrC|%d>Atz%(>lm9O^@*%#y`f*N z9Hz5f%)4Xmjq+CtDb$Wt%)YoUVml-*`ggpWH?C3dQx7ETM;D}1;j~Qs3Q1FbtDOfo z9Mq6f>b{+`vqQp-RyPI^l5jxOHzNI2q12#M%0Rk)phHj=LC18-3hryn=44*12Wm!u zunhJ0hP6JTDrg|UbcJKp*tI>LAwT&}DrzVbYQD3c^j`kU<~NJ_)o%)M4sCmrkp-O- zOfpx-nDZ+Y!o#zXDxPa+2A_OdtI+M3kahTYLJj%Layj5z9p22@2=maV1wBm)ZjZF} zq9@Q3&!VqQr!j=Zmu2`Um6Z|g=rF%yGfUfjq{<}nG&yfdkl;R>9>~$+xQ_6I!c2qi z+@u&ARiX66l3L3};}6V;!@c`sJsHuweI&~DlskjTnz4~cnC%RaNmP6xLxu80yaz6f zfr4Q>&y|bMHD=K`F%nCk$CnNh??q?st#Nzr4|7^3P!U!0^qvxK*?I#cy{wePARdxI zD43vKe!}BCkw1kCQPZKyWU^>d1<4~|sb8y>_v9tWLspv#*8e)O1CIuh?F?iJrwP%r zk-ew|_bNo!9|w2i!OB|8GUs#a1a#rf&^AB6SchUKcIxt(*Bd2E%DJ(jboctJ4vNJ* z)c*t`*BS;k1%qft?}Nw>;@9=+MYEK?ENvKKYj=<9F?Wi}UX3-ubeuXA^J+|)p-ilb zn%^ASOy7$3jO1~l#*6It?uZMv-`oa&<=H`<{T}W|C3virtTkniu>-&FdMnYZzQG17 zju?B8ftx62VSHF%tfJLfePjvXZGLN|g|3;C^14>iNx#))c5Fj@JLLA*TEU4{AwQJ; zwQu;25qs0lYod3kINOMh{&?juviC|Htm!t9`_6+G$++e9*e zR1%go3RmRT2=jH24= zqXGjHi&5ta1~U)=)f zLio>Y-zp#%;`7ZS=YE~n1NYFB8LQ;wtV5`sP-(Apnigu~LfcvE9wRnwy*W|_Nj?n< zI}vLm2sKCt|7@VJL^{~pu6JTS*|#iXVINCKM$5Iy;3C$TsiK)AAI*pyF5UsrEYkWr zc7THx6CoDsETFl%8^wM{DiLCxE}6)|h9vg6)2l78E#(~~(h`*vL#mOeC@Ap6)z|x5 z)o-$Gf7_1(uU36yTSs3z`7^7`f(SZUek?4pWr2<6aWl-7Vglb3Lw!FrWP_^@4M~fL1J{!4({@lf$*LBt>W>f>A ze&Lls9B;8)^kcF1Nq154`{a|NFqP0FpG81r^oK#s+^$TPxtz_miP9fr8#~-}z^tMc z!dMt*x-w8mQ_ZDd^9JzqTno!nR)+p0;y=__U|`z1i%0o$cwX>w(k2`^FYAm*l3Hng zi4M=~>Hc+RGk9tGN!(Ame|&GBZ3hg=wi19@B?ps_Ymi?M>fdlST3s3B`_%)HT>WA# z%<$e&>%Y0{I+bf@v+j`Fll+@i*0#)mYN{|gYfuXkPJaV$Q8S!Njv>e-|B9a)Bz_hC ztKvq9p?iD%iObacP~XveM&W?PS>aSx+^pF?8iaL}ncAt!(8mgc-W00bR8ZD36D)Lt++TK#fIKs8Cb zC^cCg@`G)bk2U2h9qir@mH&txaQOP>hj{m}XuYk6j$h;1lNmu=yt=vxk$oaotEik! zS#Lj*Z#%n?49#nSd&tX*Pr@}jug`KMp7RPZV1_3L#6s)iccVVD%_+c@m5NtNm@p)|Ogy7e=Kuq9xxY^|tmV_A z%rHEk*Jn8tW-Y=$x&#u`h|>CT9b}Z>VH!qyK}tK>E^<@X>LM<9eX+p}#BpGqU-o%B zHe(Oc>3zY}lbXQ8z#WuwTxF?=F&M3j>Olxs;Se~k(nIoZCnywR_?Hn4*rpMc*lfh$ zR}%TXd#-1Qj>ANL12}7yl7J%>vwU2y)m@1L{ps6h;}OVTOOAONjoU=m_0BKoC~Yqx zQFfO9wUj&LWlf)pxJDY3Y4T#USw@7mN+SY#L496H6HG#a*nyXJLb~|Sw$^8BE-bb6 z`)C@b-H-lyA_{V_qPwJy6HzE8{&9oYoNzwC`3#f46tUzldl(EB^R_-hL_Gf!(xThL z^-<39)@nGV2o*2X3k^H~oym3wFu{}R+1;GwkPdi{9h3NVZ>%Tja@~JY!qmu&m=N73 zRj0Vjj26@=X~?JN&e0kIjm#<)tgXyQ(b&HXHDldij)7Sh z5aC2)@&N*gQzujnFwjSlas*6-duV3w5mBOSyFkBfxv}H<#IaDS4)^2#W$1jW%H#n) z&R_JSiS+d+BG75M7-%39$~(&X3H9SNnk7We>+~o1q4b~VC$T7X_4%%!();nRr;$pH z*&^-&KKfb+Zwgm1#0@;)j!t&$>_S*8jNP@)yS2pYs|wiMg1jMI>a!j5aEEYDS=k%h z4cOY;Kd|eo#%GlTB{4aejU?Cwx*@-O@S)cWOR%y^*w+gxeEQ6ACrbfMT%YC4;xc6B1mB*8;M3$EM^0AHI12vs-A0`jP`!+h1W#$TT zq*jAwY3Z{>O?{Nct0m<8(>5&`Ju~L$9CLWRCWfKPeL2y>z}jW&82rwsCtK^J9Bo5C ztQmr|Q89ITr>VeU2wDC@l|#R0Bo}MdBI~cj|Mk3ilUt|Lck0IIzpj;W_Ch8R<*=VD z^gRt>zM&ukH}4FpE6+6Wo>20>#-cdGdt?qUAE-f5h@*e&FYXBYsW0;7I#uTKyn|NEQm`tU7>v$5=lXQoq?LV5X98>2z#@IHP!S-%t2k5 zG`&D9meEQP15H@B)T%OBvb`8DhjkmkNd3HfbZ*1OtUF!;uUit4D#CAChJj@PCIJ&ZOZo{?=)oT30=qrR8Cn*#c?rEztlLH* z2ZeC(7eatl2?ZNjE+m0mB-gA{c$ErTv{*=P(@tm5x`O4q>e~RT!wdM?MP>5_;tPod za8nORJ;6?I`A2*4N-xQ}hfXqPMhTZ#Pkq^pJ@7aL*VhL*=(lf}@#&O-<^lkP1}t-} zPGqy1I1xUw`Qj7mXF=hxHXg>5uM@aXmxb8j?K>;E|5hfPk!7AvoJVHI8FW#*Ml&E7 zwEI{)F)|7>lmVzUNjilXh4c)qjtaZyhsvmVt)t_6y2Sdu`T?@YXmEZC-y4iiQWX}D zkiIx-@O20nnXC;5gLHPUSh8Mrp^pDk_GzoDaS1r?-oc;pG8+eAU#tV!%W3@crp#H_8eyzRwCHBVZN|~(u{!5(UGu3hYsyz(6ENUqCGv8hS|g6) zH!_^1Jeq<*K5QjC{-6*HzG*UwO0wYgUN_J)8kOG~kn#M-Be{o~pT>E<+k=;6&tdon z6TSRyuUTPSo&&bTv+9*qUi2QGkk60WO70cMN+*k?(WjEeQ#bV4ZPOjz8p$ z86(Z1l`uFVW*^vgap2LIh^24l>k~zS#*Eh<@eD5OO5M1eaS<$CRZP+aZ8fWpan^tv zI}^UeX|VQGSuG1DK6zD^7Q{(!{5a+iNH99fcSFeaDF2kVB67@utmGK>%n*rv) zZ!f|9cZBHItpfrU*gAQzOHyz_H|9+n{woKQ4>vVf$YydmdLAxK?Nen;u zk@Z`^-usC(X*7z1*Rq~{J$;K3_s2MgUoyf>C*L1to=kSH4Ebq5aKmiK(xS~18QN4xoG|g7gCA1ZNmEXBc0kZ1F@NgX2==UtUGlNoG6$!gnAPk zW6QZP=3UhjX!2^CnCECMqtQl&NXr^>8`0WhAe^R?E!4SMP}>i=|!aKarvm z8aJ!n5TqgrYY_k4K>lf1sju5dY2onQoQXRGb>x=y9A&8XE_0i*?65F6+h`Hh^gdB; z<55Y0v`V@4X$lQX=BMZz@BbpdYiMK=`PkQDVV*QxbIuqmLl@Hoi%@1hHx#w3@DC1D z7&@%wLEs?Vn)Q8g)h?^q?J+I>1J^@yFx;hp$;3e5uZkwU`p4?c;kdOMW?+GNaW z%d5#APGE$OpqeGV@p+Y~R6j@q2}?i55^a;mueaj-Cnr*%-jo(*&ZwAl$c6|-R41p( zh@jJmFepkv*OiXvAj|yGZFCzKL~y{TLg!hoC(HX>jBnqt{;hv`(N#}n%ncr#l~CP< z!ufyEBfjscF!Q-FWrp6x@n&|N9^wzK&C>ZKA@4-U=pQZLMhD z@)$f(`}KXCDqgo53}SQ_1(8sbF{p)9Q|#8B&;M{0J4I{5KJYA{OI!P+X@s{x5@;!bB+P;8aFZI+CIfV)OfM__Ql5=&crvMz$h zcuc77(9?TJ%sP%Gaw?Ns1$Au`iNb9>#PD2y8c~IfXcYt>4DQ#KEn!f}cxryLZ|`Nu z+NPa8*GFmpiQPzMm##$$2M@@W(*c`qhVhx{%tyb)-P)L{kuW!aGPn9yO=8}|9RG=6 zIp>Hsvt;-lm!0hT-~tGH%%^Dkw6@cO`kT{?I9|LS{M9J1AuEp8q{pCC>)6|l61 zDV$7mq$cx31Lz%dxpm+n{NrGIv?L&yZ_7W*zK=b?@2}&;ZKd78c7={dVg)l*7&?zE zZA=e^kbB^^>;4{Qd{+SEPHx!R&>CU<9W$Fd-eOXAU$BR^tWE7CyP2jN*TJ58`w|1~ zWmB$8rL0F<7Y~^BD)UL8n85G5kCJXVm9)LCkFtfT_>EV2 zi-UDJKr?bn|J#Z^MP=b4WJ?O>)}_>mLo*7}{&BsYzggD32S@HfK(*EksRC*%B0C|h zully={8jV)9-6YEK`s=$j44~wZ5CXej1!n?a7F73GapGkS-uSEJrB&QhGmPCAJfD@ z5E!Rz5218VkAB}Xr>u(OIn1RaG;N$Enjn%H^d|#sUA=t^iuwH$ABewpn&_F!HwwxY zC?#)0u=mc-vw_qE~nU^lX0( z!MOXz7bALKuctlD`$0U|&*7qrHiz-Bo^2wyD(zZy8zdjf^v4CU5)QULtYM(Z2%mQk zQx&%9w{*RD-i!019_u?^B2*_~fkCnc(9(J%IHy*_zy7DaGymg53{Zf{7`Q=?pw|RA z*a5Q(2TdtEcw`KjU+bf-iU9v=luD>SRAAOUk4iIyp(cr3Yo$X?mfx`T2gIQshzT7) zhKRN%OYev*5~)1Z5;sxTtoyd>@rOr?j~|j8pF)1gpK-l*4kcMnf$0be!{AR$U5v{? z0$H0G%}>xJB5bf}>xQFJ7J@?tpTnN|Q)4okZktqeT_Wip>8|!zS~rCbmbqh&bnW(- zx{g=b*rcMSdkgbij<$kZCpT(SNW~j*#E^QAwj<8X>%-*3@XDV^(aG=2Mbrlx0-tXc)?+dmLU67AJrd+fSe(ajPKUkgyzo86WG3rrCIv$=Tp*ht-3)j; zK}!4@>|^Yc%+h(du_QVEbWd+7D;FcQuuw3yZs zLxyPM3}^FgM6eYSt+f9A~?BSvyg)j7YSr#wmkyp`Q)8rmxC{ z@5cV@$0?ximbbq3>L+^-KUu{0I!yVa9gB(ks zQ&LL=BELUp!dW*4>{>e=jxzJvUa`F+n_9zm&9ckM8N(%~D&tzm$E=o~w!T;(fDN&j zVs5FFu`|P0W2}|ZeGK#-cCuzDfM7#&&!s&h@xgF|^`yn*Qo)284Lkq?nIbU`1!WO* z>utT~%p#Di?)aSNu3CIgGnw501jjTNqc0l%-3Hc`&%00rm0aVsEzOi1LVO;M>qNq5 zU2(jE-*C)5KZbR`+?9W0P^21Y(L0FNRk2xqulpT?waRS1{#8Nl0PfaM=nL$43 zIzKsFCE(^1!SDS=IhqH<(+p=Zl-2SkNzZdsx)wRbBE><<2C)>MMNZf~G73t#?0w&b z;ySn0d2icU_4kW81IpYyhBzaqK&wJwww`DgQ?3P=zoaOj8mzYP(<+Kf^l zo%-j=$vc`j@%G#`Rvt3U;$Ggf@-D%%xH~6803~y`Ipi+ZQW4bUigTSc%yI?rmA!E zyDgj3+}>Zv0PmSm)?_0Zg35q}zIF3erJXmXZmm26Q=`tQ>nK=D`r3Whxp?5N4dHC$ zp=@nLlQ8QlcSTyfhnp+{E?7Cz2um9e$s8W;3$Bb}O8myB%TkLzA^pP6MQV6#A~B`y5NI6gBi8;$Np(ZU)74!GVZ#|lI7G3}C zBSzkCLiE(zUT_UXxmF70pV^qZGtFlcVelL=+(^^5Ad9a3{oK9`Dw7|p4*aNe(P=glUPQbNtKXjyoUL(G%>!=o5C+Uei~riC~#Mm_4*s=lh=(a z9*Ca57sF?-PqX~untjX31*ltefONCGBL&Lbb^E-gmO)5I%yD5uS+3{Q-l3w%NjXk^UR5naonwtbyik?0w(TG$v~jP!rTenx)w zGlZS@ZTgHYi_{yBBBo!l(pkQH6fxSM3rq{M9p@rs4F3O2Kao|`ux^aMv?sHlwtDZ% z2Y3%#SswzADZ$`2+x3Dt%&Uw6giR8EFpY$ejWB2@2%8=q;pZr&Oq#5JZ%c6u8cp|W zd0EdUt|&$%2?h?LrL_7rHfER*JZKUz^KlT8RlE@%L&`NxtM&ed;OYH{EV;*asKM*K zZUFVarLHRh#P3KrGPXN{5Db=M#dsogmJ`2}xt#r&2Ql9>eA>-$Mn57R7%if_C za!$&IazqZE;xh4->8C=mw2osGz`4f*bNo(LPA?F1CMGMC{WzY;R?1)N^n-D6u(@8y zvF56cVU@~;$DshI*xCS^lL;?#h7Dj>QM#-tPXx7Z*?(D zl&NHN2qJVH1&xBE#DRGLaEq|6cbm__%xVtD_}BQ1K%EXFm44!4SXC^2STn9d*Z1(0 z4c*^N9q=IGK^@3s7;e(Apy47GOqwdUWeTKu&`zumxx5-h3nD25z*{a}LK7-#_1;!E zEoDmd03%qk1dD~zIlW@R=qSAw|A7&LN%QK>dlCU|t0u2&9WK(Uj7`Q`L8AsFv>QC+!H*%zpH>lFJIVrx| ziQo3m%*>;?&Sj)%MJ$XtC-k5spD+!G97OnEN*62HRb*Oc&TOO9NQ3PAWuZ|$KUvHD z37s>uP)sTAV@1omNeGN!Jt3BxYW)&?jaBXAxIlM9t{p%Juy$q=cSc0%C&9CaXz$uO zZ727(Z|GD4OH-la)Nq0FF^R~pVTBKN^o#KU3go(XAmB5}xW-k`5DZFzA*;{3%~SW4 zW&TG&pXJ>8fYhQ<>t+QSoI|H1(VRN$YhL-ajO zW%4$79kN}2N6$m_n)1UlXu9Q_pC z@(p?VVo{~L6oKD~wYcjAOy(wU!(m2m5aN-(Qz= z#q}Yp&C6e@ZGW3~$1W8R^7SXob_F|fYfSejw$V|{&|4JWmbvi&9@9qp1$_JVv$}PP z^B69qNtE-dv~K34^vW{i7k~t^iK{Gs#y6|(FAmPkIj%p4531}IT|)-lx9ezUylRf( z*7)^Gq0tn9aUPsTNl%+mJNC~c+I7i~IgA}XA=pnYy!YNO#OZ9L9F?3PaCmbl7FWT-Uf5l*BWy342_KSp{088C$3GNCDmf17#Xzg9 zPENKx+Yr@?2p#H&paWp(T`v$=Kg{@p#TG4C3PH=W7Zwpee$dx7?l6@NTlF+bX$e=_ z+V%@rxFj0!Y1+;X`TTgg(DU{BRJcZDM-}M#!;ql7B)XeaDzT(Nc08l8rv& zaNNzp>M4N0j#!)i5<(OP$M-rE(o@h@-H;-|-ko*P(1TnrzA0{p%F@VNT@}nTOGpgLVV}x-n)u#R$DMiqo9sY{P!>`{(}N0)`6$*L zV^jD?-i1OFQiFSnvEF)}2|{|5ALseQ4;66MHrxBU3!T2Qkmhjoed55{BW5(A`x*6f zPZ`GNjX~yz{{(EydNv@-24nr;H~2k!DQ~!yep%*DTbXD|+X;pcm_jwJ!szz|OM){| z4)TaT81~55EL?oQ=(p+avzhO*UtvHCquFy^<_=StmC|braWY*ADgRJkMu9nm%|b`1 zZMU8PXTqHt6ZATO3lhUT-_36fjz&h8b^cW=cY4Z%%7~Ui$^k=`Q56D5LL-I^_5cUO zwZLugNGm>wn43N|hzRR{wy@G&DF2Ds+wD>gPg##?VzWAXL$lb}28G2@;3MA zekZDH*6Bgf%!JTO4>JI$_GW!QB z4Epu8B$iHaS&CkU!9*Gzw*2XtziE@_KPi!_G3nN5`lD(YG1i3G832?$+S7Bve!$&m zATHAS^c!dUjpwZ2fi+^-l}p_YIsM}-tM6XrhKZseDa~l~y(LX3yYP%%R(82491<}hRwN2$ z3`L(be;TN~VWz)Hl+$S~Yx1&QkvTQMPm<*lU-pq=Z5x2Jy9r-$+?ys0RxP974NTTSoWez-7>gejo}>f{*4y zRj?_fHO{}MTlDQx${9&Qz1~z9DY2n~3JwA`CS+npu(sU|!x(C?a(WsJYqFqfhy62S z9`6DAl{XU}^nAZ9M^B+Uxz=0u&X2FB?|R*!NhixxWhp+<#1&FjNc>(GLs#xZx(E*%Bbqi&@knqO$cQB$fmJekQ+wbv_20U4wViTcu_7K^WG(0q?GI z^?Qbb_m_Guy=Pm&04uwInS7yY)LR^qDaQrP;;9OGJtr{_xGx`e#V~DiAB$LnRD1N<`cWz(t z`-?^)y=OFU`G8OC?uE*&z0Mz09r=WWzmLtTsnX2JqhjZ-lPrbq3AKhXzkl-w4(1{D zyXM$Oeh)ESaoOkJnIfbXf90Au;-r-zkJr5*>>ra@OlO)WKRWXc~zUse!;K3Qv z_IvDVI&5dv(>zeHb-pW+fX?R2rU3i{dww&e_K8b*bKC(~$pj-l>wE6G?4Xz#WIKJZ zDcE3F-Dyf^xIog2@$L{PSXdz zk=9RW$>|Ez>$TMXtaN}a5`SgQ`V^~)YG6(K9)CzF`J2Bgogrpgb}2TpPDNRJ(q5Au-zevJp_2~@1dwabc=)uKf}(*GmVB&qj6@uq zMu@=eT24xsup)`t=oKObqdS3~k-$`pIm&O}>M)~;*6O(4T##k!zgQpD2ov_Z1B~ep zyP}TYhOOx^o;A$}Xg~Zi3-Q}DC6OdD4mYfe^ssF{SiiubAhG1)9A7VQn}hD!9&+~W zQ}lb)HF8~d&mgl%!!hj|YY5|->Q6}L!^WY5ywY2Y7Hgbd(rbg8U<42h^lBU`jLcN9uL1H8UqMTrDvb)%=FK#*zCZfswJ#5$!btcFs@?$u- zDl{v<90Ky0)vHSjt;VpV1%?$WEBb{|)v)-E=+O}^Lxsh2S(Q{LlyL14zH{Y=mR}-% z!t?qSot6&gS&x;PnrAJk*jlj$zLMf$E7&gN_2~O60!joVWi8n2v<<-@nkTK#um;9T z_u28D-=x9mw4M!pN_<69tT7PR>A)3;Cx4h)TysLW7E~mrGbJW}Ya^rkV%mVS7L=ji z^mhdN@1EF|PVuf+HhofY*TR*i8?ZY@n#}o}a_v0Rr5vF!gdu{eF`gLpU>l^Ks z$S}Tc0jK?}Iz@SQo&tn*$_O0vf~{%5G%=pf*v=+hHt2(EOE7;xT-Ua5#Lu9!ZVGz& zZLP~^cw|JpGogoGgYL#8C!S1^Nx+sI9?NZ1l`gYc*@@1g6ZeV#S9j!2W>@G^R01`LN#ft{X7-or=}BRAr= zTDQFaIWP*K2{=3)-6XIthQA!7!C^e_riE3VrWwRFiSg@D(x(w8R2H!lDUa-NnEvIw zcm|nu2VV>TI6hZ1!!WldNlT2w1BIJgklZ|US1dj2R<6pS;Dr-H%6ydcNWB~JaRckS z-;|@!^>k%8dQDr^-X9lW>}$L}>J)O_sN$+)6eVJLy&VOdBO^}vz}pw{_pQB>iqn~6 zrW6%=PU6+zRLlr1g_CqBt>Tc0Zn7Cpdw%YdZz&8JKRIybUM&I-kF*-J0`hGFTTDyT! z)EQMA@3XZwJfV(;uyCJCjlLy2mK5A|kWWzh&>Fu|PZfB!NXg0uylpkdHLEw36NCF2 z2Sw@ciyNur^)m+cwoCu55^F}vzxbswKwq6$$qv(HUemgpYC_ zLlFkdho`icTqwfZUa8n)#0q5A59@<3khhS?~=Ley%582NQ7lb|0Yr%3l&TRhj0p&-+ z4Is4~A2gwy#r~FISy3f)CsFGgBXN}oM;I>CRKGSpjXjQ&)T6t6593Iu=d9}er+4r= z_@-j9E`Vn|bRBT6E$;?B2N7 zu|~$SBKu6%0?;|!ac~Q^i*$YZ6WDhmPI7isH|-~oWV?=Ob2A&`IDTDo56fAN$Rxpf zhD;;evsUKB*bq9}nsXS$fr!SBqaxmCy5Q}VPx!_GHAUrb3v)zC#joEk=A59ZwM5kN z7-WeIN+l~=mI%I6Sds)=ukG3PIyrM~otGv9n)Tf}dB{JzqIws~4fI;~NA!<0cWetZ z&g;s;=RJ7wc!4)!--lBei8gc(Rht$dHgkP&6Macwabnrcn-V!5jHa-2vJ0F3Yn7ZEf!F#nZJzEdz}I*OmGrY>P~dq5%vmm4fVYP-qPVci=$$< zRd3z{x_^pDBt7dHvD4YaZc(_bO$KT6L$c=steA+B7QGdXXOWgww^-oqzvKp|(|l%@ z3vzKIt__hkrZ~~~AOn{Vbo{J!>-$yf`V_-{8cvo0YP)WJ!lpauJ%Gz zsLbK31xVnAnJbqmy{vrw7IglvNRss?t;MjBfq{K+y{9b4Dd0M!M0-m-gs-J^^f0C37w$=x%&ch*w36c zzr!SjEGBp-PH#F1XlF*$C0b&u4qo?bT@i}Hm(Wc&CM3@$JEsq6=9%Kx@q9Z2A8ad7 zE>QkfTwZm`NHo4Cvf5ehe&$w>uAlYSAZ1v~9at+@UHN63c3PjHd=hYbe_M`9*EW~xb1p6bzc+y!mjmhZ7w$H5QEOJgoJzaI6A@S1<^%fUFfX;{=^M;d&)f7-j zDTX*9xPY8oiNK>r4if&;+NjAbJ1_lyDer$SMa~Mm=K4q| zv6l)R9xH3M*|2*Gd`+^l(9J$RRQV&NI(+Fdqrvg{WDB zQu2Z^3;SjkAsGmnwfjBNJ6QWyDNdAFF@eoLXmcBLK(pfwDWz0(ltnw0n`?f{rN8 z6~%04c0^GrjyMz9#a?0}hfikLDlA9z5Vjgh*ah`yK4-nAbrt>STvplAtNZ&;-;^!YgMgL7{S!JCTeYoh=;aj$g6w}N zR;0xtm5|c8zCt)wd8fUnqEYVEM&-nmtEjmCfG?bm7q_Kxzm$LGW)x4aR$tB}NVB@E z09!z$zc?TQtbK*%Sst#o=wI!muLWhjM(cy3Q`5u{m;Q5*Z!L)I3ZAc5a*`F4h(d>J zax6i5;7?uuBLbqcnQMaY9QiV52x^l~nf0-^IT35cNBfNve=-~UZ|>T3#+h|_(IZEG z8Top!=p8=L4|DX4fTB4!Td%L59LMKbvA3_|Vk7t4D;D_=^rPK zTso01`DarEg!j@2t1faT*o_)z=VVg~x%Ao+*F7{AQ2#{sy`5kB5oZI<%hv_>aGrH} zxI#}g@Sr6OvI1q)Ff^D0ted3Saro1a{m5zPO_^#W~_@|RQ zw)Ko$8Y0S~oJ+3?ShGs#5{9C1y)&M3kbF;ER;xq+Yc?D`vhs6!iZt-PcjtWnp!?}O zR(rrlL<50iR~xeIE#*ANTR}&Zsy4SfLaXTW@%KcK# zMOTIl(?5g}Mdk#XO$-xR^u=oCzT=%Ahd1jxrU{}Bwy-fq!oe3il*=wG`I+w1#M6A{ z2AvG=R@|O!GawHy1hdbt+OBOQm?YDc^@e~wTBq0-nhS+>{@|orINq)-Ca4Ek8r=)q(6`uMn zb5k}g8etzkKq!i4!)b9zM&u~{%eAj7fSNq33fLxwb-RL!UVRwh?#H3Sd`2qGes4Pw zVpO=6QPv9G6MWH1nqu__rGWIs9AnrDIrVThR!+_#R^#L>*>C)r*JbYXo*A*j{5(HA zX3UR`^kU`zgEuG31Ajswybef?e z=$m~<|AcWbXA_P%DbB0)iKMbZlHqiG6>{u8_(<_%KX1^tzgA_ATyn;>_b$unJTt@uy+;PQTUc5N zW5hy(OPwc2@2`|iyzBbpWZ5wCUBx90mp5FaZw-(co(uz-(r=21{G3rcv+SviE;tGf z$RqHfObR!EfDTz$NryXPV*2=G67U)l2U%|FNMiV$Mv$h{`-@;AAJ(%brN}`m^H*56 zYhjTRLdtU_RE;MjHcDCo9*x-;ZEQF*UUT7u4sFAH4|JoQ(<_`RKmV+^ye-rM99OH_ z#_3QUPX?-H{9qsgN+F&#m<)q`1z%+l+%%i;GeF42t6M77)U$9RDx zg86d_owJ38f>882_3ao~@j)Y@ zBLQJ>{`9EGjM>UETiOH2T8nH4k~=z(!i^>QjL^na?fk5Me|h^E9-_~l#Y_@7h18G~ z1q_m^!x7fbnR$z!cp{RO47juocnp0>ovhzCtzIDqQU9*s&S$<^7sKbShLDE<3?yjW zUG+XnkJ9&uUW3{d!|V8ZR#91Da@4@mexT{L)#QBt<^G6HGP#5WPcw+aVD>t!&%7>! z=MK_Me*SYRMOIzuQ~Sa0);;P{9KC30ciC$fKWx6{g7qRW^J&f@Km!D$vbrP}JHp$HO5iZ*pQ?l?%$fM^s4& z#y<<(vpr4EZ_zvX@SPP?F$5P=GN338Wi%<%h>+fxf3(Q^e7eU0j4qRz77)b>5SDEN zw=kIpIl}nnwgz4!L-Kp}aI>O78gtwh|H0e)(o-Ec8;WiJvFB8zdjj=Z%MM8p2;3NL zHtVGkmBJU7D4Y1&XvwGT%zS6FvM0R^x|gc75+p#jU$k<_7u(Aao|GDE!=GByBJq_GY>mg|I_ z^mViHSKXU)8~5MecIVS}CVf8&r7%k}l%dx+u$|?xUFxNTH*msF<`S8tt&IUt;vC8- zLwU4&gfWfK4)ff@q|^T3nmbH5c6*2JsXZ3!{$>d<5qP&TkUKsm2n>ecL}PXEHMNWpQdR*mnfyu0oo_uT8?pYY+Y2)B*$l5{dsS!R0N%7tsLs6WGqW zZ8F{c8T~hWr{Uon3?EEGq>TX6Zmq3VznQ+tCtQkI0^2m3Tm@A1uFxRKVP~rEvm-9k zZ!7NUI1@RNj4Gi0hS9D)_I-@1Ee16;lleltLB1B${q?psgPLB6b1{PX1npZfo%=69 zoBWUC%=athu*x7mpv%!>RMVTnLi;{%sE%^?Lq`~l{(t?gj5jIRX|jIahaP$(w5!+e z`ShDva4`GJ^e|mmTh{e1XcU#dI>#KZim)&~(JUW*Z2&LrZbXrLR^_EI&qx7(nAZjO z^qU0}Kyq*#4{PwU-eF*l;WTgawF5JVbvGXl&J$2O+sPBIT0Z6${f)>uxxOlA z(%JriC=`%B@zPY=W4M?)L-|ZbRN?2A2MGUSRR(KmsS||);8ZaWC9EM8DA84%fBI*YnGsG$py~a^ zJw3kR+qR2~$e%WQ9VL>%JN!`~^)!7VjdTRdwy(-^t%PI@*ibOLLDo>rhu#fh%D)}X z&aAT@URKobuOan<$xQ|78>`7P%`IfXuX}lAep106t$GEMx{~1M2bEPL>A?TH_k8jh zEg4V*Bv4Hzl%^ux%`Yr7+$lt$1%;1pHb3rK$nZEKp1!jhICdqQHOxeBSZ7p!&W3FB(}Bx)&ak}dthv&sJi zopoES2)~JoMQ%`$um~cGWZ_4aVyf<3!w5C4cI@2(QoN)U6_`ypu@9ArWf+?21oVI9YVBsYv;> zoT((@r%%ejcvfg!*3$~8j4;+)ED@4j=E-@hjbf*Aq(&ivF^TE17WusGS_Z1i>IOty&b#mGgj z&V=UShQgz?$01c9Y$cS!Z3yjhdG?`5WjChu^@2aoI?{G!?-VZUx7rU&f%)7myW54F zJzTqc!=y{h4+=Pn2lKR1mW?ox-ZuDDAsD@gb$)qP#j=WW=GXM`{?si`F5wA@&-s2& z&ZdGmSmuT62=GxgKY$E26sib>9c90_S^m%LBv_GazZP75$X2Ku9Z!v+*UH;+{f5qv zvt~r4JCRxPO6t4T8NMNNiKaDOppRbrtz~`0BSY2f^C=Yrr_baQ%{>8k5Kft{OUvOo ztHI)7HIoS+tuI$$oA?Ytq}jZ2PORb2ZfnhlNj{9kq}>mR+wmYCkBpat~1^T4r`1D9#DB%mv79Uw+xCG@mgIT zrZe-v_bi-03dsSV1zQQDGd@q4;U$M(a!f`)m6ihv94eVrA*&_cle3tG1m%-XDo zrl@m_GGu`|vI@V(yo9p#k5r>DsO+tX@0s&+Cw|^?u8>A*X`yg#Bt zk!x~zFi1=m)BeqaW)W&dkmZB?CMr6-6IR9muUFN{CO)dY#1Ub!;eQ(uIOZhyel2IA zn`$82v5?_AXNyk4aM_}ZqDI+Dk&w~f5Kyi!Xg$yMjUXSh6wN_N4|U61H_kZ!b{IKK zXRWF}H7s4q59ilrBesDvrdMd#ag18woOdV_4Yaftj%xLFw@tz|AnjN5W=!pU6nSu zolajT1uDGgpd!wqfnX=r9NNercOKqpnsi0oSw6I z^%-T-(Io+#uNp)T(J@VjieVSe4ulZH&mf*!n=N}#*maUd{xB7wcClcc8##J^anCh3 z&=+$@uYoUa3Lhvn?P}zq9dp+UPREUL)}lb4&KtN#n3nhK{KweWbrl?vZxmvfZ_fxt)O!u~NSg za8O}?5bpU$)e~7ijPag$rB!A<&gTH}tzvJj01!aCJv!qPSaxI0)iqNB^K-kHGZDg8 zKx=z5LZeX~xRT`3(JK_>3JL89eOA zP+^LIavqfC1Nu;%Z^?Jp@K$S=7JC;|kTeFP0DAF#dzg01y^P4CcIbMAyyY|VZ0bE> z=|)52%iD}jQ?M6YY*9=*%LIR-)X!B?LWfxIRd-0512`A!%kH(a^7iX_vH#Ov*g4&F zMDeFX`=KhbEt;+AaI1_s8d3ZsfuF{xCo}?U@s-@&ei)MbRFtAZzh22%=t|GNso78Le;Ho4yZLV&Jz#I7i*GoC6jiZM|?6u|2 z`LoN50EIn8b{Xi!L?U`o-Ng%``MO5JR|aeldwjkueTSE;Bju)EKj<^^3|4W-8);A$ z<_}G|LC8wr`&X?qAB;nx?7A&b2Vi$&4ypMY#P9jc$B`kH=l%_y3NblO{EZL!S;~)8 zNmjdJ4Ean)O+hjnTb9%?nIOx>$pg`TM=nQ~L;OV~7>?_K{xqJsF7kE6K-`@%M}p4t zCxHHTsLD<<5o^2(@uB8tE&Ygt1;58YeCy^SBuVqxeJ46Y&-i8`ugj1|kPDlF%T{bw-;6lr=AfEhrTBBUF~`&-dfm8F^+u+Ybmcc;)?FZ-(P+ z;|sOvjd*pV-;Tt)5{b8}&UKSYCJ-lfoE|ZK?@mrP3gvboXWrQeDl~)m7;7RCc$|Gz zhT+(y#7f4RG9R?`pk3ZE34WfFcjeu?&2HqHNtAt8u3*|c{;6;4J|%WTIpCUf+rv0c zCNB`uBt_lSw^;}R85R#A_v@3R2i1BguzGZU@`&IN)e2W|SRQ8cueZ&m7RRjF0qLh3tB@@L}r<`G8PQ;Cs=zUnyDn%sXQe_15s#&nOZg zuz-DJz!UJ8@guT9rpoW_($q#VtGjl^g{X?ZXqRLyB?0{|;y44(oCqmLK%7?Kepg-d zoRH}M(a22<1fqFbg73FZOw;;JZv5JeOdt5uH)uMAD?M;f*DvUFojsbDJem2j-5yoYC%UxFI8E|Vj`+NKG#5Lf*fWr^i@S zl-xO>R4qjYc%v+!fSI5}s*U-9A<(xrh0xw%=XF&%&1Taci`j8Z9q?2&=cB8FCDcpZ zjw6|;$~rq9 zb`yNh+uG145Z{T=CyyAuX?;^km}7b4Xmr=L%(I2d&{SS#Oj(UBrx`Ib>0)P%)SnhOa{Fx4p!&PXE1y|sO@bNi1DvrnOdQUc5Z1qD8WslIyQdRJKF^X6I)h#d_}8p zAXgoN2noll_)Mx6T(p1may03+g8o)go`y3lWiE0FN|~t&t;%<1C?`%iOyOZ5>W^v6 z%;#BS22J?&GrU4=f7ZAVQMu$^zo2p^of$ul#4!OZUDZ&>Ba%WcH_0H%+>X4%)7N6c zfvC@UhpQtVpm z+XBSVI>FBfhPam4g}f~za%*MeU%e^ipQy8d{|XuAHDNgX2yR;ZWVQ{G zWZepKwD9GoWw;9|KRf#mKcD<5(Udaq|V$hBlE;PKV#I*mJHnpqKJ6m^za(tcd zOSemiGh#qrP21$QRV~l%B#h8p-mayb4E=3-Rw`0+kUd+BXu2VfppLYF2U39mZx;Mj zE3hU~_cr(DzkyzFfO5Zv-0f2SnV4C}(O;pB!u-^L-^A-0pjpSPk?8f}r7~dxl3Ppm zxCu4SK_mR^U1JA}SwLMcrTwR*B=aGgTEdAzPGqJ;9JmWiykSDNy$=KkH2Q)j2cm%L z!|7`DzshM#pU*F|-twH3Mq3F+%2O%Gt8t7xljJNKU^ItI+fxT73fM0ysz#!kp$s#q zdyigR3UUCIT)#h|Gd|s#fKLyPwZOK0JGzfmyn%muj zYGk>`iP`Az+umF*PBX|sOvu;>_r5b(tJVYJ^tF}(%6px zqrm~mgk(%7s>0lAoD4Ufk`p9~J@gA`WN~L_5?v$!%aL(L|BaDC{jq`>n$HGDgf@?vr{S1dEHYwh7Q8{6PX(;4d!!0le; zyTv%ilMtuH@3pBu)h}a`nBH!)#`>#LJIcbg+(yX`#J> ziX|y3&*4AQn<7miaV34N9M+SM+y^A?@sJeEq*&eV6t!|DoCQHQ?Y|$+$e$)6U2q$b zQEpBp>AO<6f8W8N4Zty9k^%tXpx{4Xg``_er1JZd{3;Xk0*2eaMr>|fs zbr&q?2AM;{HtR%VtXID2M4A0(+{|s~&y0wh`?Z|Ktta?Uv!q~5Q13ZG3e_1t{$!B!SgBoCx z4Nd%284TE__*fakcdL^c>zp!>5n-`c(_1->W@?uB5aUS3o?O`j6X7nME~z<==$t~E zJox@AGWJ4#)B2iMDcQbMoll$4c9+u1;aVl6Q=9NeX#zpQdd4GR#zQ|Yp-tfYfi;zN zPdEyiKAdJmp=K~csDHc%`3KGp#Tiim!+>}2MNzeq%Gw8%W^hv^BWS+u6##P`;TG6{ME+ z!fKx<1uqdDMd;fDHvUhJgusX(EfI#7j1IkB%i%SPQW2UPBt6g2@_ z4QjEk3POxt1krqSHc%9~+?RMp@{~#qIm@-VJ2jiVZbCnY z#D|Mz&arL;G=IP;1)v�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%0Dc_x`fW8qfC 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 Cx)LOSTJWB=Zzwl8d@ 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 @^N+(` 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 Zv$+Sa|GdV;L42Z*^8Y!x8@~Xc3IK#IgpU9K diff --git a/docs/map-generators/refactor/previews.json b/docs/map-generators/refactor/previews.json deleted file mode 100644 index 7e5da04b3..000000000 --- a/docs/map-generators/refactor/previews.json +++ /dev/null @@ -1,338 +0,0 @@ -[ - { - "method": 1, - "seed": 22001, - "success": true, - "hash": "14920531807079716896", - "role": "fixed" - }, - { - "method": 1, - "seed": 22002, - "success": true, - "hash": "11206795816039919690", - "role": "fixed" - }, - { - "method": 1, - "seed": 22003, - "success": true, - "hash": "9177038106079592176", - "role": "fixed" - }, - { - "method": 1, - "seed": 20027, - "success": true, - "hash": "544194403467468821", - "role": "weak-start" - }, - { - "method": 2, - "seed": 22001, - "success": true, - "hash": "11437096384349004182", - "role": "fixed" - }, - { - "method": 2, - "seed": 22002, - "success": true, - "hash": "14770049955031301025", - "role": "fixed" - }, - { - "method": 2, - "seed": 22003, - "success": true, - "hash": "6504539841165983025", - "role": "fixed" - }, - { - "method": 2, - "seed": 20874, - "success": true, - "hash": "1561439438145097576", - "role": "weak-start" - }, - { - "method": 3, - "seed": 22001, - "success": true, - "hash": "9860058263762090266", - "role": "fixed" - }, - { - "method": 3, - "seed": 22002, - "success": true, - "hash": "5210807649688818574", - "role": "fixed" - }, - { - "method": 3, - "seed": 22003, - "success": true, - "hash": "10803015415216018498", - "role": "fixed" - }, - { - "method": 3, - "seed": 20665, - "success": true, - "hash": "9069473070465845841", - "role": "weak-start" - }, - { - "method": 4, - "seed": 22001, - "success": true, - "hash": "5044719679931046782", - "role": "fixed" - }, - { - "method": 4, - "seed": 22002, - "success": true, - "hash": "16710061729624559854", - "role": "fixed" - }, - { - "method": 4, - "seed": 22003, - "success": true, - "hash": "5470594655607351316", - "role": "fixed" - }, - { - "method": 4, - "seed": 20992, - "success": true, - "hash": "13598708499441320366", - "role": "weak-start" - }, - { - "method": 5, - "seed": 22001, - "success": true, - "hash": "4081282500915162465", - "role": "fixed" - }, - { - "method": 5, - "seed": 22002, - "success": true, - "hash": "518787808480994771", - "role": "fixed" - }, - { - "method": 5, - "seed": 22003, - "success": true, - "hash": "10208999434179203736", - "role": "fixed" - }, - { - "method": 5, - "seed": 20382, - "success": true, - "hash": "3929662265599224957", - "role": "weak-start" - }, - { - "method": 6, - "seed": 22001, - "success": true, - "hash": "15492766370562744084", - "role": "fixed" - }, - { - "method": 6, - "seed": 22002, - "success": true, - "hash": "16842018058235473864", - "role": "fixed" - }, - { - "method": 6, - "seed": 22003, - "success": true, - "hash": "13407323231470023606", - "role": "fixed" - }, - { - "method": 6, - "seed": 20989, - "success": true, - "hash": "14400468275467415138", - "role": "weak-start" - }, - { - "method": 7, - "seed": 22001, - "success": true, - "hash": "11574187082192437596", - "role": "fixed" - }, - { - "method": 7, - "seed": 22002, - "success": true, - "hash": "4034283354063186255", - "role": "fixed" - }, - { - "method": 7, - "seed": 22003, - "success": true, - "hash": "6154660420075554243", - "role": "fixed" - }, - { - "method": 7, - "seed": 20666, - "success": true, - "hash": "5979258390125815431", - "role": "weak-start" - }, - { - "method": 8, - "seed": 22001, - "success": true, - "hash": "5116402118393623094", - "role": "fixed" - }, - { - "method": 8, - "seed": 22002, - "success": true, - "hash": "11962301441114388923", - "role": "fixed" - }, - { - "method": 8, - "seed": 22003, - "success": true, - "hash": "6485435245488238917", - "role": "fixed" - }, - { - "method": 8, - "seed": 20412, - "success": true, - "hash": "4564946222045695100", - "role": "weak-start" - }, - { - "method": 9, - "seed": 22001, - "success": true, - "hash": "17350664697783344402", - "role": "fixed" - }, - { - "method": 9, - "seed": 22002, - "success": true, - "hash": "4057095887845812880", - "role": "fixed" - }, - { - "method": 9, - "seed": 22003, - "success": true, - "hash": "5606765741091488537", - "role": "fixed" - }, - { - "method": 9, - "seed": 20335, - "success": true, - "hash": "1400871654614562697", - "role": "weak-start" - }, - { - "method": 10, - "seed": 22001, - "success": true, - "hash": "8489149481487705140", - "role": "fixed" - }, - { - "method": 10, - "seed": 22002, - "success": true, - "hash": "14038765396739683628", - "role": "fixed" - }, - { - "method": 10, - "seed": 22003, - "success": true, - "hash": "13729186607969906376", - "role": "fixed" - }, - { - "method": 10, - "seed": 20204, - "success": true, - "hash": "6476948285032075565", - "role": "weak-start" - }, - { - "method": 11, - "seed": 22001, - "success": true, - "hash": "16784330220506045212", - "role": "fixed" - }, - { - "method": 11, - "seed": 22002, - "success": true, - "hash": "8998178716352640516", - "role": "fixed" - }, - { - "method": 11, - "seed": 22003, - "success": true, - "hash": "2020498579335314712", - "role": "fixed" - }, - { - "method": 11, - "seed": 20166, - "success": true, - "hash": "8525398947293004777", - "role": "weak-start" - }, - { - "method": 12, - "seed": 22001, - "success": true, - "hash": "10008600412084261952", - "role": "fixed" - }, - { - "method": 12, - "seed": 22002, - "success": true, - "hash": "9047696792498758786", - "role": "fixed" - }, - { - "method": 12, - "seed": 22003, - "success": true, - "hash": "13791541075664039917", - "role": "fixed" - }, - { - "method": 12, - "seed": 20058, - "success": true, - "hash": "1776682512706564171", - "role": "weak-start" - } -] diff --git a/docs/map-generators/refactor/source-hashes.json b/docs/map-generators/refactor/source-hashes.json deleted file mode 100644 index cbe2fece0..000000000 --- a/docs/map-generators/refactor/source-hashes.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "src/map/MapTerrain.cpp": "6b0770b830e5793ee54ff99c53d5fcde9bfd981b9120f774e665b55884c811ba", - "src/map/generator/MapGenerationDescriptor.h": "c756e3ca6e4b9c034e64b87c16d079998ec61367bc9bb78a2c5c06c561920e79", - "src/map/generator/MapGenerator.h": "bbefa9dad74ce3f27a1df059110b52812ba14f4b46777a6def9c789f1909590d", - "src/map/generator/compatibility/LegacyGenerationDescriptor.cpp": "ced08706e0d340bca8b9f0e8fb22105d52a2491a093f75771e30eef98e89233b", - "src/map/generator/compatibility/LegacyGenerationDescriptor.h": "03d1b6d003dc844b607932eb3982cbfd9e36d7661a5339e269431a1ad7f07461", - "src/map/generator/core/GenerationContext.cpp": "240ae5475f5987fc5c4cddb9e34d9b7077bf3a1be098e7caa22896478d399172", - "src/map/generator/core/GenerationContext.h": "a237976bdfa93a0cf2ea28024be91f3e6cc7da61d2f970e4efa9415da71c3971", - "src/map/generator/core/GenerationRequest.h": "79e63cd7cef6db6d25e23fd9b8145f8f45c70d93f8f3167cc9e9961d7f8fb828", - "src/map/generator/core/GenerationResult.h": "a4b758f990b3f0ee6d499d59475dab169540f458f9c999e175f93dfa9c621b4f", - "src/map/generator/core/GenerationService.cpp": "53115fba252ec8f23ece99e3082d7784168b68ee0ae0ac2af35a0666646438ee", - "src/map/generator/core/GenerationService.h": "7564d5fbf3fd4ab126dd512974719a8cc1cecc59beb48d309cd32e4935a89db9", - "src/map/generator/core/GenerationValidation.cpp": "f4b50b8db615bc2515bdfdde53cf568d7eea7a9e16c0cdac4351514c89110677", - "src/map/generator/core/GenerationValidation.h": "f47be67ed359b2cba0fdf38db1947eae8a4b19e2f2776eeb6b9d62dd51a8abf1", - "src/map/generator/core/GeneratorControls.cpp": "66d5c3d1de47cdd22f1ff6d23e4cd1663122c3346b135e671c031ffdde22e278", - "src/map/generator/core/GeneratorControls.h": "7bc42d9c89d13b019c53b3069b3e65d50d87e87c2f27b1b9fefb337be9084b9c", - "src/map/generator/core/GeneratorDefinition.h": "c2b152b0aa882142433e2069ff78f8080a37e3753c7bb70d14cac255f5c10821", - "src/map/generator/core/GeneratorRegistry.cpp": "d9fbba6f6b3adffbf7683f1c8068d73535725f9f60b2b9c955c1c08bbd393fe7", - "src/map/generator/core/GeneratorRegistry.h": "69e230056fe1a83fcdd48ac2b0f8812e96c96318d2eb3348c8b4f098833d12a9", - "src/map/generator/generators/ConcreteIslandsGenerator.cpp": "cf0eb0e6e6f91453f1a0d3a4c000f14fb82c59945c3612f43f83479358e78f4b", - "src/map/generator/generators/ConcreteIslandsGenerator.h": "4bbfd34137c86a2e910357bf2c9282bbafc47a1886b1b577b3ea9f1ecde3d140", - "src/map/generator/generators/ContestedCommonsGenerator.cpp": "491d5c65ad3f04c743885d8d9a7486ac16f17e868da8d9f309053dd5e3cd2349", - "src/map/generator/generators/ContestedCommonsGenerator.h": "2d88d55f091434feaec60f00fd12673b12f56fe9e5182fdf291bbe3cb08f44ac", - "src/map/generator/generators/CraterLakesGenerator.cpp": "09afba075c4d47e1c5f7330747da0c99040e73abf08a23fb6030fb33666c3286", - "src/map/generator/generators/CraterLakesGenerator.h": "87114c131ffad00bcdbb51231b8b859c49fbfea0670a0a65046d61d120fbd107", - "src/map/generator/generators/FjordContinentGenerator.cpp": "a27ffc9c275de0f7a410a924ebbff5bf3a89be1e54c4e1eb0452bd17d80adbb1", - "src/map/generator/generators/FjordContinentGenerator.h": "dc148bf0b0fd2254cba15209e911c64b081ad4f7518e9fe26f36263f81fad04d", - "src/map/generator/generators/IslandsGenerator.cpp": "50ed4f5bbf296bb20509260ab686ad5f2d0b62d041b1b63395c56098fc2b04e0", - "src/map/generator/generators/IslandsGenerator.h": "cbdbe99acbc597e5395071453009983b34566eb6c650d750281a677fc9ab1978", - "src/map/generator/generators/IslesGenerator.cpp": "74942466f6b3484307d8124df092777847b09d76727d07fe63287e814afe119f", - "src/map/generator/generators/IslesGenerator.h": "866eac88f549aee03e98683d8ee95f04e0ce3df942439d51496aceb9b3fec41c", - "src/map/generator/generators/LatticeGenerator.cpp": "fd1ca0199e2c7f787bad4d021c53e107b7e57475c54dc589923746aa4d74b0c0", - "src/map/generator/generators/LatticeGenerator.h": "f03dbcd8cae6b0e35511fdb14d406c642be9fa368a30be0ab04650ec8ead4264", - "src/map/generator/generators/MazeGenerator.cpp": "c17495886a1fb9f5b7f3f2569240651b5a03fe8720a8a19fd639a82dc1baacdc", - "src/map/generator/generators/MazeGenerator.h": "a5ca3e25bcafca300a4db299adce23c6f461bebd23b5b5d7b73df87435f26131", - "src/map/generator/generators/RiverGenerator.cpp": "c854c1d9790054887193233d2eb09ddd714539f08d7179b6df9379827e1e6f18", - "src/map/generator/generators/RiverGenerator.h": "b25e24ad205686297cef1a2b574b6cf79c6148644cfe616c62c47ee027b6f799", - "src/map/generator/generators/RuggedArchipelagoGenerator.cpp": "74ed1dbb1dc4a89e1798d4d83291a4579340de582c84bf592d97f011159758f1", - "src/map/generator/generators/RuggedArchipelagoGenerator.h": "f52f6adae0acf722dcd23095cddb6d71a063657ceeff1db0a581b7dd2cf0b938", - "src/map/generator/generators/ShatteredCoastGenerator.cpp": "70749910fe77046135b288e6f967e3aabac8113b0e919db1ef7e0b3acfd65298", - "src/map/generator/generators/ShatteredCoastGenerator.h": "3eb86756b16a00540333e06603f7274334cf88d41d8ab308f27e9a7e129a7674", - "src/map/generator/generators/SwampGenerator.cpp": "e96de2707223e768686bf3a12027a2e0ca2be62924c4edb4bcc753e0cff6e675", - "src/map/generator/generators/SwampGenerator.h": "08b0dbc4530a30c67544d33520e842314950cca1f5df883344164509aa66d1c1", - "src/map/generator/generators/UniformGenerator.cpp": "22afc16de74969ae8b6cdae8f1532877a89b1c41889a5a2c1c3c5377f8a50646", - "src/map/generator/generators/UniformGenerator.h": "b14505f0fa6d04c54fcac6708efc4ce57fb9818e1ab03675144aaec97cf14859", - "src/map/generator/shared/Distances.cpp": "5c72f66a1a3b89cee1e19f7ceeffd8e9a3915fad2a3ac102cc4c72859889ec26", - "src/map/generator/shared/Distances.h": "65ba65217e7eb1b033afff228bd42c434c2b48b8cde3679e2f12030063cfb97a", - "src/map/generator/shared/Geometry.cpp": "41646048406e3ded77f33d38a80887071dd23767fa295692d5fb135ae2574f17", - "src/map/generator/shared/Geometry.h": "0a82e862a5f1fc14112372489be59b313a61d1e7ff2e24522de895fc7c2f114f", - "src/map/generator/shared/HeightMap.cpp": "bc09a2efe2b3cee3dcbc309ba9f959b61baca2a673ad14c50a5964cdfb91a226", - "src/map/generator/shared/HeightMap.h": "00372062bb44ae9ceabc1f7e53d01fd127fdf14ca4a4b08785d5e9dedc8451eb", - "src/map/generator/shared/Noise.cpp": "350f26d01eb0a53c426e769c723c78a9958ba0438d58a2bb1bb9d16e00bbc96d", - "src/map/generator/shared/Noise.h": "c17ae67a3c1e6a6bcdfb7475f789a7428bc1604fb9945e88f58334b3925505ab", - "src/map/generator/shared/Regions.cpp": "57f831a48491a5e062f83bb873d28f565de84cb63f417531919b16c849c39aad", - "src/map/generator/shared/Regions.h": "4c9bda081f36b864f8fced230355613d09c0a31baadf9b77a8b95c5377d7e187", - "src/map/generator/shared/Resources.cpp": "1ffc40e3a5bd4ef21bf247f4c76076f2126d60f04242384a06186af02cff2295", - "src/map/generator/shared/Resources.h": "81570481031dcf16e2a0ba27623575370915ceaeae5491336b9c52d7b82c0b30", - "src/map/generator/shared/Settlements.cpp": "89f3f184d259a5e610bcb7d19fea4291863f0c7b843f0514e4a56eee216d6728", - "src/map/generator/shared/Settlements.h": "303ffa9af486ecdf9d01dd281eebbdbad6ac54bd70d33d1e2efdfb01df361344", - "src/map/generator/shared/StartingPositions.cpp": "91ab2f58dcebf539d58376b2c993fa587e1cce38bae54ec3b321bedbf25dc035", - "src/map/generator/shared/StartingPositions.h": "7479f38a45ae46ca835d98fc36959be75bb800eaeeedac45aeac7dc4c238ef4e", - "src/map/generator/shared/Terrain.cpp": "20d8140845138298a57143a57db4281b61bcb534b46bfde7ad454637768e4bcf", - "src/map/generator/shared/Terrain.h": "c93fc9b9308f5b752790a21d9b29cf11a2e3282591ec07256a2e12ac5ff98e9c", - "src/map/generator/shared/Topology.cpp": "ab743eed980e9625ba6399d40e19160b44055dd08ae67f155461236886321398", - "src/map/generator/shared/Topology.h": "877e2f6567b86eaf087cbf34bf0247454a8d41b0d6e0687de377b5af16d8eaa2", - "test/MapGeneratorStudy.cpp": "c13c6288082194f3603943895258c315ac87740d48a849208c24114a1f77be74" -} diff --git a/docs/map-generators/refactor/summary.json b/docs/map-generators/refactor/summary.json deleted file mode 100644 index 2dd837175..000000000 --- a/docs/map-generators/refactor/summary.json +++ /dev/null @@ -1,364 +0,0 @@ -[ - { - "method": 1, - "id": "swamp", - "revision": 1, - "name": "Swamp", - "attempts": 1000, - "successes": 999, - "all_teams_proxy": 940, - "grass_tiles": 56.582614108248876, - "grass_tiles_p5": 55.7366943359375, - "grass_tiles_p95": 57.3980712890625, - "free": 44.65428563328954, - "free_p5": 44.329833984375, - "free_p95": 44.9462890625, - "fit4": 34.17453708591404, - "fit4_p5": 33.026123046875, - "fit4_p95": 35.321044921875, - "water_tiles": 29.685373850412912, - "water_tiles_p5": 28.863525390625, - "water_tiles_p95": 30.4571533203125, - "sand_tiles": 1.9446204016516517, - "sand_tiles_p5": 1.4520263671875, - "sand_tiles_p95": 2.4658203125, - "shore": 11.787391639686561, - "shore_p5": 10.484619140625, - "shore_p95": 13.162231445312496, - "review_flags": [], - "free_delta_pp": 0.016456043445792545, - "failure_delta_pp": 0.10000000000000009, - "proxy_delta_pp": -1.2000000000000028 - }, - { - "method": 2, - "id": "river", - "revision": 1, - "name": "River", - "attempts": 1000, - "successes": 980, - "all_teams_proxy": 780, - "grass_tiles": 55.91467409717794, - "grass_tiles_p5": 54.339599609375, - "grass_tiles_p95": 57.31353759765625, - "free": 40.72438765545281, - "free_p5": 38.5302734375, - "free_p95": 43.61236572265625, - "fit4": 30.5191725127551, - "fit4_p5": 27.374267578125, - "fit4_p95": 33.14910888671875, - "water_tiles": 33.51710728236607, - "water_tiles_p5": 32.56195068359375, - "water_tiles_p95": 34.35089111328125, - "sand_tiles": 3.6775986029177297, - "sand_tiles_p5": 2.67913818359375, - "sand_tiles_p95": 5.13336181640625, - "shore": 6.890620017538265, - "shore_p5": 5.55999755859375, - "shore_p95": 8.3013916015625, - "review_flags": [], - "free_delta_pp": -0.015043939426902853, - "failure_delta_pp": 0.20000000000000018, - "proxy_delta_pp": -2.0 - }, - { - "method": 3, - "id": "islands", - "revision": 1, - "name": "Islands", - "attempts": 1000, - "successes": 1000, - "all_teams_proxy": 986, - "grass_tiles": 48.9701416015625, - "grass_tiles_p5": 48.52294921875, - "grass_tiles_p95": 49.38385009765625, - "free": 33.59825439453125, - "free_p5": 33.41064453125, - "free_p95": 33.79547119140625, - "fit4": 23.830609130859376, - "fit4_p5": 22.97943115234375, - "fit4_p95": 24.658203125, - "water_tiles": 34.558172607421874, - "water_tiles_p5": 34.11865234375, - "water_tiles_p95": 34.94293212890625, - "sand_tiles": 3.235235595703125, - "sand_tiles_p5": 2.911376953125, - "sand_tiles_p95": 3.5952758789062496, - "shore": 13.2364501953125, - "shore_p5": 12.51220703125, - "shore_p95": 14.00177001953125, - "review_flags": [], - "free_delta_pp": -0.003216552734372158, - "failure_delta_pp": 0.0, - "proxy_delta_pp": 0.09999999999999432 - }, - { - "method": 4, - "id": "crater-lakes", - "revision": 1, - "name": "Crater lakes", - "attempts": 1000, - "successes": 1000, - "all_teams_proxy": 922, - "grass_tiles": 65.40447998046875, - "grass_tiles_p5": 64.5074462890625, - "grass_tiles_p95": 66.29730224609375, - "free": 57.11531982421875, - "free_p5": 56.817626953125, - "free_p95": 57.391357421875, - "fit4": 45.11155395507812, - "fit4_p5": 43.07159423828125, - "fit4_p95": 47.100830078125, - "water_tiles": 19.604144287109374, - "water_tiles_p5": 18.914794921875, - "water_tiles_p95": 20.39825439453125, - "sand_tiles": 3.39337158203125, - "sand_tiles_p5": 3.14300537109375, - "sand_tiles_p95": 3.6563110351562496, - "shore": 11.598004150390626, - "shore_p5": 10.0872802734375, - "shore_p95": 13.03802490234375, - "review_flags": [], - "free_delta_pp": -0.011444091796875, - "failure_delta_pp": 0.0, - "proxy_delta_pp": -0.09999999999999432 - }, - { - "method": 5, - "id": "concrete-islands", - "revision": 1, - "name": "Concrete islands", - "attempts": 1000, - "successes": 995, - "all_teams_proxy": 982, - "grass_tiles": 65.49762361612751, - "grass_tiles_p5": 62.3760986328125, - "grass_tiles_p95": 68.861083984375, - "free": 37.533885246545225, - "free_p5": 34.896240234375, - "free_p95": 40.875244140625, - "fit4": 24.51387798366834, - "fit4_p5": 21.8310546875, - "fit4_p95": 27.8106689453125, - "water_tiles": 15.638556552292714, - "water_tiles_p5": 13.9794921875, - "water_tiles_p95": 17.3785400390625, - "sand_tiles": 0.4861649556375628, - "sand_tiles_p5": 0.32958984375, - "sand_tiles_p95": 0.6591796875, - "shore": 18.377654875942213, - "shore_p5": 16.7156982421875, - "shore_p95": 19.793701171875, - "review_flags": [], - "free_delta_pp": -0.08270912955734389, - "failure_delta_pp": -0.30000000000000027, - "proxy_delta_pp": 0.10000000000000853 - }, - { - "method": 6, - "id": "isles", - "revision": 1, - "name": "Isles", - "attempts": 1000, - "successes": 1000, - "all_teams_proxy": 1000, - "grass_tiles": 42.17257080078125, - "grass_tiles_p5": 40.765380859375, - "grass_tiles_p95": 43.603515625, - "free": 23.9344482421875, - "free_p5": 22.75360107421875, - "free_p95": 25.115966796875, - "fit4": 13.21666259765625, - "fit4_p5": 12.23724365234375, - "fit4_p95": 14.27032470703125, - "water_tiles": 36.59349365234375, - "water_tiles_p5": 34.515380859375, - "water_tiles_p95": 38.60504150390625, - "sand_tiles": 3.458172607421875, - "sand_tiles_p5": 3.00872802734375, - "sand_tiles_p95": 3.936767578125, - "shore": 17.775762939453124, - "shore_p5": 16.67449951171875, - "shore_p95": 18.85406494140625, - "review_flags": [], - "free_delta_pp": -0.013067626953123579, - "failure_delta_pp": 0.0, - "proxy_delta_pp": 0.0 - }, - { - "method": 7, - "id": "shattered-coast", - "revision": 1, - "name": "Shattered Coast", - "attempts": 1000, - "successes": 974, - "all_teams_proxy": 799, - "grass_tiles": 41.89434952314874, - "grass_tiles_p5": 33.8702392578125, - "grass_tiles_p95": 48.626708984375, - "free": 36.569658784650926, - "free_p5": 28.9202880859375, - "free_p95": 42.94097900390624, - "fit4": 14.542403015512706, - "fit4_p5": 9.37286376953125, - "fit4_p95": 19.759521484374996, - "water_tiles": 18.59351438173768, - "water_tiles_p5": 12.16888427734375, - "water_tiles_p95": 23.250732421874996, - "sand_tiles": 8.555192575317633, - "sand_tiles_p5": 6.9720458984375, - "sand_tiles_p95": 10.441284179687498, - "shore": 30.956943519795946, - "shore_p5": 27.41851806640625, - "shore_p95": 35.2581787109375, - "review_flags": [ - "generation failures" - ], - "free_delta_pp": 0.14510318991842297, - "failure_delta_pp": 1.200000000000001, - "proxy_delta_pp": -2.3999999999999915 - }, - { - "method": 8, - "id": "rugged-archipelago", - "revision": 1, - "name": "Rugged Archipelago", - "attempts": 1000, - "successes": 1000, - "all_teams_proxy": 1000, - "grass_tiles": 39.84324340820312, - "grass_tiles_p5": 35.577392578125, - "grass_tiles_p95": 44.3914794921875, - "free": 29.211114501953126, - "free_p5": 25.3765869140625, - "free_p95": 33.19122314453125, - "fit4": 17.377459716796874, - "fit4_p5": 14.342041015625, - "fit4_p95": 20.52764892578125, - "water_tiles": 38.933770751953126, - "water_tiles_p5": 33.11737060546875, - "water_tiles_p95": 44.2279052734375, - "sand_tiles": 5.015771484375, - "sand_tiles_p5": 4.498291015625, - "sand_tiles_p95": 5.615234375, - "shore": 16.20721435546875, - "shore_p5": 15.03265380859375, - "shore_p95": 17.4560546875, - "review_flags": [], - "free_delta_pp": 0.023834228515625, - "failure_delta_pp": 0.0, - "proxy_delta_pp": 0.0 - }, - { - "method": 9, - "id": "contested-commons", - "revision": 1, - "name": "Contested commons", - "attempts": 1000, - "successes": 999, - "all_teams_proxy": 374, - "grass_tiles": 25.01883597464652, - "grass_tiles_p5": 24.76806640625, - "grass_tiles_p95": 25.2685546875, - "free": 10.73317580275588, - "free_p5": 9.447021484375, - "free_p95": 12.1588134765625, - "fit4": 3.7920794329485736, - "fit4_p5": 2.734375, - "fit4_p95": 4.970703124999998, - "water_tiles": 64.691357617383, - "water_tiles_p5": 64.2388916015625, - "water_tiles_p95": 65.17333984375, - "sand_tiles": 1.2643625070382882, - "sand_tiles_p5": 1.0986328125, - "sand_tiles_p95": 1.434326171875, - "shore": 9.025443900932181, - "shore_p5": 8.6541748046875, - "shore_p95": 9.3994140625, - "review_flags": [] - }, - { - "method": 10, - "id": "lattice", - "revision": 1, - "name": "Lattice", - "attempts": 1000, - "successes": 1000, - "all_teams_proxy": 932, - "grass_tiles": 18.939208984375, - "grass_tiles_p5": 18.939208984375, - "grass_tiles_p95": 18.939208984375, - "free": 12.8915771484375, - "free_p5": 12.628173828125, - "free_p95": 13.18359375, - "fit4": 1.806304931640625, - "fit4_p5": 1.300048828125, - "fit4_p95": 2.349853515625, - "water_tiles": 37.158203125, - "water_tiles_p5": 37.158203125, - "water_tiles_p95": 37.158203125, - "sand_tiles": 7.672119140625, - "sand_tiles_p5": 7.672119140625, - "sand_tiles_p95": 7.672119140625, - "shore": 36.23046875, - "shore_p5": 36.23046875, - "shore_p95": 36.23046875, - "review_flags": [] - }, - { - "method": 11, - "id": "maze", - "revision": 1, - "name": "Maze", - "attempts": 1000, - "successes": 1000, - "all_teams_proxy": 999, - "grass_tiles": 41.42435302734375, - "grass_tiles_p5": 41.009521484375, - "grass_tiles_p95": 41.85791015625, - "free": 34.49039916992187, - "free_p5": 33.94775390625, - "free_p95": 35.064697265625, - "fit4": 10.533880615234375, - "fit4_p5": 9.53369140625, - "fit4_p95": 11.492919921875, - "water_tiles": 25.06688232421875, - "water_tiles_p5": 24.786376953125, - "water_tiles_p95": 25.32379150390625, - "sand_tiles": 7.98978271484375, - "sand_tiles_p5": 7.135009765625, - "sand_tiles_p95": 8.81988525390625, - "shore": 25.51898193359375, - "shore_p5": 24.9505615234375, - "shore_p95": 26.0986328125, - "review_flags": [] - }, - { - "method": 12, - "id": "fjord-continent", - "revision": 1, - "name": "Fjord continent", - "attempts": 1000, - "successes": 1000, - "all_teams_proxy": 995, - "grass_tiles": 24.8265869140625, - "grass_tiles_p5": 24.1815185546875, - "grass_tiles_p95": 25.47607421875, - "free": 23.02662353515625, - "free_p5": 22.38739013671875, - "free_p95": 23.69415283203125, - "fit4": 13.698980712890625, - "fit4_p5": 12.774658203125, - "fit4_p95": 14.6307373046875, - "water_tiles": 64.62662963867187, - "water_tiles_p5": 64.105224609375, - "water_tiles_p95": 65.17333984375, - "sand_tiles": 0.057891845703125, - "sand_tiles_p5": 0.030517578125, - "sand_tiles_p95": 0.09765625, - "shore": 10.4888916015625, - "shore_p5": 9.8876953125, - "shore_p95": 11.102294921875, - "review_flags": [] - } -] diff --git a/docs/map-generators/refactor/validation-configs.json b/docs/map-generators/refactor/validation-configs.json deleted file mode 100644 index e828475be..000000000 --- a/docs/map-generators/refactor/validation-configs.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "id": "modular-1", - "method": 1, - "preset": true - }, - { - "id": "modular-2", - "method": 2, - "preset": true - }, - { - "id": "modular-3", - "method": 3, - "preset": true - }, - { - "id": "modular-4", - "method": 4, - "preset": true - }, - { - "id": "modular-5", - "method": 5, - "preset": true - }, - { - "id": "modular-6", - "method": 6, - "preset": true - }, - { - "id": "modular-7", - "method": 7, - "preset": true - }, - { - "id": "modular-8", - "method": 8, - "preset": true - } -] \ No newline at end of file diff --git a/docs/map-generators/refactor/validation-reproducibility.csv b/docs/map-generators/refactor/validation-reproducibility.csv deleted file mode 100644 index af73c3037..000000000 --- a/docs/map-generators/refactor/validation-reproducibility.csv +++ /dev/null @@ -1,49 +0,0 @@ -config,method,seed,success,tiles,grass_tiles,sand_tiles,water_tiles,shore,free,fit4,um_grass,um_sand,um_water,seconds,hash,min_local_fit4,worst_wheat_distance,worst_wood_distance,viable_teams,wheat_tiles,wood_tiles,stone_tiles,algae_tiles,status -modular-1,1,20001,1,16384,9252,324,4868,1940,7319,5558,9755,1293,5336,0.002564,10661502771248767044,944,13,13,4,859,682,287,993,ok -modular-1,1,20001,1,16384,9252,324,4868,1940,7319,5558,9755,1293,5336,0.001404,10661502771248767044,944,13,13,4,859,682,287,993,ok -modular-1,1,20501,1,16384,9323,303,4876,1882,7315,5686,9807,1243,5334,0.001610,17241070552458762342,752,9,9,4,828,713,358,991,ok -modular-1,1,20501,1,16384,9323,303,4876,1882,7315,5686,9807,1243,5334,0.001764,17241070552458762342,752,9,9,4,828,713,358,991,ok -modular-1,1,21000,1,16384,9341,309,4966,1768,7285,5467,9805,1192,5387,0.001610,10655558613566185882,414,11,11,4,681,875,390,994,ok -modular-1,1,21000,1,16384,9341,309,4966,1768,7285,5467,9805,1192,5387,0.002883,10655558613566185882,414,11,11,4,681,875,390,994,ok -modular-2,2,20001,0,16384,0,0,16384,0,0,0,9558,1052,5774,0.001761,2606947197152000752,16384,100000,100000,0,0,0,0,0,failed -modular-2,2,20001,0,16384,0,0,16384,0,0,0,9558,1052,5774,0.004111,2606947197152000752,16384,100000,100000,0,0,0,0,0,failed -modular-2,2,20501,1,16384,8944,716,5586,1138,6567,4750,9313,1280,5791,0.003159,10780039130517181389,629,18,13,4,688,855,727,965,ok -modular-2,2,20501,1,16384,8944,716,5586,1138,6567,4750,9313,1280,5791,0.001913,10780039130517181389,629,18,13,4,688,855,727,965,ok -modular-2,2,21000,1,16384,9139,470,5461,1314,6668,5122,9544,1120,5720,0.001810,10550789423753792819,647,16,20,4,1045,693,633,965,ok -modular-2,2,21000,1,16384,9139,470,5461,1314,6668,5122,9544,1120,5720,0.001801,10550789423753792819,647,16,20,4,1045,693,633,965,ok -modular-3,3,20001,1,16384,8014,509,5659,2202,5511,3886,8590,1608,6186,0.004006,11247383660930392137,307,13,16,4,881,1073,447,1088,ok -modular-3,3,20001,1,16384,8014,509,5659,2202,5511,3886,8590,1608,6186,0.001776,11247383660930392137,307,13,16,4,881,1073,447,1088,ok -modular-3,3,20501,1,16384,8039,535,5662,2148,5501,3877,8609,1606,6169,0.003410,11690117216618492072,761,15,13,4,959,1063,424,1087,ok -modular-3,3,20501,1,16384,8039,535,5662,2148,5501,3877,8609,1606,6169,0.002597,11690117216618492072,761,15,13,4,959,1063,424,1087,ok -modular-3,3,21000,1,16384,7986,578,5603,2217,5500,3884,8568,1684,6132,0.001485,12032433602295556753,550,11,11,4,1063,940,381,1086,ok -modular-3,3,21000,1,16384,7986,578,5603,2217,5500,3884,8568,1684,6132,0.001527,12032433602295556753,550,11,11,4,1063,940,381,1086,ok -modular-4,4,20001,1,16384,10724,535,3227,1898,9344,7349,11289,1483,3612,0.004815,16316743800069380881,1020,19,22,4,601,456,222,635,ok -modular-4,4,20001,1,16384,10724,535,3227,1898,9344,7349,11289,1483,3612,0.001432,16316743800069380881,1020,19,22,4,601,456,222,635,ok -modular-4,4,20501,1,16384,10776,530,3298,1780,9343,7431,11307,1416,3661,0.001449,6545278665695955885,947,24,15,4,548,552,232,635,ok -modular-4,4,20501,1,16384,10776,530,3298,1780,9343,7431,11307,1416,3661,0.002121,6545278665695955885,947,24,15,4,548,552,232,635,ok -modular-4,4,21000,1,16384,10707,553,3195,1929,9345,7284,11275,1511,3598,0.001425,15932849638750774831,686,9,18,4,480,537,232,634,ok -modular-4,4,21000,1,16384,10707,553,3195,1929,9345,7284,11275,1511,3598,0.001443,15932849638750774831,686,9,18,4,480,537,232,634,ok -modular-5,5,20001,1,16384,10609,72,2639,3064,5956,3812,11341,1602,3441,0.017479,14539859018350938763,577,2,9,4,2650,1883,24,2,ok -modular-5,5,20001,1,16384,10609,72,2639,3064,5956,3812,11341,1602,3441,0.022410,14539859018350938763,577,2,9,4,2650,1883,24,2,ok -modular-5,5,20501,1,16384,10917,67,2451,2949,6283,4045,11618,1540,3226,0.011279,3177999051462689531,610,2,21,4,2759,1760,24,1,ok -modular-5,5,20501,1,16384,10917,67,2451,2949,6283,4045,11618,1540,3226,0.015615,3177999051462689531,610,2,21,4,2759,1760,24,1,ok -modular-5,5,21000,1,16384,11269,55,2322,2738,6398,4326,11927,1423,3034,0.011464,3450008235584598855,629,1,13,4,2702,2054,24,0,ok -modular-5,5,21000,1,16384,11269,55,2322,2738,6398,4326,11927,1423,3034,0.012253,3450008235584598855,629,1,13,4,2702,2054,24,0,ok -modular-6,6,20001,1,16384,6927,507,6188,2762,3941,2138,7660,1902,6822,0.016924,2625225261725384295,375,3,18,4,1431,1451,24,100,ok -modular-6,6,20001,1,16384,6927,507,6188,2762,3941,2138,7660,1902,6822,0.012168,2625225261725384295,375,3,18,4,1431,1451,24,100,ok -modular-6,6,20501,1,16384,7078,608,6018,2680,4124,2283,7799,1947,6638,0.016360,15259089094901122187,498,1,19,4,1482,1368,24,100,ok -modular-6,6,20501,1,16384,7078,608,6018,2680,4124,2283,7799,1947,6638,0.009874,15259089094901122187,498,1,19,4,1482,1368,24,100,ok -modular-6,6,21000,1,16384,6872,549,6154,2809,3898,2145,7598,1978,6808,0.008573,5139624633597254587,504,3,10,4,1467,1403,24,92,ok -modular-6,6,21000,1,16384,6872,549,6154,2809,3898,2145,7598,1978,6808,0.016989,5139624633597254587,504,3,10,4,1467,1403,24,92,ok -modular-7,7,20001,1,16384,6596,1289,3673,4826,5739,2424,7856,3729,4799,0.029709,10305528355439009293,227,7,16,4,372,193,212,151,ok -modular-7,7,20001,1,16384,6596,1289,3673,4826,5739,2424,7856,3729,4799,0.018533,10305528355439009293,227,7,16,4,372,193,212,151,ok -modular-7,7,20501,1,16384,6804,1318,3410,4852,5954,2390,8196,3762,4426,0.027920,12482868172572717974,85,17,12,4,377,224,169,147,ok -modular-7,7,20501,1,16384,6804,1318,3410,4852,5954,2390,8196,3762,4426,0.019610,12482868172572717974,85,17,12,4,377,224,169,147,ok -modular-7,7,21000,1,16384,6628,1660,2479,5617,5866,2103,8239,4514,3631,0.019852,16030144911233971808,157,9,31,4,326,143,213,121,ok -modular-7,7,21000,1,16384,6628,1660,2479,5617,5866,2103,8239,4514,3631,0.018885,16030144911233971808,157,9,31,4,326,143,213,121,ok -modular-8,8,20001,1,16384,6087,744,7020,2533,4394,2588,6580,2015,7789,0.004368,9678229960813273146,409,5,6,4,811,798,4,83,ok -modular-8,8,20001,1,16384,6087,744,7020,2533,4394,2588,6580,2015,7789,0.005623,9678229960813273146,409,5,6,4,811,798,4,83,ok -modular-8,8,20501,1,16384,6430,916,6365,2673,4798,2938,6954,2267,7163,0.004244,12822830550967485530,503,5,6,4,682,866,4,100,ok -modular-8,8,20501,1,16384,6430,916,6365,2673,4798,2938,6954,2267,7163,0.004134,12822830550967485530,503,5,6,4,682,866,4,100,ok -modular-8,8,21000,1,16384,6584,834,6216,2750,4862,2843,7121,2209,7054,0.006794,5404957458423946019,536,5,6,4,747,891,4,46,ok -modular-8,8,21000,1,16384,6584,834,6216,2750,4862,2843,7121,2209,7054,0.004315,5404957458423946019,536,5,6,4,747,891,4,46,ok diff --git a/docs/map-generators/refactor/validation.csv.gz b/docs/map-generators/refactor/validation.csv.gz deleted file mode 100644 index ea6614b8ff7ed7532618664feb5293de945bc714..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 590054 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 z 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≫-Ko{9H0i793QQ=* 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

_BK#G8mCaufIAXWBroC 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⪻Q(F&`eQW_El2`Y{ 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 diff --git a/tools/plot_map_generator_refactor.py b/tools/plot_map_generator_refactor.py index c235ec809..5764f01dc 100644 --- a/tools/plot_map_generator_refactor.py +++ b/tools/plot_map_generator_refactor.py @@ -28,8 +28,8 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('csv', type=Path) - parser.add_argument('--baseline', type=Path, default=ROOT/'docs/map-generators/study/summary.json') - parser.add_argument('--output', type=Path, default=ROOT/'docs/map-generators/refactor') + parser.add_argument('--baseline', type=Path, required=True) + parser.add_argument('--output', type=Path, default=ROOT/'artifacts/map-generators/refactor') args = parser.parse_args() out = args.output.resolve() out.mkdir(parents=True, exist_ok=True) From 427a47e6ba7ca4f3394d0745d9262d4ebdaac90a Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Thu, 10 Sep 2026 18:14:43 -0400 Subject: [PATCH 007/136] Bridge CustomGamePreferences through the legacy generator descriptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #238 (already merged) added CustomGamePreferences.h, which serializes CustomGameSetup::generator by taking Sint32 MapGenerationDescriptor::* member pointers directly. #240 replaced generator's type with GenerationRequest, whose method-specific options live in a generic std::map instead of fixed struct fields — an architecture change, not a rename — so the file no longer compiled. - CustomGamePreferences::encode()/decode() now convert through toLegacyDescriptor()/fromLegacyDescriptor() (the compatibility adapter #240 already built for exactly this kind of interop), so the on-disk wire format and its corruption-recovery bounds are unchanged. decode()'s method-validity check now asks the live GeneratorRegistry instead of a hardcoded 1-8 range, so it stays correct as generators are added or retired. - Widened several fields() bounds (terrain weights 0-64 -> 0-100, riverDiameter's max 64 -> 65, oldIslandSize 1-64 -> 1-70) to match the modular registry's current ranges. These are approximate, same as before: the reused legacy fields (e.g. riverDiameter also stands in for lake size/channel width/bridge width) don't have one true bound, so this is a safe envelope, not a tight per-method one. Without this, decode() could reject a preferences file that a normal save legitimately produced (oldIslandSize's own default already exceeded the old 1-64 bound for any method other than Isles/Old Islands). - Found and fixed a related crash bug in the compatibility adapter itself while tracing this: Lattice and Maze register wheat/wood/ stone/algae controls with no entry in legacyField()'s mapping table, so converting either method through toLegacyDescriptor/ fromLegacyDescriptor threw an uncaught std::invalid_argument. Added the four missing mappings (they match pre-existing legacy struct fields exactly) and changed every other option with no legacy slot (loopiness, home-radius, cell-size, ...) from throwing to falling back to its control's default value, so a newer generator's full option set can never crash this adapter again. - test/CustomGameSetupHarness.cpp: preferencesModel()/preferencesScreen() built a GenerationRequest via the same member-pointer approach; updated both to build a temporary MapGenerationDescriptor and convert. Switched preferencesModel()'s method from Old Islands to Crater Lakes (one of the four modern height-map generators that still exposes a repeat-landscape control; Old Islands never did in the new registry, so it always round-tripped back to 0). The per-field assertions in preferencesScreen() now compare against the same achievable conversion rather than raw field maximums, since only the options a method actually registers survive a GenerationRequest round trip. Verified: full scons -j8 release=1 server=0 client build is clean. CustomGameSetupHarness passes in default, preferences-write and preferences-read modes. MapGeneratorDefaultsTest and MapGeneratorStudy --catalog also pass, unaffected by the adapter fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KFxsZmLM4qsovemHqDrHGP --- src/CustomGamePreferences.h | 50 ++++++++++++++----- .../LegacyGenerationDescriptor.cpp | 28 +++++++++-- test/CustomGameSetupHarness.cpp | 39 +++++++++++---- 3 files changed, 88 insertions(+), 29 deletions(-) diff --git a/src/CustomGamePreferences.h b/src/CustomGamePreferences.h index b199d13e8..871534463 100644 --- a/src/CustomGamePreferences.h +++ b/src/CustomGamePreferences.h @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "CustomGameSetup.h" +#include "LegacyGenerationDescriptor.h" #include "Settings.h" #include #include +#include #include #include #include @@ -30,16 +32,26 @@ struct CustomGamePreferences }; static const std::vector &fields() { + // Bounds are a safe envelope across every generator method's current + // registry range for the reused legacy field (e.g. riverDiameter also + // stands in for lake size, channel width and bridge width; grassRatio + // also stands in for Isles' island size), widened from the original + // fixed 0-64 terrain-weight bound now that the registry allows 0-100 + // and Old Islands/Rugged Archipelago's island size reaches 70. This + // only needs to be at least as wide as what setMethodDefaults() and + // every registered control can produce; it is not a tight per-method + // bound, matching how this field list worked before the modular + // registry existed. static const std::vector values = { #define FIELD(name, lo, hi) {#name, &MapGenerationDescriptor::name, lo, hi} FIELD(wDec, 6, 9), FIELD(hDec, 6, 9), - FIELD(waterRatio, 0, 64), FIELD(sandRatio, 0, 64), - FIELD(grassRatio, 0, 64), FIELD(desertRatio, 0, 64), + FIELD(waterRatio, 0, 100), FIELD(sandRatio, 0, 100), + FIELD(grassRatio, 0, 100), FIELD(desertRatio, 0, 100), FIELD(wheatRatio, 0, 64), FIELD(woodRatio, 0, 64), FIELD(fruitRatio, 0, 64), FIELD(algaeRatio, 0, 64), - FIELD(stoneRatio, 0, 64), FIELD(riverDiameter, 1, 64), + FIELD(stoneRatio, 0, 64), FIELD(riverDiameter, 1, 65), FIELD(craterDensity, 1, 64), FIELD(extraIslands, 0, 8), - FIELD(oldIslandSize, 1, 64), FIELD(oldBeach, 0, 4), + FIELD(oldIslandSize, 1, 70), FIELD(oldBeach, 0, 4), FIELD(smooth, 1, 8), FIELD(nbTeams, 2, Team::MAX_COUNT), FIELD(nbWorkers, 1, 8) #undef FIELD @@ -48,6 +60,11 @@ struct CustomGamePreferences } std::string encode() const { + // The wire format still describes the legacy fixed-field descriptor; + // the modular GenerationRequest converts through the compatibility + // adapter so the persisted format and its corruption-recovery bounds + // stay unchanged regardless of which generator module is selected. + const auto legacy = toLegacyDescriptor(setup.generator); std::ostringstream out; out << "glob2-custom-game 1\n" << "setup " << setup.random << ' ' << setup.capacity << ' ' @@ -58,12 +75,12 @@ struct CustomGamePreferences << "libraries " << std::quoted(librarySelection[0]) << ' ' << std::quoted(librarySelection[1]) << '\n' << "sections " << expanded[0] << ' ' << expanded[1] << ' ' << expanded[2] << '\n' - << "generator " << int(setup.generator.method) << ' ' - << setup.generator.logRepeatAreaTimes << '\n'; + << "generator " << int(legacy.method) << ' ' + << legacy.logRepeatAreaTimes << '\n'; for (const auto &f : fields()) - out << f.name << ' ' << setup.generator.*(f.member) << '\n'; + out << f.name << ' ' << legacy.*(f.member) << '\n'; out << "resources"; - for (auto value : setup.generator.resource) out << ' ' << value; + for (auto value : legacy.resource) out << ' ' << value; out << "\ncolonies\n"; for (const auto &c : setup.colonies) out << int(c.controller) << ' ' << int(c.ai) << ' ' << c.alliance << '\n'; @@ -99,14 +116,21 @@ struct CustomGamePreferences if (!number(value, 0, 1)) return false; expanded = value; } - if (!word("generator") || !number(method, 1, 8) || !number(repeat, 0, 5)) return false; + if (!word("generator") || !number(method, 0, 1000) || !number(repeat, 0, 5)) return false; + // Method validity is checked against the live registry rather than a + // hardcoded range, so it stays correct as generators are added or + // retired. Uniform is editor-only and never a valid lobby selection. + const auto playable = GeneratorRegistry::builtins().methods(false); + if (std::find(playable.begin(), playable.end(), method) == playable.end()) return false; + MapGenerationDescriptor legacy; + legacy.method = MapGenerationDescriptor::Method(method); for (const auto &f : fields()) { int value; if (!word(f.name) || !number(value, f.minimum, f.maximum)) return false; - s.generator.*(f.member) = value; + legacy.*(f.member) = value; } if (!word("resources")) return false; - for (auto &value : s.generator.resource) { + for (auto &value : legacy.resource) { int parsed; if (!number(parsed, 0, 64)) return false; value = parsed; @@ -126,8 +150,8 @@ struct CustomGamePreferences if (!in.eof()) return false; s.random = random; s.prestige = prestige; s.revealed = revealed; s.locked = locked; draft.userMaps = user; - s.generator.method = MapGenerationDescriptor::Method(method); - s.generator.logRepeatAreaTimes = repeat; + legacy.logRepeatAreaTimes = repeat; + s.generator = fromLegacyDescriptor(legacy, 0); if (s.random) s.capacity = s.generator.nbTeams; *this = draft; return true; diff --git a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp index 4e764bf7b..f54ff3925 100644 --- a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp +++ b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp @@ -21,6 +21,14 @@ Sint32 D::*legacyField(int method, const std::string &id) return &D::grassRatio; if (id == "desert") return &D::desertRatio; + if (id == "wheat") + return &D::wheatRatio; + if (id == "wood") + return &D::woodRatio; + if (id == "stone") + return &D::stoneRatio; + if (id == "algae") + return &D::algaeRatio; if (id == "smoothing") return &D::smooth; if (id == "fruit") @@ -49,23 +57,33 @@ Sint32 D::*legacyField(int method, const std::string &id) return &D::oldBeach; if (id == "island-size") return method == D::eISLES ? &D::grassRatio : &D::oldIslandSize; - if (id == "repeat") - return nullptr; - throw std::invalid_argument("Option has no legacy encoding: " + id); + // "repeat" and every option introduced by generators added after the + // fixed-size legacy descriptor was frozen (loopiness, home-radius, + // cell-size, room-size, corridor-width, islet-size, bridge-count, + // commons-size, home-island-size, moat-width, continent-size, + // coast-roughness, resource-islands, fjord-width, ...) have no slot in + // it. Callers fall back to logRepeatAreaTimes for "repeat" specifically + // and otherwise treat a null field as "not representable", rather than + // throwing and taking down any caller that converts a newer generator's + // full option set through this adapter. + return nullptr; } } // namespace int GeneratorControl::get(const D &d) const { auto field = legacyField(d.method, id); - return field ? d.*field : d.logRepeatAreaTimes; + if (field) + return d.*field; + return id == "repeat" ? d.logRepeatAreaTimes : defaultValue; } void GeneratorControl::set(D &d, int v) const { auto field = legacyField(d.method, id); if (field) d.*field = normalize(v); - else + else if (id == "repeat") d.logRepeatAreaTimes = normalize(v); + // Else: no legacy field for this option: nothing to store. } const std::vector &D::controls(Method m) { return GenerationRequest::controls(m); } const std::vector &D::sharedControls() { return sharedGeneratorControls(); } diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index 7144f94fa..b550e0f9b 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -37,6 +37,20 @@ struct CountingAI : AIImplementation { }; struct CustomGameSetupHarness { + // Every field() is set to its declared maximum, but only the options + // actually registered for the chosen method survive a GenerationRequest + // round trip (the rest reset to that method's own defaults) — this + // mirrors CustomGamePreferences::encode/decode's own conversion so a + // caller can compute the achievable ground truth deterministically. + static MapGenerationDescriptor maxedLegacy(MapGenerationDescriptor::Method method, int repeat) + { + MapGenerationDescriptor legacy; + for (const auto &field : CustomGamePreferences::fields()) + legacy.*(field.member) = field.maximum; + legacy.method = method; + legacy.logRepeatAreaTimes = repeat; + return legacy; + } static void preferencesModel() { CustomGamePreferences original; @@ -47,10 +61,11 @@ struct CustomGameSetupHarness original.librarySelection[0] = "maps/FourSquares1.map"; original.librarySelection[1] = original.setup.premadeMap; original.expanded[0] = original.expanded[2] = true; - for (const auto &field : CustomGamePreferences::fields()) - original.setup.generator.*(field.member) = field.maximum; - original.setup.generator.method = MapGenerationDescriptor::eOLDISLANDS; - original.setup.generator.logRepeatAreaTimes = 5; + // Crater Lakes is one of the four modern height-map generators that + // still exposes a "repeat landscape" control; Old Islands (the prior + // choice) has no repeat option in the modular registry, so it would + // always round-trip back to 0 regardless of what is written here. + original.setup.generator = fromLegacyDescriptor(maxedLegacy(MapGenerationDescriptor::eCRATERLAKES, 5), 0); original.setup.presetRules(1); original.setup.prestige = false; original.setup.revealed = true; @@ -73,7 +88,7 @@ struct CustomGameSetupHarness for (const auto &replacement : std::vector>{ {"glob2-custom-game 1", "glob2-custom-game 2"}, {"wDec 9", "wDec 31"}, {"nbWorkers 8", "nbWorkers -1"}, - {"generator 8 5", "generator 0 5"}, {"generator 8 5", "generator 8 100"}, + {"generator 4 5", "generator 0 5"}, {"generator 4 5", "generator 4 100"}, {"colonies\n1 1 0", "colonies\n99 1 0"}}) { auto corrupt = encoded; @@ -104,10 +119,7 @@ struct CustomGameSetupHarness screen.setup.colonies[11].ai = AI::NICOWAR; screen.setup.colonies[11].alliance = 7; screen.setup.presetRules(1); - for (const auto &field : CustomGamePreferences::fields()) - screen.setup.generator.*(field.member) = field.maximum; - screen.setup.generator.method = MapGenerationDescriptor::eISLANDS; - screen.setup.generator.logRepeatAreaTimes = 3; + screen.setup.generator = fromLegacyDescriptor(maxedLegacy(MapGenerationDescriptor::eISLANDS, 3), 0); screen.expanded[1] = true; screen.userMaps = screen.separateMapLibraries; screen.librarySelection[1] = "maps/favorite-user-map.map"; @@ -130,9 +142,14 @@ struct CustomGameSetupHarness assert(screen.expanded[1]); assert(screen.userMaps == screen.separateMapLibraries); assert(screen.librarySelection[1] == "maps/favorite-user-map.map"); + // Only the options Islands actually registers survive the + // GenerationRequest round trip; compare against the same + // achievable conversion rather than the raw field maximums. + const auto expected = toLegacyDescriptor(fromLegacyDescriptor(maxedLegacy(MapGenerationDescriptor::eISLANDS, 3), 0)); + const auto restoredLegacy = toLegacyDescriptor(screen.setup.generator); for (const auto &field : CustomGamePreferences::fields()) - assert(screen.setup.generator.*(field.member) == field.maximum); - assert(screen.setup.generator.logRepeatAreaTimes == 3); + assert(restoredLegacy.*(field.member) == expected.*(field.member)); + assert(restoredLegacy.logRepeatAreaTimes == expected.logRepeatAreaTimes); premade = screen.setup.premadeMap; screen.setup.generator.nbTeams = 4; screen.setMapMode(true); From db88fb0c942a10d5293078b39716f4ec6ee6b9a8 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Thu, 10 Sep 2026 19:35:07 -0400 Subject: [PATCH 008/136] Fix MenuColonyHarness for the modularized generator API Rebased onto master now that #238 has merged. That surfaced a real compile break: the harness still called Map::oldMakeIslandsMap/Game::oldMakeIslandsMap, which the registry-driven generator rewrite removed. Route it through MapGenerator::generateMap(Game&, const MapGenerationDescriptor&, seed) instead, which owns map sizing and the game association internally. Seed explicitly since generation determinism no longer follows the global sync-rand state. Also start from setMethodDefaults() rather than hand-picked constants: #238's defaults tuning tightened several control ranges (island-size moved to 50-70, the shared "workers" control caps at 8), so the harness's old literals (oldIslandSize=35, nbWorkers=48) now fail request validation. Defaulting first and overriding only what this decorative colony actually needs to differ keeps it from rotting the same way again as ranges keep moving. Verified: full client and menu-colony-harness build clean; `check`, `navigation`, and `generate` subcommands all pass (generate grows the colony from 8 to 56 units over the same 12,000-tick warmup, confirming the reduced starting worker count doesn't defeat the decorative intent). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Le23P4h9QuDjwExuK6HU96 --- tools/MenuColonyHarness.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/MenuColonyHarness.cpp b/tools/MenuColonyHarness.cpp index b175fe54d..d3a79ccba 100644 --- a/tools/MenuColonyHarness.cpp +++ b/tools/MenuColonyHarness.cpp @@ -15,6 +15,7 @@ #include "EditorMainMenu.h" #include "CreditScreen.h" #include "MapGenerationDescriptor.h" +#include "MapGenerator.h" #include "FertilityCalculator.h" #include "Order.h" #include "Player.h" @@ -119,14 +120,17 @@ void generate(const char* path) std::srand(481516); Game game(nullptr); MapGenerationDescriptor d; - d.method=MapGenerationDescriptor::eOLDISLANDS; - d.oldIslandSize=35; + // Start from the generator's own current defaults (valid control ranges + // move as generators are tuned) and override only what this decorative + // colony actually needs to differ. + d.setMethodDefaults(MapGenerationDescriptor::eOLDISLANDS); d.wDec=d.hDec=7; - d.nbTeams=1; d.nbWorkers=48; + d.nbTeams=1; d.nbWorkers=8; d.waterRatio=25; d.grassRatio=65; d.sandRatio=10; - game.map.setSize(d.wDec,d.hDec); - game.map.setGame(&game); - require(game.map.oldMakeIslandsMap(d) && game.oldMakeIslandsMap(d),"generate terrain"); + // The registry-driven generator owns map sizing and the game association; + // pass the fixed seed explicitly since it no longer follows the global + // sync-rand state seeded above. + require(MapGenerator().generateMap(game, d, 481516u),"generate terrain"); // The legacy island generator supplies terrain only. Seed small groves // and grain fields with the existing resource API, leaving walking lanes. const int bx=d.bootX[0], by=d.bootY[0]; From 99ca264fc23bdd9bda73d680e744c6979591af8d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 21:16:15 +0000 Subject: [PATCH 009/136] Guarantee reachable starting wheat and wood, fix old-random's wood-poor bias Building on #240's modular generator rewrite: swamp, river, islands, crater lakes, old-random and old-islands all pick each team's resources through a step that has no idea where any *other* team ended up, or in old-random/ old-islands' case scores compass directions independently per team without comparing outcomes across teams. A colony's proximity to wheat and wood was left almost entirely to how the (still resource-blind) starting-position search happened to place it, so one team could land next to both while another got only one, or occasionally neither within playable range. Add shared/Resources::guaranteeStartingResources, which floods outward from each team's boot tile through the same walkable-space search used to judge distance (isHardSpaceForGroundUnit), so anything it places is reachable by construction rather than merely straight-line close. It tops up only teams that are missing wheat or wood within the range map_generator_study.py already scores as viable; already-served teams are untouched. Wired into the four generators that share Terrain::generateHeightField, and into old-random/old-islands after their own start placement. Also fix a starvation bug in old-random's resource search: its 8-direction scan hardcoded a 4th slot to CORN, so every colony got a guaranteed second wheat deposit while wood only ever got one. The 4th slot is now awarded to whichever of the two came out narrower for that colony. Extend test/MapGeneratorStudy.cpp's tuning output with best_wheat_distance/ best_wood_distance (previously only the worst-served team's distance was tracked) so tools/map_generator_study.py can size the gap between a map's best- and worst-served colony, not just its worst case in isolation. Validated with 200 fixed seeds per generator via map_generator_study.py (artifacts not committed, matching this directory's own convention): generation success rate is unchanged for every generator (including old-random and old-islands' existing ~4-5% baseline failure rates); the wheat-access gap between a map's best- and worst-served colony drops for swamp, river, islands and crater lakes, and old-random's wood gap and worst case both drop by roughly two-fifths. Old-random's wheat gap grew in the same run and a handful of colonies on constrained terrain (river was the clearest case) can still end up with no reachable wheat or wood at all; both are documented as open follow-ups in docs/map-generators/FRAMEWORK_UPGRADES.md; a farthest-point starting- position search was tried against the latter and measured worse on river and islands, so it was not kept. This is a balance-affecting change to established generators and needs a maintainer's sign-off per this repo's review rules, not just this PR's own validation numbers. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Rc6FTwBEB6LKNUmamX9wWW --- docs/map-generators/FRAMEWORK_UPGRADES.md | 1 + .../generators/RuggedArchipelagoGenerator.cpp | 5 ++ .../generators/ShatteredCoastGenerator.cpp | 22 ++++++++ src/map/generator/shared/Resources.cpp | 53 +++++++++++++++++++ src/map/generator/shared/Resources.h | 8 +++ src/map/generator/shared/Terrain.cpp | 12 +++++ test/MapGeneratorStudy.cpp | 20 ++++--- tools/map_generator_study.py | 2 +- 8 files changed, 116 insertions(+), 7 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index c11a5c2c5..5d9d9f33e 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -26,5 +26,6 @@ All helpers are ordinary functions or small value objects. There is no new gener - Maze uses 20, 24 or 32-tile cells, larger home rooms and compact starter deposits. Closed graph edges carry a continuous one-tile stone seam on grass with sand and water on both sides, while connected edges remain open passages. - Fjord deposits wheat and wood in compact patches on both banks of every fjord. Bank deposits are placed last and shared clump placement never overwrites another resource type. Single-colony maps skip the vacuous bank requirement. - Lattice uses larger home clearings and puts wheat and wood on opposite rims. Its tiny repeating islets remain the dominant visual pattern without starving the colonies of construction space. +- Swamp, river, islands and crater lakes paint wheat, wood, stone and algae from map-wide noise bands with no awareness of where any team's tile ended up, and old-random/old-islands scan compass directions from each boot tile independently — both leave how close a colony's primary resources are almost entirely to where the (still resource-blind) starting-position search happened to put it, so one colony can end up with wheat and wood at its door while another has to walk far past playable range for either, or occasionally reach neither at all. `shared/Resources::guaranteeStartingResources` now floods outward from each boot tile through the same walkable-space search used to judge that distance (so anything it places is reachable by construction, not just straight-line close) and tops up any team missing wheat or wood within the range the study tool already scores as viable, leaving already-served teams untouched. Old-random's resource search also hardcoded a fourth compass slot to wheat, so every colony there got a guaranteed second wheat deposit and wood never got one; the fourth slot is now awarded to whichever of the two came out narrower for that colony instead of always the same resource. Measured on 200 fixed seeds per generator (`tools/map_generator_study.py`, `best_wheat_distance`/`best_wood_distance` columns added alongside the existing worst-case ones to size the gap between a map's best- and worst-served colony): old-random's wood gap and worst case both dropped by roughly two-fifths with no change in generation success rate; the other four generators held their success rate exactly and gained smaller, still real reductions in the wheat gap. Old-random's wheat gap grew in the same run — its two resource slots still aren't drawn from a symmetric search, so reinforcement lands more easily on whichever resource already claims the first, unconstrained compass pick — and a handful of colonies on constrained terrain (river was the clearest case) still land somewhere with no reachable wheat or wood at all; a farthest-point starting-position search was prototyped for the shared noise-band path to address that directly but measured worse on river and islands (it can spread teams onto separate landmasses of very different quality) and was not kept. Both remain open follow-ups. The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index 025dda25a..ec086b39f 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -384,6 +384,11 @@ static bool generate(Game &game, GenerationContext &context) return false; context.stage = "resources"; resources(game, context, options); + // Each island's own footprint bounds the compass search above (it stops at the first + // non-grass tile), so a small or irregular island can still leave a team short on wheat or + // wood even though the search ran in every direction. Top up anyone still missing either + // within comfortable range; islands that were already generous are left untouched. + guaranteeStartingResources(game, context, 24, 32); return true; } diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 143850154..8b42d44a5 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -643,11 +643,21 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa bool dirUsed[8]; for (int i = 0; i < 8; i++) dirUsed[i] = false; + // Wheat and wood are a colony's two primary resources; stone is secondary. This search + // gives each of the four slots below its own compass direction, so a fixed 4th entry + // doesn't just repeat a resource type, it silently starves whichever type isn't chosen + // of the extra slot every single time. It used to hardcode CORN here, guaranteeing + // wheat two placement attempts while wood only ever got one — every colony on this + // generator was systematically wood-poor relative to wheat. Decide the 4th slot after + // seeing how CORN and WOOD actually did (below) and hand it to whichever came out + // narrower, so the reinforcement goes to whichever primary resource needs it that game + // instead of the same one every time. int resOrder[4]; resOrder[0] = CORN; resOrder[1] = WOOD; resOrder[2] = STONE; resOrder[3] = CORN; + int primaryWidth[2] = {0, 0}; int distWeight[4]; distWeight[0] = 1; @@ -695,6 +705,12 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa smallestWidth = maxWidth; smallestResource = res; } + if (resI == 0 || resI == 1) + { + primaryWidth[resI] = maxWidth; + if (resI == 1) + resOrder[3] = primaryWidth[0] <= primaryWidth[1] ? CORN : WOOD; + } int dx, dy; Unit::dxDyFromDirection(maxDir, &dx, &dy); @@ -799,6 +815,12 @@ static bool generate(Game &game, GenerationContext &context) return false; context.stage = "resources"; resources(game, context, options); + // The directional search above scores each of a team's 8 compass directions by how far a + // grass run extends, so a team boxed into a small or oddly-shaped patch can still end up + // short on wheat or wood — same gap as the other generators, just reached by a different + // placement method. Top up anyone still missing either within comfortable range now that + // placeStarts() has already carved its own clearing, so there's nothing left to step on. + guaranteeStartingResources(game, context, 24, 32); return true; } diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index d2873f723..921d8f3a3 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -13,6 +13,7 @@ #include "Terrain.h" #include #include +#include using namespace MapGeneration; namespace MapGeneration { @@ -94,4 +95,56 @@ void fillInResource(Map &map, GenerationContext &context, 1 + context.stream("regions")() % maxFillSize); } } + +void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, + int woodRange, int clearRadius) { + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const int exploreLimit = std::max(wheatRange, woodRange) * 5 / 2; + const int closeRange = wheatRange / 2; + for (int team = 0; team < context.request.nbTeams; ++team) { + std::vector dist(size_t(w) * h, -1); + std::queue q; + int start = context.bootY[team] * w + context.bootX[team]; + dist[start] = 0; + q.push(start); + int wheatDist = -1, woodDist = -1; + std::vector closeGrass, farGrass; + while (!q.empty()) { + int p = q.front(); + q.pop(); + int x = p % w, y = p / w; + if (map.getUMTerrain(x, y) == GRASS && dist[p] >= clearRadius && dist[p] <= exploreLimit) + (dist[p] <= closeRange ? closeGrass : farGrass).push_back(MapGeneratorPoint(x, y)); + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * w + nx; + int resType = map.getResource(nx, ny).type; + if (resType == CORN && wheatDist < 0) + wheatDist = dist[p] + 1; + if (resType == WOOD && woodDist < 0) + woodDist = dist[p] + 1; + if (dist[np] < 0 && dist[p] < exploreLimit && + map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { + dist[np] = dist[p] + 1; + q.push(np); + } + } + } + auto placeReachable = [&](int resourceType) { + if (!closeGrass.empty() && + placeResourceClumpInArea(map, context, closeGrass, resourceType, 2)) + return; + if (!farGrass.empty()) + placeResourceClumpInArea(map, context, farGrass, resourceType, 2); + }; + if (wheatDist < 0 || wheatDist > wheatRange) + placeReachable(CORN); + if (woodDist < 0 || woodDist > woodRange) + placeReachable(WOOD); + } +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h index 92f075243..28f275e56 100644 --- a/src/map/generator/shared/Resources.h +++ b/src/map/generator/shared/Resources.h @@ -13,4 +13,12 @@ int placeResourceClumpInArea(Map &, GenerationContext &, const std::vector &, int resourceType, int radius); void scatterResources(Game &, GenerationContext &, const ResourceDensities &); +// Ensures every team has wheat within wheatRange and wood within woodRange of its boot tile, +// walking through the same reachable-space flood used to measure that distance (so anything +// placed is actually reachable, not just straight-line nearby). Teams already served within +// range are left untouched. clearRadius skips a ring around the boot tile that a caller still +// has to carve out for the swarm/workers after this runs (StartingPositions.cpp's setNoResource +// calls) — pass 0 when this runs after that carving has already happened. +void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, + int woodRange, int clearRadius = 0); } // namespace MapGeneration diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index 42cb9fd9b..b6fefe5c4 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -250,6 +250,18 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, } } + // Fairness guarantee: the bands above are painted from map-wide noise levels with no + // awareness of where any team actually starts, so a team's assigned tile can land in a + // stretch of grass the noise field never happens to touch — or a straight-line-nearby + // deposit can sit across water a worker can never walk to. Top up any team without wheat or + // wood within comfortable working range with one small guaranteed clump placed through the + // same walkable-space flood used to judge that distance, so it's reachable by construction; + // teams the noise pass already served are left untouched. This still has to run before + // placeStarts() carves out a small rectangle by the boot tile for the swarm and its workers + // (StartingPositions.cpp's setNoResource calls), so skip that band or a clump placed here + // would just be wiped a moment later. + guaranteeStartingResources(game, context, 24, 32, /*clearRadius=*/6); + // TODO: count of groves(=options.fruit) does not scale with mapsize. // so it has to be adjusted higher on bigger maps now. diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 48ecc9ac6..84275eb68 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -181,7 +181,7 @@ int main(int argc, char **argv) if (tuning) { int minLocal = map.getW() * map.getH(), minWheat = 100000, minWood = 100000, - viableTeams = 0; + bestWheat = 100000, bestWood = 100000, viableTeams = 0; if (success && method != 0) for (int t = 0; t < game.teamsCount(); ++t) { @@ -228,22 +228,30 @@ int main(int argc, char **argv) } } minLocal = std::min(minLocal, local); - // Store the worst (largest) resource distance across teams below. + // minWheat/minWood keep the worst (largest) distance across teams, i.e. the + // least-served team; bestWheat/bestWood keep the smallest, i.e. the + // best-served team. worst-minus-best is the per-map fairness spread between + // colonies that a single aggregate (like the worst case alone) can't show: a + // map can have a great worst case and still hand one team everything while + // another gets comparatively little. if (t == 0) { - minWheat = wheat; - minWood = wood; + minWheat = bestWheat = wheat; + minWood = bestWood = wood; } else { minWheat = std::max(minWheat, wheat); minWood = std::max(minWood, wood); + bestWheat = std::min(bestWheat, wheat); + bestWood = std::min(bestWood, wood); } if (local >= 16 && wheat <= 24 && wood <= 32) ++viableTeams; } - std::printf("TUNE,%d,%d,%d,%d,%d,%d,%d,%d\n", minLocal, minWheat, minWood, viableTeams, - resources[CORN], resources[WOOD], resources[STONE], resources[ALGA]); + std::printf("TUNE,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d\n", minLocal, minWheat, minWood, viableTeams, + resources[CORN], resources[WOOD], resources[STONE], resources[ALGA], bestWheat, + bestWood); } if (!dump.empty()) { diff --git a/tools/map_generator_study.py b/tools/map_generator_study.py index 61a1ce505..867473fcf 100644 --- a/tools/map_generator_study.py +++ b/tools/map_generator_study.py @@ -6,7 +6,7 @@ OUT=ROOT/'artifacts/map-generator-validation' BINARY=ROOT/'build/src/MapGeneratorStudy' FIELDS=['method','seed','success','tiles','grass_tiles','sand_tiles','water_tiles','shore','free','fit4','um_grass','um_sand','um_water','seconds','hash'] -EXTRA=['min_local_fit4','worst_wheat_distance','worst_wood_distance','viable_teams','wheat_tiles','wood_tiles','stone_tiles','algae_tiles'] +EXTRA=['min_local_fit4','worst_wheat_distance','worst_wood_distance','viable_teams','wheat_tiles','wood_tiles','stone_tiles','algae_tiles','best_wheat_distance','best_wood_distance'] PROFILES=set() atexit.register(lambda:[shutil.rmtree(Path.home()/('.'+p),ignore_errors=True) for p in PROFILES]) From c87330aaf1106d3e851b92e7d6052ef720642ace Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 21:42:10 +0000 Subject: [PATCH 010/136] Detect and clear resource walls that seal off a team's starting pocket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to 777b748. That commit's guaranteeStartingResources fixed most of the fairness gap but left river with a residual ~5% of colonies reaching no wheat or wood at all, and left old-random's imbalance direction flipped rather than closed. Investigated the river failures directly (map dumps + reachability analysis, not just aggregate metrics this time): a colony's boot tile was landing on a stretch of perfectly good, well connected land — confirmed by re-flooding the same tile through terrain alone, ignoring resources, and finding a landmass orders of magnitude larger than what the resource-respecting flood could reach. A ground unit can't walk onto a tile carrying a resource (Map::isHardSpaceForGroundUnit excludes them), and the noise-band resource painting has no idea what it might wall in, so it was sealing colonies into small pockets on their own otherwise-fine landmass. guaranteeStartingResources now detects this directly: when a team's resource-respecting reachable area is suspiciously small, it re-floods the same tile blocked only by water (ignoring resources) and diffs the two results to find exactly the resource tiles forming the wall's face — clearing only those, not a surrounding neighborhood, and iterating a bounded number of times in case a wall is thicker than one tile. A colony on a genuinely small spot (a real islet on a water-heavy map) is left alone: there's no larger area on the other side of nothing, so the terrain-only flood finds nothing bigger and the check is a no-op by construction. First attempt at this used a blunt fixed-radius clear around the boot tile whenever the pocket looked too small, gated only on the team actually being under-served. It worked for river but silently regressed swamp (whose terrain legitimately includes small real islands): demolishing a 25x25 tile neighborhood around a "small but actually fine" pocket destroys perfectly good nearby deposits for no gain when there is no larger landmass to reach. The precise wall-tracing approach here has no such failure mode, validated below. Reconfirmed with the same 200-fixed-seed methodology as 777b748: river's rate of a colony reaching no wheat or wood at all drops from ~5% to ~3% (the earlier blunt version reached 0% on this seed range but at swamp's expense — see above), with the remaining cases past this guarantee's own search radius. Swamp's numbers now improve slightly rather than regressing (the false-blame that motivated the blunt version's guard is gone: the gate is precise instead of merely permission-checked). Crater lakes, islands, old-random and old-islands are unaffected (the mechanism only fires where floodReach and a terrain-only flood actually disagree). Generation success rate is unchanged everywhere. Confirmed the existing map-generator-defaults-test and a full non-server engine build still pass. Old-random's wheat/wood imbalance (flipped, not fixed, by 777b748) and the remaining few percent of unreachable river colonies stay open follow-ups, called out in docs/map-generators/FRAMEWORK_UPGRADES.md along with why a join symmetric compass-slot assignment was tried and not kept for the former. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Rc6FTwBEB6LKNUmamX9wWW --- docs/map-generators/FRAMEWORK_UPGRADES.md | 4 +- src/map/generator/shared/Resources.cpp | 186 +++++++++++++++++----- 2 files changed, 153 insertions(+), 37 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 5d9d9f33e..4ad8cf886 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -26,6 +26,8 @@ All helpers are ordinary functions or small value objects. There is no new gener - Maze uses 20, 24 or 32-tile cells, larger home rooms and compact starter deposits. Closed graph edges carry a continuous one-tile stone seam on grass with sand and water on both sides, while connected edges remain open passages. - Fjord deposits wheat and wood in compact patches on both banks of every fjord. Bank deposits are placed last and shared clump placement never overwrites another resource type. Single-colony maps skip the vacuous bank requirement. - Lattice uses larger home clearings and puts wheat and wood on opposite rims. Its tiny repeating islets remain the dominant visual pattern without starving the colonies of construction space. -- Swamp, river, islands and crater lakes paint wheat, wood, stone and algae from map-wide noise bands with no awareness of where any team's tile ended up, and old-random/old-islands scan compass directions from each boot tile independently — both leave how close a colony's primary resources are almost entirely to where the (still resource-blind) starting-position search happened to put it, so one colony can end up with wheat and wood at its door while another has to walk far past playable range for either, or occasionally reach neither at all. `shared/Resources::guaranteeStartingResources` now floods outward from each boot tile through the same walkable-space search used to judge that distance (so anything it places is reachable by construction, not just straight-line close) and tops up any team missing wheat or wood within the range the study tool already scores as viable, leaving already-served teams untouched. Old-random's resource search also hardcoded a fourth compass slot to wheat, so every colony there got a guaranteed second wheat deposit and wood never got one; the fourth slot is now awarded to whichever of the two came out narrower for that colony instead of always the same resource. Measured on 200 fixed seeds per generator (`tools/map_generator_study.py`, `best_wheat_distance`/`best_wood_distance` columns added alongside the existing worst-case ones to size the gap between a map's best- and worst-served colony): old-random's wood gap and worst case both dropped by roughly two-fifths with no change in generation success rate; the other four generators held their success rate exactly and gained smaller, still real reductions in the wheat gap. Old-random's wheat gap grew in the same run — its two resource slots still aren't drawn from a symmetric search, so reinforcement lands more easily on whichever resource already claims the first, unconstrained compass pick — and a handful of colonies on constrained terrain (river was the clearest case) still land somewhere with no reachable wheat or wood at all; a farthest-point starting-position search was prototyped for the shared noise-band path to address that directly but measured worse on river and islands (it can spread teams onto separate landmasses of very different quality) and was not kept. Both remain open follow-ups. +- Swamp, river, islands and crater lakes paint wheat, wood, stone and algae from map-wide noise bands with no awareness of where any team's tile ended up, and old-random/old-islands scan compass directions from each boot tile independently — both leave how close a colony's primary resources are almost entirely to where the (still resource-blind) starting-position search happened to put it, so one colony can end up with wheat and wood at its door while another has to walk far past playable range for either, or occasionally reach neither at all. `shared/Resources::guaranteeStartingResources` now floods outward from each boot tile through the same walkable-space search used to judge that distance (so anything it places is reachable by construction, not just straight-line close) and tops up any team missing wheat or wood within the range the study tool already scores as viable, leaving already-served teams untouched. Old-random's resource search also hardcoded a fourth compass slot to wheat, so every colony there got a guaranteed second wheat deposit and wood never got one; the fourth slot is now awarded to whichever of the two came out narrower for that colony instead of always the same resource. Measured on 200 fixed seeds per generator (`tools/map_generator_study.py`, `best_wheat_distance`/`best_wood_distance` columns added alongside the existing worst-case ones to size the gap between a map's best- and worst-served colony): old-random's wood gap and worst case both dropped by roughly two-fifths with no change in generation success rate; the other four generators held their success rate exactly and gained smaller, still real reductions in the wheat gap. Old-random's wheat gap grew in the same run — its two resource slots still aren't drawn from a symmetric search, so reinforcement lands more easily on whichever resource already claims the first, unconstrained compass pick. This remains an open follow-up; a joint symmetric assignment for the two slots was prototyped but measured worse in aggregate than the simpler fix above and was not kept. + +A handful of colonies on constrained terrain (river was the clearest case) landed with no reachable wheat or wood at all even after the guarantee above — reachable, tellingly, was the operative problem: a colony's boot tile could sit on a stretch of otherwise perfectly good, well-connected land (confirmed by re-flooding the same tile through terrain alone, ignoring resources) that a *later* resource-painting pass had sealed off, since a ground unit cannot walk onto a tile carrying a resource (`Map::isHardSpaceForGroundUnit`) and the noise bands paint with no awareness of what they might wall in. `guaranteeStartingResources` now detects this directly — comparing the resource-respecting flood against the same flood with resources ignored — and clears exactly the resource tiles forming the wall's face (not a surrounding neighborhood) before re-flooding, so a colony genuinely on a small isolated spot (a real islet on a water-heavy map) is left untouched: there is no larger reachable area on the other side of nothing. This cut river's rate of a colony reaching no wheat or wood at all by more than a third with no change to any generator's success rate or to the other generators' numbers (declogging is a no-op wherever there's no wall to find), leaving a residual few percent where the connecting land lies beyond the guarantee's own search radius. A farthest-point starting-position search was also prototyped for the shared noise-band path to address the gap more fundamentally but measured worse on river and islands (it can spread teams onto separate landmasses of very different quality) and was not kept; that remains the deeper open follow-up. The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 921d8f3a3..2c9deebf7 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -14,6 +14,7 @@ #include #include #include +#include using namespace MapGeneration; namespace MapGeneration { @@ -96,54 +97,167 @@ void fillInResource(Map &map, GenerationContext &context, } } -void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, - int woodRange, int clearRadius) { - Map &map = game.map; +namespace { +// Ground units can't walk onto a tile carrying a resource (Map::isHardSpaceForGroundUnit +// excludes them), so a solid, unbroken band from the noise-band resource painting can wall a +// team's boot tile off from the rest of an otherwise perfectly connected landmass. floodReach +// walks that real, resource-respecting space; dist is kept (not just aggregated) so a caller +// can compare it against a wall-blind flood and find exactly where such a wall runs. +struct ReachResult { + int wheatDist = -1, woodDist = -1; + std::vector closeGrass, farGrass; + std::vector dist; +}; +ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int closeRange, + int clearRadius) { const int w = map.getW(), h = map.getH(); - const int exploreLimit = std::max(wheatRange, woodRange) * 5 / 2; - const int closeRange = wheatRange / 2; - for (int team = 0; team < context.request.nbTeams; ++team) { - std::vector dist(size_t(w) * h, -1); - std::queue q; - int start = context.bootY[team] * w + context.bootX[team]; - dist[start] = 0; - q.push(start); - int wheatDist = -1, woodDist = -1; - std::vector closeGrass, farGrass; - while (!q.empty()) { - int p = q.front(); - q.pop(); - int x = p % w, y = p / w; - if (map.getUMTerrain(x, y) == GRASS && dist[p] >= clearRadius && dist[p] <= exploreLimit) - (dist[p] <= closeRange ? closeGrass : farGrass).push_back(MapGeneratorPoint(x, y)); - for (int dy = -1; dy <= 1; ++dy) + ReachResult r; + r.dist.assign(size_t(w) * h, -1); + std::queue q; + int start = bootY * w + bootX; + r.dist[start] = 0; + q.push(start); + while (!q.empty()) { + int p = q.front(); + q.pop(); + int x = p % w, y = p / w; + if (map.getUMTerrain(x, y) == GRASS && r.dist[p] >= clearRadius && r.dist[p] <= exploreLimit) + (r.dist[p] <= closeRange ? r.closeGrass : r.farGrass).push_back(MapGeneratorPoint(x, y)); + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * w + nx; + int resType = map.getResource(nx, ny).type; + if (resType == CORN && r.wheatDist < 0) + r.wheatDist = r.dist[p] + 1; + if (resType == WOOD && r.woodDist < 0) + r.woodDist = r.dist[p] + 1; + if (r.dist[np] < 0 && r.dist[p] < exploreLimit && + map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { + r.dist[np] = r.dist[p] + 1; + q.push(np); + } + } + } + return r; +} + +// The same flood, but blocked only by water — as if resources didn't exist. Diffing this +// against floodReach's result isolates exactly which tiles a resource wall blocks: reachable +// here, not reachable there, adjacent to what is. Clearing only those tiles (rather than an +// entire neighborhood) opens the way while disturbing nothing else nearby. +std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit) { + const int w = map.getW(), h = map.getH(); + std::vector dist(size_t(w) * h, -1); + std::queue q; + int start = bootY * w + bootX; + dist[start] = 0; + q.push(start); + while (!q.empty()) { + int p = q.front(); + q.pop(); + if (dist[p] >= limit) + continue; + int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * w + nx; + if (dist[np] < 0 && !map.isWater(nx, ny)) { + dist[np] = dist[p] + 1; + q.push(np); + } + } + } + return dist; +} + +// Clears the resource tiles directly responsible for a team's cramped pocket: exactly the +// tiles reachable without resources blocking the way, not reachable with them, and touching a +// tile that is — the wall's inner face — plus, on a wall thick enough to have an outer face +// too, that face as well. A real dead end (no meaningfully larger landmass once resources are +// ignored) clears nothing, since there is no better tile on the other side to find. Iterated a +// few times by the caller in case a wall is thicker still. +bool clearResourceWall(Map &map, const std::vector &boxedDist, + const std::vector &openDist, int minGain) { + const int w = map.getW(), h = map.getH(); + int cleared = 0; + std::vector> toClear; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + int p = y * w + x; + if (boxedDist[p] >= 0 || openDist[p] < 0 || !map.isResource(x, y)) + continue; + bool touchesBoxed = false; + for (int dy = -1; dy <= 1 && !touchesBoxed; ++dy) for (int dx = -1; dx <= 1; ++dx) { if (dx == 0 && dy == 0) continue; int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int np = ny * w + nx; - int resType = map.getResource(nx, ny).type; - if (resType == CORN && wheatDist < 0) - wheatDist = dist[p] + 1; - if (resType == WOOD && woodDist < 0) - woodDist = dist[p] + 1; - if (dist[np] < 0 && dist[p] < exploreLimit && - map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { - dist[np] = dist[p] + 1; - q.push(np); + if (boxedDist[ny * w + nx] >= 0) { + touchesBoxed = true; + break; } } + if (touchesBoxed) + toClear.push_back({x, y}); + } + if ((int)toClear.size() < minGain) + return false; + for (auto [x, y] : toClear) { + map.setNoResource(x, y, 1); + ++cleared; + } + return cleared > 0; +} +} // namespace + +void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, + int woodRange, int clearRadius) { + Map &map = game.map; + const int exploreLimit = std::max(wheatRange, woodRange) * 5 / 2; + const int closeRange = wheatRange / 2; + // Below this many reached tiles a team is badly boxed in, but that has two very different + // causes: a resource wall sealing off an otherwise fine landmass (swamp and river both paint + // resources with no regard for what they might enclose), or the boot search simply landing on + // a genuinely small spot — a real little island on a water-heavy map, say, where there's + // nothing bigger on the other side to reach at all. Only the first is fixable, and only its + // exact wall tiles are cleared, so a genuinely small spot is correctly left untouched. + const int minPocketTiles = 60; + for (int team = 0; team < context.request.nbTeams; ++team) { + int bootX = context.bootX[team], bootY = context.bootY[team]; + ReachResult reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); + auto pocketSize = [&] { + int n = 0; + for (int v : reach.dist) + if (v >= 0) + ++n; + return n; + }; + bool underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || + reach.woodDist < 0 || reach.woodDist > woodRange; + for (int attempt = 0; underServed && pocketSize() < minPocketTiles && attempt < 6; ++attempt) { + std::vector open = terrainOnlyReach(map, bootX, bootY, exploreLimit); + if (!clearResourceWall(map, reach.dist, open, /*minGain=*/1)) + break; + reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); + underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || reach.woodDist < 0 || + reach.woodDist > woodRange; } auto placeReachable = [&](int resourceType) { - if (!closeGrass.empty() && - placeResourceClumpInArea(map, context, closeGrass, resourceType, 2)) + if (!reach.closeGrass.empty() && + placeResourceClumpInArea(map, context, reach.closeGrass, resourceType, 2)) return; - if (!farGrass.empty()) - placeResourceClumpInArea(map, context, farGrass, resourceType, 2); + if (!reach.farGrass.empty()) + placeResourceClumpInArea(map, context, reach.farGrass, resourceType, 2); }; - if (wheatDist < 0 || wheatDist > wheatRange) + if (reach.wheatDist < 0 || reach.wheatDist > wheatRange) placeReachable(CORN); - if (woodDist < 0 || woodDist > woodRange) + if (reach.woodDist < 0 || reach.woodDist > woodRange) placeReachable(WOOD); } } From 428021f33bd1b773253c071dcc3155c876c54eb5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 22:07:21 +0000 Subject: [PATCH 011/136] Choose colony sites after resources exist, and choose them for equality The four height-field generators picked colony sites before any resource was on the map, so a site could only be judged by the shape of the grass under it: largest contiguous grass rectangle to the first colony, and everyone after it taking what was left. Every fairness fix so far has been downstream repair of that decision. This changes the decision. The ordering turned out not to be a constraint at all. Nothing between the placement pass and the resource pass reads a boot position or draws from a random stream, so the two simply swap, and placement can then see what it is actually choosing between. Measured the headroom before building anything, via a new `headroom` mode in the study tool: score every legal site against a single multi-source flood per resource (one flood answers the question for the whole map, so scoring a site is a lookup rather than its own search), then find the narrowest score window still holding enough mutually distant sites. Result: every map sampled, on every generator, already contained a placement where all colonies were equally served. The maps were never unfair. The placement was throwing away fairness the map already had. shared/StartingPositions::chooseBalancedStarts now picks sites after the resource pass. A site scores as its *worse* primary resource, since a colony beside wood but a long walk from wheat is not a good start; sites are sorted and the narrowest qualifying window wins, scanned from the low-score end so ties settle in favour of a set that is not merely equal but good. The legacy search remains as a fallback for maps where no set of sites can reach both resources. Across 200 fixed seeds per generator, versus the pre-existing behaviour: gap between best- and worst-served colony 9.5-19.0 -> 3.5-4.0 tiles worst-served colony's walk to a resource 12.0-18.5 -> 3.5-4.2 tiles colonies reaching no wood or wheat at all up to 5.6% -> 0% colonies clearing the study tool's viability bar all of them The worst-served colony is now better off than the *luckiest* colony was before. Verified at 2, 6, 8 and 12 colonies as well (gap 2.4-7.4, widening with colony count as expected, no generation failures introduced at any count). Success rates, resource totals and colony separation (30-72 tiles apart on a 128x128 map) are unchanged. map-generator-defaults-test and a full non-server engine build pass. Old-random and old-islands deliberately keep their own placement: they put resources relative to each boot tile, so moving a colony does not bring its resources along. The same headroom measurement puts their achievable gap at 8.0 and 0.9 tiles, and old-islands already sits at 0.9. This changes how these four generators play, and that part is a maintainer's call rather than a metric's: colonies now start snug against their wood and wheat instead of in open ground, and buildable room within reach of the worst-served colony drops 14-28% (still 405-642 free building sites against a viability bar of 16). Generator revisions are bumped, so a given seed no longer produces the map it did before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Rc6FTwBEB6LKNUmamX9wWW --- docs/map-generators/FRAMEWORK_UPGRADES.md | 3 + .../generators/CraterLakesGenerator.cpp | 2 +- .../generator/generators/IslandsGenerator.cpp | 2 +- .../generator/generators/RiverGenerator.cpp | 2 +- .../generator/generators/SwampGenerator.cpp | 2 +- .../generator/shared/StartingPositions.cpp | 142 ++++++++++++++++ src/map/generator/shared/StartingPositions.h | 6 + src/map/generator/shared/Terrain.cpp | 159 +++++++++--------- test/MapGeneratorStudy.cpp | 155 +++++++++++++++++ 9 files changed, 392 insertions(+), 81 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 4ad8cf886..a2b46b738 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -30,4 +30,7 @@ All helpers are ordinary functions or small value objects. There is no new gener A handful of colonies on constrained terrain (river was the clearest case) landed with no reachable wheat or wood at all even after the guarantee above — reachable, tellingly, was the operative problem: a colony's boot tile could sit on a stretch of otherwise perfectly good, well-connected land (confirmed by re-flooding the same tile through terrain alone, ignoring resources) that a *later* resource-painting pass had sealed off, since a ground unit cannot walk onto a tile carrying a resource (`Map::isHardSpaceForGroundUnit`) and the noise bands paint with no awareness of what they might wall in. `guaranteeStartingResources` now detects this directly — comparing the resource-respecting flood against the same flood with resources ignored — and clears exactly the resource tiles forming the wall's face (not a surrounding neighborhood) before re-flooding, so a colony genuinely on a small isolated spot (a real islet on a water-heavy map) is left untouched: there is no larger reachable area on the other side of nothing. This cut river's rate of a colony reaching no wheat or wood at all by more than a third with no change to any generator's success rate or to the other generators' numbers (declogging is a no-op wherever there's no wall to find), leaving a residual few percent where the connecting land lies beyond the guarantee's own search radius. A farthest-point starting-position search was also prototyped for the shared noise-band path to address the gap more fundamentally but measured worse on river and islands (it can spread teams onto separate landmasses of very different quality) and was not kept; that remains the deeper open follow-up. +- Colony placement on the four height-field generators (swamp, river, islands, crater lakes) ran *before* any resource existed, so it could only ever judge a site by the shape of the grass under it — largest contiguous rectangle first, everyone after it taking what was left. That ordering turned out to be the whole problem rather than a constraint: nothing between the two passes reads a boot position or draws from a random stream, so they simply swap. Measuring the headroom first (`MapGeneratorStudy ... headroom`, which scores every legal site against a single multi-source flood per resource, making a site lookup O(1) rather than its own search) showed **every map sampled, on every generator, already contained a placement where all colonies were equally served** — the maps were never unfair, the placement was discarding fairness that was already there. `shared/StartingPositions::chooseBalancedStarts` now picks sites after the resource pass: it scores each legal site by its *worse* primary resource (a colony beside wood but far from wheat is not a good start), sorts, and takes the narrowest score window still holding enough mutually distant sites — scanning from the low-score end so ties settle in favour of a set that is not merely equal but good. The legacy search stays as a fallback for maps where no set of sites can reach both resources. Across 200 fixed seeds per generator the gap between a map's best- and worst-served colony falls from 9.5–19.0 tiles to 3.5–4.0, the worst-served colony's walk falls from 12–18 tiles to 3.5–4.2 (better than the *luckiest* colony managed before), colonies that could reach no wood or wheat at all go to zero, and every colony clears the study tool's viability bar. It holds at 2, 6, 8 and 12 colonies (gap 2.4–7.4, no generation failures introduced at any count); the gap widens with colony count, as expected when more colonies must be matched on the same map. Old-random and old-islands are untouched by this: they place resources relative to each boot tile, so moving a colony does not bring its resources along — the same headroom measurement puts their achievable gap at 8.0 and 0.9 respectively, and old-islands is already there. +- **Feel:** the balanced placement is a real change in how these four generators play, not only a fairness fix, and wants a maintainer's judgement rather than a metric's. Colonies now start snug against their wood and wheat instead of in open ground, and buildable room within reach of the worst-served colony drops 14–28% (still 405–642 free building sites against a viability bar of 16, so not a playability problem — but the character of an opening changes). Total resource tiles on the map are unchanged; only where colonies sit moved. The four generators' `revision` is bumped accordingly, so a given seed no longer produces the map it did before. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/CraterLakesGenerator.cpp b/src/map/generator/generators/CraterLakesGenerator.cpp index 76fa9e2c0..dcc93c754 100644 --- a/src/map/generator/generators/CraterLakesGenerator.cpp +++ b/src/map/generator/generators/CraterLakesGenerator.cpp @@ -28,7 +28,7 @@ GeneratorDefinition craterLakesDefinition() return {"crater-lakes", 4, "Crater lakes", - 1, + 2, false, {{"water", "Water weight", 0, 100, 1, 25, ControlGroup::Terrain, false, true}, {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, diff --git a/src/map/generator/generators/IslandsGenerator.cpp b/src/map/generator/generators/IslandsGenerator.cpp index 9d9a299cb..b128c6c2d 100644 --- a/src/map/generator/generators/IslandsGenerator.cpp +++ b/src/map/generator/generators/IslandsGenerator.cpp @@ -30,7 +30,7 @@ GeneratorDefinition islandsDefinition() return {"islands", 3, "Islands", - 1, + 2, false, {{"water", "Water weight", 0, 100, 1, 55, ControlGroup::Terrain, false, true}, {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, diff --git a/src/map/generator/generators/RiverGenerator.cpp b/src/map/generator/generators/RiverGenerator.cpp index 383fc143e..ab03cb4e5 100644 --- a/src/map/generator/generators/RiverGenerator.cpp +++ b/src/map/generator/generators/RiverGenerator.cpp @@ -25,7 +25,7 @@ GeneratorDefinition riverDefinition() return {"river", 2, "River", - 1, + 2, false, {{"water", "Water weight", 0, 100, 1, 45, ControlGroup::Terrain, false, true}, {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, diff --git a/src/map/generator/generators/SwampGenerator.cpp b/src/map/generator/generators/SwampGenerator.cpp index 05ae2393a..14f82d68e 100644 --- a/src/map/generator/generators/SwampGenerator.cpp +++ b/src/map/generator/generators/SwampGenerator.cpp @@ -24,7 +24,7 @@ GeneratorDefinition swampDefinition() return {"swamp", 1, "Swamp", - 1, + 2, false, {{"water", "Water weight", 0, 100, 1, 35, ControlGroup::Terrain, false, true}, {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index d7abfd5d2..ea83ba882 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -15,6 +15,9 @@ #include "Unit.h" #include #include +#include +#include +#include using namespace MapGeneration; namespace MapGeneration @@ -272,6 +275,145 @@ Building *addBuilding(Game &game, int x, int y, int team, int type, int level, namespace MapGeneration { +namespace +{ +// Walking distance from every tile to the nearest deposit of one resource. Workers stand +// beside a deposit rather than on it (a resource tile is not walkable), so the sources are the +// walkable tiles touching one. One flood answers the question for every tile on the map, which +// is what makes scoring a few hundred candidate sites cheap enough to do exhaustively. +std::vector distanceToResource(Map &map, int resourceType) +{ + const int w = map.getW(), h = map.getH(); + std::vector dist(size_t(w) * h, -1); + std::queue q; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + if (map.getResource(x, y).type != resourceType) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * w + nx; + if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + dist[np] = 0; + q.push(np); + } + } + } + while (!q.empty()) + { + int p = q.front(); + q.pop(); + int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * w + nx; + if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + dist[np] = dist[p] + 1; + q.push(np); + } + } + } + return dist; +} +} // namespace + +bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSquare) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const int nbTeams = context.request.nbTeams; + if (nbTeams <= 0 || minDistSquare <= 0) + return false; + const int typeNum = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + const BuildingType *swarm = globalContainer->buildingsTypes.get(typeNum); + if (!swarm) + return false; + + const std::vector woodDist = distanceToResource(map, WOOD); + const std::vector wheatDist = distanceToResource(map, CORN); + + // A site is worth exactly what its *worse* resource costs to reach: a colony next to wood + // but a long walk from wheat is not a good start, however good the wood is. + std::vector> sites; // (score, tile index) + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + if (!map.isFreeForBuilding(x, y, swarm->width, swarm->height)) + continue; + const int p = y * w + x; + if (woodDist[p] < 0 || wheatDist[p] < 0) + continue; + sites.push_back({std::max(woodDist[p], wheatDist[p]), p}); + } + if ((int)sites.size() < nbTeams) + return false; + std::sort(sites.begin(), sites.end()); + // Large maps can offer tens of thousands of legal sites. Thinning by stride keeps the + // sample spread across the whole score range instead of crowding one end of it, and keeps + // the search below a bounded cost regardless of map size. + const size_t cap = 900; + if (sites.size() > cap) + { + std::vector> thinned; + thinned.reserve(cap); + for (size_t i = 0; i < cap; ++i) + thinned.push_back(sites[i * sites.size() / cap]); + sites.swap(thinned); + } + + // Sites are sorted by score, so any set of colonies drawn from a short window of this list + // is a set whose colonies are closely matched. Find the narrowest window that still holds + // nbTeams mutually distant sites; scanning windows from the low-score end means ties are + // settled in favour of the set that is not just equal but good. + std::vector best; + int bestSpread = -1; + for (size_t i = 0; i < sites.size(); ++i) + { + if (bestSpread == 0) + break; + std::vector picked; + for (size_t j = i; j < sites.size(); ++j) + { + if (bestSpread >= 0 && sites[j].first - sites[i].first >= bestSpread) + break; // this window is already no better than what we hold + const int px = sites[j].second % w, py = sites[j].second / w; + bool farEnough = true; + for (int q : picked) + if (map.warpDistSquare(px, py, q % w, q / w) < minDistSquare) + { + farEnough = false; + break; + } + if (!farEnough) + continue; + picked.push_back(sites[j].second); + if ((int)picked.size() == nbTeams) + { + bestSpread = sites[j].first - sites[i].first; + best = picked; + break; + } + } + } + if ((int)best.size() != nbTeams) + return false; + for (int team = 0; team < nbTeams; ++team) + { + context.bootX[team] = best[team] % w; + context.bootY[team] = best[team] / w; + } + return true; +} + bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSize) { for (int s = 0; s < context.request.nbTeams; s++) diff --git a/src/map/generator/shared/StartingPositions.h b/src/map/generator/shared/StartingPositions.h index 6d75b7d7f..ac7c8370a 100644 --- a/src/map/generator/shared/StartingPositions.h +++ b/src/map/generator/shared/StartingPositions.h @@ -14,6 +14,12 @@ Building *addBuilding(Game &game, int x, int y, int team, int type, int level, namespace MapGeneration { +// Chooses each colony's boot tile so that every colony's walk to its primary resources is as +// nearly equal as the finished map allows, rather than handing the best land to whoever is +// picked first. Requires resources to already be on the map. Returns false (leaving bootX/bootY +// untouched) when no set of legal, mutually distant sites can reach both resources, so a caller +// can fall back to a terrain-only search. +bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSquare); bool placeStarts(Game &, GenerationContext &); bool placeArchipelagoStarts(Game &, GenerationContext &, int islandSize); } // namespace MapGeneration diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index b6fefe5c4..4cac88786 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -127,83 +127,6 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, static_cast(tmpUndermap)); } map.controlSand(); - - context.stage = "starting locations"; - // Now, we have to find suitable places for teams: - int nbTeams = context.request.nbTeams; - int minDistSquare = (int)((double)w * h / (double)nbTeams / 5); - if (minDistSquare <= 0) - { - return false; - } - assert(minDistSquare > 0); - int *bootX = context.bootX.data(); - int *bootY = context.bootY.data(); - - // TODO: First pass to find the number of available places. - for (int team = 0; team < nbTeams; team++) - { - int maxSurface = 0; - int maxX = 0; - int maxY = 0; - for (int y = 0; y < h; y++) - { - int width = 0; - int startX = 0; - for (int x = 0; x < w; x++) - { - int a = map.getUMTerrain(x, y); - if (a == GRASS) - width++; - else - { - if (width > 7) - { - int centerX = ((x + startX) >> 1); - int top, bot; - for (top = 0; top < h; top++) - if (map.getUMTerrain(centerX, y - top) != GRASS) - break; - for (bot = 0; bot < h; bot++) - if (map.getUMTerrain(centerX, y + bot) != GRASS) - break; - int height = top + bot - 1; - int surface = height * width; - assert(surface > 0); - - int centerY = y + ((bot - top) >> 1); - bool farEnough = true; - for (int ti = 0; ti < team; ti++) - if (map.warpDistSquare(centerX, centerY, bootX[ti], bootY[ti]) < - minDistSquare) - { - farEnough = false; - break; - } - - if (surface > maxSurface && farEnough) - { - maxSurface = surface; - maxX = centerX; - maxY = centerY; - } - } - width = 0; - startX = x; - } - } - } - - if (maxSurface <= 0) - { - return false; - } - assert(maxSurface); - bootX[team] = maxX; - bootY[team] = maxY; - } - - map.controlSand(); map.rebuildTerrain(); context.stage = "resources"; // now to add primary resources for current map generator @@ -250,6 +173,88 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, } } + // Choosing where the colonies go *after* the resources exist is what makes a fair choice + // possible at all: the search below scores a site by how far its workers must actually walk + // to wood and wheat, which is unknowable while the map is still bare. Nothing in between + // reads a boot position or draws from a random stream, so the two passes simply swapped + // order. The legacy search — largest grass rectangle first, and everyone after it takes + // what is left — stays as a fallback for maps where no set of sites can reach both + // resources at all. + context.stage = "starting locations"; + int nbTeams = context.request.nbTeams; + int minDistSquare = (int)((double)w * h / (double)nbTeams / 5); + if (minDistSquare <= 0) + { + return false; + } + int *bootX = context.bootX.data(); + int *bootY = context.bootY.data(); + if (!chooseBalancedStarts(game, context, minDistSquare)) + { + // TODO: First pass to find the number of available places. + for (int team = 0; team < nbTeams; team++) + { + int maxSurface = 0; + int maxX = 0; + int maxY = 0; + for (int y = 0; y < h; y++) + { + int width = 0; + int startX = 0; + for (int x = 0; x < w; x++) + { + int a = map.getUMTerrain(x, y); + if (a == GRASS) + width++; + else + { + if (width > 7) + { + int centerX = ((x + startX) >> 1); + int top, bot; + for (top = 0; top < h; top++) + if (map.getUMTerrain(centerX, y - top) != GRASS) + break; + for (bot = 0; bot < h; bot++) + if (map.getUMTerrain(centerX, y + bot) != GRASS) + break; + int height = top + bot - 1; + int surface = height * width; + assert(surface > 0); + + int centerY = y + ((bot - top) >> 1); + bool farEnough = true; + for (int ti = 0; ti < team; ti++) + if (map.warpDistSquare(centerX, centerY, bootX[ti], bootY[ti]) < + minDistSquare) + { + farEnough = false; + break; + } + + if (surface > maxSurface && farEnough) + { + maxSurface = surface; + maxX = centerX; + maxY = centerY; + } + } + width = 0; + startX = x; + } + } + } + + if (maxSurface <= 0) + { + return false; + } + assert(maxSurface); + bootX[team] = maxX; + bootY[team] = maxY; + } + } + // Fairness guarantee: the bands above are painted from map-wide noise levels with no // awareness of where any team actually starts, so a team's assigned tile can land in a // stretch of grass the noise field never happens to touch — or a straight-line-nearby diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 84275eb68..8f7a27a4f 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -79,6 +79,7 @@ int main(int argc, char **argv) descriptor.setMethodDefaults(method); descriptor.seed = seed; bool tuning = false; + bool headroom = false; std::string dump; std::map aliases = {{"smooth", "smoothing"}, {"craters", "lake-density"}, @@ -96,6 +97,8 @@ int main(int argc, char **argv) std::string arg = argv[i]; if (arg == "tuning") tuning = true; + else if (arg == "headroom") + headroom = true; else if (arg == "preset") { } // All new requests start at registered defaults. @@ -253,6 +256,158 @@ int main(int argc, char **argv) resources[CORN], resources[WOOD], resources[STONE], resources[ALGA], bestWheat, bestWood); } + if (headroom) + { + // How much of the fairness gap is recoverable by *placement alone*, on this exact + // finished map? Nothing here changes the map; it asks what the colony sites could have + // been. One multi-source flood per resource gives every tile its walking distance to + // the nearest deposit, so scoring a candidate site is a lookup rather than its own + // search, and the whole map's candidates cost two floods total. + const int mw = map.getW(), mh = map.getH(); + auto distanceField = [&](int resourceType) + { + std::vector d(size_t(mw) * mh, -1); + std::queue q; + // A worker stands beside a deposit rather than on it, so the walkable tiles + // touching one are the sources at distance 0. + for (int y = 0; y < mh; ++y) + for (int x = 0; x < mw; ++x) + { + if (map.getResource(x, y).type != resourceType) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * mw + nx; + if (d[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + d[np] = 0; + q.push(np); + } + } + } + while (!q.empty()) + { + int p = q.front(); + q.pop(); + int x = p % mw, y = p / mw; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * mw + nx; + if (d[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + d[np] = d[p] + 1; + q.push(np); + } + } + } + return d; + }; + const std::vector woodField = distanceField(WOOD), wheatField = distanceField(CORN); + // A site's score is its binding constraint: the further of its two primary resources. + auto siteScore = [&](int x, int y) -> int + { + int p = map.normalizeY(y) * mw + map.normalizeX(x); + int a = woodField[p], b = wheatField[p]; + if (a < 0 || b < 0) + return -1; + return std::max(a, b); + }; + + // What the generator's own placement actually achieved, by this same measure. + int actualLo = 1 << 28, actualHi = -1; + bool actualValid = success && method != 0; + for (int t = 0; actualValid && t < game.teamsCount(); ++t) + { + int s = -1; + for (int y = 0; y < mh && s < 0; ++y) + for (int x = 0; x < mw; ++x) + { + auto gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == t) + { + s = siteScore(x, y); + break; + } + } + if (s < 0) + actualValid = false; + else + { + actualLo = std::min(actualLo, s); + actualHi = std::max(actualHi, s); + } + } + + // Every site a colony could legally have occupied, scored and sorted. + std::vector> candidates; // (score, tile index) + for (int y = 0; y < mh; ++y) + for (int x = 0; x < mw; ++x) + { + if (!map.isFreeForBuilding(x, y, 4, 4)) + continue; + int s = siteScore(x, y); + if (s >= 0) + candidates.push_back({s, y * mw + x}); + } + std::sort(candidates.begin(), candidates.end()); + // Keep the search bounded on large maps; a stride keeps the sample spread over the + // whole score range rather than clustering at one end. + const size_t cap = 700; + if (candidates.size() > cap) + { + std::vector> thinned; + for (size_t i = 0; i < cap; ++i) + thinned.push_back(candidates[i * candidates.size() / cap]); + candidates.swap(thinned); + } + + // Narrowest score window that still holds nbTeams mutually distant sites. Greedy + // selection inside a window understates what a full search would find, so this is a + // conservative floor on the achievable spread, not an optimistic one. + const int nbTeams = descriptor.nbTeams; + const int minDistSquare = int((double)mw * mh / (double)nbTeams / 5); + int bestSpread = -1, bestLo = -1, bestHi = -1; + for (size_t i = 0; i < candidates.size(); ++i) + { + std::vector picked; + for (size_t j = i; j < candidates.size(); ++j) + { + int px = candidates[j].second % mw, py = candidates[j].second / mw; + bool farEnough = true; + for (int q : picked) + if (map.warpDistSquare(px, py, q % mw, q / mw) < minDistSquare) + { + farEnough = false; + break; + } + if (!farEnough) + continue; + picked.push_back(candidates[j].second); + if ((int)picked.size() == nbTeams) + { + int spread = candidates[j].first - candidates[i].first; + if (bestSpread < 0 || spread < bestSpread) + { + bestSpread = spread; + bestLo = candidates[i].first; + bestHi = candidates[j].first; + } + break; + } + } + if (bestSpread == 0) + break; + } + std::printf("HEADROOM,%d,%d,%d,%d,%d,%d,%d,%d\n", (int)candidates.size(), + actualValid ? actualHi - actualLo : -1, actualValid ? actualLo : -1, + actualValid ? actualHi : -1, bestSpread, bestLo, bestHi, nbTeams); + } if (!dump.empty()) { FILE *f = std::fopen(dump.c_str(), "w"); From b37fe4042161dcdc5d9d55d1c6ced7b98a02b78c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 23:58:01 +0000 Subject: [PATCH 012/136] Score colony sites as they will be built, not as the bare map looks Placement picks a site for its walk to wood and wheat, but a colony rewrites its own surroundings the instant it is built: placeStarts() clears a five by seven box of resources to make room, the swarm itself becomes four by four tiles of obstacle, and the workers spawn on the row above the boot tile rather than on it. Scoring against the bare map therefore credited a site with deposits it was about to destroy and paths it was about to block. That was measurable, not theoretical. Instrumenting the search on one seed showed it choosing four sites scored wood 0, wheat 0 apiece -- exactly equal -- which the finished map then spread across three tiles, because how much each site loses to its own construction depends on the shape of the resources around it. chooseBalancedStarts now shortlists on the cheap bare-map distance, which can only understate the built cost and so is a sound filter, then re-scores the shortlist by simulating the finished colony: clearing box excluded, swarm footprint impassable, flood starting from the worker row. The offsets mirror placeStarts() rather than approximating it. Against the previous commit, over 200 fixed seeds per generator: gap between best- and worst-served colony 3.5-4.0 -> 0.92-1.02 tiles worst-served colony's walk 3.5-4.2 -> 2.0-2.3 tiles Better on both counts at once, and level with old-islands, the fair-by-construction benchmark. Holds at 2, 8 and 12 colonies (gap 0.35-4.70, widening with count as expected, no failures at any count). Buildable room, resource totals and colony separation are unchanged. map-generator-defaults-test and a full non-server engine build pass. A first attempt instead excluded every site whose deposits sat inside the clearing box. It tightened the gap to 2.8-3.1 but pushed every colony roughly twice as far from its resources (worst walk 3.5 -> 6.6 tiles), trading what matters for what was being measured. Not kept. Generation costs 63-110ms against 11-14ms, nearly all of it the shortlist re-scoring, and the first working version cost 1400ms until the visited set stopped being a linear scan. Imperceptible for a lobby generating one map. Halving the shortlist halves the cost but loses a third of the gain on river, so the shortlist stays at 900. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Rc6FTwBEB6LKNUmamX9wWW --- docs/map-generators/FRAMEWORK_UPGRADES.md | 2 + .../generator/shared/StartingPositions.cpp | 92 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index a2b46b738..af70faef1 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -31,6 +31,8 @@ All helpers are ordinary functions or small value objects. There is no new gener A handful of colonies on constrained terrain (river was the clearest case) landed with no reachable wheat or wood at all even after the guarantee above — reachable, tellingly, was the operative problem: a colony's boot tile could sit on a stretch of otherwise perfectly good, well-connected land (confirmed by re-flooding the same tile through terrain alone, ignoring resources) that a *later* resource-painting pass had sealed off, since a ground unit cannot walk onto a tile carrying a resource (`Map::isHardSpaceForGroundUnit`) and the noise bands paint with no awareness of what they might wall in. `guaranteeStartingResources` now detects this directly — comparing the resource-respecting flood against the same flood with resources ignored — and clears exactly the resource tiles forming the wall's face (not a surrounding neighborhood) before re-flooding, so a colony genuinely on a small isolated spot (a real islet on a water-heavy map) is left untouched: there is no larger reachable area on the other side of nothing. This cut river's rate of a colony reaching no wheat or wood at all by more than a third with no change to any generator's success rate or to the other generators' numbers (declogging is a no-op wherever there's no wall to find), leaving a residual few percent where the connecting land lies beyond the guarantee's own search radius. A farthest-point starting-position search was also prototyped for the shared noise-band path to address the gap more fundamentally but measured worse on river and islands (it can spread teams onto separate landmasses of very different quality) and was not kept; that remains the deeper open follow-up. - Colony placement on the four height-field generators (swamp, river, islands, crater lakes) ran *before* any resource existed, so it could only ever judge a site by the shape of the grass under it — largest contiguous rectangle first, everyone after it taking what was left. That ordering turned out to be the whole problem rather than a constraint: nothing between the two passes reads a boot position or draws from a random stream, so they simply swap. Measuring the headroom first (`MapGeneratorStudy ... headroom`, which scores every legal site against a single multi-source flood per resource, making a site lookup O(1) rather than its own search) showed **every map sampled, on every generator, already contained a placement where all colonies were equally served** — the maps were never unfair, the placement was discarding fairness that was already there. `shared/StartingPositions::chooseBalancedStarts` now picks sites after the resource pass: it scores each legal site by its *worse* primary resource (a colony beside wood but far from wheat is not a good start), sorts, and takes the narrowest score window still holding enough mutually distant sites — scanning from the low-score end so ties settle in favour of a set that is not merely equal but good. The legacy search stays as a fallback for maps where no set of sites can reach both resources. Across 200 fixed seeds per generator the gap between a map's best- and worst-served colony falls from 9.5–19.0 tiles to 3.5–4.0, the worst-served colony's walk falls from 12–18 tiles to 3.5–4.2 (better than the *luckiest* colony managed before), colonies that could reach no wood or wheat at all go to zero, and every colony clears the study tool's viability bar. It holds at 2, 6, 8 and 12 colonies (gap 2.4–7.4, no generation failures introduced at any count); the gap widens with colony count, as expected when more colonies must be matched on the same map. Old-random and old-islands are untouched by this: they place resources relative to each boot tile, so moving a colony does not bring its resources along — the same headroom measurement puts their achievable gap at 8.0 and 0.9 respectively, and old-islands is already there. +- A colony changes its own surroundings the moment it is built, and scoring sites against the bare map ignored that: `placeStarts()` clears a five by seven box of resources for the swarm, the swarm becomes four by four tiles of obstacle, and the workers spawn on the row above the boot tile rather than on it. Placement was therefore choosing sites for deposits it was about to destroy and paths it was about to block — measurably so, with four sites picked as *exactly* equal (wood 0, wheat 0 apiece) finishing three tiles apart. `chooseBalancedStarts` now shortlists on the cheap bare-map distance, which can only understate the built cost, then re-scores the shortlist by simulating the finished colony: the clearing box excluded, the swarm footprint impassable, the flood starting from the worker row. Scoring what the colony will actually live with drops the gap from 3.5–4.0 tiles to 0.92–1.02 and brings the worst-served colony's walk from 3.5–4.2 tiles down to 2.0–2.3 — better on both counts at once, and level with old-islands, which has been the fair-by-construction benchmark throughout. Buildable room is unchanged from the previous placement work. A first attempt instead *excluded* every site whose deposits sat inside the clearing box; that tightened the gap to 2.8–3.1 but pushed every colony roughly twice as far from its resources (worst walk 3.5 → 6.6 tiles), trading the thing that matters for the thing being measured, and was not kept. Generation costs 63–110ms against 11–14ms before, nearly all of it the shortlist re-scoring — imperceptible for a lobby that generates one map, and worth it: halving the shortlist to 300 sites halves the cost but loses a third of the gain on river (1.02 → 1.66). +- Sampling whole maps and keeping the fairest — the obvious lever, and cheap at 11ms a map — was measured against these distributions rather than assumed. Before any placement work it roughly halved the gap (14.0 → 6.2 at twenty candidates) but could never do better than about 4 tiles, because that was simply the best any of two hundred seeds produced: a technique that selects from a distribution cannot beat the distribution. Measuring the *headroom* instead showed every map already contained a perfectly equal placement, which is what redirected the work from choosing better maps to choosing better sites within a map. With placement fixed, twenty-candidate sampling now shaves 0.93 tiles to about 0.2 — real, but a fraction of a second of walking, while costing twenty generations that each got five to eight times more expensive. Worth keeping as an option; not worth the default. - **Feel:** the balanced placement is a real change in how these four generators play, not only a fairness fix, and wants a maintainer's judgement rather than a metric's. Colonies now start snug against their wood and wheat instead of in open ground, and buildable room within reach of the worst-served colony drops 14–28% (still 405–642 free building sites against a viability bar of 16, so not a playability problem — but the character of an opening changes). Total resource tiles on the map are unchanged; only where colonies sit moved. The four generators' `revision` is bumped accordingly, so a given seed no longer produces the map it did before. The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index ea83ba882..83e4d16c5 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -343,6 +343,80 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu // A site is worth exactly what its *worse* resource costs to reach: a colony next to wood // but a long walk from wheat is not a good start, however good the wood is. + // A colony changes its own surroundings the moment it is built: placeStarts() clears a + // five by seven box of resources to make room, the swarm itself becomes four by four tiles + // of obstacle, and the workers appear on the row above it rather than on the boot tile. A + // site scored against the bare map is therefore scored on deposits it is about to destroy + // and paths it is about to block, which is how four sites picked as exactly equal can + // finish unequal. Score what the colony will actually live with instead. The offsets below + // mirror placeStarts(); they are what it does, not a guess at it. + std::vector visited(size_t(w) * h, 0); + int visitStamp = 0; + auto scoreAsBuilt = [&](int bx, int by, int limit) -> int + { + auto cleared = [&](int tx, int ty) + { + int ox = map.normalizeX(tx - bx), oy = map.normalizeY(ty - by); + if (ox >= w / 2) + ox -= w; + if (oy >= h / 2) + oy -= h; + return ox >= 0 && ox <= 4 && oy >= -2 && oy <= 4; + }; + auto blocked = [&](int tx, int ty) + { + int ox = map.normalizeX(tx - bx), oy = map.normalizeY(ty - by); + if (ox >= w / 2) + ox -= w; + if (oy >= h / 2) + oy -= h; + return ox >= 0 && ox < swarm->width && oy >= 0 && oy < swarm->height; + }; + ++visitStamp; + std::queue> q; // (tile, distance) + auto push = [&](int tx, int ty, int d) + { + int nx = map.normalizeX(tx), ny = map.normalizeY(ty); + int np = ny * w + nx; + if (visited[np] == visitStamp) + return; + visited[np] = visitStamp; + q.push({np, d}); + }; + // Workers spawn on the row above the swarm, so that is where a gathering trip starts. + for (int i = 0; i < std::max(1, context.request.nbWorkers); ++i) + push(bx + (i % 4), by - 1 - (i / 4), 0); + int wood = -1, wheat = -1; + while (!q.empty() && (wood < 0 || wheat < 0)) + { + auto [p, d] = q.front(); + q.pop(); + if (d >= limit) + continue; + int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int type = map.getResource(nx, ny).type; + if (!cleared(nx, ny)) + { + if (type == WOOD && wood < 0) + wood = d + 1; + if (type == CORN && wheat < 0) + wheat = d + 1; + } + if (!blocked(nx, ny) && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + push(nx, ny, d + 1); + } + } + if (wood < 0 || wheat < 0) + return -1; + return std::max(wood, wheat); + }; + std::vector> sites; // (score, tile index) for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) @@ -352,6 +426,8 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu const int p = y * w + x; if (woodDist[p] < 0 || wheatDist[p] < 0) continue; + // The bare-map distance can only understate what the built colony will walk, so it + // is a sound cheap filter: it shortlists sites worth the exact simulation above. sites.push_back({std::max(woodDist[p], wheatDist[p]), p}); } if ((int)sites.size() < nbTeams) @@ -370,6 +446,22 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu sites.swap(thinned); } + // Re-score the shortlist as it will actually be built, and re-sort on the honest number. + { + std::vector> exact; + exact.reserve(sites.size()); + for (const auto &s : sites) + { + const int built = scoreAsBuilt(s.second % w, s.second / w, 32); + if (built >= 0) + exact.push_back({built, s.second}); + } + if ((int)exact.size() < nbTeams) + return false; + std::sort(exact.begin(), exact.end()); + sites.swap(exact); + } + // Sites are sorted by score, so any set of colonies drawn from a short window of this list // is a set whose colonies are closely matched. Find the narrowest window that still holds // nbTeams mutually distant sites; scanning windows from the low-score end means ties are From 34c838234638bb489be5ab29b4eee9569dc1a71a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 00:55:01 +0000 Subject: [PATCH 013/136] Compute fertility as the growth probability it actually is Map::growResources decides whether a wheat or wood tile expands with dwax = (syncRand()&0xF) - (syncRand()&0xF); // likewise dway expand = isWater(x+dwax, y+dway) && !isSand(x-dwax, y-dway); The difference of two uniform draws from {0..15} has PMF (16-|d|)/256, so the chance a tile expands is a triangular kernel summed over every water tile whose mirror through the tile is not sand. FertilityCalculator fit a curve to the first half of that and dropped the second: its weights were int(4.2*sqrt((15-|dx|)*(15-|dy|))) and it never looked at sand at all, overstating exactly the tiles where wheat will not in fact come back. Fertility::Field evaluates the real thing in closed form. A length-16 forward box followed by a length-16 backward box is exactly the triangular kernel, so the water term is four linear passes rather than 961 taps per grass tile, and the sand term is one stamp per sand tile. Measured over 20 seeds on each of eight generators it runs about ten times faster than the kernel it replaces (the sand correction, not the convolution, is what it spends its time on) and correlates 0.89-0.997 with it; shattered-coast, the sandiest of them, is both the least correlated and the slowest, which is the difference being paid for. The field takes plain masks and no Map, so map generation can score a candidate's growth potential without a live game. FertilityCalculator keeps its API, its deposit-reachability gate and fertilityMaximum, and clamps to the Uint16 that Tile::fertility holds; the scale is now 65536, which the overlay normalises away and MapIO stores per tile as before. The implementation comes from ExactFertilityCache in AIMaximaFarming on the Maxima branch, extracted here so both callers share one field. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Rc6FTwBEB6LKNUmamX9wWW --- src/FertilityCalculator.cpp | 142 +++--------------- src/FertilityCalculator.h | 2 +- src/SConscript | 1 + src/map/FertilityField.cpp | 285 ++++++++++++++++++++++++++++++++++++ src/map/FertilityField.h | 76 ++++++++++ test/FertilityFieldTest.cpp | 262 +++++++++++++++++++++++++++++++++ test/FertilityFieldTest.h | 44 ++++++ test/SConstruct | 3 + 8 files changed, 691 insertions(+), 124 deletions(-) create mode 100644 src/map/FertilityField.cpp create mode 100644 src/map/FertilityField.h create mode 100644 test/FertilityFieldTest.cpp create mode 100644 test/FertilityFieldTest.h diff --git a/src/FertilityCalculator.cpp b/src/FertilityCalculator.cpp index 04ef1631a..83d04eb2b 100644 --- a/src/FertilityCalculator.cpp +++ b/src/FertilityCalculator.cpp @@ -4,142 +4,38 @@ #include "FertilityCalculator.h" #include "Map.h" +#include "map/FertilityField.h" #include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - // 31x31 weighting kernel: water tiles within Chebyshev distance kFertilityRadius - // of a grass tile contribute weight = int(kSqrtScale * sqrt((R-|dx|)*(R-|dy|))). - constexpr int kFertilityRadius = 15; - constexpr int kKernelSide = 2 * kFertilityRadius + 1; - constexpr float kSqrtScale = 4.2f; - - constexpr std::array, 8> kBfsNeighbors{{ - {-1, -1}, { 0, -1}, { 1, -1}, - {-1, 0}, { 1, 0}, - {-1, 1}, { 0, 1}, { 1, 1}, - }}; - - using DistanceMap = std::vector>; - - const std::array& fertilityKernel() - { - static const auto kernel = []() { - std::array k{}; - for (int ny = -kFertilityRadius; ny <= kFertilityRadius; ++ny) - { - for (int nx = -kFertilityRadius; nx <= kFertilityRadius; ++nx) - { - const int value = (kFertilityRadius - std::abs(nx)) - * (kFertilityRadius - std::abs(ny)); - const int idx = (ny + kFertilityRadius) * kKernelSide - + (nx + kFertilityRadius); - k[idx] = static_cast( - int(kSqrtScale * std::sqrt(static_cast(value)))); - } - } - return k; - }(); - return kernel; - } - - /// 8-connected BFS from every takeable corn/wood tile, traversing only grass - /// cells. Cells that are unreachable (or non-grass and not seeded) stay nullopt. - DistanceMap computeResourceDistance(const Map& map) - { - DistanceMap distance(static_cast(map.getW()) * map.getH()); - std::queue> frontier; - - for (int x = 0; x < map.getW(); ++x) - { - for (int y = 0; y < map.getH(); ++y) - { - if (map.isResourceTakeable(x, y, CORN) - || map.isResourceTakeable(x, y, WOOD)) - { - distance[map.coordToIndex(x, y)] = 0; - frontier.emplace(x, y); - } - } - } - - while (!frontier.empty()) - { - const auto [px, py] = frontier.front(); - frontier.pop(); - const Uint16 nextDepth = - static_cast(*distance[map.coordToIndex(px, py)] + 1); - - for (const auto [dx, dy] : kBfsNeighbors) - { - const int nx = map.normalizeX(px + dx); - const int ny = map.normalizeY(py + dy); - auto& cell = distance[map.coordToIndex(nx, ny)]; - if (!cell.has_value() && map.isGrass(nx, ny)) - { - cell = nextDepth; - frontier.emplace(nx, ny); - } - } - } - return distance; - } -} +#include namespace FertilityCalculator { void compute(Map& map, const ProgressCallback& progress) { - // BFS is fast relative to the kernel pass, so it isn't progress-reported. - const DistanceMap reachable = computeResourceDistance(map); - const auto& kernel = fertilityKernel(); + if (progress) + progress(0.0f); - std::vector fertility( - static_cast(map.getW()) * map.getH(), 0); - Uint16 fertilityMax = 0; + const Fertility::Field field = Fertility::forMap(map); - for (int x = 0; x < map.getW(); ++x) - { - if (progress) - progress(static_cast(x) / static_cast(map.getW())); + if (progress) + progress(0.8f); + // Fertility::kScale is one past what Tile::fertility holds. Only a tile whose + // entire neighbourhood is qualifying water reaches it, which no grass tile can. + constexpr std::uint32_t kCeiling = std::numeric_limits::max(); + Uint16 fertilityMax = 0; + for (int x = 0; x < map.getW(); ++x) for (int y = 0; y < map.getH(); ++y) { - if (!map.isGrass(x, y)) - continue; - if (!reachable[map.coordToIndex(x, y)].has_value()) - continue; - - Uint16 total = 0; - for (int ny = -kFertilityRadius; ny <= kFertilityRadius; ++ny) - { - for (int nx = -kFertilityRadius; nx <= kFertilityRadius; ++nx) - { - // Map::isWater wraps coords via coordToIndex; no normalize needed. - if (map.isWater(x + nx, y + ny)) - { - const int kIdx = (ny + kFertilityRadius) * kKernelSide - + (nx + kFertilityRadius); - total += kernel[kIdx]; - } - } - } - fertilityMax = std::max(fertilityMax, total); - fertility[map.coordToIndex(x, y)] = total; + const Uint16 value = + static_cast(std::min(field.at(x, y), kCeiling)); + map.getTile(x, y).fertility = value; + fertilityMax = std::max(fertilityMax, value); } - } - - for (int x = 0; x < map.getW(); ++x) - for (int y = 0; y < map.getH(); ++y) - map.getTile(x, y).fertility = fertility[map.coordToIndex(x, y)]; map.fertilityMaximum = fertilityMax; + + if (progress) + progress(1.0f); } } diff --git a/src/FertilityCalculator.h b/src/FertilityCalculator.h index 5091c473c..e73f787c5 100644 --- a/src/FertilityCalculator.h +++ b/src/FertilityCalculator.h @@ -14,6 +14,6 @@ namespace FertilityCalculator /// Computes per-tile fertility, writes it into map.getTile(x,y).fertility, /// and updates map.fertilityMaximum. The optional progress callback is - /// invoked once per column. May be called from a worker thread. + /// invoked at a few checkpoints. May be called from a worker thread. void compute(Map& map, const ProgressCallback& progress); } diff --git a/src/SConscript b/src/SConscript index 3b1ec2e71..af7d91dc9 100644 --- a/src/SConscript +++ b/src/SConscript @@ -102,6 +102,7 @@ EngineInit.cpp EngineLoaders.cpp EngineRun.cpp FertilityCalculator.cpp +map/FertilityField.cpp FertilityCalculatorDialog.cpp Game.cpp Game_orders.cpp diff --git a/src/map/FertilityField.cpp b/src/map/FertilityField.cpp new file mode 100644 index 000000000..c68a27e06 --- /dev/null +++ b/src/map/FertilityField.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "FertilityField.h" + +#include "Map.h" + +#include +#include +#include +#include + +namespace +{ + const int KERNEL_RADIUS = 15; + const int KERNEL_WIDTH = 31; + const int BOX_WIDTH = 16; + const std::uint32_t OFFSET_WEIGHT[KERNEL_WIDTH] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 + }; +} + +namespace Fertility +{ + +int Field::wx(int x, int offset) const +{ + return wrappedX[(offset + KERNEL_WIDTH) * width + x]; +} + +int Field::wy(int y, int offset) const +{ + return wrappedY[(offset + KERNEL_WIDTH) * height + y]; +} + +void Field::buildWrappedIndexes() +{ + // The rolling boxes reach +/-16; the mirrored sand lookups reach +/-30. + wrappedX.resize((KERNEL_WIDTH * 2 + 1) * width); + wrappedY.resize((KERNEL_WIDTH * 2 + 1) * height); + for (int offset = -KERNEL_WIDTH; offset <= KERNEL_WIDTH; ++offset) + { + for (int x = 0; x < width; ++x) + wrappedX[(offset + KERNEL_WIDTH) * width + x] = (x + offset % width + width) % width; + for (int y = 0; y < height; ++y) + wrappedY[(offset + KERNEL_WIDTH) * height + y] = (y + offset % height + height) % height; + } +} + +void Field::buildWaterConvolution(const std::vector& water) +{ + const int size = width * height; + first.assign(size, 0); + second.assign(size, 0); + fertility.assign(size, 0); + + // A length-16 forward box followed by a length-16 backward box is exactly the + // triangular 16-|offset| kernel. Four linear passes instead of 961 taps per tile. + for (int y = 0; y < height; ++y) + { + std::uint32_t sum = 0; + for (int dx = 0; dx < BOX_WIDTH; ++dx) + sum += water[y * width + wx(0, dx)]; + for (int x = 0; x < width; ++x) + { + first[y * width + x] = sum; + sum -= water[y * width + x]; + sum += water[y * width + wx(x, BOX_WIDTH)]; + } + } + for (int y = 0; y < height; ++y) + { + std::uint32_t sum = 0; + for (int dx = -KERNEL_RADIUS; dx <= 0; ++dx) + sum += first[y * width + wx(0, dx)]; + for (int x = 0; x < width; ++x) + { + second[y * width + x] = sum; + sum -= first[y * width + wx(x, -KERNEL_RADIUS)]; + sum += first[y * width + wx(x, 1)]; + } + } + for (int x = 0; x < width; ++x) + { + std::uint32_t sum = 0; + for (int dy = 0; dy < BOX_WIDTH; ++dy) + sum += second[wy(0, dy) * width + x]; + for (int y = 0; y < height; ++y) + { + first[y * width + x] = sum; + sum -= second[y * width + x]; + sum += second[wy(y, BOX_WIDTH) * width + x]; + } + } + for (int x = 0; x < width; ++x) + { + std::uint32_t sum = 0; + for (int dy = -KERNEL_RADIUS; dy <= 0; ++dy) + sum += first[wy(0, dy) * width + x]; + for (int y = 0; y < height; ++y) + { + fertility[y * width + x] = sum; + sum -= first[wy(y, -KERNEL_RADIUS) * width + x]; + sum += first[wy(y, 1) * width + x]; + } + } +} + +void Field::rebuild(int newWidth, int newHeight, const std::vector& water, + const std::vector& sand, Path requestedPath) +{ + assert(newWidth > 0 && newHeight > 0); + assert(water.size() == size_t(newWidth) * newHeight); + assert(sand.size() == water.size()); + width = newWidth; + height = newHeight; + waterTiles = 0; + sandTiles = 0; + for (size_t i = 0; i < water.size(); ++i) + { + waterTiles += water[i] != 0; + sandTiles += sand[i] != 0; + } + buildWrappedIndexes(); + usedPath = requestedPath; + if (usedPath == Path::Adaptive) + { + // The convolution is four passes over the map; each splat is a 31x31 stamp. + const std::uint32_t sandCost = std::uint32_t(4 * water.size()) + 961u * std::uint32_t(sandTiles); + const std::uint32_t waterCost = 961u * std::uint32_t(waterTiles); + usedPath = sandCost <= waterCost ? Path::SandCorrection : Path::WaterSplat; + } + + if (usedPath == Path::SandCorrection) + { + buildWaterConvolution(water); + // Every (water, sand) pair straddling a tile removes the weight the convolution + // credited it, so each subtraction cancels a contribution that was really added. + for (int sy = 0; sy < height; ++sy) + { + for (int sx = 0; sx < width; ++sx) + { + if (!sand[sy * width + sx]) + continue; + const int* const xWrap = &wrappedX[KERNEL_WIDTH * width + sx]; + const int* const yWrap = &wrappedY[KERNEL_WIDTH * height + sy]; + for (int dy = -KERNEL_RADIUS; dy <= KERNEL_RADIUS; ++dy) + { + const std::uint32_t yWeight = OFFSET_WEIGHT[dy + KERNEL_RADIUS]; + std::uint32_t* const targetRow = &fertility[yWrap[dy * height] * width]; + const std::uint8_t* const waterRow = &water[yWrap[2 * dy * height] * width]; + for (int dx = -KERNEL_RADIUS; dx <= KERNEL_RADIUS; ++dx) + if (waterRow[xWrap[2 * dx * width]]) + targetRow[xWrap[dx * width]] -= + yWeight * OFFSET_WEIGHT[dx + KERNEL_RADIUS]; + } + } + } + } + else + { + fertility.assign(size_t(width) * height, 0); + for (int waterY = 0; waterY < height; ++waterY) + { + for (int waterX = 0; waterX < width; ++waterX) + { + if (!water[waterY * width + waterX]) + continue; + const int* const xWrap = &wrappedX[KERNEL_WIDTH * width + waterX]; + const int* const yWrap = &wrappedY[KERNEL_WIDTH * height + waterY]; + for (int dy = -KERNEL_RADIUS; dy <= KERNEL_RADIUS; ++dy) + { + const std::uint32_t yWeight = OFFSET_WEIGHT[dy + KERNEL_RADIUS]; + std::uint32_t* const targetRow = &fertility[yWrap[-dy * height] * width]; + const std::uint8_t* const sandRow = &sand[yWrap[-2 * dy * height] * width]; + for (int dx = -KERNEL_RADIUS; dx <= KERNEL_RADIUS; ++dx) + if (!sandRow[xWrap[-2 * dx * width]]) + targetRow[xWrap[-dx * width]] += + yWeight * OFFSET_WEIGHT[dx + KERNEL_RADIUS]; + } + } + } + } +} + +void Field::gate(const std::vector& keep) +{ + assert(keep.size() == fertility.size()); + for (size_t i = 0; i < fertility.size(); ++i) + if (!keep[i]) + fertility[i] = 0; +} + +std::uint32_t Field::at(int x, int y) const +{ + assert(fertility.size() == size_t(width) * height); + if (x < 0 || x >= width) x = (x % width + width) % width; + if (y < 0 || y >= height) y = (y % height + height) % height; + return fertility[y * width + x]; +} + +namespace +{ + /// 8-connected over grass from every takeable wheat or wood tile. + std::vector depositReach(const Map& map) + { + const int w = map.getW(), h = map.getH(); + std::vector reached(size_t(w) * h, 0); + std::queue> frontier; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (map.isResourceTakeable(x, y, CORN) || map.isResourceTakeable(x, y, WOOD)) + { + reached[size_t(y) * w + x] = 1; + frontier.emplace(x, y); + } + while (!frontier.empty()) + { + const auto [px, py] = frontier.front(); + frontier.pop(); + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(px + dx), ny = map.normalizeY(py + dy); + std::uint8_t& cell = reached[size_t(ny) * w + nx]; + if (!cell && map.isGrass(nx, ny)) + { + cell = 1; + frontier.emplace(nx, ny); + } + } + } + return reached; + } +} + +Field forMap(const Map& map, bool gateOnReachableDeposits) +{ + const int w = map.getW(), h = map.getH(); + std::vector water(size_t(w) * h, 0), sand(size_t(w) * h, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + water[size_t(y) * w + x] = map.isWater(x, y); + sand[size_t(y) * w + x] = map.isSand(x, y); + } + Field field; + field.rebuild(w, h, water, sand); + if (gateOnReachableDeposits) + { + std::vector keep = depositReach(map); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (!map.isGrass(x, y)) + keep[size_t(y) * w + x] = 0; + field.gate(keep); + } + return field; +} + +std::uint32_t usefulExpansionCapacity(std::uint32_t fertility, int amount, + int availableNeighbors, bool wheat) +{ + amount = std::max(0, std::min(8, amount)); + availableNeighbors = std::max(0, std::min(8, availableNeighbors)); + // amount/8 * neighbours/8, and wheat only clears CORN_GROWTH_DIVISOR one time in three. + const std::uint32_t divisor = wheat ? 192u : 64u; + return fertility * std::uint32_t(amount) * std::uint32_t(availableNeighbors) / divisor; +} + +bool withinPercentBand(std::uint32_t fertility, int minimumPercent, int maximumPercent) +{ + minimumPercent = std::max(0, std::min(100, minimumPercent)); + maximumPercent = std::max(0, std::min(100, maximumPercent)); + if (minimumPercent > maximumPercent) + std::swap(minimumPercent, maximumPercent); + const std::uint64_t scaled = std::uint64_t(fertility) * 100u; + return scaled >= std::uint64_t(minimumPercent) * kScale + && scaled <= std::uint64_t(maximumPercent) * kScale; +} + +} diff --git a/src/map/FertilityField.h b/src/map/FertilityField.h new file mode 100644 index 000000000..d7ab110cf --- /dev/null +++ b/src/map/FertilityField.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#pragma once + +#include +#include + +class Map; + +/// Exact per-tile growth probability for the resources whose spread is limited by +/// nearby underground, scaled by kScale. +/// +/// Map::growResources decides whether a wheat or wood tile expands with +/// +/// dwax = (syncRand()&0xF) - (syncRand()&0xF); // likewise dway +/// expand = isWater(x+dwax, y+dway) && !isSand(x-dwax, y-dway); +/// +/// The difference of two uniform draws from {0..15} has PMF (16-|d|)/256, so the +/// chance a tile expands is the triangular kernel below summed over every water tile +/// whose mirror through the tile is not sand. That sum is what this computes, in +/// closed form, over the whole map. +namespace Fertility +{ + /// A tile surrounded entirely by qualifying water reaches exactly this. + constexpr std::uint32_t kScale = 65536; + + enum class Path + { + Adaptive, ///< whichever of the two below costs less on this map + SandCorrection, ///< convolve water, then subtract the sand-blocked pairs + WaterSplat ///< accumulate straight from the water tiles + }; + + /// Masks are row-major y*width+x. Toroidal, matching the engine's wrapping. + class Field + { + public: + void rebuild(int width, int height, const std::vector& water, + const std::vector& sand, Path path = Path::Adaptive); + + /// Zeroes every tile the mask does not keep. Masks are row-major y*width+x. + void gate(const std::vector& keep); + + std::uint32_t at(int x, int y) const; + const std::vector& values() const { return fertility; } + int getW() const { return width; } + int getH() const { return height; } + Path pathUsed() const { return usedPath; } + int waterCount() const { return waterTiles; } + int sandCount() const { return sandTiles; } + + private: + int width = 0, height = 0; + int waterTiles = 0, sandTiles = 0; + Path usedPath = Path::Adaptive; + std::vector fertility, first, second; + std::vector wrappedX, wrappedY; + int wx(int x, int offset) const; + int wy(int y, int offset) const; + void buildWrappedIndexes(); + void buildWaterConvolution(const std::vector& water); + }; + + /// Builds the masks from the undermap and computes the field. Tiles that no wheat + /// or wood deposit can reach over grass score 0 whatever their surroundings: growth + /// spreads from an existing deposit, so a tile none can reach never grows anything. + Field forMap(const Map& map, bool gateOnReachableDeposits = true); + + /// Throughput a tile can actually sustain: the growth chance, the deposit already + /// there and the room it has to spread into, all of which must be non-zero to matter. + std::uint32_t usefulExpansionCapacity(std::uint32_t fertility, int amount, + int availableNeighbors, bool wheat); + + bool withinPercentBand(std::uint32_t fertility, int minimumPercent, int maximumPercent); +} diff --git a/test/FertilityFieldTest.cpp b/test/FertilityFieldTest.cpp new file mode 100644 index 000000000..4eb109adb --- /dev/null +++ b/test/FertilityFieldTest.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 glob2 contributors + +#include "FertilityFieldTest.h" + +#include "Map.h" +#include "map/FertilityField.h" + +#include +#include +#include + +CPPUNIT_TEST_SUITE_REGISTRATION( FertilityFieldTest ); + +namespace +{ + using Mask = std::vector; + + int wrap(int v, int n) { return (v % n + n) % n; } + + /// The weight Map::growResources gives an offset: the number of (a,b) pairs in + /// {0..15} with a-b == offset, which is 16-|offset|. + std::uint32_t weight(int offset) { return offset >= 16 || offset <= -16 ? 0 : 16 - std::abs(offset); } + + /// growResources' test, evaluated tap by tap: water at the offset, and the tile + /// mirrored through the candidate not sand. + std::uint32_t directKernel(const Mask& water, const Mask& sand, int w, int h, int x, int y) + { + std::uint32_t total = 0; + for (int dy = -15; dy <= 15; ++dy) + for (int dx = -15; dx <= 15; ++dx) + { + const size_t towards = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); + const size_t away = size_t(wrap(y - dy, h)) * w + wrap(x - dx, w); + if (water[towards] && !sand[away]) + total += weight(dx) * weight(dy); + } + return total; + } + + void assertMatchesDirect(const Mask& water, const Mask& sand, int w, int h, + Fertility::Path path) + { + Fertility::Field field; + field.rebuild(w, h, water, sand, path); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + CPPUNIT_ASSERT_EQUAL(directKernel(water, sand, w, h, x, y), field.at(x, y)); + } + + /// Deterministic so a failure is reproducible; syncRand is unavailable here anyway. + struct Lcg + { + std::uint32_t state; + explicit Lcg(std::uint32_t seed): state(seed) {} + std::uint32_t next() { state = state * 1664525u + 1013904223u; return state >> 16; } + bool chance(int percent) { return int(next() % 100) < percent; } + }; + + void fillRandom(Mask& water, Mask& sand, int w, int h, std::uint32_t seed, + int waterPercent, int sandPercent) + { + Lcg rng(seed); + water.assign(size_t(w) * h, 0); + sand.assign(size_t(w) * h, 0); + for (size_t i = 0; i < water.size(); ++i) + { + water[i] = rng.chance(waterPercent); + if (!water[i]) + sand[i] = rng.chance(sandPercent); + } + } + + constexpr int kMapDec = 5; // 32x32 + + // Minimal Map for the forMap() gate, following the MapQueryTest pattern: setSize() + // would allocate a real Sector[] and drag most of the game into the link. + struct TinyMap : Map + { + TinyMap() + { + wDec = hDec = kMapDec; + w = h = 1 << kMapDec; + wMask = hMask = w - 1; + size = size_t(w) * h; + tiles.assign(size, Tile()); + } + ~TinyMap() { w = h = wMask = hMask = wDec = hDec = 0; size = 0; } + + void makeWater(int x, int y) { tiles[coordToIndex(x, y)].terrain = 256; } + void makeSand(int x, int y) { tiles[coordToIndex(x, y)].terrain = 128; } + void putResource(int x, int y, int type) + { + Resource& r = tiles[coordToIndex(x, y)].resource; + r.type = type; + r.amount = 1; + } + }; +} + +void FertilityFieldTest::testTriangularWeightsMatchGrowResourcesRng() +{ + // growResources draws dwax = (syncRand()&0xF) - (syncRand()&0xF). Enumerating both + // draws gives the kernel the field is built from. + std::array histogram{}; + for (int a = 0; a <= 15; ++a) + for (int b = 0; b <= 15; ++b) + ++histogram[a - b + 15]; + for (int offset = -15; offset <= 15; ++offset) + CPPUNIT_ASSERT_EQUAL(int(weight(offset)), histogram[offset + 15]); + + // Both axes together span exactly the scale the field is expressed in. + std::uint32_t total = 0; + for (int dy = -15; dy <= 15; ++dy) + for (int dx = -15; dx <= 15; ++dx) + total += weight(dx) * weight(dy); + CPPUNIT_ASSERT_EQUAL(Fertility::kScale, total); +} + +void FertilityFieldTest::testConvolutionMatchesDirectKernelWithoutSand() +{ + Mask water, sand; + fillRandom(water, sand, 32, 32, 7u, 30, 0); + assertMatchesDirect(water, sand, 32, 32, Fertility::Path::SandCorrection); + assertMatchesDirect(water, sand, 32, 32, Fertility::Path::WaterSplat); +} + +void FertilityFieldTest::testSandCorrectionPathMatchesDirectKernel() +{ + Mask water, sand; + for (std::uint32_t seed : {1u, 2u, 3u}) + { + fillRandom(water, sand, 32, 32, seed, 25, 40); + assertMatchesDirect(water, sand, 32, 32, Fertility::Path::SandCorrection); + } +} + +void FertilityFieldTest::testWaterSplatPathMatchesDirectKernel() +{ + Mask water, sand; + for (std::uint32_t seed : {1u, 2u, 3u}) + { + fillRandom(water, sand, 32, 32, seed, 25, 40); + assertMatchesDirect(water, sand, 32, 32, Fertility::Path::WaterSplat); + } +} + +void FertilityFieldTest::testAdaptivePathMatchesExplicitPaths() +{ + Mask water, sand; + // Little water, much sand, and the reverse: the cost rule should pick a different + // path for each, and both must land on the same numbers. + for (auto [waterPercent, sandPercent] : {std::pair{5, 60}, std::pair{60, 5}}) + { + fillRandom(water, sand, 32, 32, 11u, waterPercent, sandPercent); + Fertility::Field adaptive, correction, splat; + adaptive.rebuild(32, 32, water, sand, Fertility::Path::Adaptive); + correction.rebuild(32, 32, water, sand, Fertility::Path::SandCorrection); + splat.rebuild(32, 32, water, sand, Fertility::Path::WaterSplat); + CPPUNIT_ASSERT(adaptive.pathUsed() != Fertility::Path::Adaptive); + CPPUNIT_ASSERT(correction.values() == splat.values()); + CPPUNIT_ASSERT(adaptive.values() == correction.values()); + } +} + +void FertilityFieldTest::testOpenWaterReachesFullScale() +{ + const Mask water(32 * 32, 1), sand(32 * 32, 0); + Fertility::Field field; + field.rebuild(32, 32, water, sand, Fertility::Path::SandCorrection); + for (int y = 0; y < 32; ++y) + for (int x = 0; x < 32; ++x) + CPPUNIT_ASSERT_EQUAL(Fertility::kScale, field.at(x, y)); +} + +void FertilityFieldTest::testSandOppositeWaterRemovesCredit() +{ + // One water tile three east of the centre contributes weight(3)*weight(0) = 13*16. + Mask water(32 * 32, 0), sand(32 * 32, 0); + water[size_t(16) * 32 + 19] = 1; + Fertility::Field bare; + bare.rebuild(32, 32, water, sand, Fertility::Path::SandCorrection); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(13 * 16), bare.at(16, 16)); + + // Sand on the far side of the centre from that water cancels it, and only there. + sand[size_t(16) * 32 + 13] = 1; + Fertility::Field corrected; + corrected.rebuild(32, 32, water, sand, Fertility::Path::SandCorrection); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), corrected.at(16, 16)); + CPPUNIT_ASSERT_EQUAL(bare.at(17, 16), corrected.at(17, 16)); +} + +void FertilityFieldTest::testNonPowerOfTwoDimensionsWrap() +{ + // The field is used at generation time too, where dimensions need not be masks. + Mask water, sand; + fillRandom(water, sand, 23, 17, 5u, 25, 35); + assertMatchesDirect(water, sand, 23, 17, Fertility::Path::SandCorrection); + assertMatchesDirect(water, sand, 23, 17, Fertility::Path::WaterSplat); +} + +void FertilityFieldTest::testForMapZeroesNonGrass() +{ + TinyMap map; + map.makeWater(4, 4); + map.makeSand(6, 6); + map.putResource(10, 10, CORN); + const Fertility::Field field = Fertility::forMap(map); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), field.at(4, 4)); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), field.at(6, 6)); + CPPUNIT_ASSERT(field.at(5, 4) > 0u); +} + +void FertilityFieldTest::testForMapZeroesGrassNoDepositReaches() +{ + // An island of grass ringed by water, with the only deposit outside the ring. + TinyMap map; + for (int d = -2; d <= 2; ++d) + { + map.makeWater(14 + d, 14 - 2); + map.makeWater(14 + d, 14 + 2); + map.makeWater(14 - 2, 14 + d); + map.makeWater(14 + 2, 14 + d); + } + map.putResource(25, 25, CORN); + const Fertility::Field field = Fertility::forMap(map); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), field.at(14, 14)); + CPPUNIT_ASSERT(field.at(25, 25) > 0u); +} + +void FertilityFieldTest::testForMapUngatedKeepsUnreachableGrass() +{ + TinyMap map; + map.makeWater(4, 4); + // No deposit anywhere: the gated field is empty, the ungated one is not. + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), Fertility::forMap(map).at(5, 4)); + CPPUNIT_ASSERT(Fertility::forMap(map, false).at(5, 4) > 0u); +} + +void FertilityFieldTest::testUsefulExpansionCapacityFolds() +{ + // Wood: fertility * amount/8 * neighbours/8. Wheat additionally clears + // CORN_GROWTH_DIVISOR only one time in three. + CPPUNIT_ASSERT_EQUAL(std::uint32_t(65536), Fertility::usefulExpansionCapacity(65536, 8, 8, false)); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(65536 / 3), Fertility::usefulExpansionCapacity(65536, 8, 8, true)); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), Fertility::usefulExpansionCapacity(65536, 0, 8, false)); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), Fertility::usefulExpansionCapacity(65536, 8, 0, false)); + // Out-of-range inputs clamp rather than overflow the fold. + CPPUNIT_ASSERT_EQUAL(Fertility::usefulExpansionCapacity(1024, 8, 8, false), + Fertility::usefulExpansionCapacity(1024, 99, 99, false)); + CPPUNIT_ASSERT_EQUAL(std::uint32_t(0), Fertility::usefulExpansionCapacity(1024, -5, 8, false)); +} + +void FertilityFieldTest::testWithinPercentBand() +{ + CPPUNIT_ASSERT(Fertility::withinPercentBand(Fertility::kScale / 2, 40, 60)); + CPPUNIT_ASSERT(!Fertility::withinPercentBand(Fertility::kScale / 2, 60, 90)); + CPPUNIT_ASSERT(Fertility::withinPercentBand(0, 0, 10)); + // Inclusive at both ends, and a reversed band is read as written. + CPPUNIT_ASSERT(Fertility::withinPercentBand(Fertility::kScale, 100, 100)); + CPPUNIT_ASSERT(Fertility::withinPercentBand(Fertility::kScale / 2, 60, 40)); +} diff --git a/test/FertilityFieldTest.h b/test/FertilityFieldTest.h new file mode 100644 index 000000000..4e6e9f183 --- /dev/null +++ b/test/FertilityFieldTest.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 glob2 contributors + +#pragma once + +#include + +// Fertility::Field computes, in closed form, the probability that Map::growResources +// lets a wheat or wood tile expand. These tests pin it to that definition: the kernel +// against the RNG that produces it, and the separable four-pass convolution (plus both +// sand paths) against a direct 961-tap evaluation. +class FertilityFieldTest: public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE( FertilityFieldTest ); + CPPUNIT_TEST( testTriangularWeightsMatchGrowResourcesRng ); + CPPUNIT_TEST( testConvolutionMatchesDirectKernelWithoutSand ); + CPPUNIT_TEST( testSandCorrectionPathMatchesDirectKernel ); + CPPUNIT_TEST( testWaterSplatPathMatchesDirectKernel ); + CPPUNIT_TEST( testAdaptivePathMatchesExplicitPaths ); + CPPUNIT_TEST( testOpenWaterReachesFullScale ); + CPPUNIT_TEST( testSandOppositeWaterRemovesCredit ); + CPPUNIT_TEST( testNonPowerOfTwoDimensionsWrap ); + CPPUNIT_TEST( testForMapZeroesNonGrass ); + CPPUNIT_TEST( testForMapZeroesGrassNoDepositReaches ); + CPPUNIT_TEST( testForMapUngatedKeepsUnreachableGrass ); + CPPUNIT_TEST( testUsefulExpansionCapacityFolds ); + CPPUNIT_TEST( testWithinPercentBand ); + CPPUNIT_TEST_SUITE_END(); + +public: + void testTriangularWeightsMatchGrowResourcesRng(); + void testConvolutionMatchesDirectKernelWithoutSand(); + void testSandCorrectionPathMatchesDirectKernel(); + void testWaterSplatPathMatchesDirectKernel(); + void testAdaptivePathMatchesExplicitPaths(); + void testOpenWaterReachesFullScale(); + void testSandOppositeWaterRemovesCredit(); + void testNonPowerOfTwoDimensionsWrap(); + void testForMapZeroesNonGrass(); + void testForMapZeroesGrassNoDepositReaches(); + void testForMapUngatedKeepsUnreachableGrass(); + void testUsefulExpansionCapacityFolds(); + void testWithinPercentBand(); +}; diff --git a/test/SConstruct b/test/SConstruct index c2ad41b84..7ce30bb40 100644 --- a/test/SConstruct +++ b/test/SConstruct @@ -82,6 +82,9 @@ GradientBFSTest.cpp MapQueryTest.cpp GradientTest.cpp FetchApportionmentTest.cpp + +FertilityFieldTest.cpp +../src/map/FertilityField.cpp MapQueryTestStubs.cpp ../src/map/Map.cpp ../src/map/gradient/MapGradientField.cpp From 67bbc03c345244b0485bf642e6d0f4c048ef9097 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 01:03:50 +0000 Subject: [PATCH 014/136] Score the start each colony actually got, not just its walk Placement equalises walking distance to wood and wheat, and on four of these generators that is now down to two tiles for the worst-served colony. It is also nearly all the metric has left to give: sampling twenty maps and keeping the fairest shaves two tenths of a tile off a gap of one. Distance was always a thin account of a starting position. A deposit at the door that runs dry, a colony with no room to build, one boxed between two rivals, and one on ground where wheat never grows back all look alike to it. StartQuality measures six things per colony on the finished map -- where the swarm is built, its clearing cleared and the workers standing where they will actually start walking -- and folds them into one number per map: the weakest colony's quality, gated by how evenly the map shared quality out, worst * (worst/best)^k. Fertility carries the most weight of the six because it is the one that decides whether a colony's wheat comes back at all; the others describe what it starts with, fertility describes what it keeps. Every factor is normalised against a fixed reference rather than the map's own best colony, because the point is to rank candidate maps against each other and a per-map normalisation scores every map alike. The two distances use the viability bars the study tool already scores against; the other four have no such bar, so they sit near the ninetieth percentile of what the twelve generators actually produce, measured over 2400 maps. Scoring costs 2.5-5.4ms against 11-61ms to generate, consumes no random stream, and never rejects a map -- it ranks. Map hashes are unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Rc6FTwBEB6LKNUmamX9wWW --- src/SConscript | 1 + src/map/generator/core/GenerationResult.h | 3 + src/map/generator/core/GenerationService.cpp | 2 + src/map/generator/shared/StartQuality.cpp | 184 +++++++++++++++++++ src/map/generator/shared/StartQuality.h | 67 +++++++ test/MapGeneratorStudy.cpp | 24 +++ 6 files changed, 281 insertions(+) create mode 100644 src/map/generator/shared/StartQuality.cpp create mode 100644 src/map/generator/shared/StartQuality.h diff --git a/src/SConscript b/src/SConscript index af7d91dc9..aa69f51c2 100644 --- a/src/SConscript +++ b/src/SConscript @@ -198,6 +198,7 @@ map/generator/shared/Settlements.cpp map/generator/shared/Regions.cpp map/generator/shared/Resources.cpp map/generator/shared/StartingPositions.cpp +map/generator/shared/StartQuality.cpp map/generator/shared/Terrain.cpp map/gradient/MapGradientArea.cpp map/gradient/MapGradientBuilding.cpp diff --git a/src/map/generator/core/GenerationResult.h b/src/map/generator/core/GenerationResult.h index 2aaa7d73a..812aa9b8f 100644 --- a/src/map/generator/core/GenerationResult.h +++ b/src/map/generator/core/GenerationResult.h @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include "StartQuality.h" #include #include #include @@ -24,6 +25,8 @@ struct GenerationResult std::string stage; GenerationError error = GenerationError::None; std::string detail; + /// How good a start each colony got, and how evenly. Only filled on success. + MapGeneration::StartQualityReport quality; explicit operator bool() const { return error == GenerationError::None; } std::string diagnostic() const; }; diff --git a/src/map/generator/core/GenerationService.cpp b/src/map/generator/core/GenerationService.cpp index 54f2fc113..8b036e5f8 100644 --- a/src/map/generator/core/GenerationService.cpp +++ b/src/map/generator/core/GenerationService.cpp @@ -3,6 +3,7 @@ #include "Game.h" #include "GenerationContext.h" #include "GenerationValidation.h" +#include "StartQuality.h" #include "Utilities.h" std::string GenerationResult::diagnostic() const { @@ -81,6 +82,7 @@ GenerationResult GenerationService::generate(Game &game, const GenerationRequest return result; } } + result.quality = MapGeneration::scoreStarts(game, request.nbTeams); result.stage = "script"; const auto script = game.sgslScript.compileScript(&game); if (script.type != ErrorReport::ET_OK) diff --git a/src/map/generator/shared/StartQuality.cpp b/src/map/generator/shared/StartQuality.cpp new file mode 100644 index 000000000..a24fd3a0e --- /dev/null +++ b/src/map/generator/shared/StartQuality.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "StartQuality.h" +#include "FertilityField.h" +#include "Game.h" +#include "Map.h" +#include "Unit.h" +#include +#include +#include + +namespace MapGeneration +{ +namespace +{ +double clampUnit(double v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } + +/// Distance in walking steps from a colony's starting workers to every tile it can reach. +/// Workers, not the boot tile: the swarm occupies the boot tile and nobody walks out of it. +std::vector walkFromWorkers(const Map &map, const std::vector &workers) +{ + const int w = map.getW(), h = map.getH(); + std::vector dist(size_t(w) * h, -1); + std::queue q; + for (int p : workers) + if (dist[p] < 0) + { + dist[p] = 0; + q.push(p); + } + while (!q.empty()) + { + const int p = q.front(); + q.pop(); + const int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const int np = ny * w + nx; + if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + dist[np] = dist[p] + 1; + q.push(np); + } + } + } + return dist; +} +} // namespace + +StartQualityReport scoreStarts(const Game &game, int requestedTeams, + const StartQualityWeights &weights, const StartQualityScale &scale) +{ + StartQualityReport report; + const Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const int nbTeams = std::min(game.teamsCount(), requestedTeams); + if (nbTeams <= 0) + return report; + + std::vector> workers(nbTeams); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid == NOGUID) + continue; + const int team = Unit::GIDtoTeam(gid); + if (team >= 0 && team < nbTeams) + workers[team].push_back(y * w + x); + } + for (const auto &team : workers) + if (team.empty()) + return report; // nothing walked out of this colony; there is nothing to score + + const Fertility::Field fertility = Fertility::forMap(map); + report.colonies.resize(nbTeams); + + for (int team = 0; team < nbTeams; ++team) + { + ColonyQuality &colony = report.colonies[team]; + const std::vector dist = walkFromWorkers(map, workers[team]); + + for (int p = 0; p < w * h; ++p) + { + if (dist[p] < 0 || dist[p] > scale.catchmentSteps) + continue; + const int x = p % w, y = p / w; + ++colony.catchmentTiles; + colony.meanFertility += fertility.at(x, y); + if (map.isFreeForBuilding(x, y, 4, 4)) + ++colony.buildSites; + } + if (colony.catchmentTiles) + colony.meanFertility /= colony.catchmentTiles; + + // A deposit counts once however many catchment tiles touch it; workers gather from + // beside a deposit, so what matters is that the colony can stand next to it at all. + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Resource &resource = map.getResource(x, y); + if (resource.type != CORN && resource.type != WOOD) + continue; + int nearest = -1; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int d = dist[map.normalizeY(y + dy) * w + map.normalizeX(x + dx)]; + if (d >= 0 && (nearest < 0 || d < nearest)) + nearest = d; + } + if (nearest < 0) + continue; + const int reach = nearest + 1; + int &best = resource.type == CORN ? colony.wheatDistance : colony.woodDistance; + if (best < 0 || reach < best) + best = reach; + if (nearest <= scale.catchmentSteps) + colony.resourceAmount += resource.amount; + } + + for (int rival = 0; rival < nbTeams; ++rival) + { + if (rival == team) + continue; + int nearest = -1; + for (int p : workers[rival]) + if (dist[p] >= 0 && (nearest < 0 || dist[p] < nearest)) + nearest = dist[p]; + if (nearest < 0) + continue; // no land route to this rival, which is isolation rather than threat + if (colony.rivalDistance < 0 || nearest < colony.rivalDistance) + colony.rivalDistance = nearest; + if (nearest <= scale.threatRadius) + ++colony.rivalsWithinThreat; + } + + colony.wheat = colony.wheatDistance < 0 + ? 0 + : clampUnit(1.0 - double(colony.wheatDistance) / scale.wheatReference); + colony.wood = colony.woodDistance < 0 + ? 0 + : clampUnit(1.0 - double(colony.woodDistance) / scale.woodReference); + colony.fertility = clampUnit(colony.meanFertility / scale.fertilityReference); + colony.depth = clampUnit(double(colony.resourceAmount) / scale.depthReference); + colony.room = clampUnit(double(colony.buildSites) / scale.roomReference); + // No reachable rival is the safest a colony can be; being one of several crowded into + // the same neighbourhood is the case the raw distance alone does not describe. + const double spacing = colony.rivalDistance < 0 + ? 1.0 + : clampUnit(double(colony.rivalDistance) / scale.isolationReference); + const int crowd = std::max(0, colony.rivalsWithinThreat - 1); + colony.isolation = spacing * std::max(0.0, 1.0 - scale.crowdPenalty * crowd); + + const double sum = weights.wheat + weights.wood + weights.fertility + weights.depth + + weights.room + weights.isolation; + colony.total = sum <= 0 ? 0 + : (weights.wheat * colony.wheat + weights.wood * colony.wood + + weights.fertility * colony.fertility + weights.depth * colony.depth + + weights.room * colony.room + weights.isolation * colony.isolation) / + sum; + // A colony that cannot reach one of its primary resources has not got a start at all, + // whatever room and fertility it was given. + if (colony.wheatDistance < 0 || colony.woodDistance < 0) + colony.total = 0; + } + + report.worst = report.best = report.colonies[0].total; + for (const ColonyQuality &colony : report.colonies) + { + report.worst = std::min(report.worst, colony.total); + report.best = std::max(report.best, colony.total); + } + report.fairness = report.best > 0 ? report.worst / report.best : 0; + report.score = report.worst * std::pow(report.fairness, scale.fairnessExponent); + report.measured = true; + return report; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/StartQuality.h b/src/map/generator/shared/StartQuality.h new file mode 100644 index 000000000..67e8eaec9 --- /dev/null +++ b/src/map/generator/shared/StartQuality.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +class Game; + +namespace MapGeneration +{ +// How good a start each colony actually got, and how evenly the map shared that out. +// +// Walking distance to wood and wheat is what placement optimises, but it is a thin account of +// a starting position: a deposit at the door that runs dry, a colony with no room to build, +// one boxed between two rivals, and one on ground where wheat never grows back all look alike +// to it. These factors are measured on the finished map, where the swarm is built, its +// clearing is cleared and the workers are standing where they will actually start walking. +struct StartQualityWeights +{ + double wheat = 0.22, wood = 0.22, fertility = 0.25, depth = 0.12, room = 0.10, + isolation = 0.09; +}; + +// Every factor is normalised against an absolute reference, never against the map's own best +// colony: candidate maps are ranked against each other, and a per-map normalisation would +// score every map alike. The two distances are what a colony needs to be viable, the same +// bars the study tool scores against. The other four have no such bar, so they are set near +// the ninetieth percentile of what the generators actually produce: high enough that a good +// colony is not capped at the same 1.0 as a great one, low enough to separate the poor ones. +struct StartQualityScale +{ + int catchmentSteps = 24; ///< walking steps a young colony works within + int wheatReference = 24; ///< beyond this a colony is not viably fed + int woodReference = 32; ///< beyond this a colony is not viably supplied + int fertilityReference = 8000; ///< growth probability, on Fertility::kScale + int depthReference = 450; ///< wheat and wood amount within the catchment + int roomReference = 900; ///< swarm-sized building sites within the catchment + int isolationReference = 60; ///< walking steps to the nearest rival colony + int threatRadius = 40; ///< rivals nearer than this crowd a colony + double crowdPenalty = 0.15; ///< per rival past the first inside that radius + double fairnessExponent = 1.0; ///< 0 is pure maximin, large is pure fairness +}; + +struct ColonyQuality +{ + // As measured. + int wheatDistance = -1, woodDistance = -1; + int catchmentTiles = 0, buildSites = 0, resourceAmount = 0; + int rivalDistance = -1, rivalsWithinThreat = 0; + double meanFertility = 0; + // Normalised to [0,1]. + double wheat = 0, wood = 0, fertility = 0, depth = 0, room = 0, isolation = 0; + /// Weighted mean of the six, or 0 outright when wheat or wood cannot be reached at all. + double total = 0; +}; + +struct StartQualityReport +{ + std::vector colonies; + double worst = 0, best = 0; + double fairness = 0; ///< worst/best, 1 when a map has a single colony + double score = 0; ///< worst * pow(fairness, fairnessExponent) + bool measured = false; +}; + +/// Requires a finished map: colonies built, workers placed, resources final. +StartQualityReport scoreStarts(const Game &game, int nbTeams, + const StartQualityWeights &weights = {}, + const StartQualityScale &scale = {}); +} // namespace MapGeneration diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 8f7a27a4f..9cf21812d 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -9,6 +9,7 @@ #include "MapGenerator.h" #include "Race.h" #include "Unit.h" +#include "StartQuality.h" #include "Utilities.h" #include #include @@ -80,6 +81,7 @@ int main(int argc, char **argv) descriptor.seed = seed; bool tuning = false; bool headroom = false; + bool quality = false; std::string dump; std::map aliases = {{"smooth", "smoothing"}, {"craters", "lake-density"}, @@ -99,6 +101,8 @@ int main(int argc, char **argv) tuning = true; else if (arg == "headroom") headroom = true; + else if (arg == "quality") + quality = true; else if (arg == "preset") { } // All new requests start at registered defaults. @@ -408,6 +412,26 @@ int main(int argc, char **argv) actualValid ? actualHi - actualLo : -1, actualValid ? actualLo : -1, actualValid ? actualHi : -1, bestSpread, bestLo, bestHi, nbTeams); } + if (quality) + { + const auto &q = result.quality; + // The service already scored this map. Scoring it again times what that costs, which + // is what a sampling caller pays per candidate on top of generating it. + const auto scoreStart = std::chrono::steady_clock::now(); + MapGeneration::scoreStarts(game, descriptor.nbTeams); + const double scoreSeconds = + std::chrono::duration(std::chrono::steady_clock::now() - scoreStart).count(); + std::printf("QUALITY,%d,%.6f,%.6f,%.6f,%.6f,%d,%.6f\n", q.measured, q.score, q.fairness, + q.worst, q.best, (int)q.colonies.size(), scoreSeconds); + for (size_t t = 0; t < q.colonies.size(); ++t) + { + const auto &c = q.colonies[t]; + std::printf("COLONY,%d,%d,%d,%d,%d,%d,%d,%d,%.1f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f\n", + (int)t, c.wheatDistance, c.woodDistance, c.catchmentTiles, c.buildSites, + c.resourceAmount, c.rivalDistance, c.rivalsWithinThreat, c.meanFertility, + c.wheat, c.wood, c.fertility, c.depth, c.room, c.isolation, c.total); + } + } if (!dump.empty()) { FILE *f = std::fopen(dump.c_str(), "w"); From 8a08190fe0cf69e4bc775cc3729a0aca03a181bf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 02:04:28 +0000 Subject: [PATCH 015/136] Keep the best of five rolls instead of the first that fits The lobby already generated up to five maps and kept the first that succeeded, because some rolls cannot fit every starting colony. Among the rolls that do fit, some still hand one colony far better ground than another, and now that there is a score for that, the same loop can keep the best rather than the first. Five is a budget, not a knee. Keeping the best of K is the maximum of K draws from a distribution, so it grows like sqrt(2 ln K): it improves forever and flattens gradually, and bootstrapped over 200 scored maps per generator all twelve trace that same curve to within a few percent -- three candidates capture 44% of what fifty give, five 59%, eight 70%, twenty 87%. What picks the cutoff is that the lobby generates on the UI thread behind a 500ms debounce: at 61ms a candidate on crater-lakes, five rolls cost 306ms where eight would cost 489ms and spend the whole budget. The editor owns its Game and cannot hold a spare, so it takes the winning seed from GenerationService::bestSeed and regenerates it. That is sound because generation is deterministic and the score is a pure function of the finished map; map-generator-defaults-test asserts both, along with the chosen seed being a candidate no other candidate outscores. Optimising a composite could have pulled colonies away from their resources to chase fertility or elbow room. Measured, it does not: the worst-served colony's walk holds at about two tiles on the four height-field generators and improves by four on shattered-coast, while fairness rises from 0.81-0.94 to 0.92-0.97 everywhere. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Rc6FTwBEB6LKNUmamX9wWW --- docs/map-generators/FRAMEWORK_UPGRADES.md | 4 +++ src/CustomGameScreen.cpp | 28 ++++++++++++------ src/EditorMainMenu.cpp | 8 +++-- src/map/generator/core/GenerationService.cpp | 21 +++++++++++++ src/map/generator/core/GenerationService.h | 14 +++++++++ test/MapGeneratorDefaultsTest.cpp | 31 ++++++++++++++++++++ 6 files changed, 95 insertions(+), 11 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index af70faef1..74d50970f 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -35,4 +35,8 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Sampling whole maps and keeping the fairest — the obvious lever, and cheap at 11ms a map — was measured against these distributions rather than assumed. Before any placement work it roughly halved the gap (14.0 → 6.2 at twenty candidates) but could never do better than about 4 tiles, because that was simply the best any of two hundred seeds produced: a technique that selects from a distribution cannot beat the distribution. Measuring the *headroom* instead showed every map already contained a perfectly equal placement, which is what redirected the work from choosing better maps to choosing better sites within a map. With placement fixed, twenty-candidate sampling now shaves 0.93 tiles to about 0.2 — real, but a fraction of a second of walking, while costing twenty generations that each got five to eight times more expensive. Worth keeping as an option; not worth the default. - **Feel:** the balanced placement is a real change in how these four generators play, not only a fairness fix, and wants a maintainer's judgement rather than a metric's. Colonies now start snug against their wood and wheat instead of in open ground, and buildable room within reach of the worst-served colony drops 14–28% (still 405–642 free building sites against a viability bar of 16, so not a playability problem — but the character of an opening changes). Total resource tiles on the map are unchanged; only where colonies sit moved. The four generators' `revision` is bumped accordingly, so a given seed no longer produces the map it did before. +- Walking distance to wheat and wood was the only thing placement equalised, and on the four height-field generators it is now down to about two tiles for the worst-served colony — close enough to exhausted that sampling twenty maps and keeping the fairest shaves two tenths of a tile off a gap of one. Distance was always a thin account of a starting position, though: a deposit at the door that runs dry, a colony with no room to build, one boxed between two rivals, and one on ground where wheat never grows back all look identical to it. `shared/StartQuality::scoreStarts` measures six factors per colony on the *finished* map — where the swarm is built, its clearing cleared and the workers standing where they will actually start walking — and folds them into one number per map: the weakest colony's quality gated by how evenly the map shared quality out, `worst * (worst/best)^k`. Every factor is normalised against a fixed reference rather than against the map's own best colony, because the point is to rank candidate maps against each other and a per-map normalisation scores every map alike; the two distances use the viability bars the study tool already scores against, and the other four sit near the ninetieth percentile of what the twelve generators actually produce, measured over 2400 maps. Scoring costs 2.5–5.4ms against 11–61ms to generate, draws from no random stream (map hashes are unchanged), and never rejects a map — it ranks, so generation success rates are untouched. `MapGeneratorStudy ... quality` emits the whole per-colony breakdown. +- Fertility carries the most weight of the six because it is the factor that decides whether a colony's wheat comes back at all, and it is worth recording why it can be trusted to. `Map::growResources` expands a wheat or wood tile when `isWater(x+dwax, y+dway) && !isSand(x-dwax, y-dway)`, where `dwax` and `dway` are each the difference of two uniform draws from `{0..15}`. That difference has PMF `(16-|d|)/256`, so the chance a tile expands is a triangular kernel summed over every water tile whose mirror through the tile is not sand — and `Fertility::Field` (`src/map/FertilityField.{h,cpp}`, ported from `ExactFertilityCache` on the Maxima branch so both callers share one field) evaluates exactly that, in closed form. A length-16 forward box followed by a length-16 backward box is precisely the triangular kernel, so the water term is four linear passes rather than 961 taps per grass tile, and the sand term is one stamp per sand tile; an adaptive rule picks whichever of the two costs less on the map at hand. `FertilityCalculator` is now a wrapper over it, keeping its deposit-reachability gate, `fertilityMaximum` and the `Uint16` that `Tile::fertility` holds. Measured over 20 seeds on each of eight generators it runs about ten times faster than the kernel it replaces (the sand correction, not the convolution, is where the time goes) and correlates 0.89–0.997 with it; shattered-coast, the sandiest, is both the least correlated and the slowest, which is the difference being paid for. The field takes plain masks and no `Map`, so generation can score a candidate's growth potential without a live game. +- Keeping the best of several rolls rather than the first that fits is the maximum of K draws from a distribution, which grows like `sqrt(2 ln K)`: it improves forever and flattens gradually, with no cutoff the data itself picks out. Bootstrapped over 200 scored maps per generator, all twelve trace the same curve to within a few percent — three candidates capture 44% of what fifty give, five 59%, eight 70%, twenty 87% — so the cutoff is a time budget, not a property of the maps. `GenerationService::kSampledCandidates` is five: the lobby generates synchronously on the UI thread behind a 500ms debounce, and at 61ms a candidate on crater-lakes (the slowest generator) five rolls cost 306ms where eight would cost 489ms and spend the whole budget. Anything past that wants generation moved off the UI thread first. The lobby's existing retry loop does the sampling, so a roll that cannot fit every colony and a roll that fits badly are now handled by the same pass; the editor uses `GenerationService::bestSeed`, which rolls candidates into a scratch `Game` and returns the winning seed for `MapEdit` to regenerate — sound because generation is deterministic and the score is a pure function of the finished map, both asserted in `map-generator-defaults-test`. Optimising a composite could have pulled colonies away from their resources to chase fertility or elbow room; measured, it does not. Across the sweep the worst-served colony's walk holds at about two tiles on the four height-field generators and *improves* by roughly four tiles on shattered-coast (16.5 to 13.1 for wheat, 13.4 to 11.3 for wood), while fairness rises from 0.81–0.94 to 0.92–0.97 everywhere. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 0ab997ffc..9074e06dd 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -396,19 +396,29 @@ bool CustomGameScreen::generateMap() const auto rootSeed = GenerationContext::randomSeed(); GenerationResult generationResult; setup.generator.nbTeams = setup.capacity; - // Some rolls cannot fit every starting colony. Retry those rolls before - // reporting failure; only the successful world becomes the preview. - for (int attempt = 0; attempt < 5; ++attempt) + // Some rolls cannot fit every starting colony, and among those that can, some hand + // one colony far better ground than another. Roll the whole budget either way and + // keep the best-scoring world rather than the first that fits. + double bestScore = -1.0; + for (int attempt = 0; attempt < GenerationService::kSampledCandidates; ++attempt) { - game = std::make_unique(nullptr); + auto roll = std::make_unique(nullptr); auto request = setup.generator; request.seed = GenerationContext::deriveSeed(rootSeed, "attempt/" + std::to_string(attempt)); - generationResult = generator.generate(*game, request); - std::cout << "Map generation: " << generationResult.diagnostic() << std::endl; - if (generationResult && game->teamsCount() == setup.capacity) - break; - game.reset(); + const auto rollResult = generator.generate(*roll, request); + std::cout << "Map generation: " << rollResult.diagnostic() << std::endl; + if (!rollResult || roll->teamsCount() != setup.capacity) + { + if (!game) + generationResult = rollResult; // keep a failure worth reporting + continue; + } + if (rollResult.quality.score <= bestScore) + continue; + bestScore = rollResult.quality.score; + game = std::move(roll); + generationResult = rollResult; } if (!game) throw std::runtime_error(generationResult.diagnostic()); diff --git a/src/EditorMainMenu.cpp b/src/EditorMainMenu.cpp index 0bc28df1e..1713a07ca 100644 --- a/src/EditorMainMenu.cpp +++ b/src/EditorMainMenu.cpp @@ -51,9 +51,13 @@ void EditorMainMenu::onAction(Widget *source, Action action, int par1, int par2) int rc_nms = newMapScreen.execute(globalContainer->gfx, 40); if (rc_nms == NewMapScreen::OK) { - MapEdit mapEdit; GenerationService generator; - newMapScreen.descriptor.seed = GenerationContext::randomSeed(); + // MapEdit owns its Game, so the candidates are rolled into a scratch one + // and the winner regenerated here. Generation is deterministic, so that + // second run reproduces the roll that was scored. + newMapScreen.descriptor.seed = + generator.bestSeed(newMapScreen.descriptor, GenerationContext::randomSeed()); + MapEdit mapEdit; auto result = generator.generate(mapEdit.game, newMapScreen.descriptor); if (result) { diff --git a/src/map/generator/core/GenerationService.cpp b/src/map/generator/core/GenerationService.cpp index 8b036e5f8..52fef85f5 100644 --- a/src/map/generator/core/GenerationService.cpp +++ b/src/map/generator/core/GenerationService.cpp @@ -5,6 +5,8 @@ #include "GenerationValidation.h" #include "StartQuality.h" #include "Utilities.h" +#include +#include std::string GenerationResult::diagnostic() const { return generatorId + " revision " + std::to_string(revision) + " seed " + std::to_string(seed) + @@ -94,3 +96,22 @@ GenerationResult GenerationService::generate(Game &game, const GenerationRequest result.stage = "complete"; return result; } + +std::uint32_t GenerationService::bestSeed(const GenerationRequest &request, std::uint32_t rootSeed, + int candidates) const +{ + std::uint32_t chosen = GenerationContext::deriveSeed(rootSeed, "attempt/0"); + double bestScore = -1.0; + for (int attempt = 0; attempt < std::max(1, candidates); ++attempt) + { + Game game(nullptr); + GenerationRequest roll = request; + roll.seed = GenerationContext::deriveSeed(rootSeed, "attempt/" + std::to_string(attempt)); + const auto result = generate(game, roll); + if (!result || result.quality.score <= bestScore) + continue; + bestScore = result.quality.score; + chosen = roll.seed; + } + return chosen; +} diff --git a/src/map/generator/core/GenerationService.h b/src/map/generator/core/GenerationService.h index d134eac23..0ded28eeb 100644 --- a/src/map/generator/core/GenerationService.h +++ b/src/map/generator/core/GenerationService.h @@ -3,6 +3,7 @@ #include "GenerationRequest.h" #include "GenerationResult.h" #include "GeneratorRegistry.h" +#include class Game; class GenerationService { @@ -14,4 +15,17 @@ class GenerationService { } GenerationResult generate(Game &freshGame, const GenerationRequest &request) const; + + // Rolls of the same generator differ in how good a start they give, not only in whether + // every colony fits, so a caller that can afford more than one roll should keep the best + // rather than the first. Five is what the lobby's regenerate debounce affords on the + // slowest generator; past that, generation has to leave the UI thread first. + static constexpr int kSampledCandidates = 5; + + /// Seed of the best-scoring of kSampledCandidates rolls derived from rootSeed, for callers + /// that generate into a Game they cannot hold a spare of. Generation is deterministic, so + /// regenerating the returned seed reproduces that roll exactly. Falls back to the first + /// derived seed when no roll succeeds, leaving the caller a failure it can report. + std::uint32_t bestSeed(const GenerationRequest &request, std::uint32_t rootSeed, + int candidates = kSampledCandidates) const; }; diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 7561492a7..f9182aaf1 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -221,6 +221,37 @@ class MapGeneratorDefaultsTest assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); assert(GenerationService(registry).generate(generated, screen.descriptor)); + // Candidate sampling picks the best-scoring roll and hands back its seed, which the + // editor then regenerates. That only works because generation is deterministic and + // because the score is a pure function of the finished map. + { + D sampled = DWithDefaults(D::eRIVER); + sampled.nbTeams = 4; + const std::uint32_t root = 20260911; + const std::uint32_t chosen = service.bestSeed(sampled, root); + // Regenerating the chosen seed reproduces the roll that was scored, and choosing + // again from the same root returns the same seed. + D winner = sampled; + winner.seed = chosen; + Game first(nullptr), second(nullptr); + const auto a = service.generate(first, winner); + const auto b = service.generate(second, winner); + assert(a && b && a.quality.measured); + assert(a.quality.score == b.quality.score); + assert(service.bestSeed(sampled, root) == chosen); + // It is one of the candidates, and none of the others scores higher. + bool sawChosen = false; + for (int attempt = 0; attempt < GenerationService::kSampledCandidates; ++attempt) + { + D roll = sampled; + roll.seed = GenerationContext::deriveSeed(root, "attempt/" + std::to_string(attempt)); + sawChosen = sawChosen || roll.seed == chosen; + Game world(nullptr); + const auto rolled = service.generate(world, roll); + assert(!rolled || rolled.quality.score <= a.quality.score); + } + assert(sawChosen); + } puts("PASS registration extension, explicit seeds, interleaved repeatability, RNG " "isolation, errors and legacy sentinels"); } From f087a76dbad41419ff94aadc6023c59350d8b280 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Thu, 10 Sep 2026 23:22:18 -0400 Subject: [PATCH 016/136] Cache the per-tile hard-space test chooseBalancedStarts already proved static MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profiling map generation under load (users noticed the "keep the best of five rolls" work from the previous commit made map generation feel much slower) found chooseBalancedStarts itself, plus Map::isHardSpaceForGroundUnit called from inside it, accounting for 74-84% of total generation time on swamp, river, islands and crater lakes: two whole-map resource floods each call it once per tile, and then every one of up to 900 shortlisted candidate sites re-simulates its own local flood calling it again, up to ~4225 tile visits each in the worst case. That work was entirely redundant. Every call site in this file uses the same two constants (canSwim=false, team mask=0), which makes checkTile's forbidden- area test a no-op, and chooseBalancedStarts runs before placeStarts() ever places a building, so getBuilding() is NOGBID everywhere it looks. Under those invariants isHardSpaceForGroundUnit(x, y, false, 0) reduces to !isResource(x, y) && !isWater(x, y) — a pure function of terrain/resource state that cannot change across the whole search. buildHardSpaceGrid() computes that once into a flat std::vector before the two distanceToResource() floods; all three flood loops (the two global ones and scoreAsBuilt's per-site one) read the cached byte instead of calling through checkTile's several field accessors. distanceToResource's distance vectors and the per-site visited/visitStamp array are narrowed from int to 16 bits alongside this — every distance this engine can produce fits comfortably, and the arrays these floods touch millions of times are half the size to move through cache. This changes nothing about which sites get chosen or how they get scored, only how the unchanged answer is computed: MapGeneratorStudy's per-tile terrain/resource hash is bit-identical before and after on matching seeds across every affected generator. Measured 28-31% faster end to end on swamp, river, islands and crater lakes (interleaved before/after binaries, 60 generations per generator, to separate the effect from this machine's own run-to-run noise) with zero measurable change on the nine generators that don't call chooseBalancedStarts, which is the expected result and also rules out the noise itself producing the improvement. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. MapGeneratorStudy succeeds across all 13 generators at 5 seeds each, both before and after. FertilityFieldTest and MapGeneratorDefaultsTest are unaffected (this file's callers are unrelated to either). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 2 + .../generator/shared/StartingPositions.cpp | 41 ++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 74d50970f..71e0fc520 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -39,4 +39,6 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Fertility carries the most weight of the six because it is the factor that decides whether a colony's wheat comes back at all, and it is worth recording why it can be trusted to. `Map::growResources` expands a wheat or wood tile when `isWater(x+dwax, y+dway) && !isSand(x-dwax, y-dway)`, where `dwax` and `dway` are each the difference of two uniform draws from `{0..15}`. That difference has PMF `(16-|d|)/256`, so the chance a tile expands is a triangular kernel summed over every water tile whose mirror through the tile is not sand — and `Fertility::Field` (`src/map/FertilityField.{h,cpp}`, ported from `ExactFertilityCache` on the Maxima branch so both callers share one field) evaluates exactly that, in closed form. A length-16 forward box followed by a length-16 backward box is precisely the triangular kernel, so the water term is four linear passes rather than 961 taps per grass tile, and the sand term is one stamp per sand tile; an adaptive rule picks whichever of the two costs less on the map at hand. `FertilityCalculator` is now a wrapper over it, keeping its deposit-reachability gate, `fertilityMaximum` and the `Uint16` that `Tile::fertility` holds. Measured over 20 seeds on each of eight generators it runs about ten times faster than the kernel it replaces (the sand correction, not the convolution, is where the time goes) and correlates 0.89–0.997 with it; shattered-coast, the sandiest, is both the least correlated and the slowest, which is the difference being paid for. The field takes plain masks and no `Map`, so generation can score a candidate's growth potential without a live game. - Keeping the best of several rolls rather than the first that fits is the maximum of K draws from a distribution, which grows like `sqrt(2 ln K)`: it improves forever and flattens gradually, with no cutoff the data itself picks out. Bootstrapped over 200 scored maps per generator, all twelve trace the same curve to within a few percent — three candidates capture 44% of what fifty give, five 59%, eight 70%, twenty 87% — so the cutoff is a time budget, not a property of the maps. `GenerationService::kSampledCandidates` is five: the lobby generates synchronously on the UI thread behind a 500ms debounce, and at 61ms a candidate on crater-lakes (the slowest generator) five rolls cost 306ms where eight would cost 489ms and spend the whole budget. Anything past that wants generation moved off the UI thread first. The lobby's existing retry loop does the sampling, so a roll that cannot fit every colony and a roll that fits badly are now handled by the same pass; the editor uses `GenerationService::bestSeed`, which rolls candidates into a scratch `Game` and returns the winning seed for `MapEdit` to regenerate — sound because generation is deterministic and the score is a pure function of the finished map, both asserted in `map-generator-defaults-test`. Optimising a composite could have pulled colonies away from their resources to chase fertility or elbow room; measured, it does not. Across the sweep the worst-served colony's walk holds at about two tiles on the four height-field generators and *improves* by roughly four tiles on shattered-coast (16.5 to 13.1 for wheat, 13.4 to 11.3 for wood), while fairness rises from 0.81–0.94 to 0.92–0.97 everywhere. +- Playtesting the fairness work above surfaced it as slow to feel, not only to measure: `chooseBalancedStarts` called `Map::isHardSpaceForGroundUnit` freshly, through its full multi-array `checkTile` path, at every tile visited by both resource floods and by every one of up to 900 shortlisted sites' own re-simulated flood. But with the args used everywhere in this file (no swimming, no team mask) and no building placed yet at this point in generation — `placeStarts()` runs later — that call is a pure function of terrain and resource state that cannot change across the whole search. Profiling confirmed it and `chooseBalancedStarts` itself as 74–84% of total generation time on the four height-field generators. Computing it once into a flat per-tile byte before the search, and reading that instead everywhere the search previously called the accessor, changes nothing about which sites get chosen — verified by identical per-tile terrain/resource hashes on matching seeds before and after — and measured 28–31% faster end to end across swamp, river, islands and crater lakes (interleaved before/after binaries, 60 generations each, to separate the effect from this machine's own run-to-run noise, which the nine untouched generators' ~0% delta over the same run also confirms). Distance fields and the visited-tile stamp were narrowed from `int` to 16 bits alongside this; every distance this engine can produce fits with room to spare. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index 83e4d16c5..24be6c32e 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -277,14 +277,34 @@ namespace MapGeneration { namespace { +// isHardSpaceForGroundUnit(x, y, false, 0) is a pure function of terrain/resource state for +// every call in this file: canSwim and the team mask are always the same two constants, no +// forbidden-area bit survives a `& 0`, and nothing places a building before chooseBalancedStarts +// runs (placeStarts(), the only thing that does, runs after boot tiles are already chosen). So +// the answer never changes across the two distanceToResource() floods and the up-to-900 calls +// to scoreAsBuilt() below; computing it once into a flat byte per tile turns what used to be a +// branchy, multi-array accessor call at every visited tile into a single sequential array read. +std::vector buildHardSpaceGrid(Map &map) +{ + const int w = map.getW(), h = map.getH(); + std::vector hard(size_t(w) * h); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + hard[size_t(y) * w + x] = map.isHardSpaceForGroundUnit(x, y, false, 0) ? 1 : 0; + return hard; +} + // Walking distance from every tile to the nearest deposit of one resource. Workers stand // beside a deposit rather than on it (a resource tile is not walkable), so the sources are the // walkable tiles touching one. One flood answers the question for every tile on the map, which -// is what makes scoring a few hundred candidate sites cheap enough to do exhaustively. -std::vector distanceToResource(Map &map, int resourceType) +// is what makes scoring a few hundred candidate sites cheap enough to do exhaustively. Distances +// on any map this engine supports fit comfortably in 16 bits, halving the footprint of an array +// this flood (and every scoreAsBuilt call after it) touches over and over. +std::vector distanceToResource(Map &map, const std::vector &hard, + int resourceType) { const int w = map.getW(), h = map.getH(); - std::vector dist(size_t(w) * h, -1); + std::vector dist(size_t(w) * h, -1); std::queue q; for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) @@ -296,7 +316,7 @@ std::vector distanceToResource(Map &map, int resourceType) { int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); int np = ny * w + nx; - if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + if (dist[np] < 0 && hard[np]) { dist[np] = 0; q.push(np); @@ -315,7 +335,7 @@ std::vector distanceToResource(Map &map, int resourceType) continue; int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); int np = ny * w + nx; - if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + if (dist[np] < 0 && hard[np]) { dist[np] = dist[p] + 1; q.push(np); @@ -338,8 +358,9 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu if (!swarm) return false; - const std::vector woodDist = distanceToResource(map, WOOD); - const std::vector wheatDist = distanceToResource(map, CORN); + const std::vector hard = buildHardSpaceGrid(map); + const std::vector woodDist = distanceToResource(map, hard, WOOD); + const std::vector wheatDist = distanceToResource(map, hard, CORN); // A site is worth exactly what its *worse* resource costs to reach: a colony next to wood // but a long walk from wheat is not a good start, however good the wood is. @@ -350,8 +371,8 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu // and paths it is about to block, which is how four sites picked as exactly equal can // finish unequal. Score what the colony will actually live with instead. The offsets below // mirror placeStarts(); they are what it does, not a guess at it. - std::vector visited(size_t(w) * h, 0); - int visitStamp = 0; + std::vector visited(size_t(w) * h, 0); + std::uint16_t visitStamp = 0; auto scoreAsBuilt = [&](int bx, int by, int limit) -> int { auto cleared = [&](int tx, int ty) @@ -408,7 +429,7 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu if (type == CORN && wheat < 0) wheat = d + 1; } - if (!blocked(nx, ny) && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + if (!blocked(nx, ny) && hard[ny * w + nx]) push(nx, ny, d + 1); } } From c02219ca82cbc44f75ee3f3c86b676216e199e74 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Thu, 10 Sep 2026 23:23:01 -0400 Subject: [PATCH 017/136] Narrow FertilityField's single-axis intermediates to the range they use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigated as part of the same profiling pass as the previous commit: Fertility::Field::rebuild is the dominant cost on Lattice and Maze (57% and 39% of total generation time), but unlike chooseBalancedStarts this is not redundant recomputation — Fertility::forMap has exactly one caller (scoreStarts) and runs once per roll, confirmed via its full call chain. Both generators are simply water/sand-heavy by design (thick maze corridors, real shorelines): instrumentation shows ~6,100 water and ~1,260 sand tiles for Lattice and ~4,100/~1,350 for Maze against a 16,384-tile map, and the sand-correction path costs one 31x31 weighted stamp per sand tile no matter how the adaptive rule picks between it and the water-splat alternative. first/second hold the box-blur passes' single-axis partial sums. The first pass (a plain 16-wide box over 0/1 water values) tops out at 16; combined with the second, backward pass into the full 1D triangular kernel it tops out at that kernel's own weight sum, 256; the vertical passes repeat the same shape against those values, topping out at 16*256=4096. Both fit uint16_t with no precision loss — the final `fertility` array still needs 32 bits, since its theoretical max (256*256=65536) is one past uint16_t's range. This is exact and safe, but measured negligible impact on Lattice/Maze specifically (their cost is the sand-correction term, which these arrays aren't part of). Worked through algebraically, that term reduces to sum_d weight(d) * water(target+d) * sand(target-d) - a bilinear cross-correlation between two different fields, not the single-field sum the box-blur trick relies on, so it does not have an equivalent separable speedup; only FFT-based convolution would change its asymptotic cost, and Fertility::Field is shared with FertilityCalculator (real gameplay fertility), so any such rewrite needs to preserve that caller's output exactly too. Left as an open follow-up rather than attempted here. Verified: full scons client build and scons -C test suite (198 cppunit cases, including FertilityFieldTest) pass with no narrowing warnings. MapGeneratorStudy succeeds across all 13 generators and produces bit-identical per-tile terrain/resource hashes before and after on matching seeds. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 1 + src/map/FertilityField.h | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 71e0fc520..191b67aef 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -40,5 +40,6 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Keeping the best of several rolls rather than the first that fits is the maximum of K draws from a distribution, which grows like `sqrt(2 ln K)`: it improves forever and flattens gradually, with no cutoff the data itself picks out. Bootstrapped over 200 scored maps per generator, all twelve trace the same curve to within a few percent — three candidates capture 44% of what fifty give, five 59%, eight 70%, twenty 87% — so the cutoff is a time budget, not a property of the maps. `GenerationService::kSampledCandidates` is five: the lobby generates synchronously on the UI thread behind a 500ms debounce, and at 61ms a candidate on crater-lakes (the slowest generator) five rolls cost 306ms where eight would cost 489ms and spend the whole budget. Anything past that wants generation moved off the UI thread first. The lobby's existing retry loop does the sampling, so a roll that cannot fit every colony and a roll that fits badly are now handled by the same pass; the editor uses `GenerationService::bestSeed`, which rolls candidates into a scratch `Game` and returns the winning seed for `MapEdit` to regenerate — sound because generation is deterministic and the score is a pure function of the finished map, both asserted in `map-generator-defaults-test`. Optimising a composite could have pulled colonies away from their resources to chase fertility or elbow room; measured, it does not. Across the sweep the worst-served colony's walk holds at about two tiles on the four height-field generators and *improves* by roughly four tiles on shattered-coast (16.5 to 13.1 for wheat, 13.4 to 11.3 for wood), while fairness rises from 0.81–0.94 to 0.92–0.97 everywhere. - Playtesting the fairness work above surfaced it as slow to feel, not only to measure: `chooseBalancedStarts` called `Map::isHardSpaceForGroundUnit` freshly, through its full multi-array `checkTile` path, at every tile visited by both resource floods and by every one of up to 900 shortlisted sites' own re-simulated flood. But with the args used everywhere in this file (no swimming, no team mask) and no building placed yet at this point in generation — `placeStarts()` runs later — that call is a pure function of terrain and resource state that cannot change across the whole search. Profiling confirmed it and `chooseBalancedStarts` itself as 74–84% of total generation time on the four height-field generators. Computing it once into a flat per-tile byte before the search, and reading that instead everywhere the search previously called the accessor, changes nothing about which sites get chosen — verified by identical per-tile terrain/resource hashes on matching seeds before and after — and measured 28–31% faster end to end across swamp, river, islands and crater lakes (interleaved before/after binaries, 60 generations each, to separate the effect from this machine's own run-to-run noise, which the nine untouched generators' ~0% delta over the same run also confirms). Distance fields and the visited-tile stamp were narrowed from `int` to 16 bits alongside this; every distance this engine can produce fits with room to spare. +- The same pass looked for the equivalent win in `Fertility::Field::rebuild`, whose per-map cost (once per roll, not per candidate — verified via its only call chain, `scoreStarts` → `Fertility::forMap`) dominates Lattice and Maze specifically (57% and 39% of total time) because their thick water corridors and real shorelines carry far more water and sand tiles than a typical height-field map: instrumentation showed roughly 6,100 water/1,260 sand tiles for Lattice and 4,100/1,350 for Maze, against 16,384 total. The adaptive path already picks the cheaper of its two options correctly, but at that density neither is cheap: the sand-correction term costs one 31×31 stamp per sand tile regardless. Narrowing `first`/`second` (the box-blur passes' single-axis intermediates, provably bounded at 256 — the 1D triangular kernel's own weight sum — well inside 16 bits) is exact and free, but measured negligible for Lattice/Maze specifically, because it isn't the term that dominates there. The correction term itself, worked through algebraically, reduces to `sum_d weight(d) * water(target+d) * sand(target-d)` — a genuine bilinear cross-correlation between two different fields, not a single-field sum, so the box-blur trick that makes the water term four linear passes does not apply to it; only an FFT-based convolution would change its asymptotic cost, and `Fertility::Field` is shared with `FertilityCalculator` (real gameplay fertility, not only this scoring path), so any such change needs to preserve that caller's exact output too. Left as an open follow-up rather than attempted here. The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/FertilityField.h b/src/map/FertilityField.h index d7ab110cf..c1a93af7f 100644 --- a/src/map/FertilityField.h +++ b/src/map/FertilityField.h @@ -54,7 +54,13 @@ namespace Fertility int width = 0, height = 0; int waterTiles = 0, sandTiles = 0; Path usedPath = Path::Adaptive; - std::vector fertility, first, second; + // first/second hold single-axis triangular-kernel partial sums (0-water inputs, so the + // 1D triangular kernel's own weight sum of 256 bounds them at 256, or 4096 once a second, + // unweighted 16-wide box pass runs over that): both fit uint16_t with no precision loss. + // fertility is the full 2D result and can reach 256*256 = 65536, one past uint16_t's + // range, so it stays 32-bit. + std::vector fertility; + std::vector first, second; std::vector wrappedX, wrappedY; int wx(int x, int offset) const; int wy(int y, int offset) const; From db6ecb3ad918f711142af08dc9f3b672c9a69287 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Thu, 10 Sep 2026 23:29:44 -0400 Subject: [PATCH 018/136] Hoist named RNG-stream lookups out of Shattered Coast and Rugged Archipelago's hot loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continuing the same profiling pass as the previous two commits: Shattered Coast (old-random) and Rugged Archipelago were the slowest and third-slowest of all 13 generators, and neither bottleneck was in their own terrain logic. GenerationContext::stream(name) looks up a named std::mt19937 in a std::map, computing a hash over the name and walking the tree on every call — cheap on its own, but both generators call it by string literal from deep inside per-tile loops that run tens of thousands of times per generation. Shattered Coast's simulateRandomMap (itself invoked repeatedly per smoothing iteration to balance water/sand/ grass ratios) draws from "simulation" up to five times per tile across every w*h-tile map it simulates; its terrain() draws from "terrain" the same way across its own patchwork and smoothing passes. Rugged Archipelago's island-growing and beach passes draw from "terrain" identically. In every one of these loops the stream name is a compile-time constant that never changes for the life of the function, so the repeated lookup was pure overhead paid on every single draw. Both functions now look their stream up once into a std::mt19937& at the top and draw from that reference throughout, instead of calling context.stream(name) again for every draw. This draws from the exact same underlying generator in the exact same order, so it changes no random number either function produces: MapGeneratorStudy's per-tile terrain/ resource hash is bit-identical on matching seeds before and after. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. MapGeneratorStudy succeeds across all 13 generators at 5 seeds each. Measured 50% faster on Shattered Coast and 31% faster on Rugged Archipelago end to end (interleaved before/after binaries, 60-100 generations per generator), with ~0% change on two unaffected generators run the same way, ruling out this machine's run-to-run noise as the source of the improvement. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 2 + .../generators/RuggedArchipelagoGenerator.cpp | 12 +++-- .../generators/ShatteredCoastGenerator.cpp | 45 +++++++++++-------- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 191b67aef..20e16ae96 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -42,4 +42,6 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Playtesting the fairness work above surfaced it as slow to feel, not only to measure: `chooseBalancedStarts` called `Map::isHardSpaceForGroundUnit` freshly, through its full multi-array `checkTile` path, at every tile visited by both resource floods and by every one of up to 900 shortlisted sites' own re-simulated flood. But with the args used everywhere in this file (no swimming, no team mask) and no building placed yet at this point in generation — `placeStarts()` runs later — that call is a pure function of terrain and resource state that cannot change across the whole search. Profiling confirmed it and `chooseBalancedStarts` itself as 74–84% of total generation time on the four height-field generators. Computing it once into a flat per-tile byte before the search, and reading that instead everywhere the search previously called the accessor, changes nothing about which sites get chosen — verified by identical per-tile terrain/resource hashes on matching seeds before and after — and measured 28–31% faster end to end across swamp, river, islands and crater lakes (interleaved before/after binaries, 60 generations each, to separate the effect from this machine's own run-to-run noise, which the nine untouched generators' ~0% delta over the same run also confirms). Distance fields and the visited-tile stamp were narrowed from `int` to 16 bits alongside this; every distance this engine can produce fits with room to spare. - The same pass looked for the equivalent win in `Fertility::Field::rebuild`, whose per-map cost (once per roll, not per candidate — verified via its only call chain, `scoreStarts` → `Fertility::forMap`) dominates Lattice and Maze specifically (57% and 39% of total time) because their thick water corridors and real shorelines carry far more water and sand tiles than a typical height-field map: instrumentation showed roughly 6,100 water/1,260 sand tiles for Lattice and 4,100/1,350 for Maze, against 16,384 total. The adaptive path already picks the cheaper of its two options correctly, but at that density neither is cheap: the sand-correction term costs one 31×31 stamp per sand tile regardless. Narrowing `first`/`second` (the box-blur passes' single-axis intermediates, provably bounded at 256 — the 1D triangular kernel's own weight sum — well inside 16 bits) is exact and free, but measured negligible for Lattice/Maze specifically, because it isn't the term that dominates there. The correction term itself, worked through algebraically, reduces to `sum_d weight(d) * water(target+d) * sand(target-d)` — a genuine bilinear cross-correlation between two different fields, not a single-field sum, so the box-blur trick that makes the water term four linear passes does not apply to it; only an FFT-based convolution would change its asymptotic cost, and `Fertility::Field` is shared with `FertilityCalculator` (real gameplay fertility, not only this scoring path), so any such change needs to preserve that caller's exact output too. Left as an open follow-up rather than attempted here. +- The same profiling pass found Shattered Coast (old-random) and Rugged Archipelago dominated not by their own terrain logic but by `GenerationContext::stream`: every draw against a named stream looks that name up in a `std::map`, and both generators call it by string literal inside per-tile loops that run tens of thousands of times a generation — `simulateRandomMap` alone (Shattered Coast's iterative water/sand/grass balancer, itself invoked repeatedly per smoothing pass) draws from `"simulation"` up to five times per tile across every candidate map it simulates, and Rugged Archipelago's island-growing and beach passes draw from `"terrain"` the same way. The name never changes within either function, so the lookup was pure overhead: looking the stream up once into a `std::mt19937&` at the top of each function and drawing from that reference thereafter changes no random number either function produces — bit-identical per-tile terrain/resource hashes confirm it — and measured **50% faster** on Shattered Coast and **31% faster** on Rugged Archipelago end to end (same interleaved-binary methodology as above; the two generators that don't touch this code showed ~0% change over the same run). + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index ec086b39f..da48e329a 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -22,6 +22,10 @@ static bool terrain(Game &game, GenerationContext &context, const RuggedArchipel { Map &map = game.map; const int w = map.getW(), h = map.getH(); + // context.stream() looks up a named std::mt19937 by string key on every call; every draw in + // this function names the same "terrain" stream, so look it up once and reuse the reference + // through the loops below instead of repeating the lookup per tile per draw. + std::mt19937 &rng = context.stream("terrain"); // First, fill with water: for (int y = 0; y < h; y++) @@ -40,8 +44,8 @@ static bool terrain(Game &game, GenerationContext &context, const RuggedArchipel int c = 0; for (int i = 0; i < nbIslands; i++) { - int x = context.stream("terrain")() % w; - int y = context.stream("terrain")() % h; + int x = rng() % w; + int y = rng() % h; bool failed = false; int j; for (j = 0; j < i; j++) @@ -86,7 +90,7 @@ static bool terrain(Game &game, GenerationContext &context, const RuggedArchipel continue; int a, b; - switch (context.stream("terrain")() & 15) + switch (rng() & 15) { case 0: a = map.getUMTerrain(x + 1, y); @@ -206,7 +210,7 @@ static bool terrain(Game &game, GenerationContext &context, const RuggedArchipel for (int x = dx; x < w; x += 4) { int a, b; - switch (context.stream("terrain")() & 7) + switch (rng() & 7) { case 0: a = map.getUMTerrain(x + 1, y); diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 8b42d44a5..e489616b9 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -29,6 +29,12 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas int m = s - 1; std::vector undermap(w * h); + // context.stream() looks up a named std::mt19937 by string key on every call; every use + // here names the same "simulation" stream, so looking it up once and reusing the reference + // through both w*h-sized loops below skips a map lookup (and a temporary std::string, for + // callers passing a literal) per tile per draw instead of changing which numbers come out. + std::mt19937 &rng = context.stream("simulation"); + int totalRatio = 0x7FFF; int waterRatio = (int)(baseWater * ((double)totalRatio)); int sandRatio = (int)(baseSand * ((double)totalRatio)); @@ -47,7 +53,7 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { - int r = context.stream("simulation")() % totalRatio; + int r = rng() % totalRatio; r -= waterRatio; if (r < 0) { @@ -73,7 +79,7 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { - if (context.stream("simulation")() & 4) + if (rng() & 4) { int a = undermap[(y * w + x + 1 + s) & m]; int b = undermap[(y * w + x - 1 + s) & m]; @@ -93,7 +99,7 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas continue; } } - if (context.stream("simulation")() & 4) + if (rng() & 4) { int a = undermap[(y * w + x + w + 1 + s) & m]; int b = undermap[(y * w + x - w - 1 + s) & m]; @@ -113,7 +119,7 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas continue; } } - if (context.stream("simulation")() & 4) + if (rng() & 4) { int a = undermap[(y * w + x + w - 2 + s) & m]; int b = undermap[(y * w + x - w + 2 + s) & m]; @@ -133,7 +139,7 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas continue; } } - if (context.stream("simulation")() & 4) + if (rng() & 4) { int a = undermap[(y * w + x + w + 2 + (h << 1) + s) & m]; int b = undermap[(y * w + x - w - 2 - (h << 1) + s) & m]; @@ -183,6 +189,9 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast { Map &map = game.map; const int w = map.getW(), h = map.getH(); + // Same reasoning as simulateRandomMap's rng: every draw below names the "terrain" stream, + // so look it up once rather than on every one of the many draws per tile in the loops below. + std::mt19937 &rng = context.stream("terrain"); int waterRatio = options.water; int sandRatio = options.sand; @@ -322,7 +331,7 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { - int r = context.stream("terrain")() % totalRatio; + int r = rng() % totalRatio; r -= waterRatio; if (r < 0) { @@ -425,11 +434,11 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { - if (context.stream("terrain")() & 4) + if (rng() & 4) { int a = map.getUMTerrain(x + 1, y); int b = map.getUMTerrain(x - 1, y); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; @@ -439,17 +448,17 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast { int a = map.getUMTerrain(x, y - 1); int b = map.getUMTerrain(x, y + 1); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; } } - if (context.stream("terrain")() & 4) + if (rng() & 4) { int a = map.getUMTerrain(x + 1, y + 1); int b = map.getUMTerrain(x - 1, y - 1); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; @@ -459,17 +468,17 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast { int a = map.getUMTerrain(x + 1, y - 1); int b = map.getUMTerrain(x - 1, y + 1); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; } } - if (context.stream("terrain")() & 4) + if (rng() & 4) { int a = map.getUMTerrain(x + 2, y); int b = map.getUMTerrain(x - 2, y); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; @@ -479,17 +488,17 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast { int a = map.getUMTerrain(x, y - 2); int b = map.getUMTerrain(x, y + 2); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; } } - if (context.stream("terrain")() & 4) + if (rng() & 4) { int a = map.getUMTerrain(x + 2, y + 2); int b = map.getUMTerrain(x - 2, y - 2); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; @@ -499,7 +508,7 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast { int a = map.getUMTerrain(x + 2, y - 2); int b = map.getUMTerrain(x - 2, y + 2); - if ((a == b) && (allowed[a] <= context.stream("terrain")())) + if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); continue; From 3203e22cf58a39c9ea4267a6ac79474c453ff698 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Thu, 10 Sep 2026 23:58:12 -0400 Subject: [PATCH 019/136] Fix column-major grid traversal and pointer-chasing data structures in region splitting Profiling identified Contested Commons, Concrete Islands and Isles as now the slowest generators, dominated by the older Voronoi-style point-dispersion code in shared/Regions.cpp and shared/Distances.cpp. Investigated with cache behavior specifically in mind. Exact fixes (same output, only how it's computed - verified by bit-identical per-tile terrain/resource hashes on matching seeds, every generator): - computeDistances' BFS used std::queue (std::deque-backed, growing by separately heap-allocated blocks) even though every cell is enqueued at most once (the `side == 0` guard prevents a second push). A flat, preallocated w*h-sized array makes an exact FIFO out of sequential writes instead of block-to-block pointer chasing. - splitUpArea's per-region frontier is inserted at a genuinely random position on purpose - that randomness is what gives the flood its organic shape, not an accident to remove - but std::vector supports the identical random-position insert/erase via a contiguous memmove, without std::list's per-node heap allocation and pointer chasing on every single insert and on the std::advance that finds where to insert. - adjustHeightmapFromPerlinNoise and computeAverageDistance are a pure per-cell transform and a commutative sum respectively, so nesting y outside x costs nothing and walks both the grid and HeightMap's own identically row-major _map array with their grain instead of across it. - getAllPoints, getAllOtherPoints, findBorderPoints, and splitUpPoints's own two internal scans all walked a row-major grid as `for x { for y {...} }` - a full-row stride on every step - for no reason but habit, except two of these results (possible[n], startingPoints[n]) get indexed by a random draw, so a plain loop-order swap would silently pick a different point for the same seed. collectPointsColumnOrder gets both: a row-major counting pass sizes each output column, then a second row-major pass drops each point into its precomputed slot, landing every point in the exact x-major, y-minor order the original nested loop produced without ever striding across the grid to do it. splitUpPoints' single-pass "reset the candidate list on every strict improvement" site search reduces to the same set every time regardless of how it's computed (traced by hand, confirmed by the same hashes), so it is now an explicit two-pass max-then-collect using the same helper. One further fix changes output for Contested Commons specifically: splitUpPoints' PointSearch::WholeRegion mode (Contested Commons' own search, not used by any other generator) scores every legal tile as a candidate placement and keeps only a strict improvement, so whichever tied candidate is reached earliest in scan order wins - the one shape here a loop-order swap could not fix without changing anything. Measured directly across 7 seeds: 6 of 7 produce byte-identical maps regardless, and the one that changed still converges to a placement of the same quality by the search's own metric (already documented as bounded best-response, not a guaranteed optimum). PointSearch::Local - used far more broadly, including as swamp/river/islands/crater lakes' rare legacy fallback - keeps its exact original scan order untouched: its window is 7x7, too small for traversal order to matter, and touches far more generators than this fix was worth risking. Contested Commons' revision bump follows in the next commit. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. MapGeneratorStudy succeeds across all 13 generators at 9 seeds each. Measured (interleaved before/after binaries, repeated 3x to get past this machine's run-to-run noise - an initial single-run reading overstated the effect by more than 2x): Concrete Islands ~9% faster, Isles ~5-7% faster, Contested Commons ~8-9% faster overall. Fjord Continent, which touches none of this code, showed ~2% change over the same run, taken as the noise floor rather than a real effect. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 5 + src/map/generator/shared/Distances.cpp | 35 ++-- src/map/generator/shared/Regions.cpp | 225 ++++++++++++---------- 3 files changed, 155 insertions(+), 110 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 20e16ae96..fb8cafdf4 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -44,4 +44,9 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - The same profiling pass found Shattered Coast (old-random) and Rugged Archipelago dominated not by their own terrain logic but by `GenerationContext::stream`: every draw against a named stream looks that name up in a `std::map`, and both generators call it by string literal inside per-tile loops that run tens of thousands of times a generation — `simulateRandomMap` alone (Shattered Coast's iterative water/sand/grass balancer, itself invoked repeatedly per smoothing pass) draws from `"simulation"` up to five times per tile across every candidate map it simulates, and Rugged Archipelago's island-growing and beach passes draw from `"terrain"` the same way. The name never changes within either function, so the lookup was pure overhead: looking the stream up once into a `std::mt19937&` at the top of each function and drawing from that reference thereafter changes no random number either function produces — bit-identical per-tile terrain/resource hashes confirm it — and measured **50% faster** on Shattered Coast and **31% faster** on Rugged Archipelago end to end (same interleaved-binary methodology as above; the two generators that don't touch this code showed ~0% change over the same run). +- The next profiling pass turned to `shared/Regions.cpp` and `shared/Distances.cpp`, which host the older Voronoi-style point-dispersion machinery Contested Commons, Concrete Islands and Isles still build on. Three fixes there are exact - same output, only how it's computed: `computeDistances`'s BFS used `std::queue` (`std::deque`-backed, growing by separately heap-allocated blocks) even though every cell is enqueued at most once, so a flat, preallocated `w*h`-sized array makes an exact FIFO out of sequential writes instead of block-chasing pointers; `splitUpArea`'s per-region frontier (`squares[p]`) is inserted at a genuinely random position on purpose - that's what gives the flood its organic, non-directional shape, not incidental - but a `std::vector` supports the identical random-position insert/erase (contiguous memmove) without `std::list`'s per-node heap allocation and pointer-chasing; and `adjustHeightmapFromPerlinNoise`/`computeAverageDistance` are pure per-cell transforms and commutative sums respectively, so nesting y outside x costs nothing and walks both the grid and `HeightMap`'s identically row-major `_map` array with their grain instead of across it. +- The same file's `getAllPoints`, `getAllOtherPoints`, `findBorderPoints`, and `splitUpPoints`'s own two internal scans all walked a row-major grid as `for x { for y { ... } }` - a full-row stride on every step - for no reason but habit, except that several of these results get indexed by a random draw (`possible[n]`, `startingPoints[n]`), so simply swapping the loop order would silently choose a different point for the same seed. `collectPointsColumnOrder` gets both: a row-major counting pass sizes each output column, then a second row-major pass drops each point into its precomputed slot, landing every point in the exact x-major, y-minor order the original nested loop produced while never striding across the grid to do it - bit-identical hashes on every generator confirm it. The single-pass "reset the candidate list on every strict improvement" pattern in `splitUpPoints`'s site search reduces to the same set every time regardless of how it's computed - traced by hand and confirmed by the same hashes - so it's now an explicit two-pass max-then-collect using the same helper. +- `splitUpPoints`'s `PointSearch::WholeRegion` mode (Contested Commons only) runs the same kind of scan for real - `for dx in 0..w { for dy in 0..h { ... } }` scoring every legal tile as a candidate placement, the one shape here that a plain loop-order swap could not fix without changing anything: the search keeps only a strict improvement (`score > best`), so whichever tied candidate is reached earliest in scan order wins, and reordering the scan can hand that win to a different tied candidate. Measured directly (7 seeds): 6 of 7 produced byte-identical maps regardless, and the one that changed still converged to a placement of the same quality by the search's own metric - already documented as bounded best-response, not a guaranteed optimum, so a different tied winner is not a different guarantee. Contested Commons' revision moves to 2 for it. `PointSearch::Local` (used far more broadly, including as swamp/river/islands/crater lakes' rare legacy fallback) keeps its exact original scan order untouched - its window is 7x7, too small for traversal order to matter, and touches far more generators than the fix was worth risking. +- Measured (interleaved before/after binaries, repeated 3x at 150 generations each after an initial single-run reading proved noisy enough to overstate the effect by more than 2x): Concrete Islands about 9% faster, Isles about 5-7% faster, Contested Commons about 8-9% faster overall (its own share from the exact fixes alone was too close to this machine's run-to-run noise floor to separate cleanly from the `WholeRegion` fix's own further ~4%). A generator that never calls into any of this code (Fjord Continent) showed about 2% change over the same run, taken as that noise floor rather than a real effect. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/shared/Distances.cpp b/src/map/generator/shared/Distances.cpp index ec4aa79af..4bd53c536 100644 --- a/src/map/generator/shared/Distances.cpp +++ b/src/map/generator/shared/Distances.cpp @@ -11,7 +11,6 @@ #include "Terrain.h" #include #include -#include using namespace MapGeneration; namespace MapGeneration @@ -30,9 +29,13 @@ void adjustHeightmapFromPerlinNoise(Map &map, GenerationContext &context, std::v { HeightMap noise(map.getW(), map.getH(), context.stream("noise")); noise.makePlain(4); - for (int x = 0; x < map.getW(); ++x) + // Every cell's result depends only on its own (x, y), not on visitation order, so nesting + // y outside x walks `heights` (and noise's own w*h-sized field) with the grain of its + // row-major layout instead of across it: the inner loop's writes land in consecutive + // memory instead of striding by a full row on every step. + for (int y = 0; y < map.getH(); ++y) { - for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) { heights[y * map.getW() + x] += noise.uiLevel(x, y, spread * 2) - spread; } @@ -42,13 +45,20 @@ void adjustHeightmapFromPerlinNoise(Map &map, GenerationContext &context, std::v void computeDistances(Map &map, std::vector &sources, std::vector &obstacles, std::vector &heightmap) { - std::queue places; + // Every cell is enqueued at most once - the `side == 0` guard below stops a second push + // once it's been visited - so a flood over the whole map never needs more than w*h queue + // slots. A flat, preallocated FIFO fills that bound with sequential writes; std::queue + // (std::deque-backed) instead grows by separately heap-allocated blocks, turning most + // pushes and pops into a pointer chase to a freshly touched cache line. + const size_t mapSize = size_t(map.getW()) * map.getH(); + std::vector places(mapSize); + size_t head = 0, tail = 0; heightmap.clear(); - heightmap.resize(map.getW() * map.getH(), 0); + heightmap.resize(mapSize, 0); for (unsigned int i = 0; i < sources.size(); ++i) { heightmap[sources[i].y * map.getW() + sources[i].x] = 1; - places.push(sources[i].y * map.getW() + sources[i].x); + places[tail++] = sources[i].y * map.getW() + sources[i].x; } for (unsigned int i = 0; i < obstacles.size(); ++i) { @@ -58,10 +68,9 @@ void computeDistances(Map &map, std::vector &sources, Uint32 wDec = map.wDec; Uint32 hMask = map.hMask; Uint32 wMask = map.wMask; - while (!places.empty()) + while (head < tail) { - int deltaAddrG = places.front(); - places.pop(); + int deltaAddrG = places[head++]; size_t y = deltaAddrG >> wDec; // Calculate the coordinates of size_t x = deltaAddrG & wMask; // the current field and of the @@ -92,7 +101,7 @@ void computeDistances(Map &map, std::vector &sources, if (side == 0) { *addr = g; - places.push(deltaAddrC[ci]); + places[tail++] = deltaAddrC[ci]; } } } @@ -103,9 +112,11 @@ int computeAverageDistance(Map &map, std::vector &grid, int areaN, { long total = 0; int count = 0; - for (int x = 0; x < map.getW(); ++x) + // Addition is commutative, so summing in whichever order touches memory sequentially - + // y outside x, matching grid/heightmap's row-major layout - gives the exact same total. + for (int y = 0; y < map.getH(); ++y) { - for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) { if (grid[y * map.getW() + x] == areaN) { diff --git a/src/map/generator/shared/Regions.cpp b/src/map/generator/shared/Regions.cpp index d55582ebe..159a49c12 100644 --- a/src/map/generator/shared/Regions.cpp +++ b/src/map/generator/shared/Regions.cpp @@ -14,11 +14,45 @@ #include #include #include -#include using namespace MapGeneration; namespace MapGeneration { +namespace +{ +// Collects every (x, y) with pred(x, y) true, in the same x-major, y-minor order a plain +// `for x { for y { if (pred(x, y)) points.push_back(...) } }` scan produces - several callers +// pick a result by a random index, so the elements must land in that same order, not merely be +// the same set. But such a scan reads a row-major grid against its grain: for fixed x, striding +// by a full row (map.getW() ints) on every y step touches a new cache line almost every time, +// where the same predicate evaluated y-outer, x-inner would stay within one line for many +// consecutive tiles. This gets both: a row-major counting pass sizes each output column, then a +// second row-major pass drops each point straight into its final slot - two sequential-access +// passes over the grid instead of one that strides across it, same result either way. +template +void collectPointsColumnOrder(int w, int h, Pred pred, std::vector &points) +{ + std::vector countPerColumn(w, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (pred(x, y)) + ++countPerColumn[x]; + std::vector cursor(w); + int total = 0; + for (int x = 0; x < w; ++x) + { + cursor[x] = total; + total += countPerColumn[x]; + } + const size_t base = points.size(); + points.resize(base + total, MapGeneratorPoint(0, 0)); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (pred(x, y)) + points[base + cursor[x]++] = MapGeneratorPoint(x, y); +} +} // namespace + bool divideUpArea(Map &map, GenerationContext &context, std::vector &grid, int areaN, std::vector &weights, std::vector &areaNumbers) { @@ -67,15 +101,10 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, std::any_of(weights.begin(), weights.end(), [](int w) { return w <= 0 || w > 10000; })) throw GenerationFailure("Invalid point dispersion inputs"); std::vector startingPoints; - for (int x = 0; x < map.getW(); ++x) { - for (int y = 0; y < map.getH(); ++y) - { - if (grid[y * map.getW() + x] == areaN) - { - startingPoints.push_back(MapGeneratorPoint(x, y)); - } - } + const int w = map.getW(); + collectPointsColumnOrder(w, map.getH(), [&](int x, int y) { return grid[y * w + x] == areaN; }, + startingPoints); } if (startingPoints.size() < points.size()) @@ -93,24 +122,19 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, for (unsigned int i = 0; i < points.size(); ++i) { + const int w = map.getW(), h2 = map.getH(); + // The original single pass tracked a running max and reset its candidate list on every + // strict increase; by construction that converges to exactly "every tile at the eventual + // global max", in scan order, whatever the intermediate history was. Two explicit passes + // - find the max, then collect every tile equal to it - reach the identical set in the + // identical order without carrying that reset logic through a stride-w memory access. int max = 0; + for (int y = 0; y < h2; ++y) + for (int x = 0; x < w; ++x) + max = std::max(max, heights[y * w + x]); std::vector possible; - for (int x = 0; x < map.getW(); ++x) - { - for (int y = 0; y < map.getH(); ++y) - { - int h = heights[y * map.getW() + x]; - if (h > max) - { - max = h; - possible.clear(); - } - if (h >= max) - { - possible.push_back(MapGeneratorPoint(x, y)); - } - } - } + collectPointsColumnOrder(w, h2, [&](int x, int y) { return heights[y * w + x] >= max; }, + possible); int n = context.stream("regions")() % possible.size(); points[i] = possible[n]; sources.push_back(points[i]); @@ -149,47 +173,62 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, std::int64_t orig = best; int best_x = -1; int best_y = -1; - for (int dx = search == PointSearch::Local ? -3 : 0; - dx <= (search == PointSearch::Local ? 3 : map.getW() - 1); ++dx) + auto tryCandidate = [&](int nx, int ny) { - for (int dy = search == PointSearch::Local ? -3 : 0; - dy <= (search == PointSearch::Local ? 3 : map.getH() - 1); ++dy) + if (nx == points[i].x && ny == points[i].y) + return; + if (grid[ny * map.getW() + nx] != areaN) + return; + std::int64_t score = std::numeric_limits::max(); + bool invalid = false; + for (unsigned int j = 0; j < points.size(); ++j) { - int nx = search == PointSearch::Local ? map.normalizeX(points[i].x + dx) : dx; - int ny = search == PointSearch::Local ? map.normalizeY(points[i].y + dy) : dy; - if (nx == points[i].x && ny == points[i].y) - continue; - if (grid[ny * map.getW() + nx] != areaN) + if (i == j) continue; - std::int64_t score = std::numeric_limits::max(); - bool invalid = false; - for (unsigned int j = 0; j < points.size(); ++j) + if (nx == points[j].x && ny == points[j].y) { - if (i == j) - continue; - if (nx == points[j].x && ny == points[j].y) - { - invalid = true; - break; - } - if (search == PointSearch::WholeRegion && ++evaluations > 20000000) - throw GenerationFailure( - "Point dispersion exhausted its distance-evaluation budget"); - std::int64_t dist = - std::int64_t(map.warpDistSquare(nx, ny, points[j].x, points[j].y)) * - weights[j]; - score = std::min(dist, score); - } - if (invalid) - continue; - - if (score > best) - { - best = score; - best_x = nx; - best_y = ny; + invalid = true; + break; } + if (search == PointSearch::WholeRegion && ++evaluations > 20000000) + throw GenerationFailure( + "Point dispersion exhausted its distance-evaluation budget"); + std::int64_t dist = + std::int64_t(map.warpDistSquare(nx, ny, points[j].x, points[j].y)) * + weights[j]; + score = std::min(dist, score); } + if (invalid) + return; + if (score > best) + { + best = score; + best_x = nx; + best_y = ny; + } + }; + if (search == PointSearch::WholeRegion) + { + // A full-grid scan: nesting the row coordinate outside the column coordinate + // keeps it within the grain of grid's row-major layout instead of striding + // across it a full row at a time on every step. This can change which + // exactly-tied candidate wins a placement (best only updates on strict + // improvement), but never which candidates exist or how good the eventual + // choice is - already documented as bounded best-response, not a single + // guaranteed optimum, so a different tie winner is not a different guarantee. + for (int ny = 0; ny < map.getH(); ++ny) + for (int nx = 0; nx < map.getW(); ++nx) + tryCandidate(nx, ny); + } + else + { + // PointSearch::Local's window is the 7x7 area right around the point itself - + // small enough that traversal order was never going to matter for cache + // behavior - so its enumeration order is left exactly as it was. + for (int dx = -3; dx <= 3; ++dx) + for (int dy = -3; dy <= 3; ++dy) + tryCandidate(map.normalizeX(points[i].x + dx), + map.normalizeY(points[i].y + dy)); } if (best_x != -1) { @@ -237,7 +276,14 @@ void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, i Uint32 hMask = map.hMask; Uint32 wMask = map.wMask; - std::vector> squares(points.size()); + // Frontier cells are inserted at a random position (not just pushed/popped from one end) so + // the flood grows into an organic, non-directional shape rather than a stack's snake or a + // queue's rings - that randomness is what's wanted here, not an accident to remove. But nothing + // about that requires a linked list: std::vector supports the identical operations (random + // std::advance is O(1) pointer arithmetic on its random-access iterator; insert/erase shift + // the tail via a contiguous memmove) while never chasing a pointer to a separately allocated + // node the way every std::list link does. Same elements, same positions, same output. + std::vector> squares(points.size()); std::vector expansion(points.size(), 0); std::vector current; std::vector count; @@ -264,7 +310,7 @@ void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, i while (expansion[p] > 0 && !squares[p].empty()) { Uint32 deltaAddrG = squares[p].back(); - squares[p].erase(--squares[p].end()); + squares[p].pop_back(); size_t y = deltaAddrG >> wDec; // Calculate the coordinates of size_t x = deltaAddrG & wMask; // the current field and of the @@ -312,7 +358,7 @@ void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, i expansion[p] -= 1; Uint32 randLocation = context.stream("regions")() % count[p]; - std::list::iterator i = squares[p].begin(); + std::vector::iterator i = squares[p].begin(); std::advance(i, randLocation); squares[p].insert(i, deltaAddrC[ci]); } @@ -327,27 +373,17 @@ void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, i void getAllPoints(Map &map, std::vector &grid, int areaN, std::vector &points) { - for (int x = 0; x < map.getW(); ++x) - { - for (int y = 0; y < map.getH(); ++y) - { - if (grid[y * map.getW() + x] == areaN) - points.push_back(MapGeneratorPoint(x, y)); - } - } + const int w = map.getW(); + collectPointsColumnOrder(w, map.getH(), [&](int x, int y) { return grid[y * w + x] == areaN; }, + points); } void getAllOtherPoints(Map &map, std::vector &grid, int areaN, std::vector &points) { - for (int x = 0; x < map.getW(); ++x) - { - for (int y = 0; y < map.getH(); ++y) - { - if (grid[y * map.getW() + x] != areaN) - points.push_back(MapGeneratorPoint(x, y)); - } - } + const int w = map.getW(); + collectPointsColumnOrder(w, map.getH(), [&](int x, int y) { return grid[y * w + x] != areaN; }, + points); } void getAllPointsLine(Map &map, int x1, int y1, int x2, int y2, @@ -414,26 +450,19 @@ void getAllPointsLine(Map &map, int x1, int y1, int x2, int y2, void findBorderPoints(Map &map, std::vector &grid, std::vector &points) { - for (int x = 0; x < map.getW(); ++x) - { - for (int y = 0; y < map.getH(); ++y) + const int w = map.getW(); + collectPointsColumnOrder( + w, map.getH(), + [&](int x, int y) { - bool found = false; - for (int dx = -1; dx <= 1 && !found; ++dx) - { - for (int dy = -1; dy <= 1 && !found; ++dy) - { - if (grid[map.normalizeY(y + dy) * map.getW() + map.normalizeX(x + dx)] != - grid[y * map.getW() + x]) - { - found = true; - } - } - } - if (found) - points.push_back(MapGeneratorPoint(x, y)); - } - } + for (int dx = -1; dx <= 1; ++dx) + for (int dy = -1; dy <= 1; ++dy) + if (grid[map.normalizeY(y + dy) * w + map.normalizeX(x + dx)] != + grid[y * w + x]) + return true; + return false; + }, + points); } void chooseRandomPoints(Map &map, GenerationContext &context, From e66723d0b8296b223ea341dd37f99d1f2a4a43f3 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Thu, 10 Sep 2026 23:58:21 -0400 Subject: [PATCH 020/136] Bump Contested Commons' revision for its WholeRegion cache-order fix The previous commit's PointSearch::WholeRegion traversal-order change can change which exactly-tied candidate site wins a placement, so a given seed can produce a different (equally valid, by the search's own metric) map than before. Following this repository's existing convention for generation-output changes, bump the revision so that is visible rather than a silent side effect. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- src/map/generator/generators/ContestedCommonsGenerator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp index 0a775cf91..8df8696ce 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.cpp +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -443,7 +443,7 @@ GeneratorDefinition contestedCommonsDefinition() return {"contested-commons", 9, "Contested commons", - 1, + 2, false, {{"home-island-size", "Home island size", 20, 35, 5, 25, ControlGroup::Terrain}, {"commons-size", "Commons size", 250, 500, 50, 400, ControlGroup::Terrain}, From afcb7fd8e88381be542062c83f2ae3e3398d5de8 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 00:11:03 -0400 Subject: [PATCH 021/136] Finish replacing std::queue with preallocated flat FIFOs across the flood helpers A follow-up sweep for every remaining std::queue/std::list/std::map in the map generator code, not just the ones profiling had already pointed at. Found three more BFS floods with the exact same shape as computeDistances' earlier fix - each bounded to at most w*h enqueues by a dist[...] < 0 or visited[...] == stamp guard, so a flat preallocated buffer is always enough - that were missed the first time round: - StartingPositions.cpp's own distanceToResource and scoreAsBuilt still used std::queue internally even after the hard[] caching work in an earlier commit only replaced the isHardSpaceForGroundUnit calls, not the queue. scoreAsBuilt compounds differently from the rest: it runs this same flood up to 900 times per chooseBalancedStarts call, so a fresh std::queue (a fresh std::deque, freshly allocated) 900 times over was the real cost, not each flood's own pushes. The fix reuses one pair of preallocated buffers across all 900 calls, resetting two indices between them instead of reallocating a container each time. - Resources.cpp's floodReach and terrainOnlyReach (the resource-wall detector's reachability floods). - StartQuality.cpp's walkFromWorkers. Checked and left alone: Topology.cpp's own queue runs over Maze's cell graph (dozens of nodes, not tiles), where the difference is unmeasurable, and its std::map is exercised only by test code, not by any generator. Also swept every other context.stream() call site beyond the two fixed in the previous commit; the rest all draw well under a thousand times per generation (fruit placement, layout shuffles), far too infrequent for the map-lookup overhead to matter. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. MapGeneratorStudy succeeds across all 13 generators at 6 seeds each, with bit-identical per-tile hashes on every generator (including Contested Commons matching the previous commit's WholeRegion-affected values exactly, seed for seed). Measured (interleaved before/after binaries, repeated 3x, with two generators that never call chooseBalancedStarts run the same way as a noise check): swamp ~14%, islands ~14%, crater lakes ~18% faster; river's readings were too noisy this round to quote a single number but moved the same direction every time. Contested Commons and Fjord Continent - untouched by this change - showed ~0% and ~2% respectively, the same noise floor as the previous commit found. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 3 ++ src/map/generator/shared/Resources.cpp | 28 ++++++++------- src/map/generator/shared/StartQuality.cpp | 15 ++++---- .../generator/shared/StartingPositions.cpp | 35 ++++++++++++------- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index fb8cafdf4..40ecb8b5c 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -49,4 +49,7 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - `splitUpPoints`'s `PointSearch::WholeRegion` mode (Contested Commons only) runs the same kind of scan for real - `for dx in 0..w { for dy in 0..h { ... } }` scoring every legal tile as a candidate placement, the one shape here that a plain loop-order swap could not fix without changing anything: the search keeps only a strict improvement (`score > best`), so whichever tied candidate is reached earliest in scan order wins, and reordering the scan can hand that win to a different tied candidate. Measured directly (7 seeds): 6 of 7 produced byte-identical maps regardless, and the one that changed still converged to a placement of the same quality by the search's own metric - already documented as bounded best-response, not a guaranteed optimum, so a different tied winner is not a different guarantee. Contested Commons' revision moves to 2 for it. `PointSearch::Local` (used far more broadly, including as swamp/river/islands/crater lakes' rare legacy fallback) keeps its exact original scan order untouched - its window is 7x7, too small for traversal order to matter, and touches far more generators than the fix was worth risking. - Measured (interleaved before/after binaries, repeated 3x at 150 generations each after an initial single-run reading proved noisy enough to overstate the effect by more than 2x): Concrete Islands about 9% faster, Isles about 5-7% faster, Contested Commons about 8-9% faster overall (its own share from the exact fixes alone was too close to this machine's run-to-run noise floor to separate cleanly from the `WholeRegion` fix's own further ~4%). A generator that never calls into any of this code (Fjord Continent) showed about 2% change over the same run, taken as that noise floor rather than a real effect. +- A follow-up sweep of every remaining `std::queue`/`std::list`/`std::map` in the map generator code (not just the ones profiling had already pointed at) found three more of the same shape as `computeDistances`' earlier fix, all bounded the same way (a `dist[...] < 0` or `visited[...] == stamp` guard means no flood ever enqueues a tile twice, so a flat `w*h`-sized buffer is always enough): `StartingPositions.cpp`'s own `distanceToResource` and `scoreAsBuilt` still used `std::queue` internally even after the `hard[]` caching work above; `Resources.cpp`'s `floodReach` and `terrainOnlyReach` (the resource-wall detector); and `StartQuality.cpp`'s `walkFromWorkers`. `scoreAsBuilt`'s case compounds differently from the rest: it runs its own flood up to 900 times per `chooseBalancedStarts` call, so a fresh `std::queue` (a fresh `std::deque`, freshly allocated) 900 times over was the real cost, not just each flood's own pushes — the fix there reuses one pair of preallocated buffers across all 900 calls, resetting two indices between them instead of reallocating. Checked but left alone: `Topology.cpp`'s own queue runs over Maze's *cell* graph (dozens of nodes, not tiles) where the difference is unmeasurable, and its `std::map` is exercised only by test code, not by any generator. + Measured (interleaved before/after binaries, repeated 3x, with two generators that never call `chooseBalancedStarts` run the same way as a noise check): swamp ~14%, islands ~14%, crater lakes ~18% faster; river's readings were too noisy this round to quote a single number but moved the same direction every time. Contested Commons and Fjord Continent — untouched by this work — showed ~0% and ~2% respectively, the same noise floor as before. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 2c9deebf7..b98d0758e 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -13,7 +13,6 @@ #include "Terrain.h" #include #include -#include #include using namespace MapGeneration; @@ -113,13 +112,16 @@ ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int clo const int w = map.getW(), h = map.getH(); ReachResult r; r.dist.assign(size_t(w) * h, -1); - std::queue q; + // Bounded by the r.dist[np] < 0 guard below to at most w*h enqueues - a flat preallocated + // FIFO instead of std::queue's std::deque, which grows by separately heap-allocated + // blocks (same fix as computeDistances in Distances.cpp). + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; int start = bootY * w + bootX; r.dist[start] = 0; - q.push(start); - while (!q.empty()) { - int p = q.front(); - q.pop(); + q[qTail++] = start; + while (qHead < qTail) { + int p = q[qHead++]; int x = p % w, y = p / w; if (map.getUMTerrain(x, y) == GRASS && r.dist[p] >= clearRadius && r.dist[p] <= exploreLimit) (r.dist[p] <= closeRange ? r.closeGrass : r.farGrass).push_back(MapGeneratorPoint(x, y)); @@ -137,7 +139,7 @@ ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int clo if (r.dist[np] < 0 && r.dist[p] < exploreLimit && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { r.dist[np] = r.dist[p] + 1; - q.push(np); + q[qTail++] = np; } } } @@ -151,13 +153,13 @@ ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int clo std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit) { const int w = map.getW(), h = map.getH(); std::vector dist(size_t(w) * h, -1); - std::queue q; + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; int start = bootY * w + bootX; dist[start] = 0; - q.push(start); - while (!q.empty()) { - int p = q.front(); - q.pop(); + q[qTail++] = start; + while (qHead < qTail) { + int p = q[qHead++]; if (dist[p] >= limit) continue; int x = p % w, y = p / w; @@ -169,7 +171,7 @@ std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit) { int np = ny * w + nx; if (dist[np] < 0 && !map.isWater(nx, ny)) { dist[np] = dist[p] + 1; - q.push(np); + q[qTail++] = np; } } } diff --git a/src/map/generator/shared/StartQuality.cpp b/src/map/generator/shared/StartQuality.cpp index a24fd3a0e..c4a511718 100644 --- a/src/map/generator/shared/StartQuality.cpp +++ b/src/map/generator/shared/StartQuality.cpp @@ -6,7 +6,6 @@ #include "Unit.h" #include #include -#include namespace MapGeneration { @@ -20,17 +19,19 @@ std::vector walkFromWorkers(const Map &map, const std::vector &workers { const int w = map.getW(), h = map.getH(); std::vector dist(size_t(w) * h, -1); - std::queue q; + // Bounded by the dist[np] < 0 guard below to at most w*h enqueues - a flat preallocated FIFO + // instead of std::queue's std::deque, which grows by separately heap-allocated blocks. + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; for (int p : workers) if (dist[p] < 0) { dist[p] = 0; - q.push(p); + q[qTail++] = p; } - while (!q.empty()) + while (qHead < qTail) { - const int p = q.front(); - q.pop(); + const int p = q[qHead++]; const int x = p % w, y = p / w; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) @@ -42,7 +43,7 @@ std::vector walkFromWorkers(const Map &map, const std::vector &workers if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { dist[np] = dist[p] + 1; - q.push(np); + q[qTail++] = np; } } } diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index 24be6c32e..d559a7ad5 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -15,7 +15,6 @@ #include "Unit.h" #include #include -#include #include #include using namespace MapGeneration; @@ -305,7 +304,11 @@ std::vector distanceToResource(Map &map, const std::vector dist(size_t(w) * h, -1); - std::queue q; + // Every cell is enqueued at most once (the dist[np] < 0 guard below), so this never needs + // more than w*h slots - a flat preallocated FIFO instead of std::queue's + // std::deque-backed, block-by-block growth (see computeDistances for the same fix). + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { @@ -319,14 +322,13 @@ std::vector distanceToResource(Map &map, const std::vector distanceToResource(Map &map, const std::vector visited(size_t(w) * h, 0); std::uint16_t visitStamp = 0; + // scoreAsBuilt runs its own small flood per candidate site - up to 900 times per call to + // this function. A fresh std::queue per call means a fresh std::deque allocation (and its + // block-by-block growth) 900 times over; every one of those floods visits each tile at most + // once (the visitStamp guard below), so one pair of w*h-sized buffers, reused across every + // call and just reset to empty (an O(1) index reset, not a reallocation), is always enough. + std::vector queueTile(size_t(w) * h), queueDist(size_t(w) * h); auto scoreAsBuilt = [&](int bx, int by, int limit) -> int { auto cleared = [&](int tx, int ty) @@ -394,7 +402,7 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu return ox >= 0 && ox < swarm->width && oy >= 0 && oy < swarm->height; }; ++visitStamp; - std::queue> q; // (tile, distance) + size_t qHead = 0, qTail = 0; auto push = [&](int tx, int ty, int d) { int nx = map.normalizeX(tx), ny = map.normalizeY(ty); @@ -402,16 +410,19 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu if (visited[np] == visitStamp) return; visited[np] = visitStamp; - q.push({np, d}); + queueTile[qTail] = np; + queueDist[qTail] = d; + ++qTail; }; // Workers spawn on the row above the swarm, so that is where a gathering trip starts. for (int i = 0; i < std::max(1, context.request.nbWorkers); ++i) push(bx + (i % 4), by - 1 - (i / 4), 0); int wood = -1, wheat = -1; - while (!q.empty() && (wood < 0 || wheat < 0)) + while (qHead < qTail && (wood < 0 || wheat < 0)) { - auto [p, d] = q.front(); - q.pop(); + int p = queueTile[qHead]; + int d = queueDist[qHead]; + ++qHead; if (d >= limit) continue; int x = p % w, y = p / w; From af3bb6a75ed94a73afbc150f7aacece7ee99ae5c Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 08:21:15 -0400 Subject: [PATCH 022/136] Give Fjord Continent an ambient resource scatter to restore map personality Playtesting raised a real complaint unrelated to the performance work in recent commits: the new generators feel like they've lost personality, with resources reading as small guaranteed blobs stuck next to bases instead of a map with its own texture. Traced this concretely (MapGeneratorStudy's dump= option, rendered and inspected directly) rather than guessing. Fjord Continent's every resource is a deliberate, counted placement: a home starter kit, fjord bank resources on each side of every fjord, stone/fruit in the untouched core, 0-4 outlier islands, and a shoreline algae band - never a map-wide painting pass. That leaves the whole continent interior between those points bare grass. Rendered, it looks exactly like the complaint: a handful of isolated single-tile dots on an otherwise empty landmass, next to Swamp or River's rich, varied, noise-painted texture. Lattice and Maze already solve the identical problem with scatterResources - a light map-wide clump scatter - called before either generator's own guaranteed per-team placements, so a guarantee is placed after and can never be buried or bumped by the ambient layer. Fjord now does the same, at a modest density (wood/corn/stone; algae is left at zero since the shoreline band a few steps later already places it with a shape tuned to the coastline, and scattering more over open water would just fight that). This is a real, deliberate change to how the generator plays, not only its output - the four generators whose colony placement changed earlier in this PR got the same treatment, and the same reasoning applies here. Revision moves to 2. Contested Commons' resource layout was inspected the same way and left alone: its solid, sharply-bordered zones are an explicit, already-reasoned design in its own code ("real fields...not a light dusting", with zones deliberately never bordering a same-role neighbor) - a different design choice, not the same gap. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. Generation succeeds at realistic team counts (1, 2, 4, 8 - 0 failures across 20+ seeds each). At 12 teams - already a barely-supported configuration before this change, failing 18 of 20 seeds on "no swarm footprint fits inside the home region" with no scatter at all - it now fails all 20; a real but marginal effect on a configuration that was not reliable to begin with. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 2 ++ .../generators/FjordContinentGenerator.cpp | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 40ecb8b5c..771113071 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -52,4 +52,6 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - A follow-up sweep of every remaining `std::queue`/`std::list`/`std::map` in the map generator code (not just the ones profiling had already pointed at) found three more of the same shape as `computeDistances`' earlier fix, all bounded the same way (a `dist[...] < 0` or `visited[...] == stamp` guard means no flood ever enqueues a tile twice, so a flat `w*h`-sized buffer is always enough): `StartingPositions.cpp`'s own `distanceToResource` and `scoreAsBuilt` still used `std::queue` internally even after the `hard[]` caching work above; `Resources.cpp`'s `floodReach` and `terrainOnlyReach` (the resource-wall detector); and `StartQuality.cpp`'s `walkFromWorkers`. `scoreAsBuilt`'s case compounds differently from the rest: it runs its own flood up to 900 times per `chooseBalancedStarts` call, so a fresh `std::queue` (a fresh `std::deque`, freshly allocated) 900 times over was the real cost, not just each flood's own pushes — the fix there reuses one pair of preallocated buffers across all 900 calls, resetting two indices between them instead of reallocating. Checked but left alone: `Topology.cpp`'s own queue runs over Maze's *cell* graph (dozens of nodes, not tiles) where the difference is unmeasurable, and its `std::map` is exercised only by test code, not by any generator. Measured (interleaved before/after binaries, repeated 3x, with two generators that never call `chooseBalancedStarts` run the same way as a noise check): swamp ~14%, islands ~14%, crater lakes ~18% faster; river's readings were too noisy this round to quote a single number but moved the same direction every time. Contested Commons and Fjord Continent — untouched by this work — showed ~0% and ~2% respectively, the same noise floor as before. +- **Feel:** playing Fjord Continent after the performance work above surfaced a separate, real complaint that has nothing to do with speed — every resource on it is a deliberate, counted placement (a home starter kit, fjord bank resources, the core, one to four outlier islands), so the whole continent interior between those points is bare grass. Rendered and inspected directly (`MapGeneratorStudy ... dump=`), this reads as empty rather than as a place with its own history the way a noise-painted map does — exactly the "small blobs near bases, no personality" complaint. Lattice and Maze already solve the identical problem with `scatterResources`, a light map-wide clump scatter that runs before any guaranteed placement claims its own spot (so a guarantee can never be buried or bumped); Fjord now calls it too, at a modest density (wood/corn/stone, no algae — the shoreline band already places that with a shape tuned to the coastline, and scattering more over open water would fight it). Rendered side by side, the difference is stark: from a handful of isolated single-tile dots to a continent that reads the way Swamp, River or Maze already did. Verified generation success unaffected at realistic team counts (1, 2, 4, 8 - 0 failures across 20+ seeds each); at 12 teams, already a barely-supported configuration before this change (18/20 seeds failed on "no swarm footprint fits inside the home region" even without any scatter), it now fails all 20 - a real but marginal effect on a configuration that was not reliable to begin with. Revision moves to 2. Contested Commons' resource layout was inspected the same way and left alone: its solid, sharply-bordered zones ("a real wheat field and a real treeline, not a light dusting") are an explicit, already-reasoned design choice recorded in its own code, not the same gap. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 27d450aec..573f47209 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -257,6 +257,19 @@ static bool generate(Game &game, GenerationContext &context) { game.map.controlSand(); + // 3.5) Every resource so far is a deliberate, counted placement tied to a specific + // purpose: a home starter kit, a fjord bank, the core, an outlier island. That leaves + // the whole continent interior in between them bare grass, which reads as empty rather + // than as a place with its own history the way a noise-painted map does. A light + // map-wide scatter -- the same mechanism Lattice and Maze already rely on for this -- + // fills that gap with ordinary, unclaimed deposits before any of the guaranteed + // placements below claim their own spots, so it can never compete with or bury a + // guarantee (everything from here on is placed after, and setResource simply + // overwrites whatever an earlier scatter happened to put on that exact tile). Algae is + // left at zero here: the shoreline band in step 7 already places it with a shape tuned + // to the coastline, and scattering more over open water would just fight that. + scatterResources(game, context, {/*corn=*/18, /*wood=*/18, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); + // 4) Anchor each team out at its own tip: start just inland of the coast at // the team's angle and only back off toward the core if that exact spot turns // out to be water (a sharp jaggedness dip, or a fjord belly that swung wider @@ -446,7 +459,7 @@ GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 1, + 2, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, From 4375ae860b78acc15d806bc450130ac642407f8c Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 08:45:47 -0400 Subject: [PATCH 023/136] Populate Map::Tile::fertility during generation, not just on load or in the editor Playing raised a real question: why does the in-game fertility overlay show nothing on a freshly generated map? Traced to the source instead of assumed. OverlayArea::compute's Fertility branch (src/OverlayAreas.cpp) only ever copies whatever is already sitting in Map::Tile::fertility - it never computes anything itself. The only two things that ever populate that field are FertilityCalculator (the map editor's "compute fertility" checkbox, and Game_io.cpp's load-time migration for save files older than format version 63, which the comment there describes as "did not have fertility computed with the map, but computed it live") and, now, scoreStarts. Neither of the first two runs as part of generation, so every map that comes out of any of the 13 generators - every custom or lobby game - starts with an all-zero fertility field. This predates and is independent of the recent switch to computing fertility via Fertility::Field: it dates back to whenever version-63 saves started shipping fertility baked into the file instead of computing it live, and generation was simply never updated to do the same. scoreStarts already builds a full Fertility::Field for every roll, purely to score colonies against (see the previous fertility-related commits in this branch). Stamping that same field into the map's own tiles - the same clamp-to-Uint16 FertilityCalculator uses - is a byproduct of work already being paid for, not new work, and scoreStarts is the only point in the generation pipeline that already has a finished map (colonies built, resources final) to stamp it onto. Takes Game& instead of const Game& to do this; its only two callers (GenerationService::generate, MapGeneratorStudy) already held a non-const reference, so this needed no other changes. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. MapGeneratorStudy succeeds across all 13 generators at 5 seeds each. Confirmed directly (a temporary counter in the profiling driver, since nothing else exposes this) that a freshly generated map now carries a non-trivial fertilityMaximum and real per-tile values across every generator checked, where it was unconditionally zero before. Measured no change outside this machine's run-to-run noise on Lattice, Maze or Swamp - the write is a single O(tile count) pass over data the scoring loop already computed, not a new computation. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 2 ++ src/map/generator/shared/StartQuality.cpp | 27 ++++++++++++++++++++--- src/map/generator/shared/StartQuality.h | 9 +++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 771113071..30505d480 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -54,4 +54,6 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - **Feel:** playing Fjord Continent after the performance work above surfaced a separate, real complaint that has nothing to do with speed — every resource on it is a deliberate, counted placement (a home starter kit, fjord bank resources, the core, one to four outlier islands), so the whole continent interior between those points is bare grass. Rendered and inspected directly (`MapGeneratorStudy ... dump=`), this reads as empty rather than as a place with its own history the way a noise-painted map does — exactly the "small blobs near bases, no personality" complaint. Lattice and Maze already solve the identical problem with `scatterResources`, a light map-wide clump scatter that runs before any guaranteed placement claims its own spot (so a guarantee can never be buried or bumped); Fjord now calls it too, at a modest density (wood/corn/stone, no algae — the shoreline band already places that with a shape tuned to the coastline, and scattering more over open water would fight it). Rendered side by side, the difference is stark: from a handful of isolated single-tile dots to a continent that reads the way Swamp, River or Maze already did. Verified generation success unaffected at realistic team counts (1, 2, 4, 8 - 0 failures across 20+ seeds each); at 12 teams, already a barely-supported configuration before this change (18/20 seeds failed on "no swarm footprint fits inside the home region" even without any scatter), it now fails all 20 - a real but marginal effect on a configuration that was not reliable to begin with. Revision moves to 2. Contested Commons' resource layout was inspected the same way and left alone: its solid, sharply-bordered zones ("a real wheat field and a real treeline, not a light dusting") are an explicit, already-reasoned design choice recorded in its own code, not the same gap. +- Playing further surfaced a second, unrelated gap: the in-game fertility overlay (`GameGUIInputMenuClick.cpp`/`GameGUIStep.cpp`) showed nothing at all on a freshly generated map. Traced to the source rather than assumed: `OverlayArea::compute`'s Fertility branch only ever copies whatever is already sitting in `Map::Tile::fertility` — it never computes anything itself. The only two things that ever populate that field are `FertilityCalculator` (the map editor's "compute fertility" checkbox, and `Game_io.cpp`'s load-time migration for save files older than format version 63) and, now, `scoreStarts`. Neither of the first two runs as part of generation, so every map that comes out of the generator — every custom or lobby game — started with an all-zero fertility field regardless of which fertility algorithm computed it; this was not a regression from replacing that algorithm; it dates back to whenever version-63 saves started shipping fertility baked into the file instead of computing it live, and generation was simply never updated to do the same. `scoreStarts` already builds a full `Fertility::Field` for every roll to score colonies against — stamping that same field into the map's own tiles is a byproduct of work already being paid for, not a new cost (measured: no change outside noise on Lattice, Maze or Swamp), and it is the only point in the generation pipeline that already has a finished map to stamp it onto. `scoreStarts` takes `Game&` instead of `const Game&` for this; its only two callers already held a non-const reference. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/shared/StartQuality.cpp b/src/map/generator/shared/StartQuality.cpp index c4a511718..80b4ddb90 100644 --- a/src/map/generator/shared/StartQuality.cpp +++ b/src/map/generator/shared/StartQuality.cpp @@ -6,6 +6,7 @@ #include "Unit.h" #include #include +#include namespace MapGeneration { @@ -51,11 +52,11 @@ std::vector walkFromWorkers(const Map &map, const std::vector &workers } } // namespace -StartQualityReport scoreStarts(const Game &game, int requestedTeams, - const StartQualityWeights &weights, const StartQualityScale &scale) +StartQualityReport scoreStarts(Game &game, int requestedTeams, const StartQualityWeights &weights, + const StartQualityScale &scale) { StartQualityReport report; - const Map &map = game.map; + Map &map = game.map; const int w = map.getW(), h = map.getH(); const int nbTeams = std::min(game.teamsCount(), requestedTeams); if (nbTeams <= 0) @@ -77,6 +78,26 @@ StartQualityReport scoreStarts(const Game &game, int requestedTeams, return report; // nothing walked out of this colony; there is nothing to score const Fertility::Field fertility = Fertility::forMap(map); + + // scoreStarts already pays for this field on every roll; stamping it into the map's own + // tiles is a free byproduct of that, not a new cost, and it is otherwise the only way a + // freshly generated map ever gets one. FertilityCalculator (the map editor's "compute + // fertility" action, and Game_io.cpp's load-time migration for saves older than version 63) + // is the only other thing that ever populates these two fields, and neither runs as part of + // generation - so without this, every custom or lobby game starts with an all-zero fertility + // overlay regardless of how the field itself is computed. Same clamp FertilityCalculator + // uses: kScale's own ceiling (an all-water neighbourhood) is one past what a Uint16 holds. + constexpr std::uint32_t kFertilityCeiling = std::numeric_limits::max(); + Uint16 fertilityMax = 0; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Uint16 value = static_cast(std::min(fertility.at(x, y), kFertilityCeiling)); + map.getTile(x, y).fertility = value; + fertilityMax = std::max(fertilityMax, value); + } + map.fertilityMaximum = fertilityMax; + report.colonies.resize(nbTeams); for (int team = 0; team < nbTeams; ++team) diff --git a/src/map/generator/shared/StartQuality.h b/src/map/generator/shared/StartQuality.h index 67e8eaec9..c833c0709 100644 --- a/src/map/generator/shared/StartQuality.h +++ b/src/map/generator/shared/StartQuality.h @@ -60,8 +60,11 @@ struct StartQualityReport bool measured = false; }; -/// Requires a finished map: colonies built, workers placed, resources final. -StartQualityReport scoreStarts(const Game &game, int nbTeams, - const StartQualityWeights &weights = {}, +/// Requires a finished map: colonies built, workers placed, resources final. Also stamps +/// Map::Tile::fertility and Map::fertilityMaximum from the same Fertility::Field this already +/// computes for scoring, so a freshly generated map carries real fertility data instead of the +/// zeroes a never-computed tile defaults to (see FertilityCalculator, which is otherwise the +/// only thing that ever populates these two fields, and never runs as part of generation). +StartQualityReport scoreStarts(Game &game, int nbTeams, const StartQualityWeights &weights = {}, const StartQualityScale &scale = {}); } // namespace MapGeneration From 37024c97d3beaef805e72732cad2a267cc54b582 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 08:52:10 -0400 Subject: [PATCH 024/136] Fix a generation-failure regression in Fjord's ambient scatter, and scatter along fjord banks too The previous commit's ambient scatter (scatterResources, called right after controlSand - before any team anchor point, home area or swarm existed) was verified for generation success at the time, but not thoroughly enough: a larger seed sample at 8 teams turned up 3 of 20 seeds failing "no swarm footprint fits inside the home region" that succeeded without the scatter, confirmed by direct before/after comparison on the same seeds. isResourceAllowed refuses any tile a building or unit already occupies, but at the point the scatter ran, none existed yet, so an early scatter clump could claim the one tile a colony's swarm footprint needed with nothing downstream able to tell. Fix: move the identical scatterResources call to run after every team's swarm and workers are already placed, right before the fjord bank resources (which already had to run last for the same class of reason). This changes nothing about what the scatter places - setResource always overwrites regardless of order, so a scatter clump can now cosmetically overwrite one tile of an already-placed home-kit deposit, the same low-stakes trade already accepted in the other direction (a bank deposit overwriting a scatter tile). Re-verified on the original 3 failing seeds (now 0) and a fresh sample of 50 seeds each at 1/2/8 teams (0/50, 0/50, 1/50); the one remaining failure reproduces identically - same stage, same error, same hash - on the pre-scatter binary too, confirming it predates and is unrelated to any of today's work. Also extended the fjord banks themselves from two fixed clumps per side into a scattered strip: six more, lighter, best-effort clumps per side along the same bank, mixing corn/wood/stone with a small jitter so the spacing doesn't read as mechanical. Best-effort like the ambient scatter - nothing downstream depends on any single one existing, so a spot that doesn't pan out is simply skipped rather than failing generation. Walking a fjord's edge now reads as following a shoreline with its own economy rather than passing exactly two fixed deposits. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. MapGeneratorStudy succeeds across all 13 generators at 5 seeds each. Rendered and inspected directly (MapGeneratorStudy's dump= option) to confirm the coastal strip reads as intended alongside the interior scatter. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 3 ++ .../generators/FjordContinentGenerator.cpp | 47 ++++++++++++++----- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 30505d480..09093e587 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -56,4 +56,7 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Playing further surfaced a second, unrelated gap: the in-game fertility overlay (`GameGUIInputMenuClick.cpp`/`GameGUIStep.cpp`) showed nothing at all on a freshly generated map. Traced to the source rather than assumed: `OverlayArea::compute`'s Fertility branch only ever copies whatever is already sitting in `Map::Tile::fertility` — it never computes anything itself. The only two things that ever populate that field are `FertilityCalculator` (the map editor's "compute fertility" checkbox, and `Game_io.cpp`'s load-time migration for save files older than format version 63) and, now, `scoreStarts`. Neither of the first two runs as part of generation, so every map that comes out of the generator — every custom or lobby game — started with an all-zero fertility field regardless of which fertility algorithm computed it; this was not a regression from replacing that algorithm; it dates back to whenever version-63 saves started shipping fertility baked into the file instead of computing it live, and generation was simply never updated to do the same. `scoreStarts` already builds a full `Fertility::Field` for every roll to score colonies against — stamping that same field into the map's own tiles is a byproduct of work already being paid for, not a new cost (measured: no change outside noise on Lattice, Maze or Swamp), and it is the only point in the generation pipeline that already has a finished map to stamp it onto. `scoreStarts` takes `Game&` instead of `const Game&` for this; its only two callers already held a non-const reference. +- The ambient scatter added above (`scatterResources`, called right after `controlSand()` — before any team's anchor point, home area or swarm was chosen) was verified for generation success at the time, but not thoroughly enough: re-verified later with a larger seed sample at 8 teams and found 3 of 20 seeds now failing "no swarm footprint fits inside the home region" that succeeded before it, confirmed by direct comparison against the pre-scatter binary on the same seeds. `isResourceAllowed` refuses any tile a building or unit already occupies, but at the point the scatter ran, none existed yet — an early scatter clump could claim the one tile a colony's swarm footprint needed, with nothing downstream able to tell. Moving the identical call to run after every team's swarm and workers are already placed (right before the fjord bank resources, which already had to run last for the same reason) fixes it without changing what the scatter does: `setResource` always overwrites, so running later costs nothing but the reverse of the same low-stakes trade already accepted elsewhere in this generator — a scatter clump can now cosmetically overwrite one tile of an already-placed home-kit deposit, exactly as a bank deposit can already overwrite a scatter tile. Re-verified on the original 3 failing seeds (now 0) and on 50 fresh seeds at 1/2/8 teams (0/50, 0/50, 1/50 — the one remaining failure reproduces identically, same stage, same error, same hash, on the pre-scatter binary too, so it predates and is unrelated to this work). +- Also extended the fjord banks from two fixed clumps per side into a scattered strip: six additional lighter, best-effort clumps per side at points along the same bank, mixing corn/wood/stone with a small jitter so the spacing doesn't read as mechanical. Best-effort like the ambient scatter — nothing downstream depends on any one of them, so a spot that doesn't work out is skipped rather than failing generation. Walking a fjord's edge now reads as following a shoreline with its own economy rather than passing exactly two fixed deposits. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 573f47209..55f4a713b 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -257,19 +257,6 @@ static bool generate(Game &game, GenerationContext &context) { game.map.controlSand(); - // 3.5) Every resource so far is a deliberate, counted placement tied to a specific - // purpose: a home starter kit, a fjord bank, the core, an outlier island. That leaves - // the whole continent interior in between them bare grass, which reads as empty rather - // than as a place with its own history the way a noise-painted map does. A light - // map-wide scatter -- the same mechanism Lattice and Maze already rely on for this -- - // fills that gap with ordinary, unclaimed deposits before any of the guaranteed - // placements below claim their own spots, so it can never compete with or bury a - // guarantee (everything from here on is placed after, and setResource simply - // overwrites whatever an earlier scatter happened to put on that exact tile). Algae is - // left at zero here: the shoreline band in step 7 already places it with a shape tuned - // to the coastline, and scattering more over open water would just fight that. - scatterResources(game, context, {/*corn=*/18, /*wood=*/18, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); - // 4) Anchor each team out at its own tip: start just inland of the coast at // the team's angle and only back off toward the core if that exact spot turns // out to be water (a sharp jaggedness dip, or a fjord belly that swung wider @@ -430,6 +417,22 @@ static bool generate(Game &game, GenerationContext &context) { return false; } + // 8.5) Every resource so far is a deliberate, counted placement tied to a specific + // purpose: a home starter kit, the core, an outlier island. That leaves the whole + // continent interior in between them bare grass, which reads as empty rather than as a + // place with its own history the way a noise-painted map does. A light map-wide scatter + // -- the same mechanism Lattice and Maze already rely on for this -- fills that gap with + // ordinary, unclaimed deposits. This runs after every swarm and worker is already placed, + // not before: isResourceAllowed refuses any tile with a building or unit on it, so a + // scatter placed earlier could (and, measured directly, occasionally did) claim the one + // remaining tile a colony's swarm footprint needed, failing generation outright. Running + // last costs nothing here -- setResource always overwrites, so a scatter clump landing on + // an earlier deposit just loses that one tile to it, the same low-stakes trade every other + // generator's own layering already makes. Algae is left at zero: the shoreline band in + // step 7 already places it with a shape tuned to the coastline, and scattering more over + // open water would just fight that. + scatterResources(game, context, {/*corn=*/18, /*wood=*/18, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); + // 9) Place bank resources last so settlement and regional deposits cannot // overwrite the guarantee. Every side of every fjord receives both resources // at distinct points along its length. @@ -442,6 +445,24 @@ static bool generate(Game &game, GenerationContext &context) { !placeBankClump(game.map, context, fjordCenterlines[k], 0.68, side, WOOD)) return false; + + // A handful more, lighter clumps at other points along the same banks besides + // the two guaranteed spots above, so walking a fjord's edge feels like following + // a shoreline with its own economy rather than passing exactly two fixed + // deposits. Best-effort: unlike the guarantee above, nothing downstream depends + // on any single one of these existing, so a spot that doesn't pan out is simply + // skipped rather than failing generation. A small jitter on each progress value + // keeps the spacing from reading as mechanically regular. + static const double bankScatterProgress[] = {0.10, 0.22, 0.48, 0.58, 0.80, 0.92}; + for (double progress : bankScatterProgress) { + const double jitter = (context.bounded("resources", 41) - 20) / 1000.0; // +/-0.02 + for (int side : {-1, 1}) { + const int roll = context.bounded("resources", 8); + const int resourceType = roll < 3 ? CORN : roll < 6 ? WOOD : STONE; + placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, + resourceType); + } + } } return true; From 00a70c4b78830c89a09b2782a90cd9bc10795e55 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 09:06:53 -0400 Subject: [PATCH 025/136] Make scatterResources actually prefer farmable ground, not just legal ground Playing raised a real point: a lot of the resources scatterResources places (Fjord, Lattice and Maze's shared ambient layer) sit far enough from water that Map::growResources - which only regrows a wheat or wood tile near water - will never bring them back once harvested. The candidate search never looked at that: it drew a uniformly random legal tile and used it outright, so a corn or wood clump could as easily land on a one-time find as on real farmland. First attempt required only Fertility::Field::at(x,y) > 0 (computed ungated, since nothing has been harvested yet at this point in generation to gate against). Measured before/after and it changed nothing: on these water-heavy generators a single water tile up to 14 tiles away already contributes a nonzero - if as little as 4 out of a 65536 maximum - weight to nearly every legal tile, so ">0" was already true almost everywhere, fertile-preferring or not. Replaced the fixed bar with best-of-100: score every randomly drawn legal candidate by fertility and keep the highest seen across the same 100-draw budget, rather than stopping at the first past a threshold. A type that doesn't regrow this way (stone, algae, fruit) scores every candidate 0 and so keeps its original "first legal tile found" behaviour exactly. Verified: full scons client build and scons -C test suite (198 cppunit cases) pass. MapGeneratorStudy succeeds across all 13 generators (0 failures, 40 seeds each on Fjord/Lattice/Maze, 5 seeds each on the rest). Measured against a meaningful bar (fertility > 8000, the same "genuinely good farmland" reference StartQualityScale uses elsewhere) rather than the uninformative ">0": the fraction of scattered corn/wood clearing it rose from roughly 46% to 68% on Fjord, 40% to 88% on Lattice, and 52% to 96% on Maze. Fjord's revision moves to 3 (already at 2 from the ambient-scatter and coastal-strip work earlier in this branch); Lattice's and Maze's move to 2, their first change since this branch introduced them. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 2 + .../generators/FjordContinentGenerator.cpp | 2 +- .../generator/generators/LatticeGenerator.cpp | 2 +- .../generator/generators/MazeGenerator.cpp | 2 +- src/map/generator/shared/Resources.cpp | 41 ++++++++++++++----- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 09093e587..bf6aadbb6 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -59,4 +59,6 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - The ambient scatter added above (`scatterResources`, called right after `controlSand()` — before any team's anchor point, home area or swarm was chosen) was verified for generation success at the time, but not thoroughly enough: re-verified later with a larger seed sample at 8 teams and found 3 of 20 seeds now failing "no swarm footprint fits inside the home region" that succeeded before it, confirmed by direct comparison against the pre-scatter binary on the same seeds. `isResourceAllowed` refuses any tile a building or unit already occupies, but at the point the scatter ran, none existed yet — an early scatter clump could claim the one tile a colony's swarm footprint needed, with nothing downstream able to tell. Moving the identical call to run after every team's swarm and workers are already placed (right before the fjord bank resources, which already had to run last for the same reason) fixes it without changing what the scatter does: `setResource` always overwrites, so running later costs nothing but the reverse of the same low-stakes trade already accepted elsewhere in this generator — a scatter clump can now cosmetically overwrite one tile of an already-placed home-kit deposit, exactly as a bank deposit can already overwrite a scatter tile. Re-verified on the original 3 failing seeds (now 0) and on 50 fresh seeds at 1/2/8 teams (0/50, 0/50, 1/50 — the one remaining failure reproduces identically, same stage, same error, same hash, on the pre-scatter binary too, so it predates and is unrelated to this work). - Also extended the fjord banks from two fixed clumps per side into a scattered strip: six additional lighter, best-effort clumps per side at points along the same bank, mixing corn/wood/stone with a small jitter so the spacing doesn't read as mechanical. Best-effort like the ambient scatter — nothing downstream depends on any one of them, so a spot that doesn't work out is skipped rather than failing generation. Walking a fjord's edge now reads as following a shoreline with its own economy rather than passing exactly two fixed deposits. +- Playing raised one more real point about `scatterResources` (Fjord, Lattice and Maze's shared ambient layer): `Map::growResources` only regrows a wheat or wood tile near water, but the scatter's candidate search never looked at that - it picked a uniformly random legal tile and used it outright, so a corn or wood clump could as easily land somewhere with no realistic path back to full growth as somewhere genuinely farmable. First attempt required only `Fertility::Field::at(x,y) > 0` (computed ungated, since nothing has been harvested yet to gate against) before accepting a candidate — measured before/after and found it changed *nothing*: on these water-heavy generators, a single distant water tile up to 14 tiles away already contributes a nonzero (if minuscule - as little as 4 out of a 65536 maximum) weight to nearly every legal tile, so ">0" was true almost everywhere already, fertile-preferring or not. Replaced the fixed bar with best-of-100: score every randomly drawn legal candidate by fertility and keep the highest seen, rather than the first past a threshold. A type that doesn't care about fertility (stone, algae, fruit) scores every candidate 0 and keeps its original "first legal tile" behaviour unchanged. Measured directly against a meaningful bar (fertility > 8000, the same "genuinely good farmland" reference `StartQualityScale` uses elsewhere): the fraction of scattered corn/wood clearing that bar rose from roughly 46% to 68% on Fjord, 40% to 88% on Lattice, and 52% to 96% on Maze. Generation success is unaffected (0 failures across 40 seeds each). Fjord's revision moves to 3, Lattice's and Maze's to 2. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 55f4a713b..b496ef1f7 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -480,7 +480,7 @@ GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 2, + 3, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/LatticeGenerator.cpp b/src/map/generator/generators/LatticeGenerator.cpp index 137e08b24..ad17fa170 100644 --- a/src/map/generator/generators/LatticeGenerator.cpp +++ b/src/map/generator/generators/LatticeGenerator.cpp @@ -89,7 +89,7 @@ GeneratorDefinition latticeDefinition() { "lattice", 10, "Lattice", - 1, + 2, false, {{"islet-size", "Islet size", 1, 4, 1, 2, ControlGroup::Terrain}, {"channel-width", "Channel width", 2, 4, 1, 3, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 665dfdd8a..2e0b162e8 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -203,7 +203,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 1, + 2, false, {{"cell-size", "Cell size", diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index b98d0758e..4362b33dd 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -3,6 +3,7 @@ // Copyright (C) 2008 Bradley Arsenault #include "Resources.h" #include "Distances.h" +#include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" #include "GenerationResult.h" @@ -60,7 +61,14 @@ void scatterResources(Game &game, GenerationContext &context, const ResourceDensities &density) { const int width = game.map.getW(), height = game.map.getH(), area = width * height; - auto scatter = [&](int type, int count) { + // Map::growResources only regrows a wheat or wood tile near water (the same triangular + // kernel Fertility::Field evaluates exactly) - a corn or wood clump dropped somewhere with + // none nearby is a one-time find that can never come back, not a farm. Computed once here, + // ungated: nothing has been harvested yet at this point in generation, so "reachable from an + // existing deposit" doesn't apply - this is the more basic question of whether the tile can + // regrow at all. Stone, algae and fruit don't regrow this way, so they scatter freely. + const Fertility::Field fertility = Fertility::forMap(game.map, false); + auto scatter = [&](int type, int count, bool preferFertile) { const int radius = count < 8 ? 1 : 2; const int expectedClumpSize = radius == 1 ? 5 : 12; const int clumps = @@ -68,21 +76,34 @@ void scatterResources(Game &game, GenerationContext &context, for (int i = 0; i < clumps; ++i) { MapGeneratorPoint center(0, 0); bool found = false; - for (int attempt = 0; attempt < 100 && !found; ++attempt) { - center = {int(context.bounded("resources", width)), - int(context.bounded("resources", height))}; - found = game.map.isResourceAllowed(center.x, center.y, type); + std::uint32_t bestFertility = 0; + // Best of up to 100 random legal tiles, scored by fertility - not a fixed bar, so it + // never comes down to a coin flip between "no better candidate turned up in the budget" + // and "sacrifice this clump's density entirely". A non-fertile-sensitive type (stone, + // algae, fruit) scores every candidate 0, so this keeps its original behaviour of simply + // taking the first legal tile found. + for (int attempt = 0; attempt < 100; ++attempt) { + const MapGeneratorPoint candidate = {int(context.bounded("resources", width)), + int(context.bounded("resources", height))}; + if (!game.map.isResourceAllowed(candidate.x, candidate.y, type)) + continue; + const std::uint32_t score = preferFertile ? fertility.at(candidate.x, candidate.y) : 0; + if (!found || score > bestFertility) { + center = candidate; + bestFertility = score; + found = true; + } } if (found) placeResourceClump(game.map, context, center, type, radius); } }; - scatter(CORN, density.corn * area / 1600); - scatter(WOOD, density.wood * area / 1600); - scatter(STONE, density.stone * area / 3000); - scatter(ALGA, density.algae * area / 800); + scatter(CORN, density.corn * area / 1600, true); + scatter(WOOD, density.wood * area / 1600, true); + scatter(STONE, density.stone * area / 3000, false); + scatter(ALGA, density.algae * area / 800, false); for (int i = 0; i < density.fruit; ++i) - scatter(CHERRY + context.bounded("resources", 3), 1); + scatter(CHERRY + context.bounded("resources", 3), 1, false); } void fillInResource(Map &map, GenerationContext &context, From 1770b36261b5ec47694fdfa499050b6d4955ac60 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 09:38:44 -0400 Subject: [PATCH 026/136] Paint ambient wheat/wood as a fertility-threshold band, not independent clumps Playing surfaced two related complaints about scatterResources (Fjord, Lattice, Maze's shared ambient resource layer): the result still read as "clumpy" even after the earlier best-of-100 fertility fix, and a lot of resources were landing far from water where Map::growResources can never regrow them. Root cause of the clumpiness: each clump is an independent random draw at its own center, unlike generateHeightField (Swamp/River/Islands/Crater- Lakes), which paints its resource bands as a histogram threshold over the same Perlin noise field that carved the terrain, so resources follow the same organic contours as the water and grass. scatterResources runs after these generators' terrain is already fixed, so it can't share that field, but it can still take the threshold approach. First attempt thresholded an independent Perlin noise field. This did fix the clumpiness, but measurement showed it quietly undid the fertility fix: an unrelated noise field has no reason to prefer high-fertility ground, so the fraction of scattered corn/wood clearing a meaningful fertility bar fell back to roughly the pre-fix numbers on all three generators. Fixed by thresholding directly on Fertility::Field for corn/wood instead: the field is already smooth and highest near water, so painting its highest-fertility share is simultaneously the coastal-band shape and a genuine preference for farmable ground - one field, not two competing mechanisms. Stone and algae keep the independent noise field, since they have no regrowth rule to prefer. Re-measured against fertility > 8000: 76.5%/73.0% Fjord, 97.9%/98.4% Lattice, 97.1%/98.4% Maze corn/wood - better than the prior best-of-100 numbers, not just recovered. A single global threshold broke visibly on Lattice: its islets are separate landmasses with their own fertility range, so a handful of tiles scoring highest could absorb nearly the whole band, leaving most islets with none - the same "resources aren't balanced" problem this scatter exists to fix, at the scale of one islet instead of one player. computeLandComponents flood- fills the map into connected non-water components once per call, and scatterBand now runs its threshold independently within each component, sized to that component's own share of the candidate pool. A single connected map is one component covering everything (unchanged behaviour); Lattice's islets each get their own proportional coastal fringe now. Lattice and Maze also needed their guaranteed per-team wheat/wood placement reordered ahead of the ambient scatter: the new band-shaped scatter can solidly fill a team's small reserved area with a different resource type before the guarantee gets a turn, failing generation outright. scatterResources already skips any tile that already carries a resource, so running the guarantee first and the ambient scatter after leaves the reservation alone. (Fjord already worked this way from the earlier swarm-footprint fix.) Verified: 0 new failures across 60 seeds x {Lattice, Maze, Fjord}, the extended {teams=2,4,8} x 50-seed sweep (the only two failures found, Fjord seed 70/90 at teams=8, are confirmed pre-existing and unrelated by direct comparison against the pristine pre-ambient-scatter commit), the full 13-generator x 5-seed sanity sweep, and 198/198 cppunit tests. Fjord's revision moves to 4, Lattice's and Maze's to 3. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 3 + .../generators/FjordContinentGenerator.cpp | 2 +- .../generator/generators/LatticeGenerator.cpp | 8 +- .../generator/generators/MazeGenerator.cpp | 8 +- src/map/generator/shared/Resources.cpp | 216 ++++++++++++++---- 5 files changed, 188 insertions(+), 49 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index bf6aadbb6..6c482568e 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -61,4 +61,7 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Playing raised one more real point about `scatterResources` (Fjord, Lattice and Maze's shared ambient layer): `Map::growResources` only regrows a wheat or wood tile near water, but the scatter's candidate search never looked at that - it picked a uniformly random legal tile and used it outright, so a corn or wood clump could as easily land somewhere with no realistic path back to full growth as somewhere genuinely farmable. First attempt required only `Fertility::Field::at(x,y) > 0` (computed ungated, since nothing has been harvested yet to gate against) before accepting a candidate — measured before/after and found it changed *nothing*: on these water-heavy generators, a single distant water tile up to 14 tiles away already contributes a nonzero (if minuscule - as little as 4 out of a 65536 maximum) weight to nearly every legal tile, so ">0" was true almost everywhere already, fertile-preferring or not. Replaced the fixed bar with best-of-100: score every randomly drawn legal candidate by fertility and keep the highest seen, rather than the first past a threshold. A type that doesn't care about fertility (stone, algae, fruit) scores every candidate 0 and keeps its original "first legal tile" behaviour unchanged. Measured directly against a meaningful bar (fertility > 8000, the same "genuinely good farmland" reference `StartQualityScale` uses elsewhere): the fraction of scattered corn/wood clearing that bar rose from roughly 46% to 68% on Fjord, 40% to 88% on Lattice, and 52% to 96% on Maze. Generation success is unaffected (0 failures across 40 seeds each). Fjord's revision moves to 3, Lattice's and Maze's to 2. +- Even with fertile candidates preferred, the best-of-100 clumps still read as "clumpy" rather than as a map's intrinsic terrain: each clump is an independent random draw, so the result is a scatter of same-size circular blobs with no relationship to each other or to the coastline, unlike `generateHeightField` (Swamp/River/Islands/Crater-Lakes), which paints its resource bands as a histogram threshold over the *same* Perlin noise field that carved the terrain, so wheat/wood/stone follow the same organic contours as the water and grass. `scatterResources` runs after these generators' terrain is already fixed, so it can't share that field, but it can still take the threshold approach. First attempt thresholded an independent Perlin noise field (with the `>0` fertility gate as a floor, same as before): visually this did fix the clumpiness — Fjord and Maze came out as a clean coastal fringe instead of blobs — but measurement showed it quietly undid the fertility fix above, since an unrelated noise field has no reason to prefer high-fertility ground and the `>0` gate is nearly a no-op (fertility > 8000 fraction fell back to roughly 45-53%, essentially the pre-fix numbers, on all three generators). Fixed by thresholding directly on `Fertility::Field` itself for corn/wood instead of independent noise: the field is already a smooth, spatially continuous function that is highest near water, so painting its lowest-value (highest-fertility) share is simultaneously the "coastal band" shape and a genuine, measurable preference for farmable ground - the same field, used two ways instead of two competing mechanisms. Stone and algae keep the independent noise field, since they have no regrowth rule to prefer. Re-measured against the >8000 bar: 76.5%/73.0% Fjord, 97.9%/98.4% Lattice, 97.1%/98.4% Maze corn/wood - better than the best-of-100 numbers, not just recovered. + A single global threshold across the whole map works for one connected landmass (Fjord, Maze) but broke visibly on Lattice: its islets are separate landmasses, each with its own fertility (or noise) range, and a handful of tiles happening to score highest could absorb nearly the entire band, leaving most islets with none at all - the same "resources aren't balanced" complaint this scatter exists to fix, just at the scale of one islet instead of one player. `computeLandComponents` flood-fills the map into connected non-water components once per `scatterResources` call (flat preallocated FIFO, same pattern as `floodReach`/`computeDistances`), and `scatterBand` now runs its histogram threshold independently within each component, sized to that component's own share of the eligible candidate pool. On a single connected map this is one component covering everything, identical to before; on Lattice every islet now gets its own proportional coastal fringe. Verified visually (`dump=` renders) and numerically (0 new failures across 60 seeds x {Lattice, Maze, Fjord}, plus the extended {teams=2,4,8} x 50-seed sweep, plus the full 13-generator x 5-seed sanity sweep, plus 198/198 cppunit). Fjord's revision moves to 4, Lattice's to 3, Maze's to 3. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index b496ef1f7..fe0fcdda6 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -480,7 +480,7 @@ GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 3, + 4, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/LatticeGenerator.cpp b/src/map/generator/generators/LatticeGenerator.cpp index ad17fa170..49eb1c84f 100644 --- a/src/map/generator/generators/LatticeGenerator.cpp +++ b/src/map/generator/generators/LatticeGenerator.cpp @@ -66,13 +66,17 @@ bool generate(Game &game, GenerationContext &context) { teamWoodAreas.push_back(woodArea); } context.stage = "resources"; - scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); + // Guaranteed per-team wheat/wood claim their (small) reserved area first. scatterResources' + // noise bands skip any tile that already carries a resource, so running it after leaves these + // reservations alone; running it first let a solid band fill a whole sliver area with a + // different resource type before the guarantee got a turn, failing the placement outright. for (int team = 0; team < context.request.nbTeams; ++team) if (!placeResourceClumpInArea(game.map, context, teamWheatAreas[team], CORN, 2) || !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, 2)) return false; + scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); return true; } } // namespace @@ -89,7 +93,7 @@ GeneratorDefinition latticeDefinition() { "lattice", 10, "Lattice", - 2, + 3, false, {{"islet-size", "Islet size", 1, 4, 1, 2, ControlGroup::Terrain}, {"channel-width", "Channel width", 2, 4, 1, 3, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 2e0b162e8..2183c9503 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -177,13 +177,17 @@ bool generate(Game &game, GenerationContext &context) { teamWoodAreas.push_back(woodArea); } context.stage = "resources"; - scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); + // Guaranteed per-team wheat/wood claim their (small) reserved area first. scatterResources' + // noise bands skip any tile that already carries a resource, so running it after leaves these + // reservations alone; running it first let a solid band fill a whole sliver area with a + // different resource type before the guarantee got a turn, failing the placement outright. for (int team = 0; team < context.request.nbTeams; ++team) if (!placeResourceClumpInArea(game.map, context, teamWheatAreas[team], CORN, 2) || !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, 2)) return false; + scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); for (int y = 0; y < height; ++y) for (int x = 0; x < width; ++x) if (stoneWall[y * width + x]) @@ -203,7 +207,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 2, + 3, false, {{"cell-size", "Cell size", diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 4362b33dd..b7cfd1dac 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -14,6 +14,7 @@ #include "Terrain.h" #include #include +#include #include using namespace MapGeneration; @@ -57,53 +58,180 @@ int placeResourceClumpInArea(Map &map, GenerationContext &context, return 0; } +namespace { +// generateHeightField (Terrain.cpp) carves its own resource bands from the same noise field +// that carved the terrain: every tile's resource is a threshold on a value it already has, +// so wheat, wood and stone come out following the same contours the water and grass do, +// rather than looking like independent decisions. scatterResources runs after these +// generators' terrain is already fixed and irregular, so it cannot share that one field the +// way generateHeightField does, but it can still take the same threshold approach: fill the +// tightest (lowest-value) share of some smooth field that covers the requested tile count - +// the same histogrammed level search generateHeightField uses, just over an explicit candidate +// list instead of the whole grid. Compact circular clumps at independent random centers are a +// fundamentally different, and visibly clumpier, shape. +// +// scatterLevel selects that field: for corn and wood it is (the inverse of) Fertility::Field +// itself, so the band that gets painted is literally the most farmable ground on the map - it +// hugs real coastlines, bays and fjord banks because that is exactly the shape the field takes, +// and it still comes out organic rather than a uniform ring since every coastline this generates +// already is. An independent noise field would give an organic shape too, but a shape with no +// relation to where the resource could ever actually regrow (Map::growResources only replaces +// wheat or wood near water, the same kernel Fertility::Field evaluates exactly) - that was tried +// and measured: it let the noise contour claim tiles the fertility gate would have rejected on +// sight, undoing the fix that made these generators prefer farmable ground over merely legal +// ground in the first place. Stone and algae have no such regrowth rule, so they still use the +// independent noise field - any legal tile is as good as any other for them. +unsigned scatterLevel(int x, int y, const Fertility::Field *fertility, HeightMap *noise, + unsigned buckets) { + if (fertility) { + const std::uint32_t f = fertility->at(x, y); + const std::uint32_t scaled = + std::min(buckets - 1, (f * buckets) / Fertility::kScale); + return buckets - 1 - scaled; + } + return noise->uiLevel(x, y, buckets); +} + +// A single global threshold across the whole map works for one connected landmass (Fjord, +// Maze), but Lattice's islets are separate landmasses that each carry their own, slightly +// different fertility (or noise) range - a global "take the best tiles first" pass can end up +// spending almost the entire band on whichever one or two islets happen to score highest, +// leaving the rest with none at all. That is exactly the "resources aren't balanced between +// players" complaint this whole scatter is meant to avoid, just at the scale of one islet +// instead of one player. Grouping candidates by connected landmass and running the same +// histogram threshold independently within each group, sized to that group's own share of the +// candidate pool, keeps every landmass' band proportional to how much eligible ground it has - +// on a single connected map this is one group covering everything, identical to before. +std::vector computeLandComponents(const Map &map, int &numComponents) { + const int w = map.getW(), h = map.getH(); + std::vector component(size_t(w) * h, -1); + // Bounded by the component[np] < 0 guard below to at most w*h enqueues per component, and + // every tile belongs to exactly one component - a flat preallocated FIFO reused across + // components, the same fix as floodReach below and computeDistances in Distances.cpp. + std::vector queue(size_t(w) * h); + int nextId = 0; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const int start = y * w + x; + if (component[start] != -1 || map.isWater(x, y)) + continue; + size_t qHead = 0, qTail = 0; + component[start] = nextId; + queue[qTail++] = start; + while (qHead < qTail) { + const int p = queue[qHead++]; + const int px = p % w, py = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(px + dx), ny = map.normalizeY(py + dy); + const int np = ny * w + nx; + if (component[np] == -1 && !map.isWater(nx, ny)) { + component[np] = nextId; + queue[qTail++] = np; + } + } + } + ++nextId; + } + numComponents = nextId; + return component; +} + +void scatterBand(Map &map, HeightMap *noise, int resourceType, int targetTiles, + const Fertility::Field *fertility, const std::vector &landComponent, + int numComponents) { + if (targetTiles <= 0 || numComponents <= 0) + return; + const int width = map.getW(), height = map.getH(); + constexpr unsigned kBuckets = 2048; + std::vector> candidates(numComponents); + std::vector> level(numComponents); + std::vector> histogram(numComponents, std::vector(kBuckets, 0)); + int totalCandidates = 0; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + if (map.getResource(x, y).type != NO_RES_TYPE) + continue; + if (!map.isResourceAllowed(x, y, resourceType)) + continue; + // Map::growResources only regrows wheat or wood near water (the same triangular kernel + // Fertility::Field evaluates exactly) - a corn or wood tile with none nearby is a + // one-time find that can never come back, not a farm. Stone and algae don't regrow this + // way, so fertility is null for them and every legal tile is a candidate. + if (fertility && fertility->at(x, y) == 0) + continue; + const int comp = landComponent[y * width + x]; + if (comp < 0) + continue; + const unsigned lvl = scatterLevel(x, y, fertility, noise, kBuckets); + candidates[comp].emplace_back(x, y); + level[comp].push_back(lvl); + ++histogram[comp][lvl]; + ++totalCandidates; + } + if (totalCandidates == 0) + return; + for (int c = 0; c < numComponents; ++c) { + const auto &compCandidates = candidates[c]; + if (compCandidates.empty()) + continue; + const int share = + int((std::int64_t(targetTiles) * std::int64_t(compCandidates.size())) / totalCandidates); + const int wanted = std::min(compCandidates.size(), std::max(1, share)); + unsigned threshold = kBuckets - 1; + int accumulated = 0; + for (unsigned b = 0; b < kBuckets; ++b) { + accumulated += histogram[c][b]; + if (accumulated >= wanted) { + threshold = b; + break; + } + } + for (size_t i = 0; i < compCandidates.size(); ++i) + if (level[c][i] <= threshold) + map.setResource(compCandidates[i].x, compCandidates[i].y, resourceType, 1); + } +} +} // namespace + void scatterResources(Game &game, GenerationContext &context, const ResourceDensities &density) { - const int width = game.map.getW(), height = game.map.getH(), - area = width * height; - // Map::growResources only regrows a wheat or wood tile near water (the same triangular - // kernel Fertility::Field evaluates exactly) - a corn or wood clump dropped somewhere with - // none nearby is a one-time find that can never come back, not a farm. Computed once here, - // ungated: nothing has been harvested yet at this point in generation, so "reachable from an - // existing deposit" doesn't apply - this is the more basic question of whether the tile can - // regrow at all. Stone, algae and fruit don't regrow this way, so they scatter freely. - const Fertility::Field fertility = Fertility::forMap(game.map, false); - auto scatter = [&](int type, int count, bool preferFertile) { - const int radius = count < 8 ? 1 : 2; - const int expectedClumpSize = radius == 1 ? 5 : 12; - const int clumps = - std::max(1, (count + expectedClumpSize - 1) / expectedClumpSize); - for (int i = 0; i < clumps; ++i) { - MapGeneratorPoint center(0, 0); - bool found = false; - std::uint32_t bestFertility = 0; - // Best of up to 100 random legal tiles, scored by fertility - not a fixed bar, so it - // never comes down to a coin flip between "no better candidate turned up in the budget" - // and "sacrifice this clump's density entirely". A non-fertile-sensitive type (stone, - // algae, fruit) scores every candidate 0, so this keeps its original behaviour of simply - // taking the first legal tile found. - for (int attempt = 0; attempt < 100; ++attempt) { - const MapGeneratorPoint candidate = {int(context.bounded("resources", width)), - int(context.bounded("resources", height))}; - if (!game.map.isResourceAllowed(candidate.x, candidate.y, type)) - continue; - const std::uint32_t score = preferFertile ? fertility.at(candidate.x, candidate.y) : 0; - if (!found || score > bestFertility) { - center = candidate; - bestFertility = score; - found = true; - } - } - if (found) - placeResourceClump(game.map, context, center, type, radius); + Map &map = game.map; + const int width = map.getW(), height = map.getH(), area = width * height; + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap noise(width, height, context.stream("scatter-noise")); + noise.makePlain(24); + int numComponents = 0; + const std::vector landComponent = computeLandComponents(map, numComponents); + + // Corn first, then wood from whatever the corn band didn't already claim (scatterBand skips + // any tile that already has a resource) - wood settles for the next-best farmland the corn + // band left behind, rather than the two competing over the same prime coastal ground. + scatterBand(map, nullptr, CORN, density.corn * area / 1600, &fertility, landComponent, + numComponents); + scatterBand(map, nullptr, WOOD, density.wood * area / 1600, &fertility, landComponent, + numComponents); + scatterBand(map, &noise, STONE, density.stone * area / 3000, nullptr, landComponent, + numComponents); + scatterBand(map, &noise, ALGA, density.algae * area / 800, nullptr, landComponent, + numComponents); + + // Fruit stays a rare, discrete find rather than a background band - "a distinct little + // prize", the same role it already plays elsewhere in these generators - so it keeps the + // original single-clump-at-a-random-point search instead of joining the noise bands above. + for (int i = 0; i < density.fruit; ++i) { + const int type = CHERRY + context.bounded("resources", 3); + MapGeneratorPoint center(0, 0); + bool found = false; + for (int attempt = 0; attempt < 100 && !found; ++attempt) { + center = {int(context.bounded("resources", width)), int(context.bounded("resources", height))}; + found = map.isResourceAllowed(center.x, center.y, type); } - }; - scatter(CORN, density.corn * area / 1600, true); - scatter(WOOD, density.wood * area / 1600, true); - scatter(STONE, density.stone * area / 3000, false); - scatter(ALGA, density.algae * area / 800, false); - for (int i = 0; i < density.fruit; ++i) - scatter(CHERRY + context.bounded("resources", 3), 1, false); + if (found) + placeResourceClump(map, context, center, type, 1); + } } void fillInResource(Map &map, GenerationContext &context, From fb96b8a9b4ec441cffc01bd7e1cb1b94a044d721 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 10:12:03 -0400 Subject: [PATCH 027/136] Split ambient farmland placement into where (fertility) vs which crop (noise) Playing turned up a real bug in the fertility-band rewrite: corn and wood were thresholded on the same Fertility::Field back to back (corn claims the highest-fertility band, wood claims the next-highest of what's left), which correctly picks where farmland goes but also, as a side effect, decides which crop goes where using the same field - so corn and wood came out as two concentric rings sorted by distance from water, with wood forming a solid ring that visually hid wheat behind it, instead of sitting side by side the way real farmland does. Fertility should only ever answer "where can farmland go", never "which of the two crops belongs here". Split into two independent steps in the new scatterFarmland: fertility selects one shared candidate region for corn and wood together (per land component, keeping the earlier per-islet fairness fix), and a second, unrelated noise field splits that region into the two crops - sorted by the noise field's own level and sliced into a corn share and a wood share, so which crop lands where varies along the coast instead of stacking into bands by distance from water. Also widened the fertility-selected region to roughly 3x the requested tile count (still per component), so it reaches into lower-but-still-farmable ground instead of a razor-thin ring at only the very highest fertility values, per a direct request for more resources at lower fertility. Separately, widened Fjord's fjord-width control from 2-6 (default 3) to 2-10 (default 4). The wider default measurably increases single-seed generation failures at the most crowded setting (teams=8: 8/100 vs. the pre-existing 2/100 baseline, same "no swarm footprint fits" class as before) but this isn't player-visible: both real entry points into generation roll 5 candidate seeds and keep the best successful one rather than committing to a single explicit seed, so the chance all 5 fail at once is negligible. Settled on default 4 rather than 5 after measuring that 5 pushed the same failure rate to 24/100 - still low risk in practice via the 5-candidate roll, but 4 keeps a comfortable margin without giving up much of the visual effect. Verified: 0 new failures across 60 seeds x {Lattice, Maze, Fjord}, the extended {teams=2,4,8} x 50-seed sweep (only the same two pre-existing Fjord failures reproduced, confirmed unrelated in the prior commit), the full 13-generator x 5-seed sanity sweep, and 198/198 cppunit tests. Fjord's revision moves to 5, Lattice's and Maze's to 4. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 4 + .../generators/FjordContinentGenerator.cpp | 4 +- .../generator/generators/LatticeGenerator.cpp | 2 +- .../generator/generators/MazeGenerator.cpp | 2 +- src/map/generator/shared/Resources.cpp | 167 ++++++++++++------ 5 files changed, 123 insertions(+), 56 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 6c482568e..72b0233d6 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -64,4 +64,8 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Even with fertile candidates preferred, the best-of-100 clumps still read as "clumpy" rather than as a map's intrinsic terrain: each clump is an independent random draw, so the result is a scatter of same-size circular blobs with no relationship to each other or to the coastline, unlike `generateHeightField` (Swamp/River/Islands/Crater-Lakes), which paints its resource bands as a histogram threshold over the *same* Perlin noise field that carved the terrain, so wheat/wood/stone follow the same organic contours as the water and grass. `scatterResources` runs after these generators' terrain is already fixed, so it can't share that field, but it can still take the threshold approach. First attempt thresholded an independent Perlin noise field (with the `>0` fertility gate as a floor, same as before): visually this did fix the clumpiness — Fjord and Maze came out as a clean coastal fringe instead of blobs — but measurement showed it quietly undid the fertility fix above, since an unrelated noise field has no reason to prefer high-fertility ground and the `>0` gate is nearly a no-op (fertility > 8000 fraction fell back to roughly 45-53%, essentially the pre-fix numbers, on all three generators). Fixed by thresholding directly on `Fertility::Field` itself for corn/wood instead of independent noise: the field is already a smooth, spatially continuous function that is highest near water, so painting its lowest-value (highest-fertility) share is simultaneously the "coastal band" shape and a genuine, measurable preference for farmable ground - the same field, used two ways instead of two competing mechanisms. Stone and algae keep the independent noise field, since they have no regrowth rule to prefer. Re-measured against the >8000 bar: 76.5%/73.0% Fjord, 97.9%/98.4% Lattice, 97.1%/98.4% Maze corn/wood - better than the best-of-100 numbers, not just recovered. A single global threshold across the whole map works for one connected landmass (Fjord, Maze) but broke visibly on Lattice: its islets are separate landmasses, each with its own fertility (or noise) range, and a handful of tiles happening to score highest could absorb nearly the entire band, leaving most islets with none at all - the same "resources aren't balanced" complaint this scatter exists to fix, just at the scale of one islet instead of one player. `computeLandComponents` flood-fills the map into connected non-water components once per `scatterResources` call (flat preallocated FIFO, same pattern as `floodReach`/`computeDistances`), and `scatterBand` now runs its histogram threshold independently within each component, sized to that component's own share of the eligible candidate pool. On a single connected map this is one component covering everything, identical to before; on Lattice every islet now gets its own proportional coastal fringe. Verified visually (`dump=` renders) and numerically (0 new failures across 60 seeds x {Lattice, Maze, Fjord}, plus the extended {teams=2,4,8} x 50-seed sweep, plus the full 13-generator x 5-seed sanity sweep, plus 198/198 cppunit). Fjord's revision moves to 4, Lattice's to 3, Maze's to 3. +- Playing again turned up a subtler problem with the fertility-band scatter: corn and wood were both thresholded on the *same* `Fertility::Field`, one right after the other (corn takes the highest-fertility band, wood takes the next-highest of what's left). That answers "where should farmland go" correctly but also quietly answers "which crop goes where" with the same field, so corn and wood came out as two concentric rings sorted by distance from water instead of sitting side by side the way real farmland does - wheat ended up hidden behind a solid wood ring in practice. Fertility should only ever decide *where* farmland goes, not *which* of the two crops it becomes. Split into two independent questions in `scatterFarmland`: fertility (widened to roughly 3x the requested tile count, per land component, so the region reaches into lower-but-still-farmable ground instead of a razor-thin ring at the very highest values) selects one shared candidate region for corn and wood together, and a second, unrelated noise field then splits that region into the two crops - sorted by that field's own level and sliced into a corn share and a wood share, so which crop lands where varies along the coast instead of the two stacking into concentric bands. Verified visually (`dump=` renders now show alternating corn/wood patches along a noticeably wider coastal band on all three generators) and numerically: 0 new failures across 60 seeds x {Lattice, Maze, Fjord}, the extended {teams=2,4,8} x 50-seed sweep (the same two pre-existing Fjord failures as before, none new), the full 13-generator sanity sweep, and 198/198 cppunit. Fjord's revision moves to 5, Lattice's and Maze's to 4. + +- Also widened Fjord's `fjord-width` control from a 2-6 range (default 3) to 2-10 (default 4), per a direct request for a wider allowed range and a bigger default. Verified the new default against the same failure sweep: clean at teams 1/2/4 (0/50 each), and at the crowded teams=8 setting a real but small increase versus the pre-existing baseline (8/100 vs. 2/100 single-seed failures - still "no swarm footprint fits inside the home region", the same class already present before this change). This is not player-visible in practice: both real entry points into generation (`EditorMainMenu`, `CustomGameScreen`) roll `GenerationService::kSampledCandidates` (5) seeds and keep the best-scoring successful one rather than committing to a single explicit seed, so the chance every one of 5 independent rolls fails at once is negligible (0.08^5). `MapGeneratorStudy`'s CLI, which does take one explicit seed, is the only place this is visible at all. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index fe0fcdda6..bb9387809 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -480,13 +480,13 @@ GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 4, + 5, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, {"coast-roughness", "Coast roughness", 10, 35, 1, 22, ControlGroup::Terrain}, - {"fjord-width", "Fjord width", 2, 6, 1, 3, ControlGroup::Terrain}, + {"fjord-width", "Fjord width", 2, 10, 1, 4, ControlGroup::Terrain}, {"resource-islands", "Resource islands", 0, 4, 1, 2, ControlGroup::Resources}}, generate}; diff --git a/src/map/generator/generators/LatticeGenerator.cpp b/src/map/generator/generators/LatticeGenerator.cpp index 49eb1c84f..93ddf0240 100644 --- a/src/map/generator/generators/LatticeGenerator.cpp +++ b/src/map/generator/generators/LatticeGenerator.cpp @@ -93,7 +93,7 @@ GeneratorDefinition latticeDefinition() { "lattice", 10, "Lattice", - 3, + 4, false, {{"islet-size", "Islet size", 1, 4, 1, 2, ControlGroup::Terrain}, {"channel-width", "Channel width", 2, 4, 1, 3, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 2183c9503..15f087fe7 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -207,7 +207,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 3, + 4, false, {{"cell-size", "Cell size", diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index b7cfd1dac..11591f88f 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -60,38 +60,19 @@ int placeResourceClumpInArea(Map &map, GenerationContext &context, namespace { // generateHeightField (Terrain.cpp) carves its own resource bands from the same noise field -// that carved the terrain: every tile's resource is a threshold on a value it already has, -// so wheat, wood and stone come out following the same contours the water and grass do, -// rather than looking like independent decisions. scatterResources runs after these -// generators' terrain is already fixed and irregular, so it cannot share that one field the -// way generateHeightField does, but it can still take the same threshold approach: fill the -// tightest (lowest-value) share of some smooth field that covers the requested tile count - -// the same histogrammed level search generateHeightField uses, just over an explicit candidate -// list instead of the whole grid. Compact circular clumps at independent random centers are a -// fundamentally different, and visibly clumpier, shape. +// that carved the terrain: every tile's resource is a threshold on a value it already has, so +// stone and algae come out following the same contours the water and grass do, rather than +// looking like independent decisions. scatterResources runs after these generators' terrain is +// already fixed and irregular, so it cannot share that one field the way generateHeightField +// does, but it can still take the same threshold approach: fill the tightest (lowest-value) +// share of a noise field of its own that covers the requested tile count - the same +// histogrammed level search generateHeightField uses, just over an explicit candidate list +// instead of the whole grid. Compact circular clumps at independent random centers are a +// fundamentally different, and visibly clumpier, shape. Corn and wood go through +// scatterFarmland below instead: unlike stone and algae, they also need to prefer farmable +// ground, and that preference must only pick *where* farmland goes, not *which* of the two +// crops a given spot gets - see that function for why. // -// scatterLevel selects that field: for corn and wood it is (the inverse of) Fertility::Field -// itself, so the band that gets painted is literally the most farmable ground on the map - it -// hugs real coastlines, bays and fjord banks because that is exactly the shape the field takes, -// and it still comes out organic rather than a uniform ring since every coastline this generates -// already is. An independent noise field would give an organic shape too, but a shape with no -// relation to where the resource could ever actually regrow (Map::growResources only replaces -// wheat or wood near water, the same kernel Fertility::Field evaluates exactly) - that was tried -// and measured: it let the noise contour claim tiles the fertility gate would have rejected on -// sight, undoing the fix that made these generators prefer farmable ground over merely legal -// ground in the first place. Stone and algae have no such regrowth rule, so they still use the -// independent noise field - any legal tile is as good as any other for them. -unsigned scatterLevel(int x, int y, const Fertility::Field *fertility, HeightMap *noise, - unsigned buckets) { - if (fertility) { - const std::uint32_t f = fertility->at(x, y); - const std::uint32_t scaled = - std::min(buckets - 1, (f * buckets) / Fertility::kScale); - return buckets - 1 - scaled; - } - return noise->uiLevel(x, y, buckets); -} - // A single global threshold across the whole map works for one connected landmass (Fjord, // Maze), but Lattice's islets are separate landmasses that each carry their own, slightly // different fertility (or noise) range - a global "take the best tiles first" pass can end up @@ -139,9 +120,8 @@ std::vector computeLandComponents(const Map &map, int &numComponents) { return component; } -void scatterBand(Map &map, HeightMap *noise, int resourceType, int targetTiles, - const Fertility::Field *fertility, const std::vector &landComponent, - int numComponents) { +void scatterBand(Map &map, HeightMap &noise, int resourceType, int targetTiles, + const std::vector &landComponent, int numComponents) { if (targetTiles <= 0 || numComponents <= 0) return; const int width = map.getW(), height = map.getH(); @@ -156,16 +136,10 @@ void scatterBand(Map &map, HeightMap *noise, int resourceType, int targetTiles, continue; if (!map.isResourceAllowed(x, y, resourceType)) continue; - // Map::growResources only regrows wheat or wood near water (the same triangular kernel - // Fertility::Field evaluates exactly) - a corn or wood tile with none nearby is a - // one-time find that can never come back, not a farm. Stone and algae don't regrow this - // way, so fertility is null for them and every legal tile is a candidate. - if (fertility && fertility->at(x, y) == 0) - continue; const int comp = landComponent[y * width + x]; if (comp < 0) continue; - const unsigned lvl = scatterLevel(x, y, fertility, noise, kBuckets); + const unsigned lvl = noise.uiLevel(x, y, kBuckets); candidates[comp].emplace_back(x, y); level[comp].push_back(lvl); ++histogram[comp][lvl]; @@ -194,6 +168,100 @@ void scatterBand(Map &map, HeightMap *noise, int resourceType, int targetTiles, map.setResource(compCandidates[i].x, compCandidates[i].y, resourceType, 1); } } + +// Map::growResources only regrows a wheat or wood tile near water (the same triangular kernel +// Fertility::Field evaluates exactly) - a corn or wood tile with none nearby is a one-time find +// that can never come back, not a farm. But fertility answers only where farmland can be: it +// says nothing about whether a given fertile spot should become corn or wood, and thresholding +// corn and wood on the same field back to back (corn takes the highest band, wood the +// next-highest) turned that lopsided into two concentric rings sorted by distance from water, +// with wood pushed entirely behind corn instead of the two sitting side by side the way real +// farmland does. So the two questions are answered separately here: fertility selects the +// region - widened well past the requested tile count so it reaches into lower-but-still- +// farmable ground instead of a razor-thin ring at the very highest values - and then an +// unrelated noise field splits that region into corn and wood, so which crop lands where varies +// along the coast rather than with distance from it. +void scatterFarmland(Map &map, const Fertility::Field &fertility, HeightMap &splitNoise, + int cornTarget, int woodTarget, const std::vector &landComponent, + int numComponents) { + const int totalTarget = cornTarget + woodTarget; + if (totalTarget <= 0 || numComponents <= 0) + return; + const int width = map.getW(), height = map.getH(); + constexpr unsigned kBuckets = 2048; + constexpr int kWiden = 3; // the fertile region reaches roughly 3x past the requested tile count + std::vector> candidates(numComponents); + std::vector> fertLevel(numComponents); + std::vector> fertHistogram(numComponents, std::vector(kBuckets, 0)); + int totalCandidates = 0; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + if (map.getResource(x, y).type != NO_RES_TYPE) + continue; + // Wood and corn share the same terrain requirement (grass), so either stands in for + // eligibility here - which of the two a tile ends up with is decided below, by the split. + if (!map.isResourceAllowed(x, y, CORN)) + continue; + const std::uint32_t f = fertility.at(x, y); + if (f == 0) + continue; + const int comp = landComponent[y * width + x]; + if (comp < 0) + continue; + const unsigned lvl = + kBuckets - 1 - std::min(kBuckets - 1, unsigned((f * kBuckets) / Fertility::kScale)); + candidates[comp].emplace_back(x, y); + fertLevel[comp].push_back(lvl); + ++fertHistogram[comp][lvl]; + ++totalCandidates; + } + if (totalCandidates == 0) + return; + for (int c = 0; c < numComponents; ++c) { + const auto &compCandidates = candidates[c]; + if (compCandidates.empty()) + continue; + const int share = + int((std::int64_t(totalTarget) * std::int64_t(compCandidates.size())) / totalCandidates); + const int regionWanted = std::min(compCandidates.size(), std::max(1, share) * kWiden); + unsigned threshold = kBuckets - 1; + int accumulated = 0; + for (unsigned b = 0; b < kBuckets; ++b) { + accumulated += fertHistogram[c][b]; + if (accumulated >= regionWanted) { + threshold = b; + break; + } + } + std::vector region; + for (size_t i = 0; i < compCandidates.size(); ++i) + if (fertLevel[c][i] <= threshold) + region.push_back(compCandidates[i]); + if (region.empty()) + continue; + // Split the region by an independent noise field instead of fertility - sort it into that + // field's order and slice off the lowest share for corn, the next share for wood, so the + // two form separate patches following the noise field's own organic contours rather than + // corn's band always sitting closer to the water than wood's. + std::vector order(region.size()); + for (size_t i = 0; i < order.size(); ++i) + order[i] = i; + std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { + return splitNoise.uiLevel(region[a].x, region[a].y, kBuckets) < + splitNoise.uiLevel(region[b].x, region[b].y, kBuckets); + }); + const int cornWanted = std::min( + region.size(), + std::max(0, int((std::int64_t(cornTarget) * std::int64_t(region.size())) / totalTarget))); + const int woodWanted = std::min( + int(region.size()) - cornWanted, + std::max(0, int((std::int64_t(woodTarget) * std::int64_t(region.size())) / totalTarget))); + for (int i = 0; i < cornWanted; ++i) + map.setResource(region[order[i]].x, region[order[i]].y, CORN, 1); + for (int i = cornWanted; i < cornWanted + woodWanted; ++i) + map.setResource(region[order[i]].x, region[order[i]].y, WOOD, 1); + } +} } // namespace void scatterResources(Game &game, GenerationContext &context, @@ -203,20 +271,15 @@ void scatterResources(Game &game, GenerationContext &context, const Fertility::Field fertility = Fertility::forMap(map, false); HeightMap noise(width, height, context.stream("scatter-noise")); noise.makePlain(24); + HeightMap splitNoise(width, height, context.stream("scatter-split")); + splitNoise.makePlain(24); int numComponents = 0; const std::vector landComponent = computeLandComponents(map, numComponents); - // Corn first, then wood from whatever the corn band didn't already claim (scatterBand skips - // any tile that already has a resource) - wood settles for the next-best farmland the corn - // band left behind, rather than the two competing over the same prime coastal ground. - scatterBand(map, nullptr, CORN, density.corn * area / 1600, &fertility, landComponent, - numComponents); - scatterBand(map, nullptr, WOOD, density.wood * area / 1600, &fertility, landComponent, - numComponents); - scatterBand(map, &noise, STONE, density.stone * area / 3000, nullptr, landComponent, - numComponents); - scatterBand(map, &noise, ALGA, density.algae * area / 800, nullptr, landComponent, - numComponents); + scatterFarmland(map, fertility, splitNoise, density.corn * area / 1600, + density.wood * area / 1600, landComponent, numComponents); + scatterBand(map, noise, STONE, density.stone * area / 3000, landComponent, numComponents); + scatterBand(map, noise, ALGA, density.algae * area / 800, landComponent, numComponents); // Fruit stays a rare, discrete find rather than a background band - "a distinct little // prize", the same role it already plays elsewhere in these generators - so it keeps the From 1d9ea57cce7ab333ecda95ef3e990df4c11e0ebb Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 10:27:20 -0400 Subject: [PATCH 028/136] Fix wheat trapped behind a wood wall: finer split scale, per-component slack, reachability backstop A screenshot from actual play caught what the aggregate fertility numbers couldn't: a colony's wood had grown into one continuous, multi-tile-deep wall along the entire visible coastline, with no corn in sight. This is a real reachability bug, not a cosmetic one - resource tiles block ground units, so the wheat behind that wall was genuinely unreachable. Two compounding causes, both fixed: 1. The noise field that splits corn from wood reused the same smoothing factor (24) as the continent-shaping terrain noise, sized for whole-map features. At a player's zoomed-in scale, one crop's patch could easily span the entire visible screen - corn and wood alternated across the whole map, just not within reach of any one colony. Dropped to 6, sized for a working area instead of a continent. 2. The fertility-selected region's kWiden multiplier was checked against the map-wide candidate total, not the land component actually being painted, so a small Lattice islet's *entire* eligible pool could be absorbed by the widened region, leaving no slack to route around it. Confirmed by re-running the exact regression seeds against the prior commit: the same tiny islets had both wheat and wood reachable before this session's widening, and lost one or the other after it. Capped the widened region at 2/3 of each component's own pool, so every landmass keeps at least a third of its eligible ground unclaimed regardless of size. As a second, independent backstop, Fjord and Lattice now call guaranteeStartingResources after all resource placement - the same reachability re-check RuggedArchipelago and ShatteredCoast already rely on for this exact class of problem, clearing exactly the wall tiles responsible for a cramped pocket before topping up whichever resource is still out of range. Maze deliberately does not get this call: its maze walls are themselves stone resource tiles by design, and the same wall-clearing logic can't distinguish an intentional corridor wall from an incidental one. Verified with a new check beyond generation success: MapGeneratorStudy's quality mode reports each colony's actual wheat/wood distance (-1 when unreachable) via the same flood scoreStarts already computes. Across 60 seeds x {Lattice, Maze, Fjord} (720 colonies), unreachable cases dropped from 7 to 3, and the 3 remaining are byte-for-byte identical to the pre-session baseline on the same seeds (catchmentTiles=4 - a genuinely tiny, isolated Maze room with no larger landmass behind a wall to reveal, which guaranteeStartingResources correctly declines to force). Fjord specifically, the generator in the screenshot, went from a visible wall to 0 unreachable colonies across the sweep. Also re-verified the standard suite: 0 failures across the 60-seed x 3-generator sweep, the {teams=2,4,8} x 50-seed sweep (same class of pre-existing fjord-width failures as before, none new), the 13-generator sanity sweep, and 198/198 cppunit. Fjord's revision moves to 6, Lattice's and Maze's to 5. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 6 ++++++ .../generators/FjordContinentGenerator.cpp | 11 ++++++++++- .../generator/generators/LatticeGenerator.cpp | 10 +++++++++- src/map/generator/generators/MazeGenerator.cpp | 9 ++++++++- src/map/generator/shared/Resources.cpp | 18 ++++++++++++++++-- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 72b0233d6..70c63776f 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -68,4 +68,10 @@ A handful of colonies on constrained terrain (river was the clearest case) lande - Also widened Fjord's `fjord-width` control from a 2-6 range (default 3) to 2-10 (default 4), per a direct request for a wider allowed range and a bigger default. Verified the new default against the same failure sweep: clean at teams 1/2/4 (0/50 each), and at the crowded teams=8 setting a real but small increase versus the pre-existing baseline (8/100 vs. 2/100 single-seed failures - still "no swarm footprint fits inside the home region", the same class already present before this change). This is not player-visible in practice: both real entry points into generation (`EditorMainMenu`, `CustomGameScreen`) roll `GenerationService::kSampledCandidates` (5) seeds and keep the best-scoring successful one rather than committing to a single explicit seed, so the chance every one of 5 independent rolls fails at once is negligible (0.08^5). `MapGeneratorStudy`'s CLI, which does take one explicit seed, is the only place this is visible at all. +- A screenshot from actual play caught something the aggregate fertility numbers couldn't: a colony's wood had grown into one continuous multi-tile-deep wall along the whole visible coastline, with corn nowhere in sight - not a rendering issue, an actual reachability bug, since resource tiles block ground units. Two compounding causes. First, the noise field splitting corn from wood reused the same smoothing factor (24) as the continent-shaping terrain noise, sized for whole-map features; at a player's zoomed-in scale, one crop's patch could easily span the entire visible screen, so "alternates along the coast" was true map-wide but not within any one colony's own reach. Dropped to 6, matching a working-area scale instead of a continent scale. Second, and worse: the fertility-selected region's `kWiden` multiplier was checked against the map-wide candidate total, not the land component actually being painted, so a small Lattice islet's *entire* eligible pool could be absorbed by the widened region, leaving zero slack to route around it - confirmed by re-running the exact regression seeds against the prior commit, where the same tiny islets had both wheat and wood reachable before this session's widening, and lost one or the other after it. Capped the widened region at 2/3 of each component's own candidate pool, so every landmass keeps at least a third of its eligible ground unclaimed regardless of size. + + As a second, independent backstop, Fjord and Lattice now call `guaranteeStartingResources` after all resource placement - the same reachability re-check (walking the actual resource-respecting flood, not just distance) that RuggedArchipelago and ShatteredCoast already rely on for this exact class of problem, clearing exactly the wall tiles responsible for a cramped pocket before topping up whichever resource is still out of range. Maze deliberately does *not* get this call: its maze walls are themselves stone resource tiles by design, and the same wall-clearing logic can't distinguish an intentional corridor wall from an incidental one - it would bulldoze the maze to "fix" a legitimately small starting room. + + Verified with a new check beyond generation success: `MapGeneratorStudy quality` reports each colony's actual wheat/wood distance (-1 when unreachable) via the same flood `scoreStarts` already computes. Across 60 seeds x {Lattice, Maze, Fjord} (720 colonies), unreachable cases dropped from 7 to 3, and the 3 remaining are byte-for-byte identical to the pre-session baseline on the same seeds (`catchmentTiles=4` - a genuinely tiny, isolated Maze room with no larger landmass behind a wall to reveal, which `guaranteeStartingResources` correctly declines to force). Fjord specifically, the generator in the screenshot, went from a visible wall to 0 unreachable colonies across the sweep. Also re-verified the standard suite: 0 failures across the 60-seed x 3-generator sweep, the {teams=2,4,8} x 50-seed sweep (same class of pre-existing fjord-width failures as before, none new), the 13-generator sanity sweep, and 198/198 cppunit. Fjord's revision moves to 6, Lattice's and Maze's to 5. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index bb9387809..9c87b2c25 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -465,6 +465,15 @@ static bool generate(Game &game, GenerationContext &context) { } } + // The starter kit guarantees wheat and wood exist somewhere near each boot tile, but the + // ambient scatter and bank clumps painted afterward don't know that - a wide enough patch of + // one crop can still wall the other off from a swarm that was placed before either existed. + // guaranteeStartingResources re-checks reachability through the actual resource layout (not + // just distance) and, if a wall is responsible for a cramped pocket, clears exactly the tiles + // sealing it before topping up whichever resource is still out of range - the same backstop + // RuggedArchipelago and ShatteredCoast already rely on for the same class of problem. + guaranteeStartingResources(game, context, 24, 32); + return true; } @@ -480,7 +489,7 @@ GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 5, + 6, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/LatticeGenerator.cpp b/src/map/generator/generators/LatticeGenerator.cpp index 93ddf0240..1e743e81c 100644 --- a/src/map/generator/generators/LatticeGenerator.cpp +++ b/src/map/generator/generators/LatticeGenerator.cpp @@ -77,6 +77,14 @@ bool generate(Game &game, GenerationContext &context) { 2)) return false; scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); + // The guaranteed clumps above ensure wheat and wood exist somewhere in a team's home area, but + // the ambient scatter painted afterward doesn't know that - a wide enough patch of one crop can + // still wall the other off, or wall the swarm off from either, on a small islet where there's + // little room to route around it. guaranteeStartingResources re-checks reachability through the + // actual resource layout and clears exactly the tiles responsible before topping up whichever + // resource is still out of range - the same backstop RuggedArchipelago and ShatteredCoast + // already rely on for the same class of problem. + guaranteeStartingResources(game, context, 24, 32); return true; } } // namespace @@ -93,7 +101,7 @@ GeneratorDefinition latticeDefinition() { "lattice", 10, "Lattice", - 4, + 5, false, {{"islet-size", "Islet size", 1, 4, 1, 2, ControlGroup::Terrain}, {"channel-width", "Channel width", 2, 4, 1, 3, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 15f087fe7..4aa008c80 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -187,6 +187,13 @@ bool generate(Game &game, GenerationContext &context) { !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, 2)) return false; + // Unlike Fjord and Lattice, Maze doesn't get a guaranteeStartingResources backstop here: its + // structural walls are themselves stone resource tiles placed on grass seams by design (see + // the stamp below), and that helper's wall-clearing branch can't tell an intentional maze wall + // from an incidental one - it would happily bulldoze a corridor wall to "fix" a team whose + // small starting room is legitimately cramped, undermining the maze itself. The noise-scale + // fix above already shrinks the ambient scatter's patches enough that this is a smaller risk + // here than it was in Fjord's open banks. scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); for (int y = 0; y < height; ++y) for (int x = 0; x < width; ++x) @@ -207,7 +214,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 4, + 5, false, {{"cell-size", "Cell size", diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 11591f88f..82bf0815e 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -223,7 +223,14 @@ void scatterFarmland(Map &map, const Fertility::Field &fertility, HeightMap &spl continue; const int share = int((std::int64_t(totalTarget) * std::int64_t(compCandidates.size())) / totalCandidates); - const int regionWanted = std::min(compCandidates.size(), std::max(1, share) * kWiden); + // Widening is proportionally huge on a landmass with little eligible ground to begin with - + // a small Lattice islet can have its whole candidate pool absorbed by kWiden's multiplier + // even though the unwidened share alone would have left most of it free. Capping the region + // at 2/3 of the component's own pool guarantees slack to route around it regardless of + // component size, instead of only checking against the (unrelated) map-wide total above. + const int widenedShare = std::max(1, share) * kWiden; + const int slackCap = std::max(1, int(compCandidates.size()) * 2 / 3); + const int regionWanted = std::min(compCandidates.size(), std::min(widenedShare, slackCap)); unsigned threshold = kBuckets - 1; int accumulated = 0; for (unsigned b = 0; b < kBuckets; ++b) { @@ -271,8 +278,15 @@ void scatterResources(Game &game, GenerationContext &context, const Fertility::Field fertility = Fertility::forMap(map, false); HeightMap noise(width, height, context.stream("scatter-noise")); noise.makePlain(24); + // A much finer scale than the terrain-shaping noise above: this one only decides which of two + // *adjacent* crops a farmland tile becomes, not where the coastline itself bends, so its + // features need to be sized like a player's local working area (tens of tiles), not like a + // continent (hundreds). Reusing 24 here first produced patches wide enough that a zoomed-in + // view could sit entirely inside one - corn and wood alternated across the whole map, but not + // within reach of any one colony, so a colony's own farmland still read as a solid wall of one + // crop with the other hidden behind it. HeightMap splitNoise(width, height, context.stream("scatter-split")); - splitNoise.makePlain(24); + splitNoise.makePlain(6); int numComponents = 0; const std::vector landComponent = computeLandComponents(map, numComponents); From 9637b2ee3d1ef108b274d7cfe1aff9a96d591ec6 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 11:01:02 -0400 Subject: [PATCH 029/136] Give Fjord a central lake, richer core resources, and fix outlier islands Three related requests plus one genuine bug found along the way. A central lake: after the fjords are carved, a circle in shape-space (lake-size percent of coreR, 0 disables it) becomes the map's shared centerpiece, with a sandy no-man's-land ring around it (wider than controlSand's own coastal fringe), several stone clumps and one clump of every fruit type in the resource ring beyond the sand, and algae in the lake itself. coreR widened from 0.19 to 0.23 of the continent radius to make room for all of this without shortening the fjords. Whether the fjords actually open into the lake is a real topology choice, not a cosmetic one: doing so makes every peninsula water-isolated from its neighbors (boats required to reach anyone), where stopping short behind a solid land ring keeps today's mutual land connectivity. Asked directly rather than guessed at - the answer was to keep both available as a lake-connected control (default off), not pick one permanently. The global cross-team connectivity check only runs when the land ring is expected to exist; lake-connected maps are supposed to fail that specific check by design, and each peninsula's own viability is still verified independently further down regardless. Separately, a real bug: outlier resource islands were going unplaced almost every roll, at every map size - not just small ones, per a direct report. The island annulus's inner bound summed every noise harmonic's amplitude as if all four peaked at the same angle simultaneously, a safe but very pessimistic bound the coastline never actually reaches, and that overshoot doesn't shrink at larger map sizes since both terms scale together. Sampling the coastline's true maximum over 360 angles helped but wasn't enough on its own - the coastline's single worst direction can legitimately approach half the map by itself on plenty of rolls. The real fix: each island's center is already independently randomized, so it never needed to be safe in the coastline's worst direction everywhere, only at its own location - checking each candidate directly against the coastline at its own position (the same per-point technique the open-sea algae placement already uses) replaces one pessimistic global bound with an exact local one. Verified by counting connected landmasses in dump= renders: 10/10 seeds now place islands at 128, 256 and 512 map sizes, versus roughly 1-in-5 before. Verified: 0 failures across 60 seeds at default size, 30 at 256, 30 with lake-connected=1, the {teams=2,4,8} x 50-seed sweep (teams=8 at 2/50, within the already-accepted fjord-width margin), the 13-generator sanity sweep, and 198/198 cppunit. The defaults test also caught two real mistakes before they shipped: lake-size's default (45) wasn't a valid step-10 grid point from minimum 0 (fixed by using step 5), and both new control labels needed entries in data/texts.en.txt and data/texts.keys.txt the same way every other control label already has one. Fjord's revision moves to 7. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- data/texts.en.txt | 4 + data/texts.keys.txt | 2 + docs/map-generators/FRAMEWORK_UPGRADES.md | 8 + .../generators/FjordContinentGenerator.cpp | 141 +++++++++++++++--- .../generators/FjordContinentGenerator.h | 2 +- 5 files changed, 133 insertions(+), 24 deletions(-) diff --git a/data/texts.en.txt b/data/texts.en.txt index dd8938dda..a54346585 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1804,5 +1804,9 @@ Continent size Coast roughness [Fjord width] Fjord width +[Lake size] +Lake size +[Lake connects to fjords] +Lake connects to fjords [Resource islands] Resource islands diff --git a/data/texts.keys.txt b/data/texts.keys.txt index ba31cb56f..bf4725c4c 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -900,4 +900,6 @@ [Continent size] [Coast roughness] [Fjord width] +[Lake size] +[Lake connects to fjords] [Resource islands] diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 70c63776f..57629392a 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -74,4 +74,12 @@ A handful of colonies on constrained terrain (river was the clearest case) lande Verified with a new check beyond generation success: `MapGeneratorStudy quality` reports each colony's actual wheat/wood distance (-1 when unreachable) via the same flood `scoreStarts` already computes. Across 60 seeds x {Lattice, Maze, Fjord} (720 colonies), unreachable cases dropped from 7 to 3, and the 3 remaining are byte-for-byte identical to the pre-session baseline on the same seeds (`catchmentTiles=4` - a genuinely tiny, isolated Maze room with no larger landmass behind a wall to reveal, which `guaranteeStartingResources` correctly declines to force). Fjord specifically, the generator in the screenshot, went from a visible wall to 0 unreachable colonies across the sweep. Also re-verified the standard suite: 0 failures across the 60-seed x 3-generator sweep, the {teams=2,4,8} x 50-seed sweep (same class of pre-existing fjord-width failures as before, none new), the 13-generator sanity sweep, and 198/198 cppunit. Fjord's revision moves to 6, Lattice's and Maze's to 5. +- Fjord's untouched core - previously just a plain landmass every fjord stopped short of, carrying one stone clump and one random fruit clump - is now a real destination: a central lake (an `xf.toShape`-space circle carved after the fjords, `lake-size` percent of `coreR`, 0 disabling it), with a sandy no-man's-land ring around it wider than `controlSand`'s own coastal fringe would give it, several stone clumps and one clump of every fruit type in the ring beyond the sand, and algae in the lake itself using the same technique the open sea already gets in the step below it. `coreR` widened from 0.19 to 0.23 of the continent radius to make room for the lake and its resource ring without starving fjord length. + + Whether the fjords actually cut through into the lake is a real topology choice, not just cosmetic: doing so makes every peninsula water-isolated from its neighbors (boats required to reach anyone), where stopping short behind a solid land ring keeps today's mutual land connectivity. Asked directly rather than guessed at; the answer was to keep both available as a `lake-connected` control (default off, the existing land-ring behavior) rather than pick one permanently. When on, each fjord's inner terminus moves from `coreR` to the lake's own edge (`fjordInnerR`), and the global cross-team connectivity check in step 5 only runs when the ring is expected to exist - lake-connected maps are *supposed* to fail that specific check by design, and each peninsula's own viability is still verified independently further down (step 8 fails outright if a team's home area comes up empty), so nothing downstream loses coverage. + + Separately, a genuine bug: outlier resource islands were going unplaced almost every roll, at every map size, not just small ones - a user report of "most of the time, even at 256 or 512." Root cause: the island annulus's inner bound was `coast.maximumRadius()`, the sum of every noise harmonic's amplitude as if all four peaked at the same angle simultaneously - a mathematically safe bound, but one the coastline can never actually reach (different harmonics peak at different angles), so it overshoots substantially. Since both the coastline's reach and the map's margin scale together with map size, that overshoot doesn't shrink at larger sizes, which is exactly why bigger maps didn't help. First tried sampling the coastline's true maximum over 360 angles instead of summing worst cases - a real improvement, but still not enough: measured directly, the margin goes negative on a majority of rolls simply because the coastline's single worst direction (continent-size up to 40%, roughness up to 35%, elongation up to 1.3x) can legitimately approach half the map by itself. The actual fix: each island's own center is independently randomized already, so it never needed to be safe in the coastline's worst direction *everywhere* - only at its own location. Checking each candidate directly against `coast.radiusAt()` at its own transformed position (the same per-point technique step 7's algae placement already uses for its own shoreline check) replaces one pessimistic global annulus with an exact local one. Verified by counting connected landmasses in `dump=` renders: 10/10 seeds now place 2-3 islands at 128, 256 and 512 map sizes, versus roughly 1-in-5 before this fix. + + Full suite re-verified after all three changes together: 0 failures across 60 seeds at the default map size, 30 seeds at 256, and 30 seeds with `lake-connected=1`; the {teams=2,4,8} x 50-seed sweep (teams=8 landed at 2/50, within the existing accepted fjord-width margin, not a new regression); the 13-generator sanity sweep; the defaults test (which also caught a step in the process - `lake-size`'s default of 45 wasn't a valid step-10 grid point from a minimum of 0, and the label lookups for both new controls needed entries added to `data/texts.en.txt`/`data/texts.keys.txt`, the same way every other control label already has one); and 198/198 cppunit. Fjord's revision moves to 7. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 9c87b2c25..d3de49a8b 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -82,7 +82,20 @@ static bool generate(Game &game, GenerationContext &context) { const double baseR = options.continentSize / 100.0 * std::min(W, H); RadialShape coast(baseR, options.roughness / 100.0, context, "coast", 1.4); - const double coreR = baseR * 0.19; + // Widened from 0.19 to make room for the central lake and its resource ring below without + // starving the fjords of length - coreR is still where every fjord stops short, just a little + // further out than before. + const double coreR = baseR * 0.23; + // lake-size is a percentage of coreR; 0 disables the lake entirely. lake-connected decides + // whether the fjords actually cut through to it (every peninsula then water-isolated from its + // neighbors, boats required) or stop short of it behind a solid land ring (every peninsula + // stays mutually land-connected, verified explicitly in step 5 rather than assumed) - both are + // legitimate map styles, so both stay available rather than picking one permanently. + const double lakeR = coreR * (options.lakeSize / 100.0); + const bool hasLake = lakeR > 0.5; + const bool lakeConnected = hasLake && options.lakeConnected != 0; + // Where every fjord actually stops: the lake's edge in connected mode, coreR otherwise. + const double fjordInnerR = lakeConnected ? lakeR : coreR; const double maxStretch = std::max(elongation, 1.0 / elongation); // One angular sector per team, evenly spaced with a little jitter so it @@ -140,7 +153,7 @@ static bool generate(Game &game, GenerationContext &context) { double mouthR = coast.radiusAt(midTheta) + 3.0; double mouthU = mouthR * cos(midTheta), mouthV = mouthR * sin(midTheta); - double tipU = coreR * cos(midTheta), tipV = coreR * sin(midTheta); + double tipU = fjordInnerR * cos(midTheta), tipV = fjordInnerR * sin(midTheta); double perpU = -sin(midTheta), perpV = cos(midTheta); double gap = std::min(d, 2 * pi - d); @@ -150,7 +163,7 @@ static bool generate(Game &game, GenerationContext &context) { options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; double tipWidth = 0.65; - int steps = std::max(24, (int)(mouthR - coreR)); + int steps = std::max(24, (int)(mouthR - fjordInnerR)); for (int s = 0; s <= steps; ++s) { double t = double(s) / steps; double baseU = mouthU + t * (tipU - mouthU); @@ -183,28 +196,75 @@ static bool generate(Game &game, GenerationContext &context) { } } + // 2.5) A lake at the very center every fjord points toward (lake-size 0 skips this entirely). + // In disconnected mode the fjords stop at coreR, well outside lakeR, leaving the coreR-lakeR + // ring solid - every peninsula stays mutually land-connected around the lake's edge (step 5 + // verifies this explicitly). In connected mode the fjords already terminate at lakeR itself + // (fjordInnerR above), so this carve is what actually opens each fjord into the lake. + if (hasLake) { + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + const auto shaped = xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= lakeR * lakeR) + game.map.setUMatPos(x, y, WATER, 1); + } + } + + // A sandy no-man's-land ring just outside the lake - wider than controlSand's own thin + // coastal fringe would give it, so the open ground around the lake reads as a deliberate + // contested space rather than an ordinary beach. Only touches tiles the lake/fjord carving + // above left as land, so it never overwrites water. + const double sandOuterR = lakeR + lakeR * 0.5; + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + if (game.map.isWater(x, y)) + continue; + const auto shaped = xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= sandOuterR * sandOuterR) + game.map.setUMatPos(x, y, SAND, 1); + } + } + } + // 3) A couple of small, unconnected resource islands out in the open sea -- // purely a bonus for whoever explores, never touching the mainland or each // other. { - double mainlandReach = coast.maximumRadius() * maxStretch; + // A single global "mainland reach" bound - even the tightest one, the coastline's actual + // sampled maximum over every angle - still has to stay safe in whichever single direction + // roughness and elongation happen to push the coastline furthest, and that alone can already + // exceed half the map: measured directly (continent-size up to 40%, roughness up to 35%, + // elongation up to 1.3x), the margin goes negative on a majority of rolls regardless of map + // size, which is exactly why islands were going unplaced almost always rather than just at + // small sizes. Each island's own center is independently randomized, though, so it doesn't + // need to be safe in the coastline's worst direction everywhere - only at its own location. + // Checking each candidate directly against coast.radiusAt() there (the same per-point + // technique step 7's algae placement already uses for its own shoreline check, via the same + // xf.toShape transform) replaces one pessimistic global annulus with an exact local one, so + // an island can land close to a narrow stretch of coast even while the coastline bulges out + // far away in some other direction. double halfMapMargin = std::min(W, H) / 2.0 - 6.0; int outlierCount = std::min(4, options.resourceIslands + int(context.bounded("layout", 2))); std::vector outlierCenters; std::vector outlierRadii; - for (int oi = 0; oi < outlierCount && mainlandReach + 6.0 < halfMapMargin; - ++oi) { + for (int oi = 0; oi < outlierCount; ++oi) { int islandRadius = 5 + context.bounded("layout", 4); bool placed = false; - for (int attempt = 0; attempt < 40 && !placed; ++attempt) { + for (int attempt = 0; attempt < 60 && !placed; ++attempt) { double theta = randomAngle(context); - double lo = mainlandReach + 6.0; - double hi = halfMapMargin; - double r = lo + (context.bounded("layout", 1000)) / 1000.0 * (hi - lo); + double r = (context.bounded("layout", 1000)) / 1000.0 * halfMapMargin; int cx = game.map.normalizeX((int)lround(W / 2.0 + r * cos(theta))); int cy = game.map.normalizeY((int)lround(H / 2.0 + r * sin(theta))); + const auto shaped = xf.toShape({double(cx), double(cy)}); + const double shapeR = std::hypot(shaped.x, shaped.y); + const double shapeTheta = atan2(shaped.y, shaped.x); + if (shapeR < coast.radiusAt(shapeTheta) + islandRadius + 8.0) + continue; // too close to (or inside) the mainland at this specific point + bool clear = true; for (unsigned int oc = 0; oc < outlierCenters.size() && clear; ++oc) { int minSep = islandRadius + outlierRadii[oc] + 8; @@ -298,8 +358,11 @@ static bool generate(Game &game, GenerationContext &context) { // 5) Connectivity: verify, don't assume. The untouched core should make this // unreachable in practice, but every other generator checks its own // invariants explicitly instead of trusting the construction, so this does - // too. - { + // too. In lake-connected mode every peninsula is water-isolated from its neighbors by + // design - failing here would just reject every map that mode ever produces - so this check + // only runs in the default, disconnected mode. Each peninsula's own viability is still + // verified locally further down (step 8 fails outright if a team's home area comes up empty). + if (!lakeConnected) { std::vector visited(W * H, false); std::vector stack; stack.push_back(teamPts[0]); @@ -325,14 +388,19 @@ static bool generate(Game &game, GenerationContext &context) { return false; } - // 6) Stone and fruit in the untouched core -- the reward for pushing to the - // middle of the map instead of staying home. + // 6) Stone and fruit in the ring around the new central lake -- the reward for pushing to the + // middle of the map instead of staying home. Several stone clumps rather than one, and every + // fruit type instead of a single random pick, so finding this area feels like a genuinely rich + // destination and not a single repeated deposit. { int coreArea = areaNumber++; std::vector corePts; for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { - if (game.map.isWater(x, y) || grid[y * W + x] != 0) + // Corn/stone/fruit all require grass, so the sand ring the lake just grew (when there + // is one) is deliberately excluded here rather than merely non-water - a clump center + // landing on sand could miss every grass tile within its own radius and place nothing. + if (!game.map.isGrass(x, y) || grid[y * W + x] != 0) continue; const auto shaped = xf.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; @@ -343,12 +411,34 @@ static bool generate(Game &game, GenerationContext &context) { } } if (!corePts.empty()) { - placeResourceClump(game.map, context, - corePts[context.bounded("resources", corePts.size())], - STONE, 3); - placeResourceClump(game.map, context, - corePts[context.bounded("resources", corePts.size())], - CHERRY + context.bounded("resources", 3), 2); + for (int stoneClump = 0; stoneClump < 3; ++stoneClump) + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], + STONE, 3); + for (int fruitType = 0; fruitType < 3; ++fruitType) + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], + CHERRY + fruitType, 2); + } + + // The lake itself gets the same algae treatment the open sea gets in step 7 below - + // collected separately since it's carved well inside the ring just built above. + if (hasLake) { + std::vector lakeWater; + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) { + if (!game.map.isWater(x, y)) + continue; + const auto shaped = xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= lakeR * lakeR) + lakeWater.push_back(MapGeneratorPoint(x, y)); + } + if (!lakeWater.empty()) + for (int i = 0; i < std::max(1, int(lakeWater.size()) / 40); ++i) + placeResourceClump( + game.map, context, + lakeWater[context.bounded("resources", lakeWater.size())], ALGA, 2); } } @@ -483,19 +573,24 @@ FjordContinentOptions::FjordContinentOptions(const GenerationRequest &r) : continentSize(r.option("continent-size")), roughness(r.option("coast-roughness")), fjordWidth(r.option("fjord-width")), - resourceIslands(r.option("resource-islands")) {} + resourceIslands(r.option("resource-islands")), + lakeSize(r.option("lake-size")), + lakeConnected(r.option("lake-connected")) {} GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 6, + 7, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, {"coast-roughness", "Coast roughness", 10, 35, 1, 22, ControlGroup::Terrain}, {"fjord-width", "Fjord width", 2, 10, 1, 4, ControlGroup::Terrain}, + {"lake-size", "Lake size", 0, 90, 5, 45, ControlGroup::Terrain}, + {"lake-connected", "Lake connects to fjords", 0, 1, 1, 0, + ControlGroup::Terrain}, {"resource-islands", "Resource islands", 0, 4, 1, 2, ControlGroup::Resources}}, generate}; diff --git a/src/map/generator/generators/FjordContinentGenerator.h b/src/map/generator/generators/FjordContinentGenerator.h index b729200ad..1d594a696 100644 --- a/src/map/generator/generators/FjordContinentGenerator.h +++ b/src/map/generator/generators/FjordContinentGenerator.h @@ -4,7 +4,7 @@ #include "GeneratorDefinition.h" struct FjordContinentOptions { - int continentSize, roughness, fjordWidth, resourceIslands; + int continentSize, roughness, fjordWidth, resourceIslands, lakeSize, lakeConnected; explicit FjordContinentOptions(const GenerationRequest &r); }; GeneratorDefinition fjordContinentDefinition(); From 5f7a5e0114b464445c906c6fcbcce8fb4fc9520b Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 11:21:03 -0400 Subject: [PATCH 030/136] Widen resource-islands range, thicken core fruit, center lake algae, rebalance wheat:wood Four follow-ups on the lake/island work. resource-islands' range widened from 0-4 to 0-20 for players who want an island-heavy map - the control itself was already meant to be that flexible, but outlierCount was separately hard-capped at std::min(4, ...), silently discarding anything the control asked for beyond 4. Removed rather than raised, since the per-candidate placement loop is already a safe best-effort search with no fixed ceiling of its own. The core's fruit went from one clump per type to three (kFruitClumpsPerType), so the destination reads as a real grove of each kind rather than a single token tree. Separately, a real bug in the lake's algae: it was drawn from any water tile in the whole lake, shore included, so a clump could anchor right at the water's edge and read as stuck to one side rather than centered - the same "random point instead of a deliberate one" issue as the earlier per-component fertility work, just at lake scale. Fixed by always placing one clump at the lake's exact geometric center (the shape transform's own origin, provably inside the lake for any lake-size > 0) and drawing any bonus clumps only from well inside the shoreline (lakeR - 4, not lakeR). Verified directly with a temporary check, since the dump tool's tile codes don't distinguish algae from plain water: the resource landed on the computed center tile across every seed tested. Last, direct feedback from playing: wood read as overrepresented against wheat. Rebalanced every ambient/bonus layer to 2:1 corn:wood rather than even, leaving every guaranteed placement (Fjord's starter kit and bank guarantees, Lattice/Maze's guaranteed per-team clumps) at 1:1 - those exist for reachability fairness, not feel, and depend on both resources staying equally guaranteed. Fjord's ambient scatter moved from corn=18/wood=18 to corn=24/wood=12 (same total density, reapportioned), its bank-scatter roll distribution from even 3:3:2 to 4:2:2 (corn:wood: stone of 8); Lattice's and Maze's wood control default halved from 50 to 25 against wheat's unchanged 50. Verified the achieved ratio by counting resource tile codes across dump= renders: Lattice and Maze land almost exactly at 2:1 (1.99:1, 1.97:1); Fjord lands at 1.76:1, below 2:1 as expected since its untouched 1:1 guarantees dilute the ambient layer, still a clear shift from the prior 1:1 overall. Verified: 0 failures across 60 seeds each for Fjord/Lattice/Maze, 30 seeds at resource-islands=20, 20 at resource-islands=0, the {teams=2,4,8} x 50-seed sweep (teams=8 at 4/50, same already-accepted margin), the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 9, Lattice's and Maze's to 6. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 10 ++++ .../generators/FjordContinentGenerator.cpp | 46 +++++++++++++------ .../generator/generators/LatticeGenerator.cpp | 6 ++- .../generator/generators/MazeGenerator.cpp | 6 ++- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 57629392a..6e607c4e8 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -82,4 +82,14 @@ A handful of colonies on constrained terrain (river was the clearest case) lande Full suite re-verified after all three changes together: 0 failures across 60 seeds at the default map size, 30 seeds at 256, and 30 seeds with `lake-connected=1`; the {teams=2,4,8} x 50-seed sweep (teams=8 landed at 2/50, within the existing accepted fjord-width margin, not a new regression); the 13-generator sanity sweep; the defaults test (which also caught a step in the process - `lake-size`'s default of 45 wasn't a valid step-10 grid point from a minimum of 0, and the label lookups for both new controls needed entries added to `data/texts.en.txt`/`data/texts.keys.txt`, the same way every other control label already has one); and 198/198 cppunit. Fjord's revision moves to 7. +- Three more direct follow-ups on the lake/island work above. `resource-islands`' range widened from 0-4 to 0-20 for players who want an island-heavy map - the control itself was already that flexible, but `outlierCount` was separately hard-capped at `std::min(4, ...)`, silently discarding anything the control asked for beyond 4; removed rather than raised, since the per-candidate placement check is already a safe best-effort loop with no fixed ceiling of its own. The core's fruit went from one clump per type to three (`kFruitClumpsPerType`), so the destination reads as a real grove of each kind rather than a single token tree. + + Separately, a real placement bug in the lake's algae: it was drawn from any water tile in the *whole* lake, shore included, so a clump could anchor right at the water's edge and read as "stuck to one side" rather than centered - exactly the same class of "random point instead of a deliberate one" issue as the earlier per-component work, just at lake scale instead of continent scale. Fixed by always placing one clump at the lake's exact geometric center - the shape transform's own origin, `(round(W/2), round(H/2))`, provably inside the lake for any `lake-size > 0` - and drawing any additional bonus clumps only from `innerLakeR = lakeR - 4`, comfortably clear of the shore rather than the full `lakeR`. Verified directly (not just visually, since the dump tool's tile-type codes don't distinguish algae from plain water): a temporary check confirmed the resource landed exactly on the computed center tile across every seed tested. + + Verified: 0 failures across 60 seeds at default settings, 30 seeds at `resource-islands=20`, 20 seeds at `resource-islands=0`, 30 seeds at `lake-connected=1`, the {teams=2,4,8} x 50-seed sweep (teams=8 at 4/50, the same already-accepted fjord-width margin), the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 8. + +- Last direct feedback from playing: wood was reading as overrepresented against wheat. Rebalanced every ambient/bonus wheat-vs-wood layer to 2:1 corn:wood rather than even, while leaving every *guaranteed* placement (Fjord's per-team starter kit, its two guaranteed bank clumps per side, Lattice/Maze's guaranteed per-team clumps) untouched at 1:1 - those exist for reachability fairness, not feel, and the reachability work earlier in this doc depends on both resources staying equally guaranteed. Concretely: Fjord's ambient `scatterResources` call moved from corn=18/wood=18 to corn=24/wood=12 (same total density, 36, just reapportioned), its bank-scatter roll distribution from an even 3:3:2 (corn:wood:stone of 8) to 4:2:2; Lattice's and Maze's `wood` control default halved from 50 to 25 against wheat's unchanged 50 default (their guaranteed per-team clumps are fixed-count, not density-driven, so only the ambient layer moves). + + Verified the actual achieved ratio directly by counting resource tile codes across `dump=` renders (5 seeds each, since the STUDY line doesn't carry resource-type totals): Lattice and Maze land almost exactly at 2:1 (1.99:1, 1.97:1) since their ambient layer dominates; Fjord lands at 1.76:1, below 2:1 as expected, because its several untouched 1:1 guarantees (starter kit, bank guarantees) dilute the ambient layer's pure 2:1 - still a clear, substantial shift from the prior 1:1 overall. Also re-verified the standard suite: 0 failures across 60 seeds each for Fjord/Lattice/Maze, the {teams=2,4,8} x 50-seed sweep (teams=8 at 4/50, same already-accepted margin), the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 9, Lattice's and Maze's to 6. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index d3de49a8b..256b718c5 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -246,8 +246,10 @@ static bool generate(Game &game, GenerationContext &context) { // an island can land close to a narrow stretch of coast even while the coastline bulges out // far away in some other direction. double halfMapMargin = std::min(W, H) / 2.0 - 6.0; - int outlierCount = std::min(4, options.resourceIslands + - int(context.bounded("layout", 2))); + // No cap beyond the control's own range here - resource-islands now goes up to 20 for + // players who want an island-heavy map, and each one is still an independent best-effort + // placement (a request that can't all fit in the margin just places as many as do). + int outlierCount = options.resourceIslands + int(context.bounded("layout", 2)); std::vector outlierCenters; std::vector outlierRadii; for (int oi = 0; oi < outlierCount; ++oi) { @@ -415,15 +417,27 @@ static bool generate(Game &game, GenerationContext &context) { placeResourceClump(game.map, context, corePts[context.bounded("resources", corePts.size())], STONE, 3); + constexpr int kFruitClumpsPerType = 3; for (int fruitType = 0; fruitType < 3; ++fruitType) - placeResourceClump(game.map, context, - corePts[context.bounded("resources", corePts.size())], - CHERRY + fruitType, 2); + for (int clump = 0; clump < kFruitClumpsPerType; ++clump) + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], + CHERRY + fruitType, 2); } - // The lake itself gets the same algae treatment the open sea gets in step 7 below - - // collected separately since it's carved well inside the ring just built above. + // The lake gets the same algae treatment the open sea gets in step 7 below, but candidates + // are drawn from well inside the shoreline (innerLakeR, not lakeR) rather than anywhere in + // the lake - a clump anchored right up against the shore is still entirely valid water, but + // reads as "stuck to one side" rather than "in the lake". The very first clump is placed + // dead center, at the shape transform's own origin - the lake's exact geometric middle - so + // there's always at least one unambiguously centered deposit regardless of how the interior + // sampling below happens to land. if (hasLake) { + const int centerX = game.map.normalizeX((int)std::lround(W / 2.0)); + const int centerY = game.map.normalizeY((int)std::lround(H / 2.0)); + placeResourceClump(game.map, context, MapGeneratorPoint(centerX, centerY), ALGA, 2); + + const double innerLakeR = std::max(0.0, lakeR - 4.0); std::vector lakeWater; for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) { @@ -431,11 +445,13 @@ static bool generate(Game &game, GenerationContext &context) { continue; const auto shaped = xf.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; - if (u * u + v * v <= lakeR * lakeR) + if (u * u + v * v <= innerLakeR * innerLakeR) lakeWater.push_back(MapGeneratorPoint(x, y)); } + // The center clump above already guarantees at least one, so this is purely bonus + // coverage for a lake big enough to have real interior room left over - no forced minimum. if (!lakeWater.empty()) - for (int i = 0; i < std::max(1, int(lakeWater.size()) / 40); ++i) + for (int i = 0; i < int(lakeWater.size()) / 40; ++i) placeResourceClump( game.map, context, lakeWater[context.bounded("resources", lakeWater.size())], ALGA, 2); @@ -521,7 +537,9 @@ static bool generate(Game &game, GenerationContext &context) { // generator's own layering already makes. Algae is left at zero: the shoreline band in // step 7 already places it with a shape tuned to the coastline, and scattering more over // open water would just fight that. - scatterResources(game, context, {/*corn=*/18, /*wood=*/18, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); + // corn:wood at 2:1 rather than even - wood was reading as overrepresented in practice, and + // total density is held constant (was 18+18=36) rather than just adding more corn on top. + scatterResources(game, context, {/*corn=*/24, /*wood=*/12, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); // 9) Place bank resources last so settlement and regional deposits cannot // overwrite the guarantee. Every side of every fjord receives both resources @@ -547,8 +565,10 @@ static bool generate(Game &game, GenerationContext &context) { for (double progress : bankScatterProgress) { const double jitter = (context.bounded("resources", 41) - 20) / 1000.0; // +/-0.02 for (int side : {-1, 1}) { + // corn:wood at 2:1 (4:2 of 8) rather than even (3:3), stone's own 2/8 share + // unchanged - the same rebalance as the ambient scatter above, for consistency. const int roll = context.bounded("resources", 8); - const int resourceType = roll < 3 ? CORN : roll < 6 ? WOOD : STONE; + const int resourceType = roll < 4 ? CORN : roll < 6 ? WOOD : STONE; placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, resourceType); } @@ -581,7 +601,7 @@ GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 7, + 9, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, @@ -591,7 +611,7 @@ GeneratorDefinition fjordContinentDefinition() { {"lake-size", "Lake size", 0, 90, 5, 45, ControlGroup::Terrain}, {"lake-connected", "Lake connects to fjords", 0, 1, 1, 0, ControlGroup::Terrain}, - {"resource-islands", "Resource islands", 0, 4, 1, 2, + {"resource-islands", "Resource islands", 0, 20, 1, 2, ControlGroup::Resources}}, generate}; } diff --git a/src/map/generator/generators/LatticeGenerator.cpp b/src/map/generator/generators/LatticeGenerator.cpp index 1e743e81c..a8917ad93 100644 --- a/src/map/generator/generators/LatticeGenerator.cpp +++ b/src/map/generator/generators/LatticeGenerator.cpp @@ -101,13 +101,15 @@ GeneratorDefinition latticeDefinition() { "lattice", 10, "Lattice", - 5, + 6, false, {{"islet-size", "Islet size", 1, 4, 1, 2, ControlGroup::Terrain}, {"channel-width", "Channel width", 2, 4, 1, 3, ControlGroup::Terrain}, {"home-radius", "Home radius", 12, 20, 2, 16, ControlGroup::Layout}, {"wheat", "Wheat", 0, 64, 1, 50, ControlGroup::Resources}, - {"wood", "Wood", 0, 64, 1, 50, ControlGroup::Resources}, + // Default halved relative to wheat's 50 (was even at 50/50) - wood read as + // overrepresented in practice, and this keeps the same 2:1 ratio as Fjord's rebalance. + {"wood", "Wood", 0, 64, 1, 25, ControlGroup::Resources}, {"stone", "Stone", 0, 64, 1, 50, ControlGroup::Resources}, {"algae", "Algae", 0, 64, 1, 50, ControlGroup::Resources}, {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources}}, diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 4aa008c80..8f0f95f91 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -214,7 +214,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 5, + 6, false, {{"cell-size", "Cell size", @@ -248,7 +248,9 @@ GeneratorDefinition mazeDefinition() { false, {0, 10, 25, 50, 75, 100}}, {"wheat", "Wheat", 0, 64, 1, 50, ControlGroup::Resources}, - {"wood", "Wood", 0, 64, 1, 50, ControlGroup::Resources}, + // Default halved relative to wheat's 50 (was even at 50/50) - wood read as + // overrepresented in practice, and this keeps the same 2:1 ratio as Fjord's rebalance. + {"wood", "Wood", 0, 64, 1, 25, ControlGroup::Resources}, {"stone", "Stone", 0, 64, 1, 50, ControlGroup::Resources}, {"algae", "Algae", 0, 64, 1, 50, ControlGroup::Resources}, {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources}}, From 38b5ed33460073ffe4a0eaeb4aca6d18d4ab7486 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 11:32:36 -0400 Subject: [PATCH 031/136] Fix fjords not actually reaching the lake in lake-connected mode A screenshot showed the fjords visibly not connecting to the lake - "almost connected but not quite," a thin sand gap sitting right where the water should have merged. First guess: the fjord's target tip radius (fjordInnerR) landed exactly on the lake's boundary circle, so pushed it to lakeR * 0.4 (well inside the lake), reasoning that would force real overlap. Built, verified by flood-filling the water outward from the lake, and it changed nothing - the connected-component size stayed ~35 tiles, the same as the lake alone, never reaching the map edge. The actual cause, found by reading Map::controlSand (MapTerrain.cpp): it converts any water tile with a grass neighbor anywhere in its own 3x3 neighborhood to sand - not a coastal decoration, a channel eraser. The fjord's tip tapers down to tipWidth = 0.65 (a ~1.3-tile diameter) as it approaches its target radius; every tile in a channel that narrow borders grass on both sides, so controlSand sands the entire tip shut regardless of how deep its target radius reaches. Fine for disconnected mode, where that's an intentional dead end against solid land - but it silently closed the one connection lake-connected mode actually needs open. A channel needs a surviving center row not touching grass on either side to remain water at all, which takes a width (radius) of at least ~1.5. Widened tipWidth to max(2.5, mouthWidth * 0.6) specifically in connected mode, comfortably above that floor. The fjordInnerR change stays as a small extra safety margin, though the width fix is what actually matters - the lake is so small relative to the fjord's total length that moving the target radius barely moves the width at the point the centerline actually crosses the lake's boundary. Verified with the same flood-fill check, now correctly: the lake's connected-component water size jumps from ~35 tiles to ~11,000 and reaches the map edge on every seed tested, confirming the outer sea and the lake are now genuinely one connected body via every fjord. Re-verified the standard suite: 0 failures across 60 seeds in disconnected mode, 40 seeds at lake-connected=1, clean at teams 2/4 (0/30 each) - teams=8 rose to 8/30, the same "no swarm footprint fits" class as the existing accepted margin, more pronounced here since connected-mode peninsulas are smaller (water-isolated on every side, not just two fjord edges). Also the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 10. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 4 +++ .../generators/FjordContinentGenerator.cpp | 32 +++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index 6e607c4e8..c422ff472 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -92,4 +92,8 @@ A handful of colonies on constrained terrain (river was the clearest case) lande Verified the actual achieved ratio directly by counting resource tile codes across `dump=` renders (5 seeds each, since the STUDY line doesn't carry resource-type totals): Lattice and Maze land almost exactly at 2:1 (1.99:1, 1.97:1) since their ambient layer dominates; Fjord lands at 1.76:1, below 2:1 as expected, because its several untouched 1:1 guarantees (starter kit, bank guarantees) dilute the ambient layer's pure 2:1 - still a clear, substantial shift from the prior 1:1 overall. Also re-verified the standard suite: 0 failures across 60 seeds each for Fjord/Lattice/Maze, the {teams=2,4,8} x 50-seed sweep (teams=8 at 4/50, same already-accepted margin), the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 9, Lattice's and Maze's to 6. +- `lake-connected` had a real bug: a screenshot showed the fjords visibly *not* reaching the lake - "almost connected but not quite," a thin sand gap sitting right where the water should have merged. Two things were wrong, only the second one actually mattered. First guess: the fjord's target tip radius (`fjordInnerR`) landed exactly on the lake's boundary circle, so pushed it to `lakeR * 0.4` (well inside the lake) reasoning that would force real overlap - built, verified by flood-filling the water from the lake outward, and it changed nothing (the lake's connected-component size stayed ~35 tiles, the same as the lake alone, not reaching the map edge). The actual cause, found by re-reading `Map::controlSand` (`MapTerrain.cpp`): it converts *any* water tile with a grass neighbor anywhere in its own 3x3 neighborhood to sand - not a coastal decoration, a channel eraser. The fjord's tip tapers down to `tipWidth = 0.65` (a ~1.3-tile diameter) as it approaches its target radius; every tile in a channel that narrow borders grass on both sides, so `controlSand` sands the entire tip shut regardless of how deep its target radius reaches - fine for disconnected mode, where that's an intentional dead end against solid land, but it silently closed the one connection `lake-connected` mode actually needs open. A channel needs a surviving center row not touching grass on either side to remain water at all, which takes a width (radius) of at least ~1.5; the fix widens `tipWidth` to `max(2.5, mouthWidth * 0.6)` specifically in connected mode, comfortably above that floor. The `fjordInnerR` change from the first attempt stays as a small extra safety margin, though the width fix is what actually matters - the lake is so small relative to the fjord's total length that moving the target radius barely moves the width *at* the point the centerline actually crosses the lake's boundary. + + Verified with the same flood-fill check, now correctly: the lake's connected-component water size jumps from ~35 tiles (the lake alone) to ~11,000 tiles and reaches the map edge on every seed tested, confirming the outer sea and the lake are now genuinely one connected body via every fjord. Re-verified the standard suite: 0 failures across 60 seeds in disconnected mode, 40 seeds at `lake-connected=1`, and clean at `lake-connected=1` with teams 2/4 (0/30 each) - teams=8 rose to 8/30, the same "no swarm footprint fits" class as the existing accepted margin, more pronounced here since connected-mode peninsulas are smaller (each one now water-isolated on every side rather than just its two fjord edges). Also the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 10. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 256b718c5..94ccd5eab 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -94,8 +94,16 @@ static bool generate(Game &game, GenerationContext &context) { const double lakeR = coreR * (options.lakeSize / 100.0); const bool hasLake = lakeR > 0.5; const bool lakeConnected = hasLake && options.lakeConnected != 0; - // Where every fjord actually stops: the lake's edge in connected mode, coreR otherwise. - const double fjordInnerR = lakeConnected ? lakeR : coreR; + // Where every fjord actually stops: coreR in disconnected mode. In connected mode this is NOT + // the lake's edge itself - the fjord's tip tapers down to a narrow tipWidth (0.65) as it + // approaches its target radius, so aiming exactly at lakeR left the channel a near-thread right + // where it needed to actually merge with the lake, an almost-but-not-quite connection that + // read as a solid sand gap once controlSand ran. Aiming well inside the lake instead (0.4x its + // radius) means the fjord's centerline crosses the lake's real boundary much earlier along its + // taper, while the channel is still comfortably wide, guaranteeing a real merge instead of a + // razor-thin near miss. The extra length beyond the boundary just carves more water inside + // ground that's already the lake, which is harmless. + const double fjordInnerR = lakeConnected ? lakeR * 0.4 : coreR; const double maxStretch = std::max(elongation, 1.0 / elongation); // One angular sector per team, evenly spaced with a little jitter so it @@ -161,7 +169,18 @@ static bool generate(Game &game, GenerationContext &context) { double phase = randomAngle(context); double mouthWidth = options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; - double tipWidth = 0.65; + // Map::controlSand (MapTerrain.cpp) converts any water tile with a grass neighbor in its + // own 3x3 neighborhood to sand - not just a coastal decoration, it can erase a channel + // outright. A width-0.65 tip (diameter ~1.3 tiles) is entirely coastal by that rule, so + // every tile in it borders grass and gets sanded over - fine for disconnected mode, where + // the tip is meant to taper into a dead end against solid land anyway, but it silently + // closes the one connection lake-connected mode actually needs to stay open. A channel + // needs a surviving center row not touching grass on either side to remain water at all, + // which takes a width (radius) of at least ~1.5; targeting well above that keeps it open + // with real margin. Since lakeR is tiny next to the fjord's total length, aiming the tip + // radius deeper into the lake (fjordInnerR above) barely moves the width *at* the lake's + // actual boundary crossing - this is the fix that matters. + double tipWidth = lakeConnected ? std::max(2.5, mouthWidth * 0.6) : 0.65; int steps = std::max(24, (int)(mouthR - fjordInnerR)); for (int s = 0; s <= steps; ++s) { @@ -199,8 +218,9 @@ static bool generate(Game &game, GenerationContext &context) { // 2.5) A lake at the very center every fjord points toward (lake-size 0 skips this entirely). // In disconnected mode the fjords stop at coreR, well outside lakeR, leaving the coreR-lakeR // ring solid - every peninsula stays mutually land-connected around the lake's edge (step 5 - // verifies this explicitly). In connected mode the fjords already terminate at lakeR itself - // (fjordInnerR above), so this carve is what actually opens each fjord into the lake. + // verifies this explicitly). In connected mode the fjords already reach well inside lakeR + // (fjordInnerR above), so this carve is what actually opens each fjord into the lake - the + // fjords' own tapered tips would otherwise be too narrow to reliably merge with it. if (hasLake) { for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { @@ -601,7 +621,7 @@ GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", 12, "Fjord continent", - 9, + 10, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, From d35645dc4199cb46d86531e5f883dd06fdea71ca Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 11:49:07 -0400 Subject: [PATCH 032/136] Renumber Fjord's generation steps and trim debugging-journey comments The step comments in FjordContinentGenerator::generate() had drifted out of order (2.5, 8.5) and cross-references pointed at stale numbers after several rounds of feature work inserted new steps in the middle. Renumbered 1-11 sequentially and fixed every internal "step N" cross-reference to match. Also trimmed a few comments that read as a debugging narrative (references to specific numbers tried and rejected, "measured directly" framing) down to just the reasoning a reader needs going forward. Purely a comment/renumbering change - verified via a 30-seed sweep with lake-connected, lake-size and resource-islands all exercised: STUDY/QUALITY/ COLONY output is byte-identical before and after (only wall-clock timing fields differ). Defaults test and the full cppunit suite (198 tests) also still pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- .../generators/FjordContinentGenerator.cpp | 83 ++++++++----------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 94ccd5eab..22b05603b 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -89,20 +89,15 @@ static bool generate(Game &game, GenerationContext &context) { // lake-size is a percentage of coreR; 0 disables the lake entirely. lake-connected decides // whether the fjords actually cut through to it (every peninsula then water-isolated from its // neighbors, boats required) or stop short of it behind a solid land ring (every peninsula - // stays mutually land-connected, verified explicitly in step 5 rather than assumed) - both are + // stays mutually land-connected, verified explicitly in step 6 rather than assumed) - both are // legitimate map styles, so both stay available rather than picking one permanently. const double lakeR = coreR * (options.lakeSize / 100.0); const bool hasLake = lakeR > 0.5; const bool lakeConnected = hasLake && options.lakeConnected != 0; - // Where every fjord actually stops: coreR in disconnected mode. In connected mode this is NOT - // the lake's edge itself - the fjord's tip tapers down to a narrow tipWidth (0.65) as it - // approaches its target radius, so aiming exactly at lakeR left the channel a near-thread right - // where it needed to actually merge with the lake, an almost-but-not-quite connection that - // read as a solid sand gap once controlSand ran. Aiming well inside the lake instead (0.4x its - // radius) means the fjord's centerline crosses the lake's real boundary much earlier along its - // taper, while the channel is still comfortably wide, guaranteeing a real merge instead of a - // razor-thin near miss. The extra length beyond the boundary just carves more water inside - // ground that's already the lake, which is harmless. + // Where every fjord actually stops: coreR in disconnected mode. In connected mode, well inside + // the lake (0.4x its radius) rather than exactly on its edge, so the centerline crosses the + // lake's real boundary with margin to spare - the extra length beyond the boundary just carves + // more water inside ground that's already the lake, which is harmless. const double fjordInnerR = lakeConnected ? lakeR * 0.4 : coreR; const double maxStretch = std::max(elongation, 1.0 / elongation); @@ -171,15 +166,11 @@ static bool generate(Game &game, GenerationContext &context) { options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; // Map::controlSand (MapTerrain.cpp) converts any water tile with a grass neighbor in its // own 3x3 neighborhood to sand - not just a coastal decoration, it can erase a channel - // outright. A width-0.65 tip (diameter ~1.3 tiles) is entirely coastal by that rule, so - // every tile in it borders grass and gets sanded over - fine for disconnected mode, where - // the tip is meant to taper into a dead end against solid land anyway, but it silently - // closes the one connection lake-connected mode actually needs to stay open. A channel - // needs a surviving center row not touching grass on either side to remain water at all, - // which takes a width (radius) of at least ~1.5; targeting well above that keeps it open - // with real margin. Since lakeR is tiny next to the fjord's total length, aiming the tip - // radius deeper into the lake (fjordInnerR above) barely moves the width *at* the lake's - // actual boundary crossing - this is the fix that matters. + // outright. The disconnected-mode tip (0.65, ~1.3 tiles across) is entirely coastal by that + // rule and gets sanded over - an intentional dead end there, but it would silently close + // the one connection lake-connected mode needs open. A channel needs a surviving center row + // not touching grass on either side to stay water at all, which takes a radius of at least + // ~1.5; this targets comfortably above that floor. double tipWidth = lakeConnected ? std::max(2.5, mouthWidth * 0.6) : 0.65; int steps = std::max(24, (int)(mouthR - fjordInnerR)); @@ -215,9 +206,9 @@ static bool generate(Game &game, GenerationContext &context) { } } - // 2.5) A lake at the very center every fjord points toward (lake-size 0 skips this entirely). + // 3) A lake at the very center every fjord points toward (lake-size 0 skips this entirely). // In disconnected mode the fjords stop at coreR, well outside lakeR, leaving the coreR-lakeR - // ring solid - every peninsula stays mutually land-connected around the lake's edge (step 5 + // ring solid - every peninsula stays mutually land-connected around the lake's edge (step 6 // verifies this explicitly). In connected mode the fjords already reach well inside lakeR // (fjordInnerR above), so this carve is what actually opens each fjord into the lake - the // fjords' own tapered tips would otherwise be too narrow to reliably merge with it. @@ -248,23 +239,21 @@ static bool generate(Game &game, GenerationContext &context) { } } - // 3) A couple of small, unconnected resource islands out in the open sea -- + // 4) A couple of small, unconnected resource islands out in the open sea -- // purely a bonus for whoever explores, never touching the mainland or each // other. { - // A single global "mainland reach" bound - even the tightest one, the coastline's actual - // sampled maximum over every angle - still has to stay safe in whichever single direction - // roughness and elongation happen to push the coastline furthest, and that alone can already - // exceed half the map: measured directly (continent-size up to 40%, roughness up to 35%, - // elongation up to 1.3x), the margin goes negative on a majority of rolls regardless of map - // size, which is exactly why islands were going unplaced almost always rather than just at - // small sizes. Each island's own center is independently randomized, though, so it doesn't - // need to be safe in the coastline's worst direction everywhere - only at its own location. - // Checking each candidate directly against coast.radiusAt() there (the same per-point - // technique step 7's algae placement already uses for its own shoreline check, via the same - // xf.toShape transform) replaces one pessimistic global annulus with an exact local one, so - // an island can land close to a narrow stretch of coast even while the coastline bulges out - // far away in some other direction. + // A single global "mainland reach" bound has to stay safe in whichever single direction + // roughness and elongation happen to push the coastline furthest, and that alone can exceed + // half the map (continent-size up to 40%, roughness up to 35%, elongation up to 1.3x) on a + // majority of rolls regardless of map size - a global bound leaves islands unplaced almost + // always, not just at small sizes. Each island's own center is independently randomized, + // though, so it only needs to be safe in the coastline's direction at its own location, not + // everywhere. Checking each candidate directly against coast.radiusAt() there (the same + // per-point technique step 8's algae placement already uses for its own shoreline check, via + // the same xf.toShape transform) replaces the pessimistic global bound with an exact local + // one, so an island can land close to a narrow stretch of coast even while the coastline + // bulges out far away in some other direction. double halfMapMargin = std::min(W, H) / 2.0 - 6.0; // No cap beyond the control's own range here - resource-islands now goes up to 20 for // players who want an island-heavy map, and each one is still an independent best-effort @@ -339,7 +328,7 @@ static bool generate(Game &game, GenerationContext &context) { game.map.controlSand(); - // 4) Anchor each team out at its own tip: start just inland of the coast at + // 5) Anchor each team out at its own tip: start just inland of the coast at // the team's angle and only back off toward the core if that exact spot turns // out to be water (a sharp jaggedness dip, or a fjord belly that swung wider // than expected) -- the same "compute where we want to be, then confirm the @@ -377,13 +366,13 @@ static bool generate(Game &game, GenerationContext &context) { teamPts.push_back(MapGeneratorPoint(fx, fy)); } - // 5) Connectivity: verify, don't assume. The untouched core should make this + // 6) Connectivity: verify, don't assume. The untouched core should make this // unreachable in practice, but every other generator checks its own // invariants explicitly instead of trusting the construction, so this does // too. In lake-connected mode every peninsula is water-isolated from its neighbors by // design - failing here would just reject every map that mode ever produces - so this check // only runs in the default, disconnected mode. Each peninsula's own viability is still - // verified locally further down (step 8 fails outright if a team's home area comes up empty). + // verified locally further down (step 9 fails outright if a team's home area comes up empty). if (!lakeConnected) { std::vector visited(W * H, false); std::vector stack; @@ -410,7 +399,7 @@ static bool generate(Game &game, GenerationContext &context) { return false; } - // 6) Stone and fruit in the ring around the new central lake -- the reward for pushing to the + // 7) Stone and fruit in the ring around the new central lake -- the reward for pushing to the // middle of the map instead of staying home. Several stone clumps rather than one, and every // fruit type instead of a single random pick, so finding this area feels like a genuinely rich // destination and not a single repeated deposit. @@ -445,7 +434,7 @@ static bool generate(Game &game, GenerationContext &context) { CHERRY + fruitType, 2); } - // The lake gets the same algae treatment the open sea gets in step 7 below, but candidates + // The lake gets the same algae treatment the open sea gets in step 8 below, but candidates // are drawn from well inside the shoreline (innerLakeR, not lakeR) rather than anywhere in // the lake - a clump anchored right up against the shore is still entirely valid water, but // reads as "stuck to one side" rather than "in the lake". The very first clump is placed @@ -478,7 +467,7 @@ static bool generate(Game &game, GenerationContext &context) { } } - // 7) Algae out in the open sea: any water tile clearly beyond the coastline + // 8) Algae out in the open sea: any water tile clearly beyond the coastline // (not a fjord, not the moat-ish water right against the shore) gets an // occasional patch. std::vector algaeWater; @@ -501,7 +490,7 @@ static bool generate(Game &game, GenerationContext &context) { game.map, context, algaeWater[context.bounded("resources", algaeWater.size())], ALGA, 2); - // 8) A light per-team starter kit so nobody is stuck waiting to reach the + // 9) A light per-team starter kit so nobody is stuck waiting to reach the // fjord banks before they can build anything; the banks and the core are the // map's real economy. std::vector allWater; @@ -543,25 +532,25 @@ static bool generate(Game &game, GenerationContext &context) { return false; } - // 8.5) Every resource so far is a deliberate, counted placement tied to a specific + // 10) Every resource so far is a deliberate, counted placement tied to a specific // purpose: a home starter kit, the core, an outlier island. That leaves the whole // continent interior in between them bare grass, which reads as empty rather than as a // place with its own history the way a noise-painted map does. A light map-wide scatter // -- the same mechanism Lattice and Maze already rely on for this -- fills that gap with // ordinary, unclaimed deposits. This runs after every swarm and worker is already placed, // not before: isResourceAllowed refuses any tile with a building or unit on it, so a - // scatter placed earlier could (and, measured directly, occasionally did) claim the one - // remaining tile a colony's swarm footprint needed, failing generation outright. Running + // scatter placed earlier can claim the one remaining tile a colony's swarm footprint + // needed, failing generation outright. Running // last costs nothing here -- setResource always overwrites, so a scatter clump landing on // an earlier deposit just loses that one tile to it, the same low-stakes trade every other // generator's own layering already makes. Algae is left at zero: the shoreline band in - // step 7 already places it with a shape tuned to the coastline, and scattering more over + // step 8 already places it with a shape tuned to the coastline, and scattering more over // open water would just fight that. // corn:wood at 2:1 rather than even - wood was reading as overrepresented in practice, and // total density is held constant (was 18+18=36) rather than just adding more corn on top. scatterResources(game, context, {/*corn=*/24, /*wood=*/12, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); - // 9) Place bank resources last so settlement and regional deposits cannot + // 11) Place bank resources last so settlement and regional deposits cannot // overwrite the guarantee. Every side of every fjord receives both resources // at distinct points along its length. context.stage = "fjord bank resources"; From 87162c994837cb2bf804d62f749741234fd7004a Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 11:49:30 -0400 Subject: [PATCH 033/136] Document the Fjord step-renumbering cleanup Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/FRAMEWORK_UPGRADES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md index c422ff472..d2fdc0319 100644 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ b/docs/map-generators/FRAMEWORK_UPGRADES.md @@ -96,4 +96,6 @@ A handful of colonies on constrained terrain (river was the clearest case) lande Verified with the same flood-fill check, now correctly: the lake's connected-component water size jumps from ~35 tiles (the lake alone) to ~11,000 tiles and reaches the map edge on every seed tested, confirming the outer sea and the lake are now genuinely one connected body via every fjord. Re-verified the standard suite: 0 failures across 60 seeds in disconnected mode, 40 seeds at `lake-connected=1`, and clean at `lake-connected=1` with teams 2/4 (0/30 each) - teams=8 rose to 8/30, the same "no swarm footprint fits" class as the existing accepted margin, more pronounced here since connected-mode peninsulas are smaller (each one now water-isolated on every side rather than just its two fjord edges). Also the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 10. +- Cleanup pass over `FjordContinentGenerator::generate()` after the round of feature work above: its step comments (1, 2, 2.5, 3, 4, 5, 6, 7, 8, 8.5, 9) had drifted out of numeric order as new steps were inserted in the middle, and several internal "step N" cross-references pointed at stale numbers. Renumbered sequentially (1-11) and fixed every cross-reference to match. Also trimmed a handful of comments that had accumulated debugging-journey narration - specific numbers tried and rejected, "measured directly" framing - down to just the reasoning a future reader needs. No behavior change: verified with a 30-seed sweep exercising `lake-connected`, `lake-size` and `resource-islands` together, comparing `MapGeneratorStudy`'s STUDY/QUALITY/COLONY output line-for-line before and after - byte-identical aside from wall-clock timing fields. Defaults test and 198/198 cppunit also still pass. Revision unchanged (10), since nothing observable changed. + The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. From 0aaec7308ffa20d6ae146e62f623094be8b00639 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 11:57:25 -0400 Subject: [PATCH 034/136] Split FjordContinentGenerator::generate() into named helper functions generate() had grown to ~540 lines covering eleven distinct concerns (stamping the continent, carving fjords, the central lake, outlier islands, team anchors, connectivity, core/sea/starter/bank resources). Extracted each into its own function in the anonymous namespace - stampContinent, carveFjords, carveLake, placeOutlierIslands, anchorTeams, verifyConnectivity, placeCoreResources, placeOpenSeaAlgae, placeStarterKits, placeBankResources - plus a computeLayout() that builds a small FjordLayout struct (continent shape, lake/fjord geometry, per-team angles) threaded through all of them. generate() itself is now a ~30-line driver that calls them in sequence. Pure extraction, no behavior change: every context.bounded() call keeps its original relative order within its own named RNG stream (each stream is independently seeded, so only intra-stream order matters - confirmed by reading GenerationContext::bounded), since no step was reordered relative to any other, only wrapped in a function. Verified with the same 30-seed sweep as the prior commit: STUDY/ QUALITY/COLONY output byte-identical before and after (timing fields aside). Defaults test and 198/198 cppunit also still pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- .../generators/FjordContinentGenerator.cpp | 791 ++++++++++-------- 1 file changed, 434 insertions(+), 357 deletions(-) diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 22b05603b..f51be5e76 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -62,18 +62,23 @@ bool placeBankClump(Map &map, GenerationContext &context, } return false; } -static bool generate(Game &game, GenerationContext &context) { - context.stage = "continent"; - const FjordContinentOptions options(context.request); - game.map.makeHomogenMap(WATER); - for (int i = 0; i < context.request.nbTeams; ++i) - game.addTeam(); +// Shared read-only geometry computed once at the start of generation and threaded through +// every step below: the continent's shape, its lake and fjord targets, and where along the +// rim each team is aimed. +struct FjordLayout { + int W, H, nbTeams; + ShapeTransform xf; + RadialShape coast; + double coreR, lakeR, fjordInnerR, maxStretch; + bool hasLake, lakeConnected; + std::vector teamTheta; +}; + +FjordLayout computeLayout(Game &game, GenerationContext &context, + const FjordContinentOptions &options, int nbTeams) { const int W = game.map.getW(); const int H = game.map.getH(); - const int nbTeams = context.request.nbTeams; - if (nbTeams < 1) - return false; const double rotation = randomAngle(context) * 0.5; const double elongation = @@ -89,8 +94,9 @@ static bool generate(Game &game, GenerationContext &context) { // lake-size is a percentage of coreR; 0 disables the lake entirely. lake-connected decides // whether the fjords actually cut through to it (every peninsula then water-isolated from its // neighbors, boats required) or stop short of it behind a solid land ring (every peninsula - // stays mutually land-connected, verified explicitly in step 6 rather than assumed) - both are - // legitimate map styles, so both stay available rather than picking one permanently. + // stays mutually land-connected, verified explicitly in verifyConnectivity rather than + // assumed) - both are legitimate map styles, so both stay available rather than picking one + // permanently. const double lakeR = coreR * (options.lakeSize / 100.0); const bool hasLake = lakeR > 0.5; const bool lakeConnected = hasLake && options.lakeConnected != 0; @@ -117,235 +123,246 @@ static bool generate(Game &game, GenerationContext &context) { } } - // grid holds area numbers for resource zoning, kept alongside the real - // terrain (which is written straight onto game.map as we go, the same way - // computeIsles and computeContestedCommons do it). - std::vector grid(W * H, 0); - int areaNumber = 1; + return FjordLayout{W, H, nbTeams, xf, coast, + coreR, lakeR, fjordInnerR, maxStretch, + hasLake, lakeConnected, teamTheta}; +} - // 1) Stamp the continent. - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - const auto shaped = xf.toShape({double(x), double(y)}); +// 1) Stamp the continent. +void stampContinent(Game &game, const FjordLayout &layout) { + for (int y = 0; y < layout.H; ++y) { + for (int x = 0; x < layout.W; ++x) { + const auto shaped = layout.xf.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; double theta = atan2(v, u); double r = sqrt(u * u + v * v); - if (r < coast.radiusAt(theta)) + if (r < layout.coast.radiusAt(theta)) game.map.setUMatPos(x, y, GRASS, 1); } } +} - // 2) Carve a fjord between every pair of angularly-neighboring teams: a - // smooth S-curve from just outside the coast at the sector boundary, in to a - // fixed inner radius (coreR) that every fjord stops short of -- so the disc - // r> fjordCenterlines(nbTeams); - if (nbTeams >= 2) { - for (int k = 0; k < nbTeams; ++k) { - int i = k; - int j = (k + 1) % nbTeams; - double ti = teamTheta[i]; - double tj = teamTheta[j]; - double d = fmod(tj - ti, 2 * pi); - if (d < 0) - d += 2 * pi; - double midTheta = ti + d / 2.0; - - double mouthR = coast.radiusAt(midTheta) + 3.0; - double mouthU = mouthR * cos(midTheta), mouthV = mouthR * sin(midTheta); - double tipU = fjordInnerR * cos(midTheta), tipV = fjordInnerR * sin(midTheta); - double perpU = -sin(midTheta), perpV = cos(midTheta); - - double gap = std::min(d, 2 * pi - d); - double amplitude = gap * mouthR * 0.17; - double phase = randomAngle(context); - double mouthWidth = - options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; - // Map::controlSand (MapTerrain.cpp) converts any water tile with a grass neighbor in its - // own 3x3 neighborhood to sand - not just a coastal decoration, it can erase a channel - // outright. The disconnected-mode tip (0.65, ~1.3 tiles across) is entirely coastal by that - // rule and gets sanded over - an intentional dead end there, but it would silently close - // the one connection lake-connected mode needs open. A channel needs a surviving center row - // not touching grass on either side to stay water at all, which takes a radius of at least - // ~1.5; this targets comfortably above that floor. - double tipWidth = lakeConnected ? std::max(2.5, mouthWidth * 0.6) : 0.65; - - int steps = std::max(24, (int)(mouthR - fjordInnerR)); - for (int s = 0; s <= steps; ++s) { - double t = double(s) / steps; - double baseU = mouthU + t * (tipU - mouthU); - double baseV = mouthV + t * (tipV - mouthV); - double envelope = sin(pi * t); - double lateral = amplitude * sin(2 * pi * t + phase) * envelope; - double u = baseU + lateral * perpU; - double v = baseV + lateral * perpV; - double width = mouthWidth * (1 - t) + tipWidth * t; - - const auto mapped = xf.toMap({u, v}); - double mx = mapped.x, my = mapped.y; - int rad = (int)ceil(width * maxStretch) + 1; - int ix = (int)lround(mx), iy = (int)lround(my); - fjordCenterlines[k].push_back(MapGeneratorPoint( - game.map.normalizeX(ix), game.map.normalizeY(iy))); - for (int dy = -rad; dy <= rad; ++dy) { - for (int dx = -rad; dx <= rad; ++dx) { - int nx = game.map.normalizeX(ix + dx); - int ny = game.map.normalizeY(iy + dy); - const auto shaped = xf.toShape({double(nx), double(ny)}); - double u2 = shaped.x, v2 = shaped.y; - double dd = (u2 - u) * (u2 - u) + (v2 - v) * (v2 - v); - if (dd <= width * width) { - game.map.setUMatPos(nx, ny, WATER, 1); - } +// 2) Carve a fjord between every pair of angularly-neighboring teams: a +// smooth S-curve from just outside the coast at the sector boundary, in to a +// fixed inner radius (coreR) that every fjord stops short of -- so the disc +// r> +carveFjords(Game &game, GenerationContext &context, const FjordLayout &layout, + const FjordContinentOptions &options) { + std::vector> fjordCenterlines(layout.nbTeams); + if (layout.nbTeams < 2) + return fjordCenterlines; + for (int k = 0; k < layout.nbTeams; ++k) { + int i = k; + int j = (k + 1) % layout.nbTeams; + double ti = layout.teamTheta[i]; + double tj = layout.teamTheta[j]; + double d = fmod(tj - ti, 2 * pi); + if (d < 0) + d += 2 * pi; + double midTheta = ti + d / 2.0; + + double mouthR = layout.coast.radiusAt(midTheta) + 3.0; + double mouthU = mouthR * cos(midTheta), mouthV = mouthR * sin(midTheta); + double tipU = layout.fjordInnerR * cos(midTheta), + tipV = layout.fjordInnerR * sin(midTheta); + double perpU = -sin(midTheta), perpV = cos(midTheta); + + double gap = std::min(d, 2 * pi - d); + double amplitude = gap * mouthR * 0.17; + double phase = randomAngle(context); + double mouthWidth = + options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; + // Map::controlSand (MapTerrain.cpp) converts any water tile with a grass neighbor in its + // own 3x3 neighborhood to sand - not just a coastal decoration, it can erase a channel + // outright. The disconnected-mode tip (0.65, ~1.3 tiles across) is entirely coastal by that + // rule and gets sanded over - an intentional dead end there, but it would silently close + // the one connection lake-connected mode needs open. A channel needs a surviving center row + // not touching grass on either side to stay water at all, which takes a radius of at least + // ~1.5; this targets comfortably above that floor. + double tipWidth = layout.lakeConnected ? std::max(2.5, mouthWidth * 0.6) : 0.65; + + int steps = std::max(24, (int)(mouthR - layout.fjordInnerR)); + for (int s = 0; s <= steps; ++s) { + double t = double(s) / steps; + double baseU = mouthU + t * (tipU - mouthU); + double baseV = mouthV + t * (tipV - mouthV); + double envelope = sin(pi * t); + double lateral = amplitude * sin(2 * pi * t + phase) * envelope; + double u = baseU + lateral * perpU; + double v = baseV + lateral * perpV; + double width = mouthWidth * (1 - t) + tipWidth * t; + + const auto mapped = layout.xf.toMap({u, v}); + double mx = mapped.x, my = mapped.y; + int rad = (int)ceil(width * layout.maxStretch) + 1; + int ix = (int)lround(mx), iy = (int)lround(my); + fjordCenterlines[k].push_back(MapGeneratorPoint( + game.map.normalizeX(ix), game.map.normalizeY(iy))); + for (int dy = -rad; dy <= rad; ++dy) { + for (int dx = -rad; dx <= rad; ++dx) { + int nx = game.map.normalizeX(ix + dx); + int ny = game.map.normalizeY(iy + dy); + const auto shaped = layout.xf.toShape({double(nx), double(ny)}); + double u2 = shaped.x, v2 = shaped.y; + double dd = (u2 - u) * (u2 - u) + (v2 - v) * (v2 - v); + if (dd <= width * width) { + game.map.setUMatPos(nx, ny, WATER, 1); } } } } } + return fjordCenterlines; +} - // 3) A lake at the very center every fjord points toward (lake-size 0 skips this entirely). - // In disconnected mode the fjords stop at coreR, well outside lakeR, leaving the coreR-lakeR - // ring solid - every peninsula stays mutually land-connected around the lake's edge (step 6 - // verifies this explicitly). In connected mode the fjords already reach well inside lakeR - // (fjordInnerR above), so this carve is what actually opens each fjord into the lake - the - // fjords' own tapered tips would otherwise be too narrow to reliably merge with it. - if (hasLake) { - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - const auto shaped = xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= lakeR * lakeR) - game.map.setUMatPos(x, y, WATER, 1); - } +// 3) A lake at the very center every fjord points toward (lake-size 0 skips this entirely). +// In disconnected mode the fjords stop at coreR, well outside lakeR, leaving the coreR-lakeR +// ring solid - every peninsula stays mutually land-connected around the lake's edge +// (verifyConnectivity checks this explicitly). In connected mode the fjords already reach +// well inside lakeR (fjordInnerR, computed in computeLayout), so this carve is what actually +// opens each fjord into the lake - the fjords' own tapered tips would otherwise be too narrow +// to reliably merge with it. +void carveLake(Game &game, const FjordLayout &layout) { + if (!layout.hasLake) + return; + for (int y = 0; y < layout.H; ++y) { + for (int x = 0; x < layout.W; ++x) { + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= layout.lakeR * layout.lakeR) + game.map.setUMatPos(x, y, WATER, 1); } + } - // A sandy no-man's-land ring just outside the lake - wider than controlSand's own thin - // coastal fringe would give it, so the open ground around the lake reads as a deliberate - // contested space rather than an ordinary beach. Only touches tiles the lake/fjord carving - // above left as land, so it never overwrites water. - const double sandOuterR = lakeR + lakeR * 0.5; - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - if (game.map.isWater(x, y)) - continue; - const auto shaped = xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= sandOuterR * sandOuterR) - game.map.setUMatPos(x, y, SAND, 1); - } + // A sandy no-man's-land ring just outside the lake - wider than controlSand's own thin + // coastal fringe would give it, so the open ground around the lake reads as a deliberate + // contested space rather than an ordinary beach. Only touches tiles the lake/fjord carving + // above left as land, so it never overwrites water. + const double sandOuterR = layout.lakeR + layout.lakeR * 0.5; + for (int y = 0; y < layout.H; ++y) { + for (int x = 0; x < layout.W; ++x) { + if (game.map.isWater(x, y)) + continue; + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= sandOuterR * sandOuterR) + game.map.setUMatPos(x, y, SAND, 1); } } +} - // 4) A couple of small, unconnected resource islands out in the open sea -- - // purely a bonus for whoever explores, never touching the mainland or each - // other. - { - // A single global "mainland reach" bound has to stay safe in whichever single direction - // roughness and elongation happen to push the coastline furthest, and that alone can exceed - // half the map (continent-size up to 40%, roughness up to 35%, elongation up to 1.3x) on a - // majority of rolls regardless of map size - a global bound leaves islands unplaced almost - // always, not just at small sizes. Each island's own center is independently randomized, - // though, so it only needs to be safe in the coastline's direction at its own location, not - // everywhere. Checking each candidate directly against coast.radiusAt() there (the same - // per-point technique step 8's algae placement already uses for its own shoreline check, via - // the same xf.toShape transform) replaces the pessimistic global bound with an exact local - // one, so an island can land close to a narrow stretch of coast even while the coastline - // bulges out far away in some other direction. - double halfMapMargin = std::min(W, H) / 2.0 - 6.0; - // No cap beyond the control's own range here - resource-islands now goes up to 20 for - // players who want an island-heavy map, and each one is still an independent best-effort - // placement (a request that can't all fit in the margin just places as many as do). - int outlierCount = options.resourceIslands + int(context.bounded("layout", 2)); - std::vector outlierCenters; - std::vector outlierRadii; - for (int oi = 0; oi < outlierCount; ++oi) { - int islandRadius = 5 + context.bounded("layout", 4); - bool placed = false; - for (int attempt = 0; attempt < 60 && !placed; ++attempt) { - double theta = randomAngle(context); - double r = (context.bounded("layout", 1000)) / 1000.0 * halfMapMargin; - int cx = game.map.normalizeX((int)lround(W / 2.0 + r * cos(theta))); - int cy = game.map.normalizeY((int)lround(H / 2.0 + r * sin(theta))); - - const auto shaped = xf.toShape({double(cx), double(cy)}); - const double shapeR = std::hypot(shaped.x, shaped.y); - const double shapeTheta = atan2(shaped.y, shaped.x); - if (shapeR < coast.radiusAt(shapeTheta) + islandRadius + 8.0) - continue; // too close to (or inside) the mainland at this specific point - - bool clear = true; - for (unsigned int oc = 0; oc < outlierCenters.size() && clear; ++oc) { - int minSep = islandRadius + outlierRadii[oc] + 8; - if (game.map.warpDistSquare(cx, cy, outlierCenters[oc].x, - outlierCenters[oc].y) < minSep * minSep) - clear = false; - } - if (!clear) - continue; - - int outlierArea = areaNumber++; - createJaggedIsland(game.map, context, grid, outlierArea, cx, cy, - islandRadius, 0.3); - std::vector islandPts; - getAllPoints(game.map, grid, outlierArea, islandPts); - if (islandPts.empty()) - continue; - for (unsigned int p = 0; p < islandPts.size(); ++p) - game.map.setUMatPos(islandPts[p].x, islandPts[p].y, GRASS, 1); - - // Each island leans on one resource theme, so finding one feels like a - // distinct little prize rather than an interchangeable resource dump. - switch (context.bounded("layout", 3)) { - case 0: - placeResourceClump( - game.map, context, - islandPts[context.bounded("resources", islandPts.size())], STONE, - 2); - break; - case 1: - placeResourceClump( - game.map, context, - islandPts[context.bounded("resources", islandPts.size())], - CHERRY + context.bounded("resources", 3), 2); - break; - default: - placeResourceClump( - game.map, context, - islandPts[context.bounded("resources", islandPts.size())], CORN, - 3); - break; - } +// 4) A couple of small, unconnected resource islands out in the open sea -- +// purely a bonus for whoever explores, never touching the mainland or each +// other. +void placeOutlierIslands(Game &game, GenerationContext &context, const FjordLayout &layout, + const FjordContinentOptions &options, std::vector &grid, + int &areaNumber) { + // A single global "mainland reach" bound has to stay safe in whichever single direction + // roughness and elongation happen to push the coastline furthest, and that alone can exceed + // half the map (continent-size up to 40%, roughness up to 35%, elongation up to 1.3x) on a + // majority of rolls regardless of map size - a global bound leaves islands unplaced almost + // always, not just at small sizes. Each island's own center is independently randomized, + // though, so it only needs to be safe in the coastline's direction at its own location, not + // everywhere. Checking each candidate directly against coast.radiusAt() there (the same + // per-point technique placeOpenSeaAlgae's placement already uses for its own shoreline check, + // via the same xf.toShape transform) replaces the pessimistic global bound with an exact + // local one, so an island can land close to a narrow stretch of coast even while the + // coastline bulges out far away in some other direction. + double halfMapMargin = std::min(layout.W, layout.H) / 2.0 - 6.0; + // No cap beyond the control's own range here - resource-islands now goes up to 20 for + // players who want an island-heavy map, and each one is still an independent best-effort + // placement (a request that can't all fit in the margin just places as many as do). + int outlierCount = options.resourceIslands + int(context.bounded("layout", 2)); + std::vector outlierCenters; + std::vector outlierRadii; + for (int oi = 0; oi < outlierCount; ++oi) { + int islandRadius = 5 + context.bounded("layout", 4); + bool placed = false; + for (int attempt = 0; attempt < 60 && !placed; ++attempt) { + double theta = randomAngle(context); + double r = (context.bounded("layout", 1000)) / 1000.0 * halfMapMargin; + int cx = game.map.normalizeX((int)lround(layout.W / 2.0 + r * cos(theta))); + int cy = game.map.normalizeY((int)lround(layout.H / 2.0 + r * sin(theta))); + + const auto shaped = layout.xf.toShape({double(cx), double(cy)}); + const double shapeR = std::hypot(shaped.x, shaped.y); + const double shapeTheta = atan2(shaped.y, shaped.x); + if (shapeR < layout.coast.radiusAt(shapeTheta) + islandRadius + 8.0) + continue; // too close to (or inside) the mainland at this specific point + + bool clear = true; + for (unsigned int oc = 0; oc < outlierCenters.size() && clear; ++oc) { + int minSep = islandRadius + outlierRadii[oc] + 8; + if (game.map.warpDistSquare(cx, cy, outlierCenters[oc].x, + outlierCenters[oc].y) < minSep * minSep) + clear = false; + } + if (!clear) + continue; - outlierCenters.push_back(MapGeneratorPoint(cx, cy)); - outlierRadii.push_back(islandRadius); - placed = true; + int outlierArea = areaNumber++; + createJaggedIsland(game.map, context, grid, outlierArea, cx, cy, + islandRadius, 0.3); + std::vector islandPts; + getAllPoints(game.map, grid, outlierArea, islandPts); + if (islandPts.empty()) + continue; + for (unsigned int p = 0; p < islandPts.size(); ++p) + game.map.setUMatPos(islandPts[p].x, islandPts[p].y, GRASS, 1); + + // Each island leans on one resource theme, so finding one feels like a + // distinct little prize rather than an interchangeable resource dump. + switch (context.bounded("layout", 3)) { + case 0: + placeResourceClump( + game.map, context, + islandPts[context.bounded("resources", islandPts.size())], STONE, + 2); + break; + case 1: + placeResourceClump( + game.map, context, + islandPts[context.bounded("resources", islandPts.size())], + CHERRY + context.bounded("resources", 3), 2); + break; + default: + placeResourceClump( + game.map, context, + islandPts[context.bounded("resources", islandPts.size())], CORN, + 3); + break; } + + outlierCenters.push_back(MapGeneratorPoint(cx, cy)); + outlierRadii.push_back(islandRadius); + placed = true; } } +} - game.map.controlSand(); - - // 5) Anchor each team out at its own tip: start just inland of the coast at - // the team's angle and only back off toward the core if that exact spot turns - // out to be water (a sharp jaggedness dip, or a fjord belly that swung wider - // than expected) -- the same "compute where we want to be, then confirm the - // grid agrees" shape as chooseFreeForBuildingSquares. +// 5) Anchor each team out at its own tip: start just inland of the coast at +// the team's angle and only back off toward the core if that exact spot turns +// out to be water (a sharp jaggedness dip, or a fjord belly that swung wider +// than expected) -- the same "compute where we want to be, then confirm the +// grid agrees" shape as chooseFreeForBuildingSquares. +std::vector anchorTeams(Game &game, const FjordLayout &layout) { std::vector teamPts; - teamPts.reserve(nbTeams); - for (int i = 0; i < nbTeams; ++i) { - double theta = teamTheta[i]; - double maxR = coast.radiusAt(theta); - double r = std::max(coreR + 6.0, maxR - 8.0); - double stepIn = (maxR - (coreR + 2.0)) / 30.0; + teamPts.reserve(layout.nbTeams); + for (int i = 0; i < layout.nbTeams; ++i) { + double theta = layout.teamTheta[i]; + double maxR = layout.coast.radiusAt(theta); + double r = std::max(layout.coreR + 6.0, maxR - 8.0); + double stepIn = (maxR - (layout.coreR + 2.0)) / 30.0; if (stepIn <= 0.0) stepIn = 1.0; int fx = -1, fy = -1; for (int tries = 0; tries < 30; ++tries) { - const auto mapped = xf.toMap({r * cos(theta), r * sin(theta)}); + const auto mapped = layout.xf.toMap({r * cos(theta), r * sin(theta)}); double mx = mapped.x, my = mapped.y; int ix = game.map.normalizeX((int)lround(mx)); int iy = game.map.normalizeY((int)lround(my)); @@ -357,129 +374,139 @@ static bool generate(Game &game, GenerationContext &context) { r -= stepIn; } if (fx < 0) { - const auto mapped = - xf.toMap({(coreR + 6.0) * cos(theta), (coreR + 6.0) * sin(theta)}); + const auto mapped = layout.xf.toMap( + {(layout.coreR + 6.0) * cos(theta), (layout.coreR + 6.0) * sin(theta)}); double mx = mapped.x, my = mapped.y; fx = game.map.normalizeX((int)lround(mx)); fy = game.map.normalizeY((int)lround(my)); } teamPts.push_back(MapGeneratorPoint(fx, fy)); } + return teamPts; +} - // 6) Connectivity: verify, don't assume. The untouched core should make this - // unreachable in practice, but every other generator checks its own - // invariants explicitly instead of trusting the construction, so this does - // too. In lake-connected mode every peninsula is water-isolated from its neighbors by - // design - failing here would just reject every map that mode ever produces - so this check - // only runs in the default, disconnected mode. Each peninsula's own viability is still - // verified locally further down (step 9 fails outright if a team's home area comes up empty). - if (!lakeConnected) { - std::vector visited(W * H, false); - std::vector stack; - stack.push_back(teamPts[0]); - visited[teamPts[0].y * W + teamPts[0].x] = true; - while (!stack.empty()) { - MapGeneratorPoint p = stack.back(); - stack.pop_back(); - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - int nx = game.map.normalizeX(p.x + dx); - int ny = game.map.normalizeY(p.y + dy); - if (!visited[ny * W + nx] && !game.map.isWater(nx, ny)) { - visited[ny * W + nx] = true; - stack.push_back(MapGeneratorPoint(nx, ny)); - } +// 6) Connectivity: verify, don't assume. The untouched core should make this +// unreachable in practice, but every other generator checks its own +// invariants explicitly instead of trusting the construction, so this does +// too. In lake-connected mode every peninsula is water-isolated from its neighbors by +// design - failing here would just reject every map that mode ever produces - so this check +// only runs in the default, disconnected mode. Each peninsula's own viability is still +// verified locally further down (placeStarterKits fails outright if a team's home area comes +// up empty). +bool verifyConnectivity(Game &game, const FjordLayout &layout, + const std::vector &teamPts) { + if (layout.lakeConnected) + return true; + const int W = layout.W, H = layout.H; + std::vector visited(W * H, false); + std::vector stack; + stack.push_back(teamPts[0]); + visited[teamPts[0].y * W + teamPts[0].x] = true; + while (!stack.empty()) { + MapGeneratorPoint p = stack.back(); + stack.pop_back(); + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + int nx = game.map.normalizeX(p.x + dx); + int ny = game.map.normalizeY(p.y + dy); + if (!visited[ny * W + nx] && !game.map.isWater(nx, ny)) { + visited[ny * W + nx] = true; + stack.push_back(MapGeneratorPoint(nx, ny)); } } } - for (int i = 0; i < nbTeams; ++i) - if (!visited[teamPts[i].y * W + teamPts[i].x]) - return false; } + for (int i = 0; i < layout.nbTeams; ++i) + if (!visited[teamPts[i].y * W + teamPts[i].x]) + return false; + return true; +} - // 7) Stone and fruit in the ring around the new central lake -- the reward for pushing to the - // middle of the map instead of staying home. Several stone clumps rather than one, and every - // fruit type instead of a single random pick, so finding this area feels like a genuinely rich - // destination and not a single repeated deposit. - { - int coreArea = areaNumber++; - std::vector corePts; - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - // Corn/stone/fruit all require grass, so the sand ring the lake just grew (when there - // is one) is deliberately excluded here rather than merely non-water - a clump center - // landing on sand could miss every grass tile within its own radius and place nothing. - if (!game.map.isGrass(x, y) || grid[y * W + x] != 0) - continue; - const auto shaped = xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= (coreR + 4.0) * (coreR + 4.0)) { - grid[y * W + x] = coreArea; - corePts.push_back(MapGeneratorPoint(x, y)); - } +// 7) Stone and fruit in the ring around the new central lake -- the reward for pushing to the +// middle of the map instead of staying home. Several stone clumps rather than one, and every +// fruit type instead of a single random pick, so finding this area feels like a genuinely rich +// destination and not a single repeated deposit. +void placeCoreResources(Game &game, GenerationContext &context, const FjordLayout &layout, + std::vector &grid, int &areaNumber) { + int coreArea = areaNumber++; + std::vector corePts; + for (int y = 0; y < layout.H; ++y) { + for (int x = 0; x < layout.W; ++x) { + // Corn/stone/fruit all require grass, so the sand ring the lake just grew (when there + // is one) is deliberately excluded here rather than merely non-water - a clump center + // landing on sand could miss every grass tile within its own radius and place nothing. + if (!game.map.isGrass(x, y) || grid[y * layout.W + x] != 0) + continue; + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= (layout.coreR + 4.0) * (layout.coreR + 4.0)) { + grid[y * layout.W + x] = coreArea; + corePts.push_back(MapGeneratorPoint(x, y)); } } - if (!corePts.empty()) { - for (int stoneClump = 0; stoneClump < 3; ++stoneClump) + } + if (!corePts.empty()) { + for (int stoneClump = 0; stoneClump < 3; ++stoneClump) + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], + STONE, 3); + constexpr int kFruitClumpsPerType = 3; + for (int fruitType = 0; fruitType < 3; ++fruitType) + for (int clump = 0; clump < kFruitClumpsPerType; ++clump) placeResourceClump(game.map, context, corePts[context.bounded("resources", corePts.size())], - STONE, 3); - constexpr int kFruitClumpsPerType = 3; - for (int fruitType = 0; fruitType < 3; ++fruitType) - for (int clump = 0; clump < kFruitClumpsPerType; ++clump) - placeResourceClump(game.map, context, - corePts[context.bounded("resources", corePts.size())], - CHERRY + fruitType, 2); - } + CHERRY + fruitType, 2); + } - // The lake gets the same algae treatment the open sea gets in step 8 below, but candidates - // are drawn from well inside the shoreline (innerLakeR, not lakeR) rather than anywhere in - // the lake - a clump anchored right up against the shore is still entirely valid water, but - // reads as "stuck to one side" rather than "in the lake". The very first clump is placed - // dead center, at the shape transform's own origin - the lake's exact geometric middle - so - // there's always at least one unambiguously centered deposit regardless of how the interior - // sampling below happens to land. - if (hasLake) { - const int centerX = game.map.normalizeX((int)std::lround(W / 2.0)); - const int centerY = game.map.normalizeY((int)std::lround(H / 2.0)); - placeResourceClump(game.map, context, MapGeneratorPoint(centerX, centerY), ALGA, 2); - - const double innerLakeR = std::max(0.0, lakeR - 4.0); - std::vector lakeWater; - for (int y = 0; y < H; ++y) - for (int x = 0; x < W; ++x) { - if (!game.map.isWater(x, y)) - continue; - const auto shaped = xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= innerLakeR * innerLakeR) - lakeWater.push_back(MapGeneratorPoint(x, y)); - } - // The center clump above already guarantees at least one, so this is purely bonus - // coverage for a lake big enough to have real interior room left over - no forced minimum. - if (!lakeWater.empty()) - for (int i = 0; i < int(lakeWater.size()) / 40; ++i) - placeResourceClump( - game.map, context, - lakeWater[context.bounded("resources", lakeWater.size())], ALGA, 2); - } + // The lake gets the same algae treatment the open sea gets in placeOpenSeaAlgae below, but + // candidates are drawn from well inside the shoreline (innerLakeR, not lakeR) rather than + // anywhere in the lake - a clump anchored right up against the shore is still entirely valid + // water, but reads as "stuck to one side" rather than "in the lake". The very first clump is + // placed dead center, at the shape transform's own origin - the lake's exact geometric middle + // - so there's always at least one unambiguously centered deposit regardless of how the + // interior sampling below happens to land. + if (layout.hasLake) { + const int centerX = game.map.normalizeX((int)std::lround(layout.W / 2.0)); + const int centerY = game.map.normalizeY((int)std::lround(layout.H / 2.0)); + placeResourceClump(game.map, context, MapGeneratorPoint(centerX, centerY), ALGA, 2); + + const double innerLakeR = std::max(0.0, layout.lakeR - 4.0); + std::vector lakeWater; + for (int y = 0; y < layout.H; ++y) + for (int x = 0; x < layout.W; ++x) { + if (!game.map.isWater(x, y)) + continue; + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= innerLakeR * innerLakeR) + lakeWater.push_back(MapGeneratorPoint(x, y)); + } + // The center clump above already guarantees at least one, so this is purely bonus + // coverage for a lake big enough to have real interior room left over - no forced minimum. + if (!lakeWater.empty()) + for (int i = 0; i < int(lakeWater.size()) / 40; ++i) + placeResourceClump( + game.map, context, + lakeWater[context.bounded("resources", lakeWater.size())], ALGA, 2); } +} - // 8) Algae out in the open sea: any water tile clearly beyond the coastline - // (not a fjord, not the moat-ish water right against the shore) gets an - // occasional patch. +// 8) Algae out in the open sea: any water tile clearly beyond the coastline +// (not a fjord, not the moat-ish water right against the shore) gets an +// occasional patch. +void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout &layout) { std::vector algaeWater; - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { + for (int y = 0; y < layout.H; ++y) { + for (int x = 0; x < layout.W; ++x) { if (!game.map.isWater(x, y)) continue; - const auto shaped = xf.toShape({double(x), double(y)}); + const auto shaped = layout.xf.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; double theta = atan2(v, u); double r = sqrt(u * u + v * v); - double shoreR = coast.radiusAt(theta); + double shoreR = layout.coast.radiusAt(theta); if (r > shoreR + 2.0 && r < shoreR + 14.0) algaeWater.emplace_back(x, y); } @@ -489,26 +516,29 @@ static bool generate(Game &game, GenerationContext &context) { placeResourceClump( game.map, context, algaeWater[context.bounded("resources", algaeWater.size())], ALGA, 2); +} - // 9) A light per-team starter kit so nobody is stuck waiting to reach the - // fjord banks before they can build anything; the banks and the core are the - // map's real economy. +// 9) A light per-team starter kit so nobody is stuck waiting to reach the +// fjord banks before they can build anything; the banks and the core are the +// map's real economy. +bool placeStarterKits(Game &game, GenerationContext &context, const FjordLayout &layout, + const std::vector &teamPts) { std::vector allWater; - for (int y = 0; y < H; ++y) - for (int x = 0; x < W; ++x) + for (int y = 0; y < layout.H; ++y) + for (int x = 0; x < layout.W; ++x) if (game.map.isWater(x, y)) allWater.push_back(MapGeneratorPoint(x, y)); - for (int i = 0; i < nbTeams; ++i) { + for (int i = 0; i < layout.nbTeams; ++i) { std::vector sources; sources.push_back(teamPts[i]); std::vector distances; computeDistances(game.map, sources, allWater, distances); std::vector homePoints; - for (int y = 0; y < H; ++y) - for (int x = 0; x < W; ++x) - if (distances[y * W + x] >= 1 && distances[y * W + x] <= 10) + for (int y = 0; y < layout.H; ++y) + for (int x = 0; x < layout.W; ++x) + if (distances[y * layout.W + x] >= 1 && distances[y * layout.W + x] <= 10) homePoints.push_back(MapGeneratorPoint(x, y)); if (homePoints.empty()) return false; @@ -525,12 +555,89 @@ static bool generate(Game &game, GenerationContext &context) { for (unsigned int j = 0; j < stonePts.size(); ++j) game.map.setResource(stonePts[j].x, stonePts[j].y, STONE, 1); - std::vector home(size_t(W) * H, 0); + std::vector home(size_t(layout.W) * layout.H, 0); for (const auto &point : homePoints) - home[point.y * W + point.x] = 1; + home[point.y * layout.W + point.x] = 1; if (!placeSettlement(game, context, i, home, teamPts[i], "starts")) return false; } + return true; +} + +// 11) Place bank resources last so settlement and regional deposits cannot +// overwrite the guarantee. Every side of every fjord receives both resources +// at distinct points along its length. +bool placeBankResources(Game &game, GenerationContext &context, const FjordLayout &layout, + const std::vector> &fjordCenterlines) { + if (layout.nbTeams < 2) + return true; + for (int k = 0; k < layout.nbTeams; ++k) { + for (int side : {-1, 1}) + if (!placeBankClump(game.map, context, fjordCenterlines[k], 0.35, side, + CORN) || + !placeBankClump(game.map, context, fjordCenterlines[k], 0.68, side, + WOOD)) + return false; + + // A handful more, lighter clumps at other points along the same banks besides + // the two guaranteed spots above, so walking a fjord's edge feels like following + // a shoreline with its own economy rather than passing exactly two fixed + // deposits. Best-effort: unlike the guarantee above, nothing downstream depends + // on any single one of these existing, so a spot that doesn't pan out is simply + // skipped rather than failing generation. A small jitter on each progress value + // keeps the spacing from reading as mechanically regular. + static const double bankScatterProgress[] = {0.10, 0.22, 0.48, 0.58, 0.80, 0.92}; + for (double progress : bankScatterProgress) { + const double jitter = (context.bounded("resources", 41) - 20) / 1000.0; // +/-0.02 + for (int side : {-1, 1}) { + // corn:wood at 2:1 (4:2 of 8) rather than even (3:3), stone's own 2/8 share + // unchanged - the same rebalance as the ambient scatter above, for consistency. + const int roll = context.bounded("resources", 8); + const int resourceType = roll < 4 ? CORN : roll < 6 ? WOOD : STONE; + placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, + resourceType); + } + } + } + return true; +} + +static bool generate(Game &game, GenerationContext &context) { + context.stage = "continent"; + const FjordContinentOptions options(context.request); + game.map.makeHomogenMap(WATER); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + const int nbTeams = context.request.nbTeams; + if (nbTeams < 1) + return false; + + const FjordLayout layout = computeLayout(game, context, options, nbTeams); + + // grid holds area numbers for resource zoning, kept alongside the real + // terrain (which is written straight onto game.map as we go, the same way + // computeIsles and computeContestedCommons do it). + std::vector grid(layout.W * layout.H, 0); + int areaNumber = 1; + + stampContinent(game, layout); + std::vector> fjordCenterlines = + carveFjords(game, context, layout, options); + carveLake(game, layout); + placeOutlierIslands(game, context, layout, options, grid, areaNumber); + + game.map.controlSand(); + + std::vector teamPts = anchorTeams(game, layout); + if (!verifyConnectivity(game, layout, teamPts)) + return false; + + placeCoreResources(game, context, layout, grid, areaNumber); + placeOpenSeaAlgae(game, context, layout); + + if (!placeStarterKits(game, context, layout, teamPts)) + return false; // 10) Every resource so far is a deliberate, counted placement tied to a specific // purpose: a home starter kit, the core, an outlier island. That leaves the whole @@ -540,49 +647,19 @@ static bool generate(Game &game, GenerationContext &context) { // ordinary, unclaimed deposits. This runs after every swarm and worker is already placed, // not before: isResourceAllowed refuses any tile with a building or unit on it, so a // scatter placed earlier can claim the one remaining tile a colony's swarm footprint - // needed, failing generation outright. Running - // last costs nothing here -- setResource always overwrites, so a scatter clump landing on - // an earlier deposit just loses that one tile to it, the same low-stakes trade every other - // generator's own layering already makes. Algae is left at zero: the shoreline band in - // step 8 already places it with a shape tuned to the coastline, and scattering more over - // open water would just fight that. + // needed, failing generation outright. Running last costs nothing here -- setResource + // always overwrites, so a scatter clump landing on an earlier deposit just loses that one + // tile to it, the same low-stakes trade every other generator's own layering already + // makes. Algae is left at zero: the shoreline band in placeOpenSeaAlgae already places it + // with a shape tuned to the coastline, and scattering more over open water would just + // fight that. // corn:wood at 2:1 rather than even - wood was reading as overrepresented in practice, and // total density is held constant (was 18+18=36) rather than just adding more corn on top. scatterResources(game, context, {/*corn=*/24, /*wood=*/12, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); - // 11) Place bank resources last so settlement and regional deposits cannot - // overwrite the guarantee. Every side of every fjord receives both resources - // at distinct points along its length. context.stage = "fjord bank resources"; - if (nbTeams >= 2) - for (int k = 0; k < nbTeams; ++k) { - for (int side : {-1, 1}) - if (!placeBankClump(game.map, context, fjordCenterlines[k], 0.35, side, - CORN) || - !placeBankClump(game.map, context, fjordCenterlines[k], 0.68, side, - WOOD)) - return false; - - // A handful more, lighter clumps at other points along the same banks besides - // the two guaranteed spots above, so walking a fjord's edge feels like following - // a shoreline with its own economy rather than passing exactly two fixed - // deposits. Best-effort: unlike the guarantee above, nothing downstream depends - // on any single one of these existing, so a spot that doesn't pan out is simply - // skipped rather than failing generation. A small jitter on each progress value - // keeps the spacing from reading as mechanically regular. - static const double bankScatterProgress[] = {0.10, 0.22, 0.48, 0.58, 0.80, 0.92}; - for (double progress : bankScatterProgress) { - const double jitter = (context.bounded("resources", 41) - 20) / 1000.0; // +/-0.02 - for (int side : {-1, 1}) { - // corn:wood at 2:1 (4:2 of 8) rather than even (3:3), stone's own 2/8 share - // unchanged - the same rebalance as the ambient scatter above, for consistency. - const int roll = context.bounded("resources", 8); - const int resourceType = roll < 4 ? CORN : roll < 6 ? WOOD : STONE; - placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, - resourceType); - } - } - } + if (!placeBankResources(game, context, layout, fjordCenterlines)) + return false; // The starter kit guarantees wheat and wood exist somewhere near each boot tile, but the // ambient scatter and bank clumps painted afterward don't know that - a wide enough patch of From 123a95b9da193d0823e4f1408967fbedb93d0167 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 12:01:52 -0400 Subject: [PATCH 035/136] Remove the Lattice map generator Lattice was broken and not worth fixing - removing it rather than continuing to patch it. It never shipped (introduced entirely within this branch, never merged to master), so there's no save/replay/ network compatibility concern in dropping it. - Deleted LatticeGenerator.cpp/.h and its SConscript entry. - Unregistered it from GeneratorRegistry::builtins() and dropped its now-orphaned eLATTICE legacy method id (also never shipped). - Removed its exclusive localization entries (Lattice, Islet size, Home radius) from texts.en.txt/texts.keys.txt; kept Channel width, still used by ConcreteIslandsGenerator. - Updated comments in Resources.cpp, MazeGenerator.cpp and FjordContinentGenerator.cpp that referenced Lattice as an example, pointing them at generators that still exist. - Fixed MapGeneratorDefaultsTest's hardcoded registry-size assertion (13 -> 12 registered generators). Verified: 12/12 remaining generators build and generate cleanly across 10 seeds each (0 failures), the defaults test, and 198/198 cppunit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- data/texts.en.txt | 6 - data/texts.keys.txt | 3 - src/SConscript | 1 - src/map/generator/core/GenerationRequest.h | 1 - src/map/generator/core/GeneratorRegistry.cpp | 4 +- .../generators/FjordContinentGenerator.cpp | 2 +- .../generator/generators/LatticeGenerator.cpp | 117 ------------------ .../generator/generators/LatticeGenerator.h | 11 -- .../generator/generators/MazeGenerator.cpp | 2 +- src/map/generator/shared/Resources.cpp | 23 ++-- test/MapGeneratorDefaultsTest.cpp | 2 +- 11 files changed, 16 insertions(+), 156 deletions(-) delete mode 100644 src/map/generator/generators/LatticeGenerator.cpp delete mode 100644 src/map/generator/generators/LatticeGenerator.h diff --git a/data/texts.en.txt b/data/texts.en.txt index a54346585..00b1f3ca8 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1772,16 +1772,10 @@ Land bridge width Lake size [Contested commons] Contested commons -[Lattice] -Lattice [Maze] Maze [Fjord continent] Fjord continent -[Islet size] -Islet size -[Home radius] -Home radius [Cell size] Cell size [Room size] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index bf4725c4c..d66da65d7 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -884,11 +884,8 @@ [Land bridge width] [Lake size] [Contested commons] -[Lattice] [Maze] [Fjord continent] -[Islet size] -[Home radius] [Cell size] [Room size] [Corridor width] diff --git a/src/SConscript b/src/SConscript index aa69f51c2..da1ad0345 100644 --- a/src/SConscript +++ b/src/SConscript @@ -183,7 +183,6 @@ map/generator/generators/CraterLakesGenerator.cpp map/generator/generators/FjordContinentGenerator.cpp map/generator/generators/IslandsGenerator.cpp map/generator/generators/IslesGenerator.cpp -map/generator/generators/LatticeGenerator.cpp map/generator/generators/RiverGenerator.cpp map/generator/generators/RuggedArchipelagoGenerator.cpp map/generator/generators/ShatteredCoastGenerator.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index b406b9550..d0342b85c 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -27,7 +27,6 @@ struct GenerationRequest { eOLDRANDOM = 7, eOLDISLANDS = 8, eCONTESTEDCOMMONS = 9, - eLATTICE = 10, eMAZE = 11, eFJORDCONTINENT = 12 }; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index e358bad8f..28dddad67 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -6,7 +6,6 @@ #include "FjordContinentGenerator.h" #include "IslandsGenerator.h" #include "IslesGenerator.h" -#include "LatticeGenerator.h" #include "MazeGenerator.h" #include "RiverGenerator.h" #include "RuggedArchipelagoGenerator.h" @@ -76,7 +75,6 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { shatteredCoastDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), mazeDefinition(), latticeDefinition(), - uniformDefinition()}); + riverDefinition(), mazeDefinition(), uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index f51be5e76..3542fa517 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -643,7 +643,7 @@ static bool generate(Game &game, GenerationContext &context) { // purpose: a home starter kit, the core, an outlier island. That leaves the whole // continent interior in between them bare grass, which reads as empty rather than as a // place with its own history the way a noise-painted map does. A light map-wide scatter - // -- the same mechanism Lattice and Maze already rely on for this -- fills that gap with + // -- the same mechanism Maze already relies on for this -- fills that gap with // ordinary, unclaimed deposits. This runs after every swarm and worker is already placed, // not before: isResourceAllowed refuses any tile with a building or unit on it, so a // scatter placed earlier can claim the one remaining tile a colony's swarm footprint diff --git a/src/map/generator/generators/LatticeGenerator.cpp b/src/map/generator/generators/LatticeGenerator.cpp deleted file mode 100644 index a8917ad93..000000000 --- a/src/map/generator/generators/LatticeGenerator.cpp +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#include "LatticeGenerator.h" -#include "Game.h" -#include "GenerationContext.h" -#include "Resources.h" -#include "Settlements.h" -#include -using namespace MapGeneration; -namespace { -constexpr double pi = 3.14159265358979323846; - -bool generate(Game &game, GenerationContext &context) { - context.stage = "lattice terrain"; - const LatticeOptions o(context.request); - const int width = game.map.getW(), height = game.map.getH(); - game.map.makeHomogenMap(WATER); - for (int i = 0; i < context.request.nbTeams; ++i) - game.addTeam(); - - // controlSand consumes one tile from each edge of both terrain bands. Build - // the undermap two tiles wider so the controls describe the playable result. - const int rawIsletSize = o.isletSize + 2; - const int period = rawIsletSize + o.channelWidth + 2; - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - game.map.setUMTerrain( - x, y, - (x % period < rawIsletSize && y % period < rawIsletSize) ? GRASS - : WATER); - - std::vector homes; - const double ringRadius = std::min(width, height) * 0.35; - for (int i = 0; i < context.request.nbTeams; ++i) { - const double angle = 2.0 * pi * i / context.request.nbTeams + 0.4; - const int hx = - game.map.normalizeX(int(width / 2 + ringRadius * std::cos(angle))); - const int hy = - game.map.normalizeY(int(height / 2 + ringRadius * std::sin(angle))); - homes.emplace_back(hx, hy); - for (int dy = -o.homeRadius; dy <= o.homeRadius; ++dy) - for (int dx = -o.homeRadius; dx <= o.homeRadius; ++dx) - if (dx * dx + dy * dy <= o.homeRadius * o.homeRadius) - game.map.setUMTerrain(hx + dx, hy + dy, GRASS); - } - game.map.controlSand(); - game.map.rebuildTerrain(); - - std::vector> teamWheatAreas, teamWoodAreas; - for (int team = 0; team < context.request.nbTeams; ++team) { - std::vector home(size_t(width) * height, 0); - std::vector wheatArea, woodArea; - for (int dy = -o.homeRadius; dy <= o.homeRadius; ++dy) - for (int dx = -o.homeRadius; dx <= o.homeRadius; ++dx) - if (dx * dx + dy * dy <= o.homeRadius * o.homeRadius) { - const int x = game.map.normalizeX(homes[team].x + dx); - const int y = game.map.normalizeY(homes[team].y + dy); - home[y * width + x] = 1; - if (game.map.isGrass(x, y) && dx < -o.homeRadius / 2) - wheatArea.emplace_back(x, y); - if (game.map.isGrass(x, y) && dx > o.homeRadius / 2) - woodArea.emplace_back(x, y); - } - if (!placeSettlement(game, context, team, home, homes[team], "starts")) - return false; - teamWheatAreas.push_back(wheatArea); - teamWoodAreas.push_back(woodArea); - } - context.stage = "resources"; - // Guaranteed per-team wheat/wood claim their (small) reserved area first. scatterResources' - // noise bands skip any tile that already carries a resource, so running it after leaves these - // reservations alone; running it first let a solid band fill a whole sliver area with a - // different resource type before the guarantee got a turn, failing the placement outright. - for (int team = 0; team < context.request.nbTeams; ++team) - if (!placeResourceClumpInArea(game.map, context, teamWheatAreas[team], CORN, - 2) || - !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, - 2)) - return false; - scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); - // The guaranteed clumps above ensure wheat and wood exist somewhere in a team's home area, but - // the ambient scatter painted afterward doesn't know that - a wide enough patch of one crop can - // still wall the other off, or wall the swarm off from either, on a small islet where there's - // little room to route around it. guaranteeStartingResources re-checks reachability through the - // actual resource layout and clears exactly the tiles responsible before topping up whichever - // resource is still out of range - the same backstop RuggedArchipelago and ShatteredCoast - // already rely on for the same class of problem. - guaranteeStartingResources(game, context, 24, 32); - return true; -} -} // namespace - -LatticeOptions::LatticeOptions(const GenerationRequest &r) - : isletSize(r.option("islet-size")), - channelWidth(r.option("channel-width")), - homeRadius(r.option("home-radius")), corn(r.option("wheat")), - wood(r.option("wood")), stone(r.option("stone")), - algae(r.option("algae")), fruit(r.option("fruit")) {} - -GeneratorDefinition latticeDefinition() { - return { - "lattice", - 10, - "Lattice", - 6, - false, - {{"islet-size", "Islet size", 1, 4, 1, 2, ControlGroup::Terrain}, - {"channel-width", "Channel width", 2, 4, 1, 3, ControlGroup::Terrain}, - {"home-radius", "Home radius", 12, 20, 2, 16, ControlGroup::Layout}, - {"wheat", "Wheat", 0, 64, 1, 50, ControlGroup::Resources}, - // Default halved relative to wheat's 50 (was even at 50/50) - wood read as - // overrepresented in practice, and this keeps the same 2:1 ratio as Fjord's rebalance. - {"wood", "Wood", 0, 64, 1, 25, ControlGroup::Resources}, - {"stone", "Stone", 0, 64, 1, 50, ControlGroup::Resources}, - {"algae", "Algae", 0, 64, 1, 50, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources}}, - generate}; -} diff --git a/src/map/generator/generators/LatticeGenerator.h b/src/map/generator/generators/LatticeGenerator.h deleted file mode 100644 index ff88ded7e..000000000 --- a/src/map/generator/generators/LatticeGenerator.h +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#pragma once -#include "GenerationRequest.h" -#include "GeneratorDefinition.h" -struct LatticeOptions -{ - int isletSize, channelWidth, homeRadius; - int corn, wood, stone, algae, fruit; - explicit LatticeOptions(const GenerationRequest &r); -}; -GeneratorDefinition latticeDefinition(); diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 8f0f95f91..1f24b884c 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -187,7 +187,7 @@ bool generate(Game &game, GenerationContext &context) { !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, 2)) return false; - // Unlike Fjord and Lattice, Maze doesn't get a guaranteeStartingResources backstop here: its + // Unlike Fjord, Maze doesn't get a guaranteeStartingResources backstop here: its // structural walls are themselves stone resource tiles placed on grass seams by design (see // the stamp below), and that helper's wall-clearing branch can't tell an intentional maze wall // from an incidental one - it would happily bulldoze a corridor wall to "fix" a team whose diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 82bf0815e..735db252a 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -74,15 +74,16 @@ namespace { // crops a given spot gets - see that function for why. // // A single global threshold across the whole map works for one connected landmass (Fjord, -// Maze), but Lattice's islets are separate landmasses that each carry their own, slightly -// different fertility (or noise) range - a global "take the best tiles first" pass can end up -// spending almost the entire band on whichever one or two islets happen to score highest, -// leaving the rest with none at all. That is exactly the "resources aren't balanced between -// players" complaint this whole scatter is meant to avoid, just at the scale of one islet -// instead of one player. Grouping candidates by connected landmass and running the same -// histogram threshold independently within each group, sized to that group's own share of the -// candidate pool, keeps every landmass' band proportional to how much eligible ground it has - -// on a single connected map this is one group covering everything, identical to before. +// Maze), but a generator whose islands are separate landmasses (Isles, ConcreteIslands) gives +// each one its own, slightly different fertility (or noise) range - a global "take the best +// tiles first" pass can end up spending almost the entire band on whichever one or two islands +// happen to score highest, leaving the rest with none at all. That is exactly the "resources +// aren't balanced between players" complaint this whole scatter is meant to avoid, just at the +// scale of one island instead of one player. Grouping candidates by connected landmass and +// running the same histogram threshold independently within each group, sized to that group's +// own share of the candidate pool, keeps every landmass' band proportional to how much eligible +// ground it has - on a single connected map this is one group covering everything, identical to +// before. std::vector computeLandComponents(const Map &map, int &numComponents) { const int w = map.getW(), h = map.getH(); std::vector component(size_t(w) * h, -1); @@ -224,8 +225,8 @@ void scatterFarmland(Map &map, const Fertility::Field &fertility, HeightMap &spl const int share = int((std::int64_t(totalTarget) * std::int64_t(compCandidates.size())) / totalCandidates); // Widening is proportionally huge on a landmass with little eligible ground to begin with - - // a small Lattice islet can have its whole candidate pool absorbed by kWiden's multiplier - // even though the unwidened share alone would have left most of it free. Capping the region + // a small island can have its whole candidate pool absorbed by kWiden's multiplier even + // though the unwidened share alone would have left most of it free. Capping the region // at 2/3 of the component's own pool guarantees slack to route around it regardless of // component size, instead of only checking against the (unrelated) map-wide total above. const int widenedShare = std::max(1, share) * kWiden; diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index f9182aaf1..c2405530a 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -216,7 +216,7 @@ class MapGeneratorDefaultsTest assert(screen.descriptor.option("test-cell") == 16 && screen.descriptor.option("test-gap") == 5); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 13); + assert(registry.selectionIndex(101) == 12); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); From 560ec8df5159c3ec30ab85f71544c236305516c1 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 12:05:23 -0400 Subject: [PATCH 036/136] Replace the framework changelog with a current-state architecture doc FRAMEWORK_UPGRADES.md had grown into a long chronological changelog (what changed, what was measured, what was tried and discarded) that was increasingly hard to use as a reference for how the framework actually works today. Replaced it with MAP_GENERATOR_FRAMEWORK.md, which describes only the current state: the shared building blocks (Geometry, Topology, Regions, Settlements, Resources), the generator catalog, the resource placement system (scatterResources, scatterFarmland, land components, the reachability backstop), fertility, colony placement/fairness scoring, Fjord Continent's feature set, compatibility notes (legacy ids, revision numbers, control validation), and the verification tools. Every fact carried over was re-checked directly against the current source rather than assumed from the old doc. Updated the two other references to the old filename (README.md, ADDING_A_GENERATOR.md). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/ADDING_A_GENERATOR.md | 2 +- docs/map-generators/FRAMEWORK_UPGRADES.md | 101 --------- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 214 ++++++++++++++++++ docs/map-generators/README.md | 4 +- 4 files changed, 218 insertions(+), 103 deletions(-) delete mode 100644 docs/map-generators/FRAMEWORK_UPGRADES.md create mode 100644 docs/map-generators/MAP_GENERATOR_FRAMEWORK.md diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index 9a2c69b73..3dd50b162 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -50,7 +50,7 @@ Use `shared/Topology` for graph distances, sparse-label adjacency and passable-g Add optional pure `validateRequest` and `validateWorld` callbacks to the definition for relationships between settings and generator-specific invariants. Return an empty string on success or useful diagnostic detail on failure. Request validation is shared by the editor, lobby and service. Final validation runs after the common structural checks. Validate the finished terrain when later carving, resources or buildings can alter connectivity; never silently change the request to satisfy an invariant. -[Framework upgrades for the new concepts](FRAMEWORK_UPGRADES.md) explains the motivating cases and the terrain, placement and resource bugs found during integration. +[Map generator framework](MAP_GENERATOR_FRAMEWORK.md) describes the shared building blocks, the generator catalog, and how resource placement and colony fairness currently work. ## Compatibility and verification diff --git a/docs/map-generators/FRAMEWORK_UPGRADES.md b/docs/map-generators/FRAMEWORK_UPGRADES.md deleted file mode 100644 index d2fdc0319..000000000 --- a/docs/map-generators/FRAMEWORK_UPGRADES.md +++ /dev/null @@ -1,101 +0,0 @@ -# Map generator framework capabilities - -## Generalized capabilities - -| Requirement observed in the prototypes | Framework support | Responsibility retained by a future generator | -|---|---|---| -| Literal cell sizes and odd corridor widths | `GeneratorControl::allowedValues`, domain indexing, centralized normalization/display and catalog export | Named options, domains and tuned defaults | -| Rooms fitting cells, dimensions divisible by a cell size | Pure `validateRequest` callback, consumed by service, lobby and editor | Cross-control and dimension constraints; no silent settings rewrites | -| Jagged islands and stretched/rotated continents | `shared/Geometry`: invertible shape transform, seeded radial shape, conservative radius bound, label-grid rasterization | Coastline recipe, fjord paths, moat geometry and bridge endpoints | -| Joint dispersion of homes and shared objectives | Optional whole-region search in `Regions`, paired weight shuffling, convergence and evaluation budgets | Weight policy and checks on actual separation; best-response is not an equal-distance or global-optimum guarantee | -| Distances through a maze, connected peninsulas, neighboring resource zones | `shared/Topology`: graph distances, grid components with explicit wrapping/neighbor policy, sparse-label adjacency | Graph construction, corridor carving and resource-role assignment | -| Settlements restricted to a home island or room | `shared/Settlements`: whole-footprint mask, nearest legal anchor, exact worker count, per-colony diagnostics | Home mask, anchor choice and when to place starter resources | -| Generator-specific connectivity guarantees | Optional `validateWorld` callback after structural checks | Checking actual finished terrain or movement masks, with useful error detail | - -All helpers are ordinary functions or small value objects. There is no new generator superclass, pipeline dispatcher, terrain repair policy or serialization extension. Geometry and topology operate on dimensions and grids, independently of `Game`. Settlements use `Game` because buildings and units require its mutation APIs. - -## Bugs and safeguards - -- The point splitter previously shuffled points without weights. It now moves both together. Existing callers use uniform weights, so their sampled default outputs are unchanged. -- Whole-region dispersion is opt-in. It fails explicitly after the configured pass limit or 20 million distance evaluations rather than silently treating an unfinished search as convergence. Invalid weight arrays and insufficient region capacity are rejected. A single site uses half the shorter map dimension as its finite spacing budget. -- The prototypes multiply coastline amplitudes by as much as 1.4 but budget space using `radius * (1 + roughness)`. The new radial primitive bounds the amplitude sum and exposes its actual conservative maximum radius. Future moat/outlier placement must include the transform's stretch and both shapes' extents. -- The prototype settlement helper can succeed with fewer workers than requested. The shared helper fails with the requested/available count and the service retains its exact-count structural check. The entire swarm footprint must fit inside the home mask. -- The editor previously placed every Layout control at the same vertical position. Controls now occupy independent rows. Literal discrete values and exponent display use the same metadata in both UIs. -- Ellipse rasterization used 32-bit products that can overflow at supported large dimensions. Intermediate arithmetic is now 64-bit. Resource patch sizes are checked before modulo operations and engine assertions. -- Lattice and the prototype maze placed grass directly beside water. Those undermap combinations render as grass in the terrain lookup, so the apparent water pattern was not gameplay water. Both modules now budget shoreline width before calling `controlSand()` and expose dimensions for the finished terrain. -- Maze uses 20, 24 or 32-tile cells, larger home rooms and compact starter deposits. Closed graph edges carry a continuous one-tile stone seam on grass with sand and water on both sides, while connected edges remain open passages. -- Fjord deposits wheat and wood in compact patches on both banks of every fjord. Bank deposits are placed last and shared clump placement never overwrites another resource type. Single-colony maps skip the vacuous bank requirement. -- Lattice uses larger home clearings and puts wheat and wood on opposite rims. Its tiny repeating islets remain the dominant visual pattern without starving the colonies of construction space. -- Swamp, river, islands and crater lakes paint wheat, wood, stone and algae from map-wide noise bands with no awareness of where any team's tile ended up, and old-random/old-islands scan compass directions from each boot tile independently — both leave how close a colony's primary resources are almost entirely to where the (still resource-blind) starting-position search happened to put it, so one colony can end up with wheat and wood at its door while another has to walk far past playable range for either, or occasionally reach neither at all. `shared/Resources::guaranteeStartingResources` now floods outward from each boot tile through the same walkable-space search used to judge that distance (so anything it places is reachable by construction, not just straight-line close) and tops up any team missing wheat or wood within the range the study tool already scores as viable, leaving already-served teams untouched. Old-random's resource search also hardcoded a fourth compass slot to wheat, so every colony there got a guaranteed second wheat deposit and wood never got one; the fourth slot is now awarded to whichever of the two came out narrower for that colony instead of always the same resource. Measured on 200 fixed seeds per generator (`tools/map_generator_study.py`, `best_wheat_distance`/`best_wood_distance` columns added alongside the existing worst-case ones to size the gap between a map's best- and worst-served colony): old-random's wood gap and worst case both dropped by roughly two-fifths with no change in generation success rate; the other four generators held their success rate exactly and gained smaller, still real reductions in the wheat gap. Old-random's wheat gap grew in the same run — its two resource slots still aren't drawn from a symmetric search, so reinforcement lands more easily on whichever resource already claims the first, unconstrained compass pick. This remains an open follow-up; a joint symmetric assignment for the two slots was prototyped but measured worse in aggregate than the simpler fix above and was not kept. - -A handful of colonies on constrained terrain (river was the clearest case) landed with no reachable wheat or wood at all even after the guarantee above — reachable, tellingly, was the operative problem: a colony's boot tile could sit on a stretch of otherwise perfectly good, well-connected land (confirmed by re-flooding the same tile through terrain alone, ignoring resources) that a *later* resource-painting pass had sealed off, since a ground unit cannot walk onto a tile carrying a resource (`Map::isHardSpaceForGroundUnit`) and the noise bands paint with no awareness of what they might wall in. `guaranteeStartingResources` now detects this directly — comparing the resource-respecting flood against the same flood with resources ignored — and clears exactly the resource tiles forming the wall's face (not a surrounding neighborhood) before re-flooding, so a colony genuinely on a small isolated spot (a real islet on a water-heavy map) is left untouched: there is no larger reachable area on the other side of nothing. This cut river's rate of a colony reaching no wheat or wood at all by more than a third with no change to any generator's success rate or to the other generators' numbers (declogging is a no-op wherever there's no wall to find), leaving a residual few percent where the connecting land lies beyond the guarantee's own search radius. A farthest-point starting-position search was also prototyped for the shared noise-band path to address the gap more fundamentally but measured worse on river and islands (it can spread teams onto separate landmasses of very different quality) and was not kept; that remains the deeper open follow-up. - -- Colony placement on the four height-field generators (swamp, river, islands, crater lakes) ran *before* any resource existed, so it could only ever judge a site by the shape of the grass under it — largest contiguous rectangle first, everyone after it taking what was left. That ordering turned out to be the whole problem rather than a constraint: nothing between the two passes reads a boot position or draws from a random stream, so they simply swap. Measuring the headroom first (`MapGeneratorStudy ... headroom`, which scores every legal site against a single multi-source flood per resource, making a site lookup O(1) rather than its own search) showed **every map sampled, on every generator, already contained a placement where all colonies were equally served** — the maps were never unfair, the placement was discarding fairness that was already there. `shared/StartingPositions::chooseBalancedStarts` now picks sites after the resource pass: it scores each legal site by its *worse* primary resource (a colony beside wood but far from wheat is not a good start), sorts, and takes the narrowest score window still holding enough mutually distant sites — scanning from the low-score end so ties settle in favour of a set that is not merely equal but good. The legacy search stays as a fallback for maps where no set of sites can reach both resources. Across 200 fixed seeds per generator the gap between a map's best- and worst-served colony falls from 9.5–19.0 tiles to 3.5–4.0, the worst-served colony's walk falls from 12–18 tiles to 3.5–4.2 (better than the *luckiest* colony managed before), colonies that could reach no wood or wheat at all go to zero, and every colony clears the study tool's viability bar. It holds at 2, 6, 8 and 12 colonies (gap 2.4–7.4, no generation failures introduced at any count); the gap widens with colony count, as expected when more colonies must be matched on the same map. Old-random and old-islands are untouched by this: they place resources relative to each boot tile, so moving a colony does not bring its resources along — the same headroom measurement puts their achievable gap at 8.0 and 0.9 respectively, and old-islands is already there. -- A colony changes its own surroundings the moment it is built, and scoring sites against the bare map ignored that: `placeStarts()` clears a five by seven box of resources for the swarm, the swarm becomes four by four tiles of obstacle, and the workers spawn on the row above the boot tile rather than on it. Placement was therefore choosing sites for deposits it was about to destroy and paths it was about to block — measurably so, with four sites picked as *exactly* equal (wood 0, wheat 0 apiece) finishing three tiles apart. `chooseBalancedStarts` now shortlists on the cheap bare-map distance, which can only understate the built cost, then re-scores the shortlist by simulating the finished colony: the clearing box excluded, the swarm footprint impassable, the flood starting from the worker row. Scoring what the colony will actually live with drops the gap from 3.5–4.0 tiles to 0.92–1.02 and brings the worst-served colony's walk from 3.5–4.2 tiles down to 2.0–2.3 — better on both counts at once, and level with old-islands, which has been the fair-by-construction benchmark throughout. Buildable room is unchanged from the previous placement work. A first attempt instead *excluded* every site whose deposits sat inside the clearing box; that tightened the gap to 2.8–3.1 but pushed every colony roughly twice as far from its resources (worst walk 3.5 → 6.6 tiles), trading the thing that matters for the thing being measured, and was not kept. Generation costs 63–110ms against 11–14ms before, nearly all of it the shortlist re-scoring — imperceptible for a lobby that generates one map, and worth it: halving the shortlist to 300 sites halves the cost but loses a third of the gain on river (1.02 → 1.66). -- Sampling whole maps and keeping the fairest — the obvious lever, and cheap at 11ms a map — was measured against these distributions rather than assumed. Before any placement work it roughly halved the gap (14.0 → 6.2 at twenty candidates) but could never do better than about 4 tiles, because that was simply the best any of two hundred seeds produced: a technique that selects from a distribution cannot beat the distribution. Measuring the *headroom* instead showed every map already contained a perfectly equal placement, which is what redirected the work from choosing better maps to choosing better sites within a map. With placement fixed, twenty-candidate sampling now shaves 0.93 tiles to about 0.2 — real, but a fraction of a second of walking, while costing twenty generations that each got five to eight times more expensive. Worth keeping as an option; not worth the default. -- **Feel:** the balanced placement is a real change in how these four generators play, not only a fairness fix, and wants a maintainer's judgement rather than a metric's. Colonies now start snug against their wood and wheat instead of in open ground, and buildable room within reach of the worst-served colony drops 14–28% (still 405–642 free building sites against a viability bar of 16, so not a playability problem — but the character of an opening changes). Total resource tiles on the map are unchanged; only where colonies sit moved. The four generators' `revision` is bumped accordingly, so a given seed no longer produces the map it did before. - -- Walking distance to wheat and wood was the only thing placement equalised, and on the four height-field generators it is now down to about two tiles for the worst-served colony — close enough to exhausted that sampling twenty maps and keeping the fairest shaves two tenths of a tile off a gap of one. Distance was always a thin account of a starting position, though: a deposit at the door that runs dry, a colony with no room to build, one boxed between two rivals, and one on ground where wheat never grows back all look identical to it. `shared/StartQuality::scoreStarts` measures six factors per colony on the *finished* map — where the swarm is built, its clearing cleared and the workers standing where they will actually start walking — and folds them into one number per map: the weakest colony's quality gated by how evenly the map shared quality out, `worst * (worst/best)^k`. Every factor is normalised against a fixed reference rather than against the map's own best colony, because the point is to rank candidate maps against each other and a per-map normalisation scores every map alike; the two distances use the viability bars the study tool already scores against, and the other four sit near the ninetieth percentile of what the twelve generators actually produce, measured over 2400 maps. Scoring costs 2.5–5.4ms against 11–61ms to generate, draws from no random stream (map hashes are unchanged), and never rejects a map — it ranks, so generation success rates are untouched. `MapGeneratorStudy ... quality` emits the whole per-colony breakdown. -- Fertility carries the most weight of the six because it is the factor that decides whether a colony's wheat comes back at all, and it is worth recording why it can be trusted to. `Map::growResources` expands a wheat or wood tile when `isWater(x+dwax, y+dway) && !isSand(x-dwax, y-dway)`, where `dwax` and `dway` are each the difference of two uniform draws from `{0..15}`. That difference has PMF `(16-|d|)/256`, so the chance a tile expands is a triangular kernel summed over every water tile whose mirror through the tile is not sand — and `Fertility::Field` (`src/map/FertilityField.{h,cpp}`, ported from `ExactFertilityCache` on the Maxima branch so both callers share one field) evaluates exactly that, in closed form. A length-16 forward box followed by a length-16 backward box is precisely the triangular kernel, so the water term is four linear passes rather than 961 taps per grass tile, and the sand term is one stamp per sand tile; an adaptive rule picks whichever of the two costs less on the map at hand. `FertilityCalculator` is now a wrapper over it, keeping its deposit-reachability gate, `fertilityMaximum` and the `Uint16` that `Tile::fertility` holds. Measured over 20 seeds on each of eight generators it runs about ten times faster than the kernel it replaces (the sand correction, not the convolution, is where the time goes) and correlates 0.89–0.997 with it; shattered-coast, the sandiest, is both the least correlated and the slowest, which is the difference being paid for. The field takes plain masks and no `Map`, so generation can score a candidate's growth potential without a live game. -- Keeping the best of several rolls rather than the first that fits is the maximum of K draws from a distribution, which grows like `sqrt(2 ln K)`: it improves forever and flattens gradually, with no cutoff the data itself picks out. Bootstrapped over 200 scored maps per generator, all twelve trace the same curve to within a few percent — three candidates capture 44% of what fifty give, five 59%, eight 70%, twenty 87% — so the cutoff is a time budget, not a property of the maps. `GenerationService::kSampledCandidates` is five: the lobby generates synchronously on the UI thread behind a 500ms debounce, and at 61ms a candidate on crater-lakes (the slowest generator) five rolls cost 306ms where eight would cost 489ms and spend the whole budget. Anything past that wants generation moved off the UI thread first. The lobby's existing retry loop does the sampling, so a roll that cannot fit every colony and a roll that fits badly are now handled by the same pass; the editor uses `GenerationService::bestSeed`, which rolls candidates into a scratch `Game` and returns the winning seed for `MapEdit` to regenerate — sound because generation is deterministic and the score is a pure function of the finished map, both asserted in `map-generator-defaults-test`. Optimising a composite could have pulled colonies away from their resources to chase fertility or elbow room; measured, it does not. Across the sweep the worst-served colony's walk holds at about two tiles on the four height-field generators and *improves* by roughly four tiles on shattered-coast (16.5 to 13.1 for wheat, 13.4 to 11.3 for wood), while fairness rises from 0.81–0.94 to 0.92–0.97 everywhere. - -- Playtesting the fairness work above surfaced it as slow to feel, not only to measure: `chooseBalancedStarts` called `Map::isHardSpaceForGroundUnit` freshly, through its full multi-array `checkTile` path, at every tile visited by both resource floods and by every one of up to 900 shortlisted sites' own re-simulated flood. But with the args used everywhere in this file (no swimming, no team mask) and no building placed yet at this point in generation — `placeStarts()` runs later — that call is a pure function of terrain and resource state that cannot change across the whole search. Profiling confirmed it and `chooseBalancedStarts` itself as 74–84% of total generation time on the four height-field generators. Computing it once into a flat per-tile byte before the search, and reading that instead everywhere the search previously called the accessor, changes nothing about which sites get chosen — verified by identical per-tile terrain/resource hashes on matching seeds before and after — and measured 28–31% faster end to end across swamp, river, islands and crater lakes (interleaved before/after binaries, 60 generations each, to separate the effect from this machine's own run-to-run noise, which the nine untouched generators' ~0% delta over the same run also confirms). Distance fields and the visited-tile stamp were narrowed from `int` to 16 bits alongside this; every distance this engine can produce fits with room to spare. -- The same pass looked for the equivalent win in `Fertility::Field::rebuild`, whose per-map cost (once per roll, not per candidate — verified via its only call chain, `scoreStarts` → `Fertility::forMap`) dominates Lattice and Maze specifically (57% and 39% of total time) because their thick water corridors and real shorelines carry far more water and sand tiles than a typical height-field map: instrumentation showed roughly 6,100 water/1,260 sand tiles for Lattice and 4,100/1,350 for Maze, against 16,384 total. The adaptive path already picks the cheaper of its two options correctly, but at that density neither is cheap: the sand-correction term costs one 31×31 stamp per sand tile regardless. Narrowing `first`/`second` (the box-blur passes' single-axis intermediates, provably bounded at 256 — the 1D triangular kernel's own weight sum — well inside 16 bits) is exact and free, but measured negligible for Lattice/Maze specifically, because it isn't the term that dominates there. The correction term itself, worked through algebraically, reduces to `sum_d weight(d) * water(target+d) * sand(target-d)` — a genuine bilinear cross-correlation between two different fields, not a single-field sum, so the box-blur trick that makes the water term four linear passes does not apply to it; only an FFT-based convolution would change its asymptotic cost, and `Fertility::Field` is shared with `FertilityCalculator` (real gameplay fertility, not only this scoring path), so any such change needs to preserve that caller's exact output too. Left as an open follow-up rather than attempted here. - -- The same profiling pass found Shattered Coast (old-random) and Rugged Archipelago dominated not by their own terrain logic but by `GenerationContext::stream`: every draw against a named stream looks that name up in a `std::map`, and both generators call it by string literal inside per-tile loops that run tens of thousands of times a generation — `simulateRandomMap` alone (Shattered Coast's iterative water/sand/grass balancer, itself invoked repeatedly per smoothing pass) draws from `"simulation"` up to five times per tile across every candidate map it simulates, and Rugged Archipelago's island-growing and beach passes draw from `"terrain"` the same way. The name never changes within either function, so the lookup was pure overhead: looking the stream up once into a `std::mt19937&` at the top of each function and drawing from that reference thereafter changes no random number either function produces — bit-identical per-tile terrain/resource hashes confirm it — and measured **50% faster** on Shattered Coast and **31% faster** on Rugged Archipelago end to end (same interleaved-binary methodology as above; the two generators that don't touch this code showed ~0% change over the same run). - -- The next profiling pass turned to `shared/Regions.cpp` and `shared/Distances.cpp`, which host the older Voronoi-style point-dispersion machinery Contested Commons, Concrete Islands and Isles still build on. Three fixes there are exact - same output, only how it's computed: `computeDistances`'s BFS used `std::queue` (`std::deque`-backed, growing by separately heap-allocated blocks) even though every cell is enqueued at most once, so a flat, preallocated `w*h`-sized array makes an exact FIFO out of sequential writes instead of block-chasing pointers; `splitUpArea`'s per-region frontier (`squares[p]`) is inserted at a genuinely random position on purpose - that's what gives the flood its organic, non-directional shape, not incidental - but a `std::vector` supports the identical random-position insert/erase (contiguous memmove) without `std::list`'s per-node heap allocation and pointer-chasing; and `adjustHeightmapFromPerlinNoise`/`computeAverageDistance` are pure per-cell transforms and commutative sums respectively, so nesting y outside x costs nothing and walks both the grid and `HeightMap`'s identically row-major `_map` array with their grain instead of across it. -- The same file's `getAllPoints`, `getAllOtherPoints`, `findBorderPoints`, and `splitUpPoints`'s own two internal scans all walked a row-major grid as `for x { for y { ... } }` - a full-row stride on every step - for no reason but habit, except that several of these results get indexed by a random draw (`possible[n]`, `startingPoints[n]`), so simply swapping the loop order would silently choose a different point for the same seed. `collectPointsColumnOrder` gets both: a row-major counting pass sizes each output column, then a second row-major pass drops each point into its precomputed slot, landing every point in the exact x-major, y-minor order the original nested loop produced while never striding across the grid to do it - bit-identical hashes on every generator confirm it. The single-pass "reset the candidate list on every strict improvement" pattern in `splitUpPoints`'s site search reduces to the same set every time regardless of how it's computed - traced by hand and confirmed by the same hashes - so it's now an explicit two-pass max-then-collect using the same helper. -- `splitUpPoints`'s `PointSearch::WholeRegion` mode (Contested Commons only) runs the same kind of scan for real - `for dx in 0..w { for dy in 0..h { ... } }` scoring every legal tile as a candidate placement, the one shape here that a plain loop-order swap could not fix without changing anything: the search keeps only a strict improvement (`score > best`), so whichever tied candidate is reached earliest in scan order wins, and reordering the scan can hand that win to a different tied candidate. Measured directly (7 seeds): 6 of 7 produced byte-identical maps regardless, and the one that changed still converged to a placement of the same quality by the search's own metric - already documented as bounded best-response, not a guaranteed optimum, so a different tied winner is not a different guarantee. Contested Commons' revision moves to 2 for it. `PointSearch::Local` (used far more broadly, including as swamp/river/islands/crater lakes' rare legacy fallback) keeps its exact original scan order untouched - its window is 7x7, too small for traversal order to matter, and touches far more generators than the fix was worth risking. -- Measured (interleaved before/after binaries, repeated 3x at 150 generations each after an initial single-run reading proved noisy enough to overstate the effect by more than 2x): Concrete Islands about 9% faster, Isles about 5-7% faster, Contested Commons about 8-9% faster overall (its own share from the exact fixes alone was too close to this machine's run-to-run noise floor to separate cleanly from the `WholeRegion` fix's own further ~4%). A generator that never calls into any of this code (Fjord Continent) showed about 2% change over the same run, taken as that noise floor rather than a real effect. - -- A follow-up sweep of every remaining `std::queue`/`std::list`/`std::map` in the map generator code (not just the ones profiling had already pointed at) found three more of the same shape as `computeDistances`' earlier fix, all bounded the same way (a `dist[...] < 0` or `visited[...] == stamp` guard means no flood ever enqueues a tile twice, so a flat `w*h`-sized buffer is always enough): `StartingPositions.cpp`'s own `distanceToResource` and `scoreAsBuilt` still used `std::queue` internally even after the `hard[]` caching work above; `Resources.cpp`'s `floodReach` and `terrainOnlyReach` (the resource-wall detector); and `StartQuality.cpp`'s `walkFromWorkers`. `scoreAsBuilt`'s case compounds differently from the rest: it runs its own flood up to 900 times per `chooseBalancedStarts` call, so a fresh `std::queue` (a fresh `std::deque`, freshly allocated) 900 times over was the real cost, not just each flood's own pushes — the fix there reuses one pair of preallocated buffers across all 900 calls, resetting two indices between them instead of reallocating. Checked but left alone: `Topology.cpp`'s own queue runs over Maze's *cell* graph (dozens of nodes, not tiles) where the difference is unmeasurable, and its `std::map` is exercised only by test code, not by any generator. - Measured (interleaved before/after binaries, repeated 3x, with two generators that never call `chooseBalancedStarts` run the same way as a noise check): swamp ~14%, islands ~14%, crater lakes ~18% faster; river's readings were too noisy this round to quote a single number but moved the same direction every time. Contested Commons and Fjord Continent — untouched by this work — showed ~0% and ~2% respectively, the same noise floor as before. - -- **Feel:** playing Fjord Continent after the performance work above surfaced a separate, real complaint that has nothing to do with speed — every resource on it is a deliberate, counted placement (a home starter kit, fjord bank resources, the core, one to four outlier islands), so the whole continent interior between those points is bare grass. Rendered and inspected directly (`MapGeneratorStudy ... dump=`), this reads as empty rather than as a place with its own history the way a noise-painted map does — exactly the "small blobs near bases, no personality" complaint. Lattice and Maze already solve the identical problem with `scatterResources`, a light map-wide clump scatter that runs before any guaranteed placement claims its own spot (so a guarantee can never be buried or bumped); Fjord now calls it too, at a modest density (wood/corn/stone, no algae — the shoreline band already places that with a shape tuned to the coastline, and scattering more over open water would fight it). Rendered side by side, the difference is stark: from a handful of isolated single-tile dots to a continent that reads the way Swamp, River or Maze already did. Verified generation success unaffected at realistic team counts (1, 2, 4, 8 - 0 failures across 20+ seeds each); at 12 teams, already a barely-supported configuration before this change (18/20 seeds failed on "no swarm footprint fits inside the home region" even without any scatter), it now fails all 20 - a real but marginal effect on a configuration that was not reliable to begin with. Revision moves to 2. Contested Commons' resource layout was inspected the same way and left alone: its solid, sharply-bordered zones ("a real wheat field and a real treeline, not a light dusting") are an explicit, already-reasoned design choice recorded in its own code, not the same gap. - -- Playing further surfaced a second, unrelated gap: the in-game fertility overlay (`GameGUIInputMenuClick.cpp`/`GameGUIStep.cpp`) showed nothing at all on a freshly generated map. Traced to the source rather than assumed: `OverlayArea::compute`'s Fertility branch only ever copies whatever is already sitting in `Map::Tile::fertility` — it never computes anything itself. The only two things that ever populate that field are `FertilityCalculator` (the map editor's "compute fertility" checkbox, and `Game_io.cpp`'s load-time migration for save files older than format version 63) and, now, `scoreStarts`. Neither of the first two runs as part of generation, so every map that comes out of the generator — every custom or lobby game — started with an all-zero fertility field regardless of which fertility algorithm computed it; this was not a regression from replacing that algorithm; it dates back to whenever version-63 saves started shipping fertility baked into the file instead of computing it live, and generation was simply never updated to do the same. `scoreStarts` already builds a full `Fertility::Field` for every roll to score colonies against — stamping that same field into the map's own tiles is a byproduct of work already being paid for, not a new cost (measured: no change outside noise on Lattice, Maze or Swamp), and it is the only point in the generation pipeline that already has a finished map to stamp it onto. `scoreStarts` takes `Game&` instead of `const Game&` for this; its only two callers already held a non-const reference. - -- The ambient scatter added above (`scatterResources`, called right after `controlSand()` — before any team's anchor point, home area or swarm was chosen) was verified for generation success at the time, but not thoroughly enough: re-verified later with a larger seed sample at 8 teams and found 3 of 20 seeds now failing "no swarm footprint fits inside the home region" that succeeded before it, confirmed by direct comparison against the pre-scatter binary on the same seeds. `isResourceAllowed` refuses any tile a building or unit already occupies, but at the point the scatter ran, none existed yet — an early scatter clump could claim the one tile a colony's swarm footprint needed, with nothing downstream able to tell. Moving the identical call to run after every team's swarm and workers are already placed (right before the fjord bank resources, which already had to run last for the same reason) fixes it without changing what the scatter does: `setResource` always overwrites, so running later costs nothing but the reverse of the same low-stakes trade already accepted elsewhere in this generator — a scatter clump can now cosmetically overwrite one tile of an already-placed home-kit deposit, exactly as a bank deposit can already overwrite a scatter tile. Re-verified on the original 3 failing seeds (now 0) and on 50 fresh seeds at 1/2/8 teams (0/50, 0/50, 1/50 — the one remaining failure reproduces identically, same stage, same error, same hash, on the pre-scatter binary too, so it predates and is unrelated to this work). -- Also extended the fjord banks from two fixed clumps per side into a scattered strip: six additional lighter, best-effort clumps per side at points along the same bank, mixing corn/wood/stone with a small jitter so the spacing doesn't read as mechanical. Best-effort like the ambient scatter — nothing downstream depends on any one of them, so a spot that doesn't work out is skipped rather than failing generation. Walking a fjord's edge now reads as following a shoreline with its own economy rather than passing exactly two fixed deposits. - -- Playing raised one more real point about `scatterResources` (Fjord, Lattice and Maze's shared ambient layer): `Map::growResources` only regrows a wheat or wood tile near water, but the scatter's candidate search never looked at that - it picked a uniformly random legal tile and used it outright, so a corn or wood clump could as easily land somewhere with no realistic path back to full growth as somewhere genuinely farmable. First attempt required only `Fertility::Field::at(x,y) > 0` (computed ungated, since nothing has been harvested yet to gate against) before accepting a candidate — measured before/after and found it changed *nothing*: on these water-heavy generators, a single distant water tile up to 14 tiles away already contributes a nonzero (if minuscule - as little as 4 out of a 65536 maximum) weight to nearly every legal tile, so ">0" was true almost everywhere already, fertile-preferring or not. Replaced the fixed bar with best-of-100: score every randomly drawn legal candidate by fertility and keep the highest seen, rather than the first past a threshold. A type that doesn't care about fertility (stone, algae, fruit) scores every candidate 0 and keeps its original "first legal tile" behaviour unchanged. Measured directly against a meaningful bar (fertility > 8000, the same "genuinely good farmland" reference `StartQualityScale` uses elsewhere): the fraction of scattered corn/wood clearing that bar rose from roughly 46% to 68% on Fjord, 40% to 88% on Lattice, and 52% to 96% on Maze. Generation success is unaffected (0 failures across 40 seeds each). Fjord's revision moves to 3, Lattice's and Maze's to 2. - -- Even with fertile candidates preferred, the best-of-100 clumps still read as "clumpy" rather than as a map's intrinsic terrain: each clump is an independent random draw, so the result is a scatter of same-size circular blobs with no relationship to each other or to the coastline, unlike `generateHeightField` (Swamp/River/Islands/Crater-Lakes), which paints its resource bands as a histogram threshold over the *same* Perlin noise field that carved the terrain, so wheat/wood/stone follow the same organic contours as the water and grass. `scatterResources` runs after these generators' terrain is already fixed, so it can't share that field, but it can still take the threshold approach. First attempt thresholded an independent Perlin noise field (with the `>0` fertility gate as a floor, same as before): visually this did fix the clumpiness — Fjord and Maze came out as a clean coastal fringe instead of blobs — but measurement showed it quietly undid the fertility fix above, since an unrelated noise field has no reason to prefer high-fertility ground and the `>0` gate is nearly a no-op (fertility > 8000 fraction fell back to roughly 45-53%, essentially the pre-fix numbers, on all three generators). Fixed by thresholding directly on `Fertility::Field` itself for corn/wood instead of independent noise: the field is already a smooth, spatially continuous function that is highest near water, so painting its lowest-value (highest-fertility) share is simultaneously the "coastal band" shape and a genuine, measurable preference for farmable ground - the same field, used two ways instead of two competing mechanisms. Stone and algae keep the independent noise field, since they have no regrowth rule to prefer. Re-measured against the >8000 bar: 76.5%/73.0% Fjord, 97.9%/98.4% Lattice, 97.1%/98.4% Maze corn/wood - better than the best-of-100 numbers, not just recovered. - A single global threshold across the whole map works for one connected landmass (Fjord, Maze) but broke visibly on Lattice: its islets are separate landmasses, each with its own fertility (or noise) range, and a handful of tiles happening to score highest could absorb nearly the entire band, leaving most islets with none at all - the same "resources aren't balanced" complaint this scatter exists to fix, just at the scale of one islet instead of one player. `computeLandComponents` flood-fills the map into connected non-water components once per `scatterResources` call (flat preallocated FIFO, same pattern as `floodReach`/`computeDistances`), and `scatterBand` now runs its histogram threshold independently within each component, sized to that component's own share of the eligible candidate pool. On a single connected map this is one component covering everything, identical to before; on Lattice every islet now gets its own proportional coastal fringe. Verified visually (`dump=` renders) and numerically (0 new failures across 60 seeds x {Lattice, Maze, Fjord}, plus the extended {teams=2,4,8} x 50-seed sweep, plus the full 13-generator x 5-seed sanity sweep, plus 198/198 cppunit). Fjord's revision moves to 4, Lattice's to 3, Maze's to 3. - -- Playing again turned up a subtler problem with the fertility-band scatter: corn and wood were both thresholded on the *same* `Fertility::Field`, one right after the other (corn takes the highest-fertility band, wood takes the next-highest of what's left). That answers "where should farmland go" correctly but also quietly answers "which crop goes where" with the same field, so corn and wood came out as two concentric rings sorted by distance from water instead of sitting side by side the way real farmland does - wheat ended up hidden behind a solid wood ring in practice. Fertility should only ever decide *where* farmland goes, not *which* of the two crops it becomes. Split into two independent questions in `scatterFarmland`: fertility (widened to roughly 3x the requested tile count, per land component, so the region reaches into lower-but-still-farmable ground instead of a razor-thin ring at the very highest values) selects one shared candidate region for corn and wood together, and a second, unrelated noise field then splits that region into the two crops - sorted by that field's own level and sliced into a corn share and a wood share, so which crop lands where varies along the coast instead of the two stacking into concentric bands. Verified visually (`dump=` renders now show alternating corn/wood patches along a noticeably wider coastal band on all three generators) and numerically: 0 new failures across 60 seeds x {Lattice, Maze, Fjord}, the extended {teams=2,4,8} x 50-seed sweep (the same two pre-existing Fjord failures as before, none new), the full 13-generator sanity sweep, and 198/198 cppunit. Fjord's revision moves to 5, Lattice's and Maze's to 4. - -- Also widened Fjord's `fjord-width` control from a 2-6 range (default 3) to 2-10 (default 4), per a direct request for a wider allowed range and a bigger default. Verified the new default against the same failure sweep: clean at teams 1/2/4 (0/50 each), and at the crowded teams=8 setting a real but small increase versus the pre-existing baseline (8/100 vs. 2/100 single-seed failures - still "no swarm footprint fits inside the home region", the same class already present before this change). This is not player-visible in practice: both real entry points into generation (`EditorMainMenu`, `CustomGameScreen`) roll `GenerationService::kSampledCandidates` (5) seeds and keep the best-scoring successful one rather than committing to a single explicit seed, so the chance every one of 5 independent rolls fails at once is negligible (0.08^5). `MapGeneratorStudy`'s CLI, which does take one explicit seed, is the only place this is visible at all. - -- A screenshot from actual play caught something the aggregate fertility numbers couldn't: a colony's wood had grown into one continuous multi-tile-deep wall along the whole visible coastline, with corn nowhere in sight - not a rendering issue, an actual reachability bug, since resource tiles block ground units. Two compounding causes. First, the noise field splitting corn from wood reused the same smoothing factor (24) as the continent-shaping terrain noise, sized for whole-map features; at a player's zoomed-in scale, one crop's patch could easily span the entire visible screen, so "alternates along the coast" was true map-wide but not within any one colony's own reach. Dropped to 6, matching a working-area scale instead of a continent scale. Second, and worse: the fertility-selected region's `kWiden` multiplier was checked against the map-wide candidate total, not the land component actually being painted, so a small Lattice islet's *entire* eligible pool could be absorbed by the widened region, leaving zero slack to route around it - confirmed by re-running the exact regression seeds against the prior commit, where the same tiny islets had both wheat and wood reachable before this session's widening, and lost one or the other after it. Capped the widened region at 2/3 of each component's own candidate pool, so every landmass keeps at least a third of its eligible ground unclaimed regardless of size. - - As a second, independent backstop, Fjord and Lattice now call `guaranteeStartingResources` after all resource placement - the same reachability re-check (walking the actual resource-respecting flood, not just distance) that RuggedArchipelago and ShatteredCoast already rely on for this exact class of problem, clearing exactly the wall tiles responsible for a cramped pocket before topping up whichever resource is still out of range. Maze deliberately does *not* get this call: its maze walls are themselves stone resource tiles by design, and the same wall-clearing logic can't distinguish an intentional corridor wall from an incidental one - it would bulldoze the maze to "fix" a legitimately small starting room. - - Verified with a new check beyond generation success: `MapGeneratorStudy quality` reports each colony's actual wheat/wood distance (-1 when unreachable) via the same flood `scoreStarts` already computes. Across 60 seeds x {Lattice, Maze, Fjord} (720 colonies), unreachable cases dropped from 7 to 3, and the 3 remaining are byte-for-byte identical to the pre-session baseline on the same seeds (`catchmentTiles=4` - a genuinely tiny, isolated Maze room with no larger landmass behind a wall to reveal, which `guaranteeStartingResources` correctly declines to force). Fjord specifically, the generator in the screenshot, went from a visible wall to 0 unreachable colonies across the sweep. Also re-verified the standard suite: 0 failures across the 60-seed x 3-generator sweep, the {teams=2,4,8} x 50-seed sweep (same class of pre-existing fjord-width failures as before, none new), the 13-generator sanity sweep, and 198/198 cppunit. Fjord's revision moves to 6, Lattice's and Maze's to 5. - -- Fjord's untouched core - previously just a plain landmass every fjord stopped short of, carrying one stone clump and one random fruit clump - is now a real destination: a central lake (an `xf.toShape`-space circle carved after the fjords, `lake-size` percent of `coreR`, 0 disabling it), with a sandy no-man's-land ring around it wider than `controlSand`'s own coastal fringe would give it, several stone clumps and one clump of every fruit type in the ring beyond the sand, and algae in the lake itself using the same technique the open sea already gets in the step below it. `coreR` widened from 0.19 to 0.23 of the continent radius to make room for the lake and its resource ring without starving fjord length. - - Whether the fjords actually cut through into the lake is a real topology choice, not just cosmetic: doing so makes every peninsula water-isolated from its neighbors (boats required to reach anyone), where stopping short behind a solid land ring keeps today's mutual land connectivity. Asked directly rather than guessed at; the answer was to keep both available as a `lake-connected` control (default off, the existing land-ring behavior) rather than pick one permanently. When on, each fjord's inner terminus moves from `coreR` to the lake's own edge (`fjordInnerR`), and the global cross-team connectivity check in step 5 only runs when the ring is expected to exist - lake-connected maps are *supposed* to fail that specific check by design, and each peninsula's own viability is still verified independently further down (step 8 fails outright if a team's home area comes up empty), so nothing downstream loses coverage. - - Separately, a genuine bug: outlier resource islands were going unplaced almost every roll, at every map size, not just small ones - a user report of "most of the time, even at 256 or 512." Root cause: the island annulus's inner bound was `coast.maximumRadius()`, the sum of every noise harmonic's amplitude as if all four peaked at the same angle simultaneously - a mathematically safe bound, but one the coastline can never actually reach (different harmonics peak at different angles), so it overshoots substantially. Since both the coastline's reach and the map's margin scale together with map size, that overshoot doesn't shrink at larger sizes, which is exactly why bigger maps didn't help. First tried sampling the coastline's true maximum over 360 angles instead of summing worst cases - a real improvement, but still not enough: measured directly, the margin goes negative on a majority of rolls simply because the coastline's single worst direction (continent-size up to 40%, roughness up to 35%, elongation up to 1.3x) can legitimately approach half the map by itself. The actual fix: each island's own center is independently randomized already, so it never needed to be safe in the coastline's worst direction *everywhere* - only at its own location. Checking each candidate directly against `coast.radiusAt()` at its own transformed position (the same per-point technique step 7's algae placement already uses for its own shoreline check) replaces one pessimistic global annulus with an exact local one. Verified by counting connected landmasses in `dump=` renders: 10/10 seeds now place 2-3 islands at 128, 256 and 512 map sizes, versus roughly 1-in-5 before this fix. - - Full suite re-verified after all three changes together: 0 failures across 60 seeds at the default map size, 30 seeds at 256, and 30 seeds with `lake-connected=1`; the {teams=2,4,8} x 50-seed sweep (teams=8 landed at 2/50, within the existing accepted fjord-width margin, not a new regression); the 13-generator sanity sweep; the defaults test (which also caught a step in the process - `lake-size`'s default of 45 wasn't a valid step-10 grid point from a minimum of 0, and the label lookups for both new controls needed entries added to `data/texts.en.txt`/`data/texts.keys.txt`, the same way every other control label already has one); and 198/198 cppunit. Fjord's revision moves to 7. - -- Three more direct follow-ups on the lake/island work above. `resource-islands`' range widened from 0-4 to 0-20 for players who want an island-heavy map - the control itself was already that flexible, but `outlierCount` was separately hard-capped at `std::min(4, ...)`, silently discarding anything the control asked for beyond 4; removed rather than raised, since the per-candidate placement check is already a safe best-effort loop with no fixed ceiling of its own. The core's fruit went from one clump per type to three (`kFruitClumpsPerType`), so the destination reads as a real grove of each kind rather than a single token tree. - - Separately, a real placement bug in the lake's algae: it was drawn from any water tile in the *whole* lake, shore included, so a clump could anchor right at the water's edge and read as "stuck to one side" rather than centered - exactly the same class of "random point instead of a deliberate one" issue as the earlier per-component work, just at lake scale instead of continent scale. Fixed by always placing one clump at the lake's exact geometric center - the shape transform's own origin, `(round(W/2), round(H/2))`, provably inside the lake for any `lake-size > 0` - and drawing any additional bonus clumps only from `innerLakeR = lakeR - 4`, comfortably clear of the shore rather than the full `lakeR`. Verified directly (not just visually, since the dump tool's tile-type codes don't distinguish algae from plain water): a temporary check confirmed the resource landed exactly on the computed center tile across every seed tested. - - Verified: 0 failures across 60 seeds at default settings, 30 seeds at `resource-islands=20`, 20 seeds at `resource-islands=0`, 30 seeds at `lake-connected=1`, the {teams=2,4,8} x 50-seed sweep (teams=8 at 4/50, the same already-accepted fjord-width margin), the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 8. - -- Last direct feedback from playing: wood was reading as overrepresented against wheat. Rebalanced every ambient/bonus wheat-vs-wood layer to 2:1 corn:wood rather than even, while leaving every *guaranteed* placement (Fjord's per-team starter kit, its two guaranteed bank clumps per side, Lattice/Maze's guaranteed per-team clumps) untouched at 1:1 - those exist for reachability fairness, not feel, and the reachability work earlier in this doc depends on both resources staying equally guaranteed. Concretely: Fjord's ambient `scatterResources` call moved from corn=18/wood=18 to corn=24/wood=12 (same total density, 36, just reapportioned), its bank-scatter roll distribution from an even 3:3:2 (corn:wood:stone of 8) to 4:2:2; Lattice's and Maze's `wood` control default halved from 50 to 25 against wheat's unchanged 50 default (their guaranteed per-team clumps are fixed-count, not density-driven, so only the ambient layer moves). - - Verified the actual achieved ratio directly by counting resource tile codes across `dump=` renders (5 seeds each, since the STUDY line doesn't carry resource-type totals): Lattice and Maze land almost exactly at 2:1 (1.99:1, 1.97:1) since their ambient layer dominates; Fjord lands at 1.76:1, below 2:1 as expected, because its several untouched 1:1 guarantees (starter kit, bank guarantees) dilute the ambient layer's pure 2:1 - still a clear, substantial shift from the prior 1:1 overall. Also re-verified the standard suite: 0 failures across 60 seeds each for Fjord/Lattice/Maze, the {teams=2,4,8} x 50-seed sweep (teams=8 at 4/50, same already-accepted margin), the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 9, Lattice's and Maze's to 6. - -- `lake-connected` had a real bug: a screenshot showed the fjords visibly *not* reaching the lake - "almost connected but not quite," a thin sand gap sitting right where the water should have merged. Two things were wrong, only the second one actually mattered. First guess: the fjord's target tip radius (`fjordInnerR`) landed exactly on the lake's boundary circle, so pushed it to `lakeR * 0.4` (well inside the lake) reasoning that would force real overlap - built, verified by flood-filling the water from the lake outward, and it changed nothing (the lake's connected-component size stayed ~35 tiles, the same as the lake alone, not reaching the map edge). The actual cause, found by re-reading `Map::controlSand` (`MapTerrain.cpp`): it converts *any* water tile with a grass neighbor anywhere in its own 3x3 neighborhood to sand - not a coastal decoration, a channel eraser. The fjord's tip tapers down to `tipWidth = 0.65` (a ~1.3-tile diameter) as it approaches its target radius; every tile in a channel that narrow borders grass on both sides, so `controlSand` sands the entire tip shut regardless of how deep its target radius reaches - fine for disconnected mode, where that's an intentional dead end against solid land, but it silently closed the one connection `lake-connected` mode actually needs open. A channel needs a surviving center row not touching grass on either side to remain water at all, which takes a width (radius) of at least ~1.5; the fix widens `tipWidth` to `max(2.5, mouthWidth * 0.6)` specifically in connected mode, comfortably above that floor. The `fjordInnerR` change from the first attempt stays as a small extra safety margin, though the width fix is what actually matters - the lake is so small relative to the fjord's total length that moving the target radius barely moves the width *at* the point the centerline actually crosses the lake's boundary. - - Verified with the same flood-fill check, now correctly: the lake's connected-component water size jumps from ~35 tiles (the lake alone) to ~11,000 tiles and reaches the map edge on every seed tested, confirming the outer sea and the lake are now genuinely one connected body via every fjord. Re-verified the standard suite: 0 failures across 60 seeds in disconnected mode, 40 seeds at `lake-connected=1`, and clean at `lake-connected=1` with teams 2/4 (0/30 each) - teams=8 rose to 8/30, the same "no swarm footprint fits" class as the existing accepted margin, more pronounced here since connected-mode peninsulas are smaller (each one now water-isolated on every side rather than just its two fjord edges). Also the 13-generator sanity sweep, the defaults test, and 198/198 cppunit. Fjord's revision moves to 10. - -- Cleanup pass over `FjordContinentGenerator::generate()` after the round of feature work above: its step comments (1, 2, 2.5, 3, 4, 5, 6, 7, 8, 8.5, 9) had drifted out of numeric order as new steps were inserted in the middle, and several internal "step N" cross-references pointed at stale numbers. Renumbered sequentially (1-11) and fixed every cross-reference to match. Also trimmed a handful of comments that had accumulated debugging-journey narration - specific numbers tried and rejected, "measured directly" framing - down to just the reasoning a future reader needs. No behavior change: verified with a 30-seed sweep exercising `lake-connected`, `lake-size` and `resource-islands` together, comparing `MapGeneratorStudy`'s STUDY/QUALITY/COLONY output line-for-line before and after - byte-identical aside from wall-clock timing fields. Defaults test and 198/198 cppunit also still pass. Revision unchanged (10), since nothing observable changed. - -The generators still validate their actual construction results. A moat must connect to land at both bridge ends, jagged outlines must leave legal settlement footprints, and the fjord core must keep every player peninsula connected. Difficult small/crowded combinations may fail, but return reproducible stage diagnostics and are discarded by the lifecycle service. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md new file mode 100644 index 000000000..56e03994c --- /dev/null +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -0,0 +1,214 @@ +# Map generator framework + +A generator is a pure function, `bool generate(Game &, GenerationContext &)`, registered +alongside its metadata in a `GeneratorDefinition` (id, stable legacy numeric id, display name, +revision, its `GeneratorControl`s, and optional `validateRequest`/`validateWorld` callbacks). +`GeneratorRegistry::builtins()` holds the fixed list; `GenerationService` drives the actual +lifecycle (validate the request, sample candidate seeds, run `generate`, score the result, +validate the finished world). There is no generator superclass, pipeline dispatcher or terrain +repair policy — a generator is free to build its terrain and resources however it likes, and +leans on the shared modules below only where doing so is actually less work than not. + +## Shared building blocks + +| Concern | Lives in | What it gives a generator | +|---|---|---| +| Discrete/stepped/power-of-two option domains, one shared UI and catalog export | `GeneratorControls` | Declares options once; normalization, display and validation are centralized | +| Cross-control and dimension constraints | `GeneratorDefinition::validateRequest` | A pure check run before generation; no silent settings rewrites | +| Jagged islands, stretched/rotated continents, coastline shape | `shared/Geometry` (`ShapeTransform`, `RadialShape`, `stampShape`) | An invertible shape transform and a seeded radial shape with a per-angle `radiusAt()` query — checking a specific candidate against its own angle is exact, where a single global "furthest possible" bound is only ever a conservative overestimate | +| Distances through corridors, connected components, resource-role adjacency | `shared/Topology`, `shared/Distances` | Graph distances and grid-component labelling with explicit wrapping/neighbor policy | +| Whole-region or local point dispersion (Contested Commons, Concrete Islands, Isles) | `shared/Regions` | Paired weight/point shuffling, a bounded local search and an opt-in whole-region search with an explicit pass/evaluation budget rather than silently treating an unfinished search as converged | +| Settlements restricted to a home island or room | `shared/Settlements` | Whole-footprint mask, nearest legal anchor, exact worker count, per-colony diagnostics | +| Generator-specific connectivity guarantees | `GeneratorDefinition::validateWorld` | Runs after structural checks, against the actual finished terrain/movement mask | + +Geometry, Topology, Distances and Regions operate on plain dimensions and grids, independent of +`Game`. Settlements needs `Game` because placing buildings and units requires its mutation APIs. + +## The generator catalog + +`GeneratorRegistry::builtins()` orders these for the product-facing catalog; the numeric legacy +id is a separate, stable compatibility identifier that never changes once assigned and is never +reused after a generator is retired. + +| id | legacy id | Display name | Family | +|---|---|---|---| +| `fjord-continent` | 12 | Fjord continent | Its own — see below | +| `maze` | 11 | Maze | Its own — a graph of cells and corridors, not a coastline | +| `contested-commons` | 9 | Contested commons | Point dispersion (`shared/Regions`) | +| `shattered-coast` | 7 | Old random | Iterative water/sand/grass balancer, own resource search | +| `isles` | 6 | Isles | Point dispersion; islands linked by land bridges | +| `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | +| `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | +| `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | +| `islands` | 3 | Islands | Height-field noise; organic islands with no inter-island passage | +| `swamp` | 1 | Swamp | Height-field noise; water interleaved with land | +| `river` | 2 | River | Height-field noise; a winding river through connected land | +| `uniform` | 0 | uniform terrain | Editor-only; one terrain type, unstructured | + +Swamp, River, Islands and Crater Lakes ("the height-field generators") shape their terrain and +paint their resource bands from the same Perlin noise field via `generateHeightField`, and use +`chooseBalancedStarts` (below) for colony placement. Contested Commons, Concrete Islands and +Isles build on the older Voronoi-style point-dispersion machinery in `shared/Regions` and +`shared/Distances`; Contested Commons additionally paints solid, sharply-bordered resource zones +as a deliberate design choice rather than a noise scatter. Shattered Coast and Rugged +Archipelago predate the shared resource/placement machinery and still place resources relative +to each boot tile with their own compass-direction search, rather than through +`scatterResources`/`chooseBalancedStarts`. + +## Resource placement + +`shared/Resources.cpp` gives generators a small set of composable primitives rather than one +fixed resource pass: + +- `placeResourceClump` / `placeResourceClumpInArea` grow a resource outward from a point to a + requested tile count — the primitive every guaranteed placement (starter kits, bank deposits) + is built from. +- `computeLandComponents` flood-fills the map into its connected non-water components. A + generator whose landmasses are separate (an island generator) needs this so that a + map-wide "take the best tiles first" pass can't spend an entire resource band on whichever one + or two islands score highest, starving the rest — every subsequent per-component pass below + runs independently within each component, sized to that component's own share of the eligible + candidate pool. On a single connected map this is one component covering everything. +- `scatterFarmland` answers two independent questions separately: *where* farmland goes is a + `Fertility::Field` threshold (see below) over each land component's own candidate pool, widened + a little beyond the raw requested tile count but capped at two-thirds of that component's pool + so every landmass keeps real slack to route around whatever gets placed; *which* of corn or + wood a given spot becomes is a second, independent noise field, sorted and sliced into a corn + share and a wood share so the two alternate along the region instead of forming two concentric + rings sorted by fertility. +- `scatterBand` is the same per-component noise-threshold technique for stone and algae, which + have no growth rule to prefer and so use a plain noise field rather than fertility. +- `scatterResources(game, context, densities)` is the shared ambient layer — ordinary, unclaimed + deposits filling the interior between a generator's deliberate placements — used by Fjord and + Maze. It runs after every colony's swarm, workers and guaranteed clumps already exist: + `isResourceAllowed` refuses any tile already holding a building or unit, so scattering earlier + can claim a tile a swarm footprint needed; scattering last only ever loses a clump's own tile + to whatever was already there, the same low-stakes trade every generator's own resource + layering already makes. +- `guaranteeStartingResources(game, context, wheatRange, woodRange, clearRadius=0)` is a + reachability backstop, not a placement pass: it compares a resource-respecting flood from each + boot tile against the same flood with resources ignored, and where the two disagree — a + colony's own tile is on well-connected land, just walled off from wheat or wood by a resource + tile a ground unit can't stand on — it clears exactly the wall tiles responsible before topping + up whichever resource is still out of range. A colony genuinely isolated on its own small spot + (nothing reachable even through terrain alone) is left untouched, since there's nothing on the + other side of a wall that isn't there. RuggedArchipelago, ShatteredCoast and Fjord call this; + Maze deliberately does not, since its maze walls are themselves stone resource tiles by design + and this backstop's wall-clearing can't distinguish an intentional corridor wall from an + incidental one. + +Every guaranteed placement (starter kits, bank guarantees, per-team clumps) keeps wheat and wood +at a 1:1 ratio, since those exist for reachability fairness between colonies. Ambient/bonus +layers (`scatterResources`, Fjord's bank-scatter roll) instead use 2:1 corn:wood, which is purely +a feel decision independent of the fairness guarantee. + +### Fertility + +`Fertility::Field` (`src/map/FertilityField.{h,cpp}`) is a closed-form evaluation of +`Map::growResources`'s own regrowth rule — a wheat or wood tile expands when a randomly-offset +mirror point is water and its own mirror isn't sand, which integrates to a triangular kernel +over every nearby water tile with a per-sand-tile correction. `FertilityCalculator` (the map +editor's fertility tool, and old-save-format migration) is a thin wrapper over the same field, so +gameplay fertility and generation-time fertility scoring are the same computation. The field +takes plain terrain/sand masks, not a live `Game`, so a generator or the scoring pass below can +evaluate a candidate's growth potential without needing a finished map. + +## Colony placement and fairness + +Two further shared pieces score and choose where colonies actually start, beyond a generator's +own terrain and resources: + +- `shared/StartingPositions::chooseBalancedStarts` (used by the four height-field generators) + scores every legal site by its *worse* primary resource — a colony beside wood but far from + wheat is not a good start — using the finished, as-built state: the swarm's clearing already + cleared, its footprint impassable, the flood starting from where workers actually stand. It + takes the narrowest score window that still holds enough mutually distant sites, falling back + to the older any-legal-site search on maps where no set of sites can reach both resources at + all. +- `shared/StartQuality::scoreStarts` scores the *finished* map's colonies on six factors — + fertility (weighted highest, since it decides whether a colony's wheat ever comes back at all), + wheat/wood distance, buildable room, resource depth, and isolation from rivals — normalized + against fixed reference values (`StartQualityScale`, e.g. `wheatReference = 24`, + `fertilityReference = 8000` on `Fertility::kScale`) so that candidate maps are ranked against a + constant yardstick, not against each other's own best colony. A map's score is + `worst * (worst/best)^fairnessExponent`: the weakest colony's quality, gated by how evenly the + map shared quality out. `scoreStarts` also stamps the same `Fertility::Field` it computes for + scoring into `Map::Tile::fertility`/`Map::fertilityMaximum`, which is otherwise only ever + populated by the editor's fertility tool or old-save migration — without this, every generated + map's in-game fertility overlay would show nothing at all. + +`GenerationService` rolls `kSampledCandidates` (5) seeds from a root seed and keeps the +best-scoring one that generates successfully; `GenerationService::bestSeed` is the same search +used by the map editor's regeneration action. Generation is deterministic and the score is a +pure function of the finished map (asserted in `map-generator-defaults-test`), which is what +makes "roll several, keep the best" and "regenerate the winning seed later" both sound. + +## Fjord continent + +The richest generator, and the one most of this framework's resource work was proven against: + +- **Shape.** A `ShapeTransform`-warped `RadialShape` coastline; an untouched core disc + (`coreR`, currently 0.23x the continent radius) that every fjord stops short of, so it always + stays connected land regardless of coastline roughness. A fjord is carved between every pair of + angularly-neighboring teams as a smooth S-curve centerline from just outside the coast in to + `coreR` (or, in lake-connected mode, well inside the lake — see below). +- **Central lake.** `lake-size` (0–90%, of `coreR`; 0 disables it) carves a lake at the exact + center, ringed by a sandy no-man's-land wider than `Map::controlSand()`'s own coastal fringe. + `lake-connected` (default off) decides whether the fjords actually cut through into the lake — + every peninsula then water-isolated from its neighbors, boats required — or stop short behind a + solid land ring, keeping mutual land connectivity; the latter is verified with an explicit + flood-fill after construction rather than assumed, and only runs in that mode, since a + lake-connected map is *supposed* to fail a same-landmass check by design. A fjord's carved tip + is widened specifically in connected mode, since `controlSand()` erases any water tile with a + grass neighbor in its own 3x3 neighborhood and a narrow tip is entirely coastal by that rule. +- **Core resources.** The ring around the lake carries several stone clumps and a grove of every + fruit type — a genuinely rich destination, not a single token deposit. The lake itself gets a + center-anchored algae clump plus bonus clumps drawn from well inside its shoreline, so a + deposit never reads as merely stuck to one edge. +- **Outlier islands.** `resource-islands` (0–20) places small, unconnected islands out in the + open sea, each themed to one resource. Each candidate is checked directly against the + coastline's `radiusAt()` at its own position rather than against one global worst-case bound, + so an island can land close to a narrow stretch of coast even while the coastline bulges out + far away in some other direction. +- **Fjord banks.** Every fjord guarantees one corn and one wood clump per side, placed last so + nothing else can overwrite the guarantee, plus six lighter best-effort clumps per side mixing + corn/wood/stone along the same bank. +- **Ambient layer and backstop.** `scatterResources` fills the continent interior at the end + (corn:wood 2:1, fruit, stone; algae left to the dedicated shoreline/lake passes above), and + `guaranteeStartingResources` runs last as the reachability backstop described above. + +## Compatibility notes + +- A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort + order — `GeneratorRegistry::builtins()`'s constructor order is purely the product-facing + catalog order. Once assigned, an id is never reused, including after a generator is retired. +- A generator's `revision` changes only when a change to `generate()` changes what a given seed + actually produces; a pure refactor, comment or renumbering change leaves it untouched. Bumping + it is how a reviewer knows a seed's output isn't expected to match a prior build byte-for-byte. +- `GeneratorControl`s are validated by `GeneratorRegistry`'s constructor: every control's default + must land on a valid step from its minimum, and (`allowedValues` aside) `(maximum - minimum)` + must be evenly divisible by `step`. Every control label needs matching entries in + `data/texts.en.txt` (`[Label]` / `Label`) and `data/texts.keys.txt` (`[Label]`), the same as any + other UI string. + +## Verification tools + +- `test/MapGeneratorStudy.cpp` builds to `MapGeneratorStudy`, a CLI harness that invokes the real + production generators directly: `MapGeneratorStudy [key=value...] + [tuning] [headroom] [quality] [dump=path]`. `quality` reports `StartQualityReport`/ + `ColonyQuality` per colony; `dump=` writes a plain-text terrain/resource grid for direct + inspection or scripted flood-fill checks; `--catalog` dumps every registered generator's + controls as JSON. +- `test/MapGeneratorDefaultsTest.cpp` builds to `MapGeneratorDefaultsTest`, asserting the + registry's and every control's contract: discrete domains, shape bounds, topology, home + footprints, exact worker counts, seed repeatability and RNG stream isolation, and the + lobby/editor UI's own control-editing behavior. +- The cppunit suite under `test/` (`scons && ./TestsRunner`) covers the rest of the engine and + must stay green alongside both of the above. + +Generators validate their own construction results rather than trusting the geometry to always +succeed: a moat must connect to land at both bridge ends, jagged outlines must leave legal +settlement footprints, and Fjord's core must keep every player peninsula connected (outside +lake-connected mode, where that's expected not to hold). Difficult small or crowded combinations +can still fail outright, but do so with a reproducible stage diagnostic, and are discarded by +`GenerationService`'s candidate sampling rather than surfaced to a player. diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index d161e9212..db114f328 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -12,4 +12,6 @@ Use `python3 tools/map_generator_study.py --help` for seeded generation studies. Study results and screenshots belong in ignored `artifacts/`, not in this directory. Record investigation findings and validation results in the relevant pull request. -See [Adding a generator](ADDING_A_GENERATOR.md) for the registry and module interfaces. +See [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) for how the shared modules, generator +catalog, resource placement and colony fairness currently work, and +[Adding a generator](ADDING_A_GENERATOR.md) for the registry and module interfaces. From 8acc378379849201add779dc4f51bead91ffc685 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 12:50:35 -0400 Subject: [PATCH 037/136] Rewrite the Maze generator as a real pen-and-paper maze The previous Maze didn't produce what it was designed to: its wall strips never actually received their stone (STONE needs a pure-grass tile, and a 3-wide strip loses its flanks to controlSand, so no tile ever qualifies), the ambient resource scatter painted corn, wood and permanent stone bands across the narrow corridors (the most fertile land on the map), and home selection could put a colony sealed behind scatter stone or one thin wall away from a rival. Rewritten from scratch: - The world is tiled into cells on the map's own torus (boundaries are chosen to tile the map exactly, so the maze wraps across the seam). - A recursive backtracker carves a spanning tree of narrow sand corridors; loopiness knocks through a few extra walls. - Every closed boundary is a wall: a thick water channel with a stone spine covering the whole boundary line corner to corner, stamped on a two-wide grass core inside a sand ring so every spine tile is solid grass and perpendicular walls share their corner tile. - Terrain is stamped directly instead of through controlSand, and corridors are sand, so no crop can grow across one mid-game. - Every colony's home is a true cul-de-sac: homes are chosen on a spaced sublattice, the tree is carved over the other cells, and each home is attached by exactly one corridor. - Resources live only in rooms: homes get identical 1:1 wheat/wood and stone behind a lane that keeps the exit clear; other dead ends become bonus rooms shaped by the wheat/wood/stone/fruit controls; algae is seeded in open channel water. - validateRequest keeps a guaranteed water gap between rooms and wall flanks (so no one can walk along a wall), and a new validateWorld confirms every colony can walk to every other one. Verified on dumps at 128, 256 and 512: all colonies mutually reachable, 0 walkable tiles within 2 of any wall spine, 0 partially-walled boundaries, every home exactly one exit and identical resources. 0 failures across 30 seeds each at 1/2/4 colonies, 8 and 12 colonies at 256, 512x128/128x512, and every control extreme. Start-quality score rose to 0.66-0.68 with fairness 0.94-0.96. Defaults test and 198/198 cppunit pass. Revision 7. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- .../generator/generators/MazeGenerator.cpp | 686 ++++++++++++------ 1 file changed, 472 insertions(+), 214 deletions(-) diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 1f24b884c..e2865efd5 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -4,257 +4,515 @@ #include "GenerationContext.h" #include "Resources.h" #include "Settlements.h" -#include "Topology.h" +#include "Unit.h" #include -#include +#include +#include +#include using namespace MapGeneration; + +// A maze in the pen-and-paper sense. The world is tiled into square cells on the map's own +// torus; a recursive backtracker carves a spanning tree of narrow corridors between cell +// centres; every boundary the tree leaves closed becomes a wall - a thick water channel with a +// continuous line of stone down its middle. Every colony starts in its own cul-de-sac: a room +// whose only way out is a single corridor. +// +// Terrain is stamped directly rather than through Map::controlSand(), whose in-place raster pass +// shifts shorelines unevenly. Each tile's terrain comes from its four undermap corners +// (Map::regenerateMap), so a pure-grass tile needs grass at all four, and grass must never touch +// water; every grass area here is therefore stamped inside a one-tile sand ring. Corridors are +// sand throughout: walkable, but no crop can ever grow across one, so no corridor can silt shut +// during a game. namespace { + +enum Direction { East, South, West, North }; + +// Any tile with a non-water corner is walkable, including a wall's sandy flanks. If those flanks +// came near a room or corridor, a unit could step onto them and walk along the wall around the +// maze. Keeping half a cell's pitch at least this much larger than half a room leaves two +// all-water tiles between the room's shore and the wall's. +constexpr int kWallClearance = 7; + +// Home rooms are guaranteed placements, so their wheat and wood stay 1:1 regardless of the +// wheat/wood controls, which only shape the bonus rooms at the maze's other dead ends. +constexpr int kHomeFarmland = 40; // of 64 + +// The cell lattice. Boundaries are integer tile coordinates chosen so the cells tile the map +// exactly, which lets the maze wrap across the torus seam like any other boundary; when +// cell-size doesn't divide the map, neighbouring cells differ in pitch by at most one tile. +struct MazeGrid { + int width, height, columns, rows; + std::vector xs, ys; + + int cells() const { return columns * rows; } + int column(int cell) const { return cell % columns; } + int row(int cell) const { return cell / columns; } + int centerX(int cell) const { return (xs[column(cell)] + xs[column(cell) + 1]) / 2; } + int centerY(int cell) const { return (ys[row(cell)] + ys[row(cell) + 1]) / 2; } + int minimumPitch() const { return std::min(width / columns, height / rows); } + + int neighbour(int cell, int direction) const { + const int c = column(cell), r = row(cell); + switch (direction) { + case East: + return r * columns + (c + 1) % columns; + case South: + return ((r + 1) % rows) * columns + c; + case West: + return r * columns + (c + columns - 1) % columns; + default: + return ((r + rows - 1) % rows) * columns + c; + } + } + // Each undirected edge is stored once, owned by its west or north cell. + int edgeId(int cell, int direction) const { + switch (direction) { + case East: + return 2 * cell; + case South: + return 2 * cell + 1; + case West: + return 2 * neighbour(cell, West); + default: + return 2 * neighbour(cell, North) + 1; + } + } +}; + +MazeGrid mazeGrid(int width, int height, int cellSize) { + MazeGrid g{width, height, width / cellSize, height / cellSize, {}, {}}; + for (int i = 0; i <= g.columns; ++i) + g.xs.push_back(i * width / std::max(1, g.columns)); + for (int i = 0; i <= g.rows; ++i) + g.ys.push_back(i * height / std::max(1, g.rows)); + return g; +} + std::string validate(const GenerationRequest &r) { const MazeOptions o(r); - if (o.roomSize + 2 > o.cellSize) - return "Room size must not exceed cell size."; - if (o.corridorWidth + 2 > o.cellSize) - return "Corridor width must not exceed cell size."; - const int cells = (1 << r.wDec) / o.cellSize * ((1 << r.hDec) / o.cellSize); - return cells < r.nbTeams ? "The map needs at least one maze room per colony." - : ""; + const MazeGrid g = mazeGrid(1 << r.wDec, 1 << r.hDec, o.cellSize); + if (g.columns < 3 || g.rows < 3) + return "The maze needs at least three cells across and down; use a bigger map or smaller cells."; + if (g.minimumPitch() / 2 - o.roomSize / 2 < kWallClearance) + return "Rooms this large leave no room for water channels between cells; use smaller rooms " + "or bigger cells."; + if (r.nbTeams > (g.columns / 2) * (g.rows / 2)) + return "The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller " + "cells."; + return ""; } -bool generate(Game &game, GenerationContext &context) { - context.stage = "maze layout"; - const MazeOptions o(context.request); - const int width = game.map.getW(), height = game.map.getH(); - const int columns = width / o.cellSize, rows = height / o.cellSize; - const int cellCount = columns * rows; - game.map.makeHomogenMap(WATER); - for (int i = 0; i < context.request.nbTeams; ++i) - game.addTeam(); - - std::vector> edges; - // Shore generation consumes the outer terrain tile. The room and corridor - // controls describe the grass remaining after that shoreline is built. - const int rawRoomSize = o.roomSize + 2; - for (int y = 0; y < rows; ++y) - for (int x = 0; x < columns; ++x) { - const int cell = y * columns + x; - if (x + 1 < columns) - edges.emplace_back(cell, cell + 1); - if (y + 1 < rows) - edges.emplace_back(cell, cell + columns); - } - for (size_t i = edges.size(); i > 1; --i) - std::swap(edges[i - 1], edges[context.bounded("maze", i)]); +// Home candidates sit on every other column and row, so no two homes touch even diagonally. +// That spacing is what keeps the remaining cells connected (every home is ringed by eight +// non-home cells) and gives every home a non-home neighbour on all four sides to open its one +// corridor into. Among those candidates, homes are spread by farthest-point selection on the +// torus. +std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int teams) { + const int offsetX = context.bounded("maze", g.columns); + const int offsetY = context.bounded("maze", g.rows); + std::vector candidates; + for (int j = 0; j < g.rows / 2; ++j) + for (int i = 0; i < g.columns / 2; ++i) + candidates.push_back(((offsetY + 2 * j) % g.rows) * g.columns + + (offsetX + 2 * i) % g.columns); + for (size_t i = candidates.size(); i > 1; --i) + std::swap(candidates[i - 1], candidates[context.bounded("maze", i)]); - std::vector parent(cellCount); - for (int i = 0; i < cellCount; ++i) - parent[i] = i; - auto root = [&](int a) { - int r = a; - while (parent[r] != r) - r = parent[r]; - while (parent[a] != a) { - const int next = parent[a]; - parent[a] = r; - a = next; - } - return r; + auto distance = [&](int a, int b) { + int dx = std::abs(g.column(a) - g.column(b)), dy = std::abs(g.row(a) - g.row(b)); + dx = std::min(dx, g.columns - dx); + dy = std::min(dy, g.rows - dy); + return dx * dx + dy * dy; }; - std::vector connected(edges.size(), 0); - int treeEdges = 0; - for (size_t i = 0; i < edges.size() && treeEdges < cellCount - 1; ++i) { - const int a = root(edges[i].first), b = root(edges[i].second); - if (a != b) { - parent[a] = b; - connected[i] = 1; - ++treeEdges; - } + std::vector homes{candidates[0]}; + std::vector nearest(candidates.size()); + for (size_t i = 0; i < candidates.size(); ++i) + nearest[i] = distance(candidates[i], homes[0]); + while (int(homes.size()) < teams) { + size_t best = 0; + for (size_t i = 1; i < candidates.size(); ++i) + if (nearest[i] > nearest[best]) + best = i; + homes.push_back(candidates[best]); + for (size_t i = 0; i < candidates.size(); ++i) + nearest[i] = std::min(nearest[i], distance(candidates[i], candidates[best])); } - int loops = - std::min(cellCount * o.loopiness / 100, int(edges.size()) - treeEdges); - for (size_t i = 0; i < edges.size() && loops; ++i) - if (!connected[i]) { - connected[i] = 1; - --loops; + return homes; +} + +// Recursive backtracker (iterative, explicit stack) over the non-home cells: long winding +// corridors with comparatively few, deep dead ends - the classic hand-drawn maze texture. +bool carveSpanningTree(const MazeGrid &g, GenerationContext &context, + const std::vector &isHome, + std::vector &open) { + std::vector free; + for (int cell = 0; cell < g.cells(); ++cell) + if (!isHome[cell]) + free.push_back(cell); + if (free.empty()) + return false; + std::vector visited(isHome); + std::vector stack{free[context.bounded("maze", free.size())]}; + visited[stack.back()] = 1; + while (!stack.empty()) { + const int cell = stack.back(); + int choices[4], count = 0; + for (int d = 0; d < 4; ++d) + if (!visited[g.neighbour(cell, d)]) + choices[count++] = d; + if (!count) { + stack.pop_back(); + continue; } + const int d = choices[context.bounded("maze", count)]; + const int next = g.neighbour(cell, d); + open[g.edgeId(cell, d)] = 1; + visited[next] = 1; + stack.push_back(next); + } + return std::all_of(visited.begin(), visited.end(), [](unsigned char v) { return v != 0; }); +} + +// Loopiness knocks through extra walls between non-home cells, giving routes to flank along; +// a home is never touched, so it stays a cul-de-sac. +void addLoops(const MazeGrid &g, GenerationContext &context, + const std::vector &isHome, int loopiness, + std::vector &open) { + std::vector closed; + int freeCells = 0; + for (int cell = 0; cell < g.cells(); ++cell) { + if (isHome[cell]) + continue; + ++freeCells; + for (int d : {East, South}) + if (!open[g.edgeId(cell, d)] && !isHome[g.neighbour(cell, d)]) + closed.push_back(g.edgeId(cell, d)); + } + for (size_t i = closed.size(); i > 1; --i) + std::swap(closed[i - 1], closed[context.bounded("maze", i)]); + const size_t extra = std::min(closed.size(), size_t(freeCells * loopiness / 100)); + for (size_t i = 0; i < extra; ++i) + open[closed[i]] = 1; +} + +void fillUndermap(Map &map, int x0, int y0, int w, int h, TerrainType t) { + for (int dy = 0; dy < h; ++dy) + for (int dx = 0; dx < w; ++dx) + map.setUMTerrain(map.normalizeX(x0 + dx), map.normalizeY(y0 + dy), t); +} + +// A corridor of walkable width 2m+1 centred on the tile row (or column) through both cell +// centres needs sand on the 2m undermap lines from centre-m+1 to centre+m. It is extended past +// each centre by the same amount so corridors meeting at a cell form a square junction. +void carveCorridor(Map &map, const MazeGrid &g, int cell, int direction, int corridorWidth) { + const int x = g.centerX(cell), y = g.centerY(cell), m = corridorWidth / 2; + const int next = g.neighbour(cell, direction); + if (direction == East) { + int farX = g.centerX(next); + if (farX < x) + farX += g.width; + fillUndermap(map, x - m + 1, y - m + 1, farX - x + 2 * m, 2 * m, SAND); + } else { + int farY = g.centerY(next); + if (farY < y) + farY += g.height; + fillUndermap(map, x - m + 1, y - m + 1, 2 * m, farY - y + 2 * m, SAND); + } +} + +// One wall per closed boundary: a stone spine covering every tile of the boundary line from +// corner to corner inclusive, so perpendicular walls share their corner tile and nothing can slip +// between them. The spine sits on a two-wide grass core (so each spine tile is pure grass, which +// STONE requires) inside a sand ring. +struct Wall { + int x, y, dx, dy, length; +}; + +Wall wallFor(const MazeGrid &g, int cell, int direction) { + const int c = g.column(cell), r = g.row(cell); + if (direction == East) + return {g.xs[c + 1], g.ys[r], 0, 1, g.ys[r + 1] - g.ys[r]}; + return {g.xs[c], g.ys[r + 1], 1, 0, g.xs[c + 1] - g.xs[c]}; +} + +struct RoomTile { + int x, y, u, v; // u: towards the room's exit; v: to the exit's right +}; - for (int y = 0; y < rows; ++y) - for (int x = 0; x < columns; ++x) { - const int cx = x * o.cellSize + o.cellSize / 2; - const int cy = y * o.cellSize + o.cellSize / 2; - for (int dy = 0; dy < rawRoomSize; ++dy) - for (int dx = 0; dx < rawRoomSize; ++dx) - game.map.setUMTerrain(cx - rawRoomSize / 2 + dx, - cy - rawRoomSize / 2 + dy, GRASS); +// The free pure-grass tiles of the room at `cell`, in coordinates relative to its single exit. +// Rooms and corridors are centred on a tile and have odd sizes, so the layout is identical +// whichever way the exit faces. +std::vector roomTiles(const Map &map, const MazeGrid &g, int cell, int exit, + int roomSize) { + static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + const int fx = forward[exit][0], fy = forward[exit][1]; + const int rx = -fy, ry = fx; + const int cx = g.centerX(cell), cy = g.centerY(cell), half = roomSize / 2; + std::vector tiles; + for (int oy = -half; oy <= half; ++oy) + for (int ox = -half; ox <= half; ++ox) { + const int x = map.normalizeX(cx + ox), y = map.normalizeY(cy + oy); + if (map.isGrass(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID) + tiles.push_back({x, y, ox * fx + oy * fy, ox * rx + oy * ry}); } - for (size_t i = 0; i < edges.size(); ++i) - if (connected[i]) { - const int a = edges[i].first, b = edges[i].second; - const int x0 = (a % columns) * o.cellSize + o.cellSize / 2; - const int y0 = (a / columns) * o.cellSize + o.cellSize / 2; - const int x1 = (b % columns) * o.cellSize + o.cellSize / 2; - const int y1 = (b / columns) * o.cellSize + o.cellSize / 2; - const int half = o.corridorWidth / 2 + 1; - for (int y = std::min(y0, y1) - half; y <= std::max(y0, y1) + half; ++y) - for (int x = std::min(x0, x1) - half; x <= std::max(x0, x1) + half; ++x) - game.map.setUMTerrain(x, y, GRASS); + return tiles; +} + +// Fills `count` tiles of a region, outermost first, so a deposit grows inward from the room's +// walls and the tiles nearest the lane stay free to harvest from. +void fillRegion(Map &map, std::vector region, int count, int resourceType, + bool acrossLane) { + std::stable_sort(region.begin(), region.end(), [&](const RoomTile &a, const RoomTile &b) { + if (acrossLane) + return std::abs(a.v) != std::abs(b.v) ? std::abs(a.v) > std::abs(b.v) : a.u < b.u; + return a.u != b.u ? a.u < b.u : std::abs(a.v) < std::abs(b.v); + }); + for (int i = 0; i < count && i < int(region.size()); ++i) + map.setResource(region[i].x, region[i].y, resourceType, 1); +} + +// Room layout, seen from inside looking out through the exit: a lane one tile wider on each side +// than the corridor runs from the back wall to the exit and never gets resources, so the exit can +// never be sealed; wheat lines the left wall, wood the right, stone the back wall behind the +// lane. A home also keeps a clear square around its swarm so every worker can walk out. +void furnishRoom(Map &map, GenerationContext &context, const MazeGrid &g, int cell, int exit, + const MazeOptions &o, bool home, bool fruit) { + const int half = o.roomSize / 2, lane = o.corridorWidth / 2 + 1; + std::vector left, right, back, fruitRow; + for (const RoomTile &t : roomTiles(map, g, cell, exit, o.roomSize)) { + if (home && std::abs(t.u) <= 4 && std::abs(t.v) <= 4) + continue; + if (t.v < -lane) + left.push_back(t); + else if (t.v > lane) + right.push_back(t); + else if (t.u <= -half + 1) + back.push_back(t); + else if (fruit && t.u == -half + 2 && std::abs(t.v) <= 1) + fruitRow.push_back(t); + } + if (home) { + const int farmland = int(std::min(left.size(), right.size())) * kHomeFarmland / 64; + fillRegion(map, left, farmland, CORN, true); + fillRegion(map, right, farmland, WOOD, true); + } else { + fillRegion(map, left, int(left.size()) * o.corn / 64, CORN, true); + fillRegion(map, right, int(right.size()) * o.wood / 64, WOOD, true); + } + fillRegion(map, back, int(back.size()) * o.stone / 64, STONE, false); + if (!fruitRow.empty()) + fillRegion(map, fruitRow, int(fruitRow.size()), CHERRY + context.bounded("resources", 3), + false); +} + +// Algae belongs in the channels, not the corridors: clumps are seeded only in open water at +// least two tiles from any land, where they read as part of the moat and are still reachable +// from the nearest shore. +void seedAlgae(Map &map, GenerationContext &context, int algae) { + const int w = map.getW(), h = map.getH(); + std::vector openWater; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + bool clear = true; + for (int dy = -2; dy <= 2 && clear; ++dy) + for (int dx = -2; dx <= 2 && clear; ++dx) + clear = map.isWater(map.normalizeX(x + dx), map.normalizeY(y + dy)); + if (clear) + openWater.emplace_back(x, y); } + if (openWater.empty()) + return; + int remaining = int(openWater.size()) * algae / 1600; + for (int attempt = 0; remaining > 0 && attempt < 4 * int(openWater.size()) / 25 + 16; + ++attempt) + remaining -= placeResourceClump(map, context, + openWater[context.bounded("resources", openWater.size())], + ALGA, 1); +} - // Closed cell edges are real maze walls: a grass seam carrying stone, with - // shoreline and water on both sides. Stone blocks ground units as well as - // swimmers; open tree/loop edges remain gaps through the wall grid. - std::vector stoneWall(size_t(width) * height, 0); - for (size_t i = 0; i < edges.size(); ++i) - if (!connected[i]) { - const int a = edges[i].first, b = edges[i].second; - const int ax = (a % columns) * o.cellSize + o.cellSize / 2; - const int ay = (a / columns) * o.cellSize + o.cellSize / 2; - const int bx = (b % columns) * o.cellSize + o.cellSize / 2; - const int by = (b / columns) * o.cellSize + o.cellSize / 2; - if (ay == by) { - const int wallX = (ax + bx) / 2; - for (int y = ay - o.cellSize / 2; y <= ay + o.cellSize / 2; ++y) { - for (int dx = -1; dx <= 1; ++dx) - game.map.setUMTerrain(wallX + dx, y, GRASS); - stoneWall[game.map.normalizeY(y) * width + - game.map.normalizeX(wallX)] = 1; - } - } else { - const int wallY = (ay + by) / 2; - for (int x = ax - o.cellSize / 2; x <= ax + o.cellSize / 2; ++x) { - for (int dy = -1; dy <= 1; ++dy) - game.map.setUMTerrain(x, wallY + dy, GRASS); - stoneWall[game.map.normalizeY(wallY) * width + - game.map.normalizeX(x)] = 1; - } +bool generate(Game &game, GenerationContext &context) { + context.stage = "maze layout"; + const MazeOptions o(context.request); + Map &map = game.map; + const MazeGrid g = mazeGrid(map.getW(), map.getH(), o.cellSize); + const int teams = context.request.nbTeams; + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + if (g.columns < 3 || g.rows < 3 || teams > (g.columns / 2) * (g.rows / 2)) { + context.detail = "the maze grid has too few cul-de-sacs for every colony"; + return false; + } + + const std::vector homes = chooseHomes(g, context, teams); + std::vector isHome(g.cells(), 0); + for (int home : homes) + isHome[home] = 1; + std::vector open(2 * g.cells(), 0); + if (!carveSpanningTree(g, context, isHome, open)) { + context.detail = "the non-home cells did not form one connected maze"; + return false; + } + std::vector exitOf(g.cells(), -1); + for (int home : homes) { + const int d = context.bounded("maze", 4); + open[g.edgeId(home, d)] = 1; + exitOf[home] = d; + } + addLoops(g, context, isHome, o.loopiness, open); + std::vector bonusRooms; + for (int cell = 0; cell < g.cells(); ++cell) { + if (isHome[cell]) + continue; + int degree = 0, exit = -1; + for (int d = 0; d < 4; ++d) + if (open[g.edgeId(cell, d)]) { + ++degree; + exit = d; } + if (degree == 1) { + bonusRooms.push_back(cell); + exitOf[cell] = exit; } - game.map.controlSand(); - game.map.rebuildTerrain(); + } - RegionGraph graph(cellCount); - for (size_t i = 0; i < edges.size(); ++i) - if (connected[i]) { - graph[edges[i].first].push_back(edges[i].second); - graph[edges[i].second].push_back(edges[i].first); + // All sand before any grass, so a grass core is never overwritten by a neighbouring ring. + context.stage = "maze terrain"; + const int half = o.roomSize / 2; + std::vector walls; + for (int cell = 0; cell < g.cells(); ++cell) + for (int d : {East, South}) { + if (open[g.edgeId(cell, d)]) + carveCorridor(map, g, cell, d, o.corridorWidth); + else + walls.push_back(wallFor(g, cell, d)); } - std::vector homes{0}; - std::vector chosen(cellCount, 0); - chosen[0] = 1; - for (int team = 1; team < context.request.nbTeams; ++team) { - const auto distances = graphDistances(graph, homes); - int best = -1; - for (int cell = 0; cell < cellCount; ++cell) - if (!chosen[cell] && (best < 0 || distances[cell] > distances[best])) - best = cell; - if (best < 0) + for (int cell = 0; cell < g.cells(); ++cell) + if (exitOf[cell] >= 0) + fillUndermap(map, g.centerX(cell) - half - 1, g.centerY(cell) - half - 1, 2 * half + 4, + 2 * half + 4, SAND); + for (const Wall &w : walls) + fillUndermap(map, w.x - 1, w.y - 1, w.dx ? w.length + 4 : 4, w.dy ? w.length + 4 : 4, SAND); + for (int cell = 0; cell < g.cells(); ++cell) + if (exitOf[cell] >= 0) + fillUndermap(map, g.centerX(cell) - half, g.centerY(cell) - half, 2 * half + 2, + 2 * half + 2, GRASS); + for (const Wall &w : walls) + fillUndermap(map, w.x, w.y, w.dx ? w.length + 2 : 2, w.dy ? w.length + 2 : 2, GRASS); + map.rebuildTerrain(); + for (const Wall &w : walls) + for (int i = 0; i <= w.length; ++i) { + const int x = map.normalizeX(w.x + i * w.dx), y = map.normalizeY(w.y + i * w.dy); + if (map.getTerrainType(x, y) != GRASS) { + context.detail = "a wall spine tile is not solid grass"; + return false; + } + map.setResource(x, y, STONE, 1); + } + + for (int team = 0; team < teams; ++team) { + const int cx = g.centerX(homes[team]), cy = g.centerY(homes[team]); + std::vector home(size_t(map.getW()) * map.getH(), 0); + for (int oy = -half - 2; oy <= half + 2; ++oy) + for (int ox = -half - 2; ox <= half + 2; ++ox) { + const int x = map.normalizeX(cx + ox), y = map.normalizeY(cy + oy); + if (!map.isWater(x, y)) + home[size_t(y) * map.getW() + x] = 1; + } + // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. + if (!placeSettlement(game, context, team, home, {cx - 2, cy - 2}, "starts")) return false; - chosen[best] = 1; - homes.push_back(best); } - std::vector> teamWheatAreas, teamWoodAreas; - for (int team = 0; team < context.request.nbTeams; ++team) { - const int cell = homes[team]; - const int cx = (cell % columns) * o.cellSize + o.cellSize / 2; - const int cy = (cell / columns) * o.cellSize + o.cellSize / 2; - std::vector home(size_t(width) * height, 0); - std::vector wheatArea, woodArea; - for (int dy = -1; dy < rawRoomSize + 1; ++dy) - for (int dx = -1; dx < rawRoomSize + 1; ++dx) { - const int x = game.map.normalizeX(cx - rawRoomSize / 2 + dx); - const int y = game.map.normalizeY(cy - rawRoomSize / 2 + dy); - if (!game.map.isWater(x, y)) { - home[y * width + x] = 1; - if (game.map.isGrass(x, y) && dx < rawRoomSize / 3) - wheatArea.emplace_back(x, y); - if (game.map.isGrass(x, y) && dx > rawRoomSize * 2 / 3) - woodArea.emplace_back(x, y); + context.stage = "maze resources"; + for (int home : homes) + furnishRoom(map, context, g, home, exitOf[home], o, true, false); + std::vector hasFruit(g.cells(), 0); + for (int i = 0; i < o.fruit && i < int(bonusRooms.size()); ++i) { + const size_t pick = i + context.bounded("resources", bonusRooms.size() - i); + std::swap(bonusRooms[i], bonusRooms[pick]); + hasFruit[bonusRooms[i]] = 1; + } + for (int cell : bonusRooms) + furnishRoom(map, context, g, cell, exitOf[cell], o, false, hasFruit[cell]); + seedAlgae(map, context, o.algae); + return true; +} + +// The maze's defining guarantee, checked on the finished world rather than trusted: every +// colony can walk to every other one. Water, buildings and every resource (including the wall +// spines) block the flood; units don't, since they move. +std::string validateWorld(const Game &game, const GenerationContext &context) { + const Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + std::vector reached(size_t(w) * h, 0); + std::vector queue; + queue.reserve(size_t(w) * h); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) { + reached[size_t(y) * w + x] = 1; + queue.push_back(y * w + x); + } + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const size_t n = size_t(ny) * w + nx; + if (!reached[n] && !map.isWater(nx, ny) && !map.isResource(nx, ny) && + map.getBuilding(nx, ny) == NOGBID) { + reached[n] = 1; + queue.push_back(int(n)); } } - if (!placeSettlement(game, context, team, home, {cx, cy}, "starts")) - return false; - teamWheatAreas.push_back(wheatArea); - teamWoodAreas.push_back(woodArea); } - context.stage = "resources"; - // Guaranteed per-team wheat/wood claim their (small) reserved area first. scatterResources' - // noise bands skip any tile that already carries a resource, so running it after leaves these - // reservations alone; running it first let a solid band fill a whole sliver area with a - // different resource type before the guarantee got a turn, failing the placement outright. - for (int team = 0; team < context.request.nbTeams; ++team) - if (!placeResourceClumpInArea(game.map, context, teamWheatAreas[team], CORN, - 2) || - !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, - 2)) - return false; - // Unlike Fjord, Maze doesn't get a guaranteeStartingResources backstop here: its - // structural walls are themselves stone resource tiles placed on grass seams by design (see - // the stamp below), and that helper's wall-clearing branch can't tell an intentional maze wall - // from an incidental one - it would happily bulldoze a corridor wall to "fix" a team whose - // small starting room is legitimately cramped, undermining the maze itself. The noise-scale - // fix above already shrinks the ambient scatter's patches enough that this is a smaller risk - // here than it was in Fjord's open banks. - scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - if (stoneWall[y * width + x]) - game.map.setResource(x, y, STONE, 1); - return true; + std::vector teamReached(teams, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && reached[size_t(y) * w + x] && Unit::GIDtoTeam(gid) < teams) + teamReached[Unit::GIDtoTeam(gid)] = 1; + } + for (int team = 0; team < teams; ++team) + if (!teamReached[team]) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 through the maze."; + return ""; } } // namespace MazeOptions::MazeOptions(const GenerationRequest &r) : cellSize(r.option("cell-size")), roomSize(r.option("room-size")), - corridorWidth(r.option("corridor-width")), - loopiness(r.option("loopiness")), corn(r.option("wheat")), - wood(r.option("wood")), stone(r.option("stone")), + corridorWidth(r.option("corridor-width")), loopiness(r.option("loopiness")), + corn(r.option("wheat")), wood(r.option("wood")), stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")) {} GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 6, + 7, false, - {{"cell-size", - "Cell size", - 20, - 32, - 1, - 24, - ControlGroup::Layout, - false, - false, - {20, 24, 32}}, - {"room-size", "Room size", 12, 16, 1, 16, ControlGroup::Layout}, - {"corridor-width", - "Corridor width", - 1, - 5, - 1, - 3, - ControlGroup::Layout, - false, - false, - {1, 3, 5}}, - {"loopiness", - "Loopiness", - 0, - 100, - 1, - 25, - ControlGroup::Layout, - false, - false, - {0, 10, 25, 50, 75, 100}}, - {"wheat", "Wheat", 0, 64, 1, 50, ControlGroup::Resources}, - // Default halved relative to wheat's 50 (was even at 50/50) - wood read as - // overrepresented in practice, and this keeps the same 2:1 ratio as Fjord's rebalance. - {"wood", "Wood", 0, 64, 1, 25, ControlGroup::Resources}, - {"stone", "Stone", 0, 64, 1, 50, ControlGroup::Resources}, - {"algae", "Algae", 0, 64, 1, 50, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources}}, + {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, + {24, 32, 40, 48}}, + // Room and corridor widths are walkable tiles and always odd, so both centre on a tile. + {"room-size", "Room size", 9, 17, 2, 15, ControlGroup::Layout}, + {"corridor-width", "Corridor width", 3, 7, 1, 5, ControlGroup::Layout, false, false, + {3, 5, 7}}, + {"loopiness", "Loopiness", 0, 50, 1, 10, ControlGroup::Layout, false, false, + {0, 5, 10, 20, 35, 50}}, + // Wheat and wood shape only the bonus rooms (homes are fixed at 1:1); fruit is a + // count of fruit patches, one per bonus room at most. + {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, + {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, + {"stone", "Stone", 0, 64, 1, 32, ControlGroup::Resources}, + {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, + {"fruit", "Fruit", 0, 16, 1, 4, ControlGroup::Resources}}, generate, true, - validate}; + validate, + validateWorld}; } From 894bfeca5acb5cab2d1347632f0dbde03be871c5 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 12:52:45 -0400 Subject: [PATCH 038/136] Document the rewritten Maze and drop stale scatter references Adds a Maze section to MAP_GENERATOR_FRAMEWORK.md describing the grid, cul-de-sac homes, stone-spined water walls, sand corridors, room-only resources and the validateWorld walkability check. Maze no longer uses scatterResources, so the doc's resource-placement notes and two source comments (Resources.cpp, FjordContinentGenerator.cpp) that cited it as a scatter user are updated. Comment and doc changes only. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 52 ++++++++++++++++--- .../generators/FjordContinentGenerator.cpp | 2 +- src/map/generator/shared/Resources.cpp | 4 +- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 56e03994c..1e0e2765b 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -33,7 +33,7 @@ reused after a generator is retired. | id | legacy id | Display name | Family | |---|---|---|---| | `fjord-continent` | 12 | Fjord continent | Its own — see below | -| `maze` | 11 | Maze | Its own — a graph of cells and corridors, not a coastline | +| `maze` | 11 | Maze | Its own — see below | | `contested-commons` | 9 | Contested commons | Point dispersion (`shared/Regions`) | | `shattered-coast` | 7 | Old random | Iterative water/sand/grass balancer, own resource search | | `isles` | 6 | Isles | Point dispersion; islands linked by land bridges | @@ -79,8 +79,8 @@ fixed resource pass: - `scatterBand` is the same per-component noise-threshold technique for stone and algae, which have no growth rule to prefer and so use a plain noise field rather than fertility. - `scatterResources(game, context, densities)` is the shared ambient layer — ordinary, unclaimed - deposits filling the interior between a generator's deliberate placements — used by Fjord and - Maze. It runs after every colony's swarm, workers and guaranteed clumps already exist: + deposits filling the interior between a generator's deliberate placements — used by Fjord. + It runs after every colony's swarm, workers and guaranteed clumps already exist: `isResourceAllowed` refuses any tile already holding a building or unit, so scattering earlier can claim a tile a swarm footprint needed; scattering last only ever loses a clump's own tile to whatever was already there, the same low-stakes trade every generator's own resource @@ -92,14 +92,13 @@ fixed resource pass: tile a ground unit can't stand on — it clears exactly the wall tiles responsible before topping up whichever resource is still out of range. A colony genuinely isolated on its own small spot (nothing reachable even through terrain alone) is left untouched, since there's nothing on the - other side of a wall that isn't there. RuggedArchipelago, ShatteredCoast and Fjord call this; - Maze deliberately does not, since its maze walls are themselves stone resource tiles by design - and this backstop's wall-clearing can't distinguish an intentional corridor wall from an - incidental one. + other side of a wall that isn't there. RuggedArchipelago, ShatteredCoast and Fjord call this. + Maze doesn't need it: its resources are placed only inside rooms, behind a lane that keeps + every exit clear, and its `validateWorld` confirms every colony can still walk to every other. Every guaranteed placement (starter kits, bank guarantees, per-team clumps) keeps wheat and wood at a 1:1 ratio, since those exist for reachability fairness between colonies. Ambient/bonus -layers (`scatterResources`, Fjord's bank-scatter roll) instead use 2:1 corn:wood, which is purely +layers (`scatterResources`, Fjord's bank-scatter roll, Maze's bonus-room defaults) instead use 2:1 corn:wood, which is purely a feel decision independent of the fairness guarantee. ### Fertility @@ -177,6 +176,43 @@ The richest generator, and the one most of this framework's resource work was pr (corn:wood 2:1, fruit, stone; algae left to the dedicated shoreline/lake passes above), and `guaranteeStartingResources` runs last as the reachability backstop described above. +## Maze + +A maze in the pen-and-paper sense: narrow corridors between thick walls, with every colony in +its own cul-de-sac. + +- **Grid.** The map is tiled into cells of about `cell-size` tiles on the map's own torus. + Boundaries are chosen so the cells tile the map exactly (neighbouring cells differ in pitch by + at most one tile), so the maze wraps across the seam like any other boundary. At least three + cells are needed in each direction. +- **Maze and homes.** Homes are chosen first, on every other column and row and spread by + farthest-point selection. That spacing keeps all other cells connected and gives each home four + non-home neighbours. A recursive backtracker then carves a spanning tree of corridors over the + non-home cells, and each home is attached by exactly one corridor, so every colony starts in a + genuine dead end. `loopiness` knocks through extra walls between non-home cells only, so homes + stay cul-de-sacs. +- **Walls.** Every closed boundary is a thick water channel with a stone spine covering the whole + boundary line from corner to corner. Perpendicular walls share their corner tile, so a boundary + can only be crossed — on foot or swimming — where it's open. STONE only places on a pure-grass + tile, which needs grass at all four undermap corners (`Map::regenerateMap`), so each spine sits + on a two-wide grass core inside a sand ring. Terrain is stamped directly rather than through + `Map::controlSand()`, whose in-place raster pass shifts shorelines unevenly. +- **No walking along a wall.** A wall's sandy flanks are walkable land. `validateRequest` + requires `floor(pitch / 2) - roomSize / 2 >= 7`, which leaves at least two all-water tiles + between any room or corridor shore and any wall flank. +- **Corridors are sand**: walkable, but no crop can grow across one mid-game and nothing can be + built on one, so buildings stay in rooms. +- **Rooms and resources.** Rooms exist only at dead ends: colony homes, plus bonus rooms at every + other dead end. Room and corridor widths are odd and centred on a tile, so a room's layout is + identical whichever way its exit faces. Each room keeps a lane from its back wall to its exit + free of resources; wheat lines the left wall, wood the right, stone the back. Homes also keep a + clear square around the swarm and get fixed 1:1 wheat and wood. Bonus rooms follow the + wheat/wood/stone controls, and `fruit` is a count of fruit patches placed in bonus rooms. Algae + is seeded in open channel water at least two tiles from land. +- **Checked, not assumed.** `validateWorld` floods walkable tiles (water, buildings and every + resource, including wall spines, block it) from colony 0's workers, and fails the candidate if + any colony isn't reached. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 3542fa517..47dc21d39 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -643,7 +643,7 @@ static bool generate(Game &game, GenerationContext &context) { // purpose: a home starter kit, the core, an outlier island. That leaves the whole // continent interior in between them bare grass, which reads as empty rather than as a // place with its own history the way a noise-painted map does. A light map-wide scatter - // -- the same mechanism Maze already relies on for this -- fills that gap with + // fills that gap with // ordinary, unclaimed deposits. This runs after every swarm and worker is already placed, // not before: isResourceAllowed refuses any tile with a building or unit on it, so a // scatter placed earlier can claim the one remaining tile a colony's swarm footprint diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 735db252a..626db3023 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -73,8 +73,8 @@ namespace { // ground, and that preference must only pick *where* farmland goes, not *which* of the two // crops a given spot gets - see that function for why. // -// A single global threshold across the whole map works for one connected landmass (Fjord, -// Maze), but a generator whose islands are separate landmasses (Isles, ConcreteIslands) gives +// A single global threshold across the whole map works for one connected landmass (Fjord), +// but a generator whose islands are separate landmasses (Isles, ConcreteIslands) gives // each one its own, slightly different fertility (or noise) range - a global "take the best // tiles first" pass can end up spending almost the entire band on whichever one or two islands // happen to score highest, leaving the rest with none at all. That is exactly the "resources From 047788a3c9a8b30427b8f3782ebb58b038a76d34 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 13:11:04 -0400 Subject: [PATCH 039/136] Make the maze livable: grass passages, thin channels, scattered resources Playing the previous version, the maze itself was right but there was too little room to live in it: corridors were narrow sand strips, rooms existed only at dead ends, and each wall sat in a thick water channel. - Every cell is now a grass chamber, and every open boundary a band of the same width joining two chambers, so colonies can build and farm their way out into the maze instead of only fighting across it. - Walls keep their continuous corner-to-corner stone spine, but the water on either side is now a control, "Channel width", defaulting to the minimum that still works: one all-water tile, since a unit can't step across a tile it can't stand on. Passages widen to fill whatever each cell leaves (21 grass tiles across at the default 32-tile cells). This replaces the Room size and Corridor width controls, whose Maze-only translation entries are removed. - Resources are scattered throughout: clumps of wheat, wood, stone and fruit along every passage's shores, never more than three tiles in, so each passage keeps a clear lane however the maze turns. Homes still start identical, with fixed 1:1 wheat/wood and a compact stone deposit at the back wall. Verified: every colony walkable to every other, 0 walkable tiles within 2 of any complete wall's spine, every open boundary carries a passage, every home one exit with identical resources; maps are about 59% grass and 6% water. Colonies now have 435-548 swarm-sized build sites each (was 23-34), with fairness 0.93-0.96. 0 generation failures across 30 seeds at 1/2/4 colonies, 8 and 12 colonies at 256, 512x128/128x512, every channel width, cell sizes 24-48, loopiness 0/50, and resource extremes. Defaults test and 198/198 cppunit pass. Revision 8. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- data/texts.en.txt | 4 - data/texts.keys.txt | 2 - .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 51 +- .../LegacyGenerationDescriptor.cpp | 5 +- .../generator/generators/MazeGenerator.cpp | 442 ++++++++++-------- src/map/generator/generators/MazeGenerator.h | 2 +- 6 files changed, 287 insertions(+), 219 deletions(-) diff --git a/data/texts.en.txt b/data/texts.en.txt index 00b1f3ca8..0774c1daa 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1778,10 +1778,6 @@ Maze Fjord continent [Cell size] Cell size -[Room size] -Room size -[Corridor width] -Corridor width [Loopiness] Loopiness [Home island size] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index d66da65d7..603ae6e29 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -887,8 +887,6 @@ [Maze] [Fjord continent] [Cell size] -[Room size] -[Corridor width] [Loopiness] [Home island size] [Commons size] diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 1e0e2765b..f62b9e3d4 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -93,12 +93,13 @@ fixed resource pass: up whichever resource is still out of range. A colony genuinely isolated on its own small spot (nothing reachable even through terrain alone) is left untouched, since there's nothing on the other side of a wall that isn't there. RuggedArchipelago, ShatteredCoast and Fjord call this. - Maze doesn't need it: its resources are placed only inside rooms, behind a lane that keeps - every exit clear, and its `validateWorld` confirms every colony can still walk to every other. + Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane + down every passage, and its `validateWorld` confirms every colony can still walk to every + other. Every guaranteed placement (starter kits, bank guarantees, per-team clumps) keeps wheat and wood at a 1:1 ratio, since those exist for reachability fairness between colonies. Ambient/bonus -layers (`scatterResources`, Fjord's bank-scatter roll, Maze's bonus-room defaults) instead use 2:1 corn:wood, which is purely +layers (`scatterResources`, Fjord's bank-scatter roll, Maze's scattered deposits) instead use 2:1 corn:wood, which is purely a feel decision independent of the fairness guarantee. ### Fertility @@ -178,8 +179,9 @@ The richest generator, and the one most of this framework's resource work was pr ## Maze -A maze in the pen-and-paper sense: narrow corridors between thick walls, with every colony in -its own cul-de-sac. +A maze in the pen-and-paper sense, built to be lived in: grass passages that colonies farm and +build out into, separated by thin stone-and-water walls, with every colony in its own +cul-de-sac. - **Grid.** The map is tiled into cells of about `cell-size` tiles on the map's own torus. Boundaries are chosen so the cells tile the map exactly (neighbouring cells differ in pitch by @@ -191,24 +193,27 @@ its own cul-de-sac. non-home cells, and each home is attached by exactly one corridor, so every colony starts in a genuine dead end. `loopiness` knocks through extra walls between non-home cells only, so homes stay cul-de-sacs. -- **Walls.** Every closed boundary is a thick water channel with a stone spine covering the whole - boundary line from corner to corner. Perpendicular walls share their corner tile, so a boundary - can only be crossed — on foot or swimming — where it's open. STONE only places on a pure-grass - tile, which needs grass at all four undermap corners (`Map::regenerateMap`), so each spine sits - on a two-wide grass core inside a sand ring. Terrain is stamped directly rather than through - `Map::controlSand()`, whose in-place raster pass shifts shorelines unevenly. -- **No walking along a wall.** A wall's sandy flanks are walkable land. `validateRequest` - requires `floor(pitch / 2) - roomSize / 2 >= 7`, which leaves at least two all-water tiles - between any room or corridor shore and any wall flank. -- **Corridors are sand**: walkable, but no crop can grow across one mid-game and nothing can be - built on one, so buildings stay in rooms. -- **Rooms and resources.** Rooms exist only at dead ends: colony homes, plus bonus rooms at every - other dead end. Room and corridor widths are odd and centred on a tile, so a room's layout is - identical whichever way its exit faces. Each room keeps a lane from its back wall to its exit - free of resources; wheat lines the left wall, wood the right, stone the back. Homes also keep a - clear square around the swarm and get fixed 1:1 wheat and wood. Bonus rooms follow the - wheat/wood/stone controls, and `fruit` is a count of fruit patches placed in bonus rooms. Algae - is seeded in open channel water at least two tiles from land. +- **Passages.** Every cell is a grass chamber and every open boundary a band of the same width + joining two chambers, so a run of passage reads as one continuous strip of buildable, + farmable grass. Passage width isn't a control of its own: passages fill whatever the narrowest + cell leaves once its walls and channels are taken out, and stay odd so they centre on a tile. +- **Walls.** Every closed boundary has a stone spine covering the whole boundary line from corner + to corner, with `channel-width` all-water tiles on either side (default 1). Perpendicular walls + share their corner tile, so a boundary can only be crossed — on foot or swimming — where it's + open. STONE only places on a pure-grass tile, which needs grass at all four undermap corners + (`Map::regenerateMap`), so each spine sits on a two-wide grass core inside a sand ring. Terrain + is stamped directly rather than through `Map::controlSand()`, whose in-place raster pass shifts + shorelines unevenly. +- **No walking along a wall.** A wall's sandy flanks are walkable land, so they are always kept at + least one all-water tile from every passage's shore — a unit can't step across a tile it can't + stand on. From a cell's centre, a passage's grass therefore reaches + `floor(pitch / 2) - 5 - channelWidth` tiles; `validateRequest` rejects settings that would + leave a passage narrower than 9 tiles. +- **Resources.** Every home starts identical: fixed 1:1 wheat and wood banking the dead end's side + shores, a compact stone deposit at its back wall, and a clear square around the swarm. Outside + the homes, clumps of wheat, wood and stone (densities per 256 shore tiles) and `fruit` patches + are scattered along every passage's shores, never more than three tiles in, so each passage + keeps a clear lane down its middle however the maze turns. Algae is seeded in open water. - **Checked, not assumed.** `validateWorld` floods walkable tiles (water, buildings and every resource, including wall spines, block it) from colony 0's workers, and fails the candidate if any colony isn't reached. diff --git a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp index f54ff3925..f8dd61a19 100644 --- a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp +++ b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp @@ -58,9 +58,8 @@ Sint32 D::*legacyField(int method, const std::string &id) if (id == "island-size") return method == D::eISLES ? &D::grassRatio : &D::oldIslandSize; // "repeat" and every option introduced by generators added after the - // fixed-size legacy descriptor was frozen (loopiness, home-radius, - // cell-size, room-size, corridor-width, islet-size, bridge-count, - // commons-size, home-island-size, moat-width, continent-size, + // fixed-size legacy descriptor was frozen (loopiness, cell-size, + // bridge-count, commons-size, home-island-size, moat-width, continent-size, // coast-roughness, resource-islands, fjord-width, ...) have no slot in // it. Callers fall back to logRepeatAreaTimes for "repeat" specifically // and otherwise treat a null field as "not representable", rather than diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index e2865efd5..ac89d1467 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -8,34 +8,44 @@ #include #include #include +#include #include using namespace MapGeneration; -// A maze in the pen-and-paper sense. The world is tiled into square cells on the map's own -// torus; a recursive backtracker carves a spanning tree of narrow corridors between cell -// centres; every boundary the tree leaves closed becomes a wall - a thick water channel with a -// continuous line of stone down its middle. Every colony starts in its own cul-de-sac: a room -// whose only way out is a single corridor. +// A maze in the pen-and-paper sense, built to be lived in. The world is tiled into square cells +// on the map's own torus; a recursive backtracker carves a spanning tree of passages between +// them; every boundary the tree leaves closed becomes a wall - a line of stone in a narrow water +// channel. Passages fill their cells with grass, so colonies farm and build their way out into +// the maze, and every colony starts in its own cul-de-sac. // // Terrain is stamped directly rather than through Map::controlSand(), whose in-place raster pass // shifts shorelines unevenly. Each tile's terrain comes from its four undermap corners // (Map::regenerateMap), so a pure-grass tile needs grass at all four, and grass must never touch -// water; every grass area here is therefore stamped inside a one-tile sand ring. Corridors are -// sand throughout: walkable, but no crop can ever grow across one, so no corridor can silt shut -// during a game. +// water; every grass area here is therefore stamped inside a one-tile sand ring. namespace { enum Direction { East, South, West, North }; -// Any tile with a non-water corner is walkable, including a wall's sandy flanks. If those flanks -// came near a room or corridor, a unit could step onto them and walk along the wall around the -// maze. Keeping half a cell's pitch at least this much larger than half a room leaves two -// all-water tiles between the room's shore and the wall's. -constexpr int kWallClearance = 7; +// Walking out from a cell's centre towards a wall: the passage's grass, then two walkable tiles of +// its sand ring, then channel-width all-water tiles, then the wall - two walkable tiles of flank and +// the stone spine on the boundary itself. Any tile with a non-water corner is walkable, so without +// that water a unit could step onto a wall's flank and follow the wall around the maze; a single +// water tile is enough, since a unit can't step across a tile it can't stand on. The ring, flank +// and spine account for this many tiles of every half-cell. +constexpr int kWallFootprint = 5; -// Home rooms are guaranteed placements, so their wheat and wood stay 1:1 regardless of the -// wheat/wood controls, which only shape the bonus rooms at the maze's other dead ends. -constexpr int kHomeFarmland = 40; // of 64 +// Half the width of the narrowest passage allowed: a 9-wide chamber still seats the 4x4 swarm +// with room for its workers. +constexpr int kMinimumPassageHalf = 4; + +// Every home starts identical: fixed tile counts rather than densities, so the start doesn't +// depend on the resource controls, with wheat and wood 1:1 as for any guaranteed placement. +constexpr int kHomeFarmland = 32; +constexpr int kHomeStone = 12; + +// Deposits hug each passage's shores, at most this many tiles in. That leaves a clear lane down +// the middle of every passage however the maze turns, so no deposit can seal a route. +constexpr int kShoreBand = 3; // The cell lattice. Boundaries are integer tile coordinates chosen so the cells tile the map // exactly, which lets the maze wrap across the torus seam like any other boundary; when @@ -50,6 +60,11 @@ struct MazeGrid { int centerX(int cell) const { return (xs[column(cell)] + xs[column(cell) + 1]) / 2; } int centerY(int cell) const { return (ys[row(cell)] + ys[row(cell) + 1]) / 2; } int minimumPitch() const { return std::min(width / columns, height / rows); } + int cellAt(int x, int y) const { + const int c = int(std::upper_bound(xs.begin(), xs.end(), x) - xs.begin()) - 1; + const int r = int(std::upper_bound(ys.begin(), ys.end(), y) - ys.begin()) - 1; + return std::min(r, rows - 1) * columns + std::min(c, columns - 1); + } int neighbour(int cell, int direction) const { const int c = column(cell), r = row(cell); @@ -88,14 +103,20 @@ MazeGrid mazeGrid(int width, int height, int cellSize) { return g; } +// Passages are as wide as the narrowest cell allows once the wall and its channels are taken +// out, and odd so they centre on a tile. +int passageHalf(const MazeGrid &g, int channelWidth) { + return g.minimumPitch() / 2 - kWallFootprint - channelWidth; +} + std::string validate(const GenerationRequest &r) { const MazeOptions o(r); const MazeGrid g = mazeGrid(1 << r.wDec, 1 << r.hDec, o.cellSize); if (g.columns < 3 || g.rows < 3) return "The maze needs at least three cells across and down; use a bigger map or smaller cells."; - if (g.minimumPitch() / 2 - o.roomSize / 2 < kWallClearance) - return "Rooms this large leave no room for water channels between cells; use smaller rooms " - "or bigger cells."; + if (passageHalf(g, o.channelWidth) < kMinimumPassageHalf) + return "Channels this wide leave too little grass in each cell; use narrower channels or " + "bigger cells."; if (r.nbTeams > (g.columns / 2) * (g.rows / 2)) return "The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller " "cells."; @@ -105,7 +126,7 @@ std::string validate(const GenerationRequest &r) { // Home candidates sit on every other column and row, so no two homes touch even diagonally. // That spacing is what keeps the remaining cells connected (every home is ringed by eight // non-home cells) and gives every home a non-home neighbour on all four sides to open its one -// corridor into. Among those candidates, homes are spread by farthest-point selection on the +// passage into. Among those candidates, homes are spread by farthest-point selection on the // torus. std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int teams) { const int offsetX = context.bounded("maze", g.columns); @@ -141,7 +162,7 @@ std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int } // Recursive backtracker (iterative, explicit stack) over the non-home cells: long winding -// corridors with comparatively few, deep dead ends - the classic hand-drawn maze texture. +// passages with comparatively few, deep dead ends - the classic hand-drawn maze texture. bool carveSpanningTree(const MazeGrid &g, GenerationContext &context, const std::vector &isHome, std::vector &open) { @@ -201,113 +222,178 @@ void fillUndermap(Map &map, int x0, int y0, int w, int h, TerrainType t) { map.setUMTerrain(map.normalizeX(x0 + dx), map.normalizeY(y0 + dy), t); } -// A corridor of walkable width 2m+1 centred on the tile row (or column) through both cell -// centres needs sand on the 2m undermap lines from centre-m+1 to centre+m. It is extended past -// each centre by the same amount so corridors meeting at a cell form a square junction. -void carveCorridor(Map &map, const MazeGrid &g, int cell, int direction, int corridorWidth) { - const int x = g.centerX(cell), y = g.centerY(cell), m = corridorWidth / 2; - const int next = g.neighbour(cell, direction); - if (direction == East) { - int farX = g.centerX(next); - if (farX < x) - farX += g.width; - fillUndermap(map, x - m + 1, y - m + 1, farX - x + 2 * m, 2 * m, SAND); - } else { - int farY = g.centerY(next); - if (farY < y) - farY += g.height; - fillUndermap(map, x - m + 1, y - m + 1, 2 * m, farY - y + 2 * m, SAND); - } +// An inclusive rectangle of pure-grass tiles. Stamping one takes undermap grass on +// [x0, x1 + 1] x [y0, y1 + 1] inside undermap sand one line further out. +struct TileRect { + int x0, y0, x1, y1; +}; + +void stampRing(Map &map, const TileRect &r) { + fillUndermap(map, r.x0 - 1, r.y0 - 1, r.x1 - r.x0 + 4, r.y1 - r.y0 + 4, SAND); +} +void stampCore(Map &map, const TileRect &r) { + fillUndermap(map, r.x0, r.y0, r.x1 - r.x0 + 2, r.y1 - r.y0 + 2, GRASS); } // One wall per closed boundary: a stone spine covering every tile of the boundary line from // corner to corner inclusive, so perpendicular walls share their corner tile and nothing can slip -// between them. The spine sits on a two-wide grass core (so each spine tile is pure grass, which -// STONE requires) inside a sand ring. -struct Wall { - int x, y, dx, dy, length; -}; - -Wall wallFor(const MazeGrid &g, int cell, int direction) { +// between them. As a tile rectangle one tile wide, its pure-grass core is exactly the spine, +// which is what lets STONE be placed on every tile of it. +TileRect wallFor(const MazeGrid &g, int cell, int direction) { const int c = g.column(cell), r = g.row(cell); if (direction == East) - return {g.xs[c + 1], g.ys[r], 0, 1, g.ys[r + 1] - g.ys[r]}; - return {g.xs[c], g.ys[r + 1], 1, 0, g.xs[c + 1] - g.xs[c]}; + return {g.xs[c + 1], g.ys[r], g.xs[c + 1], g.ys[r + 1]}; + return {g.xs[c], g.ys[r + 1], g.xs[c + 1], g.ys[r + 1]}; +} + +// Distance in 8-neighbour steps from every tile to the nearest tile that isn't pure grass. +std::vector grassDepth(const Map &map) { + const int w = map.getW(), h = map.getH(); + std::vector depth(size_t(w) * h, -1); + std::vector queue; + queue.reserve(size_t(w) * h); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (!map.isGrass(x, y)) { + depth[size_t(y) * w + x] = 0; + queue.push_back(y * w + x); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t n = size_t(map.normalizeY(y + dy)) * w + map.normalizeX(x + dx); + if (depth[n] < 0) { + depth[n] = depth[size_t(queue[head])] + 1; + queue.push_back(int(n)); + } + } + } + return depth; } -struct RoomTile { - int x, y, u, v; // u: towards the room's exit; v: to the exit's right +struct HomeTile { + int x, y, u, v; // u: towards the home's exit; v: to the exit's right }; -// The free pure-grass tiles of the room at `cell`, in coordinates relative to its single exit. -// Rooms and corridors are centred on a tile and have odd sizes, so the layout is identical -// whichever way the exit faces. -std::vector roomTiles(const Map &map, const MazeGrid &g, int cell, int exit, - int roomSize) { +// Places the first `count` tiles of a region in the order `before` ranks them. +template +void fillInOrder(Map &map, std::vector region, int count, int resourceType, + Order before) { + std::stable_sort(region.begin(), region.end(), before); + for (int i = 0; i < count && i < int(region.size()); ++i) + map.setResource(region[i].x, region[i].y, resourceType, 1); +} + +// A home's own cell, seen from inside looking out through its exit: wheat banks the left shore +// and wood the right, both growing forward from the dead end's back wall; stone is a compact +// deposit at the back wall's centre (a line along it would read as a second wall). The middle of +// the passage and a square around the swarm stay clear, so workers always walk straight out. +void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) { static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; - const int fx = forward[exit][0], fy = forward[exit][1]; - const int rx = -fy, ry = fx; - const int cx = g.centerX(cell), cy = g.centerY(cell), half = roomSize / 2; - std::vector tiles; - for (int oy = -half; oy <= half; ++oy) - for (int ox = -half; ox <= half; ++ox) { - const int x = map.normalizeX(cx + ox), y = map.normalizeY(cy + oy); - if (map.isGrass(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID) - tiles.push_back({x, y, ox * fx + oy * fy, ox * rx + oy * ry}); + const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; + const int cx = g.centerX(cell), cy = g.centerY(cell); + const int lane = std::max(1, half - kShoreBand); + std::vector left, right, back; + const int c = g.column(cell), r = g.row(cell); + for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) + for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) { + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + const int ox = x - cx, oy = y - cy; + const int u = ox * fx + oy * fy, v = ox * rx + oy * ry; + if (std::abs(u) <= 4 && std::abs(v) <= 4) + continue; + if (v < -lane) + left.push_back({x, y, u, v}); + else if (v > lane) + right.push_back({x, y, u, v}); + else if (u <= -half + 2) + back.push_back({x, y, u, v}); } - return tiles; + const auto shoreFromBack = [](const HomeTile &a, const HomeTile &b) { + return a.u != b.u ? a.u < b.u : std::abs(a.v) > std::abs(b.v); + }; + const auto nearBackCentre = [half](const HomeTile &a, const HomeTile &b) { + const int da = std::max(a.u + half, std::abs(a.v)), db = std::max(b.u + half, std::abs(b.v)); + if (da != db) + return da < db; + if (std::abs(a.v) != std::abs(b.v)) + return std::abs(a.v) < std::abs(b.v); + return a.u != b.u ? a.u < b.u : a.v < b.v; + }; + fillInOrder(map, left, kHomeFarmland, CORN, shoreFromBack); + fillInOrder(map, right, kHomeFarmland, WOOD, shoreFromBack); + fillInOrder(map, back, kHomeStone, STONE, nearBackCentre); } -// Fills `count` tiles of a region, outermost first, so a deposit grows inward from the room's -// walls and the tiles nearest the lane stay free to harvest from. -void fillRegion(Map &map, std::vector region, int count, int resourceType, - bool acrossLane) { - std::stable_sort(region.begin(), region.end(), [&](const RoomTile &a, const RoomTile &b) { - if (acrossLane) - return std::abs(a.v) != std::abs(b.v) ? std::abs(a.v) > std::abs(b.v) : a.u < b.u; - return a.u != b.u ? a.u < b.u : std::abs(a.v) < std::abs(b.v); - }); - for (int i = 0; i < count && i < int(region.size()); ++i) - map.setResource(region[i].x, region[i].y, resourceType, 1); -} +// Deposits scattered along the shores of every passage outside the homes, as compact clumps +// grown over free shore tiles. Only tiles within kShoreBand of a shore are eligible, which is +// what keeps each passage's middle clear. Densities are per 256 shore tiles. +void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, + const std::vector &isHome, int half, + const MazeOptions &o) { + const int w = map.getW(), h = map.getH(); + const std::vector depth = grassDepth(map); + const int band = std::min(kShoreBand, half - 1); + std::vector free(size_t(w) * h, 0); + std::vector pool; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + if (depth[i] >= 1 && depth[i] <= band && !isHome[g.cellAt(x, y)] && + !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID) { + free[i] = 1; + pool.push_back(int(i)); + } + } + if (pool.empty()) + return; -// Room layout, seen from inside looking out through the exit: a lane one tile wider on each side -// than the corridor runs from the back wall to the exit and never gets resources, so the exit can -// never be sealed; wheat lines the left wall, wood the right, stone the back wall behind the -// lane. A home also keeps a clear square around its swarm so every worker can walk out. -void furnishRoom(Map &map, GenerationContext &context, const MazeGrid &g, int cell, int exit, - const MazeOptions &o, bool home, bool fruit) { - const int half = o.roomSize / 2, lane = o.corridorWidth / 2 + 1; - std::vector left, right, back, fruitRow; - for (const RoomTile &t : roomTiles(map, g, cell, exit, o.roomSize)) { - if (home && std::abs(t.u) <= 4 && std::abs(t.v) <= 4) - continue; - if (t.v < -lane) - left.push_back(t); - else if (t.v > lane) - right.push_back(t); - else if (t.u <= -half + 1) - back.push_back(t); - else if (fruit && t.u == -half + 2 && std::abs(t.v) <= 1) - fruitRow.push_back(t); - } - if (home) { - const int farmland = int(std::min(left.size(), right.size())) * kHomeFarmland / 64; - fillRegion(map, left, farmland, CORN, true); - fillRegion(map, right, farmland, WOOD, true); - } else { - fillRegion(map, left, int(left.size()) * o.corn / 64, CORN, true); - fillRegion(map, right, int(right.size()) * o.wood / 64, WOOD, true); + auto clump = [&](int resourceType, int size) { + int seed = -1; + for (int attempt = 0; attempt < 32 && seed < 0; ++attempt) { + const int candidate = pool[context.bounded("resources", pool.size())]; + if (free[candidate]) + seed = candidate; + } + if (seed < 0) + return 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + std::vector frontier{seed}; + free[seed] = 0; + int placed = 0; + for (size_t head = 0; head < frontier.size() && placed < size; ++head, ++placed) { + const int x = frontier[head] % w, y = frontier[head] / w; + map.setResource(x, y, resourceType, 1); + for (const auto &s : steps) { + const size_t n = size_t(map.normalizeY(y + s[1])) * w + map.normalizeX(x + s[0]); + if (free[n]) { + free[n] = 0; + frontier.push_back(int(n)); + } + } + } + for (size_t k = placed; k < frontier.size(); ++k) + free[frontier[k]] = 1; + return placed; + }; + + const int shore = int(pool.size()); + for (const auto &layer : {std::pair{CORN, o.corn}, std::pair{WOOD, o.wood}, + std::pair{STONE, o.stone}}) { + int remaining = shore * layer.second / 256; + for (int attempt = 0; remaining > 0 && attempt < shore; ++attempt) + remaining -= + clump(layer.first, std::min(remaining, 4 + int(context.bounded("resources", 9)))); } - fillRegion(map, back, int(back.size()) * o.stone / 64, STONE, false); - if (!fruitRow.empty()) - fillRegion(map, fruitRow, int(fruitRow.size()), CHERRY + context.bounded("resources", 3), - false); + for (int i = 0; i < o.fruit; ++i) + clump(CHERRY + context.bounded("resources", 3), 3); } -// Algae belongs in the channels, not the corridors: clumps are seeded only in open water at -// least two tiles from any land, where they read as part of the moat and are still reachable +// Algae is seeded in open water at least one tile from any land, where it's still reachable // from the nearest shore. void seedAlgae(Map &map, GenerationContext &context, int algae) { const int w = map.getW(), h = map.getH(); @@ -315,8 +401,8 @@ void seedAlgae(Map &map, GenerationContext &context, int algae) { for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { bool clear = true; - for (int dy = -2; dy <= 2 && clear; ++dy) - for (int dx = -2; dx <= 2 && clear; ++dx) + for (int dy = -1; dy <= 1 && clear; ++dy) + for (int dx = -1; dx <= 1 && clear; ++dx) clear = map.isWater(map.normalizeX(x + dx), map.normalizeY(y + dy)); if (clear) openWater.emplace_back(x, y); @@ -324,8 +410,7 @@ void seedAlgae(Map &map, GenerationContext &context, int algae) { if (openWater.empty()) return; int remaining = int(openWater.size()) * algae / 1600; - for (int attempt = 0; remaining > 0 && attempt < 4 * int(openWater.size()) / 25 + 16; - ++attempt) + for (int attempt = 0; remaining > 0 && attempt < int(openWater.size()) / 4 + 16; ++attempt) remaining -= placeResourceClump(map, context, openWater[context.bounded("resources", openWater.size())], ALGA, 1); @@ -337,11 +422,13 @@ bool generate(Game &game, GenerationContext &context) { Map &map = game.map; const MazeGrid g = mazeGrid(map.getW(), map.getH(), o.cellSize); const int teams = context.request.nbTeams; + const int half = passageHalf(g, o.channelWidth); map.makeHomogenMap(WATER); for (int i = 0; i < teams; ++i) game.addTeam(); - if (g.columns < 3 || g.rows < 3 || teams > (g.columns / 2) * (g.rows / 2)) { - context.detail = "the maze grid has too few cul-de-sacs for every colony"; + if (g.columns < 3 || g.rows < 3 || half < kMinimumPassageHalf || + teams > (g.columns / 2) * (g.rows / 2)) { + context.detail = "the maze grid has too few cells or cul-de-sacs for these settings"; return false; } @@ -361,81 +448,67 @@ bool generate(Game &game, GenerationContext &context) { exitOf[home] = d; } addLoops(g, context, isHome, o.loopiness, open); - std::vector bonusRooms; - for (int cell = 0; cell < g.cells(); ++cell) { - if (isHome[cell]) - continue; - int degree = 0, exit = -1; - for (int d = 0; d < 4; ++d) - if (open[g.edgeId(cell, d)]) { - ++degree; - exit = d; - } - if (degree == 1) { - bonusRooms.push_back(cell); - exitOf[cell] = exit; - } - } - // All sand before any grass, so a grass core is never overwritten by a neighbouring ring. + // Every cell is a chamber as wide as a passage, and every open boundary a band of that width + // joining two chambers, so a run of passage reads as one continuous strip of grass. context.stage = "maze terrain"; - const int half = o.roomSize / 2; - std::vector walls; - for (int cell = 0; cell < g.cells(); ++cell) + std::vector passages, walls; + for (int cell = 0; cell < g.cells(); ++cell) { + const int cx = g.centerX(cell), cy = g.centerY(cell); + passages.push_back({cx - half, cy - half, cx + half, cy + half}); for (int d : {East, South}) { - if (open[g.edgeId(cell, d)]) - carveCorridor(map, g, cell, d, o.corridorWidth); - else + if (!open[g.edgeId(cell, d)]) { walls.push_back(wallFor(g, cell, d)); + continue; + } + const int next = g.neighbour(cell, d); + if (d == East) { + const int farX = g.centerX(next) < cx ? g.centerX(next) + g.width : g.centerX(next); + passages.push_back({cx - half, cy - half, farX + half, cy + half}); + } else { + const int farY = g.centerY(next) < cy ? g.centerY(next) + g.height : g.centerY(next); + passages.push_back({cx - half, cy - half, cx + half, farY + half}); + } } - for (int cell = 0; cell < g.cells(); ++cell) - if (exitOf[cell] >= 0) - fillUndermap(map, g.centerX(cell) - half - 1, g.centerY(cell) - half - 1, 2 * half + 4, - 2 * half + 4, SAND); - for (const Wall &w : walls) - fillUndermap(map, w.x - 1, w.y - 1, w.dx ? w.length + 4 : 4, w.dy ? w.length + 4 : 4, SAND); - for (int cell = 0; cell < g.cells(); ++cell) - if (exitOf[cell] >= 0) - fillUndermap(map, g.centerX(cell) - half, g.centerY(cell) - half, 2 * half + 2, - 2 * half + 2, GRASS); - for (const Wall &w : walls) - fillUndermap(map, w.x, w.y, w.dx ? w.length + 2 : 2, w.dy ? w.length + 2 : 2, GRASS); + } + // All sand before any grass, so a grass core is never overwritten by a neighbouring ring. + for (const TileRect &r : passages) + stampRing(map, r); + for (const TileRect &r : walls) + stampRing(map, r); + for (const TileRect &r : passages) + stampCore(map, r); + for (const TileRect &r : walls) + stampCore(map, r); map.rebuildTerrain(); - for (const Wall &w : walls) - for (int i = 0; i <= w.length; ++i) { - const int x = map.normalizeX(w.x + i * w.dx), y = map.normalizeY(w.y + i * w.dy); - if (map.getTerrainType(x, y) != GRASS) { - context.detail = "a wall spine tile is not solid grass"; - return false; + for (const TileRect &r : walls) + for (int y = r.y0; y <= r.y1; ++y) + for (int x = r.x0; x <= r.x1; ++x) { + const int nx = map.normalizeX(x), ny = map.normalizeY(y); + if (map.getTerrainType(nx, ny) != GRASS) { + context.detail = "a wall spine tile is not solid grass"; + return false; + } + map.setResource(nx, ny, STONE, 1); } - map.setResource(x, y, STONE, 1); - } for (int team = 0; team < teams; ++team) { - const int cx = g.centerX(homes[team]), cy = g.centerY(homes[team]); + const int cell = homes[team], c = g.column(cell), r = g.row(cell); std::vector home(size_t(map.getW()) * map.getH(), 0); - for (int oy = -half - 2; oy <= half + 2; ++oy) - for (int ox = -half - 2; ox <= half + 2; ++ox) { - const int x = map.normalizeX(cx + ox), y = map.normalizeY(cy + oy); + for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) + for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) if (!map.isWater(x, y)) home[size_t(y) * map.getW() + x] = 1; - } // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. - if (!placeSettlement(game, context, team, home, {cx - 2, cy - 2}, "starts")) + if (!placeSettlement(game, context, team, home, {g.centerX(cell) - 2, g.centerY(cell) - 2}, + "starts")) return false; } context.stage = "maze resources"; for (int home : homes) - furnishRoom(map, context, g, home, exitOf[home], o, true, false); - std::vector hasFruit(g.cells(), 0); - for (int i = 0; i < o.fruit && i < int(bonusRooms.size()); ++i) { - const size_t pick = i + context.bounded("resources", bonusRooms.size() - i); - std::swap(bonusRooms[i], bonusRooms[pick]); - hasFruit[bonusRooms[i]] = 1; - } - for (int cell : bonusRooms) - furnishRoom(map, context, g, cell, exitOf[cell], o, false, hasFruit[cell]); + furnishHome(map, g, home, exitOf[home], half); + scatterThroughMaze(map, context, g, isHome, half, o); seedAlgae(map, context, o.algae); return true; } @@ -485,32 +558,29 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { } // namespace MazeOptions::MazeOptions(const GenerationRequest &r) - : cellSize(r.option("cell-size")), roomSize(r.option("room-size")), - corridorWidth(r.option("corridor-width")), loopiness(r.option("loopiness")), - corn(r.option("wheat")), wood(r.option("wood")), stone(r.option("stone")), - algae(r.option("algae")), fruit(r.option("fruit")) {} + : cellSize(r.option("cell-size")), channelWidth(r.option("channel-width")), + loopiness(r.option("loopiness")), corn(r.option("wheat")), wood(r.option("wood")), + stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")) {} GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 7, + 8, false, {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, {24, 32, 40, 48}}, - // Room and corridor widths are walkable tiles and always odd, so both centre on a tile. - {"room-size", "Room size", 9, 17, 2, 15, ControlGroup::Layout}, - {"corridor-width", "Corridor width", 3, 7, 1, 5, ControlGroup::Layout, false, false, - {3, 5, 7}}, + // Open water on each side of a wall's stone line; passages widen to fill the rest. + {"channel-width", "Channel width", 1, 6, 1, 1, ControlGroup::Layout}, {"loopiness", "Loopiness", 0, 50, 1, 10, ControlGroup::Layout, false, false, {0, 5, 10, 20, 35, 50}}, - // Wheat and wood shape only the bonus rooms (homes are fixed at 1:1); fruit is a - // count of fruit patches, one per bonus room at most. + // Densities for the deposits scattered along the passages (per 256 shore tiles); + // homes always get the same fixed amounts. Fruit is a count of patches. {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, - {"stone", "Stone", 0, 64, 1, 32, ControlGroup::Resources}, + {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 16, 1, 4, ControlGroup::Resources}}, + {"fruit", "Fruit", 0, 32, 1, 8, ControlGroup::Resources}}, generate, true, validate, diff --git a/src/map/generator/generators/MazeGenerator.h b/src/map/generator/generators/MazeGenerator.h index 4bb3d1008..c518434cd 100644 --- a/src/map/generator/generators/MazeGenerator.h +++ b/src/map/generator/generators/MazeGenerator.h @@ -3,7 +3,7 @@ #include "GenerationRequest.h" #include "GeneratorDefinition.h" struct MazeOptions { - int cellSize, roomSize, corridorWidth, loopiness; + int cellSize, channelWidth, loopiness; int corn, wood, stone, algae, fruit; explicit MazeOptions(const GenerationRequest &r); }; From bc54ddb580e382b6bd3702fe26a45abfed44276c Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 14:07:11 -0400 Subject: [PATCH 040/136] Maze: fit homes to any grid that can hold them, and translate its messages Trying cell sizes other than 32 in the lobby, every one was rejected, and the lobby couldn't even say why: Maze's validation messages had no translation entries, so each redraw logged "no such key" instead of showing the reason. Two fixes: - Homes used to sit on every other column and row, which caps a 3x3 grid (40-tile cells on a 128x128 map) at one colony and rejects 2x2 grids outright. Homes are now placed by farthest-point spreading that accepts any cell as long as every home keeps a non-home neighbour and the rest of the maze stays connected. The pattern depends only on the grid, so validateRequest checks exactly what generation does; each map places it at a random offset and mirror image. Grids down to 2x2 are allowed. Default layouts spread exactly as before. - Maze's three validation messages are added to texts.en/texts.keys. At 128x128: 2 colonies now work at every cell size (24/32/40/48), 4 at 24/32/40, 8 at 24/32, 12 at 24; 48-tile cells give a 2x2 grid, which can't hold 4 homes and still leave a maze. At 256 and 512 every cell size works with up to 12 colonies. Structural checks on 3x3, 2x2, 4x4 and 5x5 grids: every home one exit and reachable, walls complete, no leaks, identical home wheat/wood. 0 failures across the seed sweep. Defaults test passes. Revision 9. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- data/texts.en.txt | 6 + data/texts.keys.txt | 3 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 13 +- .../generator/generators/MazeGenerator.cpp | 140 +++++++++++++----- 4 files changed, 117 insertions(+), 45 deletions(-) diff --git a/data/texts.en.txt b/data/texts.en.txt index 0774c1daa..6b68bad6d 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1780,6 +1780,12 @@ Fjord continent Cell size [Loopiness] Loopiness +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +The maze needs at least two cells across and down; use a bigger map or smaller cells. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Channels this wide leave too little grass in each cell; use narrower channels or bigger cells. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells. [Home island size] Home island size [Commons size] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 603ae6e29..629e7cb34 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -888,6 +888,9 @@ [Fjord continent] [Cell size] [Loopiness] +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] [Home island size] [Commons size] [Moat width] diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index f62b9e3d4..6bcce6503 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -185,13 +185,14 @@ cul-de-sac. - **Grid.** The map is tiled into cells of about `cell-size` tiles on the map's own torus. Boundaries are chosen so the cells tile the map exactly (neighbouring cells differ in pitch by - at most one tile), so the maze wraps across the seam like any other boundary. At least three + at most one tile), so the maze wraps across the seam like any other boundary. At least two cells are needed in each direction. -- **Maze and homes.** Homes are chosen first, on every other column and row and spread by - farthest-point selection. That spacing keeps all other cells connected and gives each home four - non-home neighbours. A recursive backtracker then carves a spanning tree of corridors over the - non-home cells, and each home is attached by exactly one corridor, so every colony starts in a - genuine dead end. `loopiness` knocks through extra walls between non-home cells only, so homes +- **Maze and homes.** Homes are chosen first, by farthest-point spreading on the torus that only + accepts a cell if every home still has a non-home neighbour and the non-home cells stay + connected. The pattern depends only on the grid, so `validateRequest` checks exactly how many + colonies fit; each map then places it at a random offset and mirror image. A recursive + backtracker carves a spanning tree of passages over the non-home cells, and each home is + attached by exactly one passage, so every colony starts in a genuine dead end. `loopiness` knocks through extra walls between non-home cells only, so homes stay cul-de-sacs. - **Passages.** Every cell is a grass chamber and every open boundary a band of the same width joining two chambers, so a run of passage reads as one continuous strip of buildable, diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index ac89d1467..267471ca9 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -109,55 +109,110 @@ int passageHalf(const MazeGrid &g, int channelWidth) { return g.minimumPitch() / 2 - kWallFootprint - channelWidth; } +// Whether a set of home cells leaves a valid layout: every home has a non-home neighbour to open +// its one passage into, and the non-home cells form a single connected region for the maze to +// span. +bool homesFit(const MazeGrid &g, const std::vector &isHome) { + int start = -1, freeCells = 0; + for (int cell = 0; cell < g.cells(); ++cell) { + if (!isHome[cell]) { + ++freeCells; + start = cell; + continue; + } + bool opening = false; + for (int d = 0; d < 4; ++d) + opening |= !isHome[g.neighbour(cell, d)]; + if (!opening) + return false; + } + if (!freeCells) + return false; + std::vector seen(g.cells(), 0); + std::vector stack{start}; + seen[start] = 1; + int reached = 1; + while (!stack.empty()) { + const int cell = stack.back(); + stack.pop_back(); + for (int d = 0; d < 4; ++d) { + const int next = g.neighbour(cell, d); + if (!isHome[next] && !seen[next]) { + seen[next] = 1; + ++reached; + stack.push_back(next); + } + } + } + return reached == freeCells; +} + +// The home cells as a pattern fixed by the grid alone, so validation checks exactly what +// generation will do: farthest-point spreading on the torus, taking at each step the farthest +// cell that still leaves a valid layout. Empty if the grid can't hold every colony. +std::vector homePattern(const MazeGrid &g, int teams) { + auto distance = [&](int a, int b) { + int dx = std::abs(g.column(a) - g.column(b)), dy = std::abs(g.row(a) - g.row(b)); + dx = std::min(dx, g.columns - dx); + dy = std::min(dy, g.rows - dy); + return dx * dx + dy * dy; + }; + std::vector isHome(g.cells(), 0); + std::vector homes, nearest(g.cells(), 0); + for (int team = 0; team < teams; ++team) { + std::vector order; + for (int cell = 0; cell < g.cells(); ++cell) + if (!isHome[cell]) + order.push_back(cell); + std::stable_sort(order.begin(), order.end(), + [&](int a, int b) { return nearest[a] > nearest[b]; }); + int chosen = -1; + for (int cell : order) { + isHome[cell] = 1; + if (homesFit(g, isHome)) { + chosen = cell; + break; + } + isHome[cell] = 0; + } + if (chosen < 0) + return {}; + homes.push_back(chosen); + for (int cell = 0; cell < g.cells(); ++cell) + nearest[cell] = homes.size() == 1 ? distance(cell, chosen) + : std::min(nearest[cell], distance(cell, chosen)); + } + return homes; +} + std::string validate(const GenerationRequest &r) { const MazeOptions o(r); const MazeGrid g = mazeGrid(1 << r.wDec, 1 << r.hDec, o.cellSize); - if (g.columns < 3 || g.rows < 3) - return "The maze needs at least three cells across and down; use a bigger map or smaller cells."; + if (g.columns < 2 || g.rows < 2) + return "The maze needs at least two cells across and down; use a bigger map or smaller cells."; if (passageHalf(g, o.channelWidth) < kMinimumPassageHalf) return "Channels this wide leave too little grass in each cell; use narrower channels or " "bigger cells."; - if (r.nbTeams > (g.columns / 2) * (g.rows / 2)) + if (int(homePattern(g, r.nbTeams).size()) != r.nbTeams) return "The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller " "cells."; return ""; } -// Home candidates sit on every other column and row, so no two homes touch even diagonally. -// That spacing is what keeps the remaining cells connected (every home is ringed by eight -// non-home cells) and gives every home a non-home neighbour on all four sides to open its one -// passage into. Among those candidates, homes are spread by farthest-point selection on the -// torus. +// The home pattern at a random offset and mirror image on the torus - a translation or reflection +// of a valid layout is still valid - dealt to the colonies in random order. std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int teams) { + std::vector homes = homePattern(g, teams); const int offsetX = context.bounded("maze", g.columns); const int offsetY = context.bounded("maze", g.rows); - std::vector candidates; - for (int j = 0; j < g.rows / 2; ++j) - for (int i = 0; i < g.columns / 2; ++i) - candidates.push_back(((offsetY + 2 * j) % g.rows) * g.columns + - (offsetX + 2 * i) % g.columns); - for (size_t i = candidates.size(); i > 1; --i) - std::swap(candidates[i - 1], candidates[context.bounded("maze", i)]); - - auto distance = [&](int a, int b) { - int dx = std::abs(g.column(a) - g.column(b)), dy = std::abs(g.row(a) - g.row(b)); - dx = std::min(dx, g.columns - dx); - dy = std::min(dy, g.rows - dy); - return dx * dx + dy * dy; - }; - std::vector homes{candidates[0]}; - std::vector nearest(candidates.size()); - for (size_t i = 0; i < candidates.size(); ++i) - nearest[i] = distance(candidates[i], homes[0]); - while (int(homes.size()) < teams) { - size_t best = 0; - for (size_t i = 1; i < candidates.size(); ++i) - if (nearest[i] > nearest[best]) - best = i; - homes.push_back(candidates[best]); - for (size_t i = 0; i < candidates.size(); ++i) - nearest[i] = std::min(nearest[i], distance(candidates[i], candidates[best])); + const bool mirrorX = context.bounded("maze", 2), mirrorY = context.bounded("maze", 2); + for (int &home : homes) { + const int c = mirrorX ? (g.columns - g.column(home)) % g.columns : g.column(home); + const int r = mirrorY ? (g.rows - g.row(home)) % g.rows : g.row(home); + home = ((r + offsetY) % g.rows) * g.columns + (c + offsetX) % g.columns; } + for (size_t i = homes.size(); i > 1; --i) + std::swap(homes[i - 1], homes[context.bounded("maze", i)]); return homes; } @@ -426,13 +481,16 @@ bool generate(Game &game, GenerationContext &context) { map.makeHomogenMap(WATER); for (int i = 0; i < teams; ++i) game.addTeam(); - if (g.columns < 3 || g.rows < 3 || half < kMinimumPassageHalf || - teams > (g.columns / 2) * (g.rows / 2)) { - context.detail = "the maze grid has too few cells or cul-de-sacs for these settings"; + if (g.columns < 2 || g.rows < 2 || half < kMinimumPassageHalf) { + context.detail = "the maze grid is too small for these settings"; return false; } const std::vector homes = chooseHomes(g, context, teams); + if (int(homes.size()) != teams) { + context.detail = "the maze grid has too few cul-de-sacs for every colony"; + return false; + } std::vector isHome(g.cells(), 0); for (int home : homes) isHome[home] = 1; @@ -443,7 +501,11 @@ bool generate(Game &game, GenerationContext &context) { } std::vector exitOf(g.cells(), -1); for (int home : homes) { - const int d = context.bounded("maze", 4); + int choices[4], count = 0; + for (int d = 0; d < 4; ++d) + if (!isHome[g.neighbour(home, d)]) + choices[count++] = d; + const int d = choices[context.bounded("maze", count)]; open[g.edgeId(home, d)] = 1; exitOf[home] = d; } @@ -566,7 +628,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 8, + 9, false, {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, {24, 32, 40, 48}}, From ef1682124aa8e1b8ab3d50c137334b623331ec54 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 14:14:31 -0400 Subject: [PATCH 041/136] Maze: fruit becomes treasure at dead ends; algae returns to the channels Fruit used to be scattered along passage shores like any other deposit. It is now treasure: every dead end that isn't a home gets one compact patch near its far end, a few tiles back from the end wall so it always faces the passage's clear lane. Fruit types are dealt round-robin across the dead ends, so every kind is somewhere in the maze and a colony has to go and take the ones it lacks. The Fruit control is now the size of each treasure in tiles (0-25, default 9, a 3x3 patch). Checking this also showed algae had effectively stopped being placed: it was seeded only in water at least a tile from any land, which the one-tile channels almost never have. Any water tile can now seed a clump; channel water borders a passage's shore, so it stays harvestable. To see either of these, MapGeneratorStudy's dump now marks fruit (8) and algae (9) instead of reporting them as grass and water; existing codes are unchanged, and both plotting tools gained the two colours. Verified at 128, 256 (loopiness 10 and 0) and 512 with 8 colonies: exactly one treasure per non-home dead end, each the requested size, all reachable on foot, none elsewhere; algae present (61 to 921 tiles) and 98-100% harvestable from a passage; walls intact, every home one exit. 0 generation failures across fruit and algae extremes, cell sizes, colony counts and rectangular maps. Defaults test passes. Revision 10. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 8 +- .../generator/generators/MazeGenerator.cpp | 101 +++++++++++++----- test/MapGeneratorStudy.cpp | 4 + tools/plot_map_generator_refactor.py | 9 +- tools/plot_map_generator_study.py | 8 +- 5 files changed, 92 insertions(+), 38 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 6bcce6503..0921f1f03 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -212,9 +212,11 @@ cul-de-sac. leave a passage narrower than 9 tiles. - **Resources.** Every home starts identical: fixed 1:1 wheat and wood banking the dead end's side shores, a compact stone deposit at its back wall, and a clear square around the swarm. Outside - the homes, clumps of wheat, wood and stone (densities per 256 shore tiles) and `fruit` patches - are scattered along every passage's shores, never more than three tiles in, so each passage - keeps a clear lane down its middle however the maze turns. Algae is seeded in open water. + the homes, clumps of wheat, wood and stone (densities per 256 shore tiles) are scattered along + every passage's shores, never more than three tiles in, so each passage keeps a clear lane down + its middle however the maze turns. Fruit is treasure: every dead end that isn't a home gets one + compact patch of `fruit` tiles near its far end, with fruit types dealt round-robin so every + kind is somewhere in the maze. Algae is seeded along the channels. - **Checked, not assumed.** `validateWorld` floods walkable tiles (water, buildings and every resource, including wall spines, block it) from colony 0's workers, and fails the candidate if any colony isn't reached. diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 267471ca9..4d728fa2b 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -327,13 +327,13 @@ std::vector grassDepth(const Map &map) { return depth; } -struct HomeTile { - int x, y, u, v; // u: towards the home's exit; v: to the exit's right +struct CellTile { + int x, y, u, v; // u: towards the cell's exit; v: to the exit's right }; // Places the first `count` tiles of a region in the order `before` ranks them. template -void fillInOrder(Map &map, std::vector region, int count, int resourceType, +void fillInOrder(Map &map, std::vector region, int count, int resourceType, Order before) { std::stable_sort(region.begin(), region.end(), before); for (int i = 0; i < count && i < int(region.size()); ++i) @@ -349,7 +349,7 @@ void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) { const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; const int cx = g.centerX(cell), cy = g.centerY(cell); const int lane = std::max(1, half - kShoreBand); - std::vector left, right, back; + std::vector left, right, back; const int c = g.column(cell), r = g.row(cell); for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) { @@ -367,10 +367,10 @@ void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) { else if (u <= -half + 2) back.push_back({x, y, u, v}); } - const auto shoreFromBack = [](const HomeTile &a, const HomeTile &b) { + const auto shoreFromBack = [](const CellTile &a, const CellTile &b) { return a.u != b.u ? a.u < b.u : std::abs(a.v) > std::abs(b.v); }; - const auto nearBackCentre = [half](const HomeTile &a, const HomeTile &b) { + const auto nearBackCentre = [half](const CellTile &a, const CellTile &b) { const int da = std::max(a.u + half, std::abs(a.v)), db = std::max(b.u + half, std::abs(b.v)); if (da != db) return da < db; @@ -383,6 +383,33 @@ void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) { fillInOrder(map, back, kHomeStone, STONE, nearBackCentre); } +// A treasure: a compact patch of one fruit at the far end of a dead end, so colonies have to +// go and take it rather than passing it on the way somewhere else. It sits a few tiles back +// from the end wall, so its front always faces the passage's clear lane. +void placeTreasure(Map &map, const MazeGrid &g, int cell, int exit, int half, int size, + int fruitType) { + static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; + const int cx = g.centerX(cell), cy = g.centerY(cell), anchor = -half + 3; + std::vector tiles; + const int c = g.column(cell), r = g.row(cell); + for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) + for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) + if (map.isGrass(x, y) && !map.isResource(x, y)) { + const int ox = x - cx, oy = y - cy; + tiles.push_back({x, y, ox * fx + oy * fy, ox * rx + oy * ry}); + } + fillInOrder(map, tiles, size, fruitType, [anchor](const CellTile &a, const CellTile &b) { + const int da = std::max(std::abs(a.u - anchor), std::abs(a.v)); + const int db = std::max(std::abs(b.u - anchor), std::abs(b.v)); + if (da != db) + return da < db; + if (std::abs(a.v) != std::abs(b.v)) + return std::abs(a.v) < std::abs(b.v); + return a.u != b.u ? a.u < b.u : a.v < b.v; + }); +} + // Deposits scattered along the shores of every passage outside the homes, as compact clumps // grown over free shore tiles. Only tiles within kShoreBand of a shore are eligible, which is // what keeps each passage's middle clear. Densities are per 256 shore tiles. @@ -444,30 +471,23 @@ void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, remaining -= clump(layer.first, std::min(remaining, 4 + int(context.bounded("resources", 9)))); } - for (int i = 0; i < o.fruit; ++i) - clump(CHERRY + context.bounded("resources", 3), 3); } -// Algae is seeded in open water at least one tile from any land, where it's still reachable -// from the nearest shore. +// Algae is seeded along the channels. They are only a tile or two wide, so any water tile can +// seed a clump: channel water borders a passage's shore, where the algae stays harvestable, and +// algae on water never blocks anyone on foot. void seedAlgae(Map &map, GenerationContext &context, int algae) { const int w = map.getW(), h = map.getH(); - std::vector openWater; + std::vector water; for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - bool clear = true; - for (int dy = -1; dy <= 1 && clear; ++dy) - for (int dx = -1; dx <= 1 && clear; ++dx) - clear = map.isWater(map.normalizeX(x + dx), map.normalizeY(y + dy)); - if (clear) - openWater.emplace_back(x, y); - } - if (openWater.empty()) + for (int x = 0; x < w; ++x) + if (map.isWater(x, y)) + water.emplace_back(x, y); + if (water.empty()) return; - int remaining = int(openWater.size()) * algae / 1600; - for (int attempt = 0; remaining > 0 && attempt < int(openWater.size()) / 4 + 16; ++attempt) - remaining -= placeResourceClump(map, context, - openWater[context.bounded("resources", openWater.size())], + int remaining = int(water.size()) * algae / 400; + for (int attempt = 0; remaining > 0 && attempt < int(water.size()) / 4 + 16; ++attempt) + remaining -= placeResourceClump(map, context, water[context.bounded("resources", water.size())], ALGA, 1); } @@ -510,6 +530,21 @@ bool generate(Game &game, GenerationContext &context) { exitOf[home] = d; } addLoops(g, context, isHome, o.loopiness, open); + std::vector deadEnds; + for (int cell = 0; cell < g.cells(); ++cell) { + if (isHome[cell]) + continue; + int degree = 0, exit = -1; + for (int d = 0; d < 4; ++d) + if (open[g.edgeId(cell, d)]) { + ++degree; + exit = d; + } + if (degree == 1) { + deadEnds.push_back(cell); + exitOf[cell] = exit; + } + } // Every cell is a chamber as wide as a passage, and every open boundary a band of that width // joining two chambers, so a run of passage reads as one continuous strip of grass. @@ -570,6 +605,17 @@ bool generate(Game &game, GenerationContext &context) { context.stage = "maze resources"; for (int home : homes) furnishHome(map, g, home, exitOf[home], half); + // Fruit is treasure: one patch at the far end of every dead end that isn't a home, with fruit + // types dealt round-robin so every kind is somewhere in the maze and a colony has to go and + // fight for the ones it lacks. Placed before the shore scatter, which works around them. + if (o.fruit > 0) { + for (size_t i = deadEnds.size(); i > 1; --i) + std::swap(deadEnds[i - 1], deadEnds[context.bounded("resources", i)]); + const int firstType = context.bounded("resources", 3); + for (size_t i = 0; i < deadEnds.size(); ++i) + placeTreasure(map, g, deadEnds[i], exitOf[deadEnds[i]], half, o.fruit, + CHERRY + int((firstType + i) % 3)); + } scatterThroughMaze(map, context, g, isHome, half, o); seedAlgae(map, context, o.algae); return true; @@ -628,7 +674,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 9, + 10, false, {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, {24, 32, 40, 48}}, @@ -637,12 +683,13 @@ GeneratorDefinition mazeDefinition() { {"loopiness", "Loopiness", 0, 50, 1, 10, ControlGroup::Layout, false, false, {0, 5, 10, 20, 35, 50}}, // Densities for the deposits scattered along the passages (per 256 shore tiles); - // homes always get the same fixed amounts. Fruit is a count of patches. + // homes always get the same fixed amounts. Fruit is the size, in tiles, of the + // treasure at every dead end that isn't a home. {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 32, 1, 8, ControlGroup::Resources}}, + {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}}, generate, true, validate, diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 9cf21812d..577654b18 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -449,6 +449,10 @@ int main(int argc, char **argv) c = 5; if (map.getResource(x, y).type == STONE) c = 6; + if (map.getResource(x, y).type >= CHERRY && map.getResource(x, y).type <= CHERRY + 2) + c = 8; + if (map.getResource(x, y).type == ALGA) + c = 9; if (map.getBuilding(x, y) != NOGBID) c = 7; std::fprintf(f, "%d ", c); diff --git a/tools/plot_map_generator_refactor.py b/tools/plot_map_generator_refactor.py index 5764f01dc..cdce9080f 100644 --- a/tools/plot_map_generator_refactor.py +++ b/tools/plot_map_generator_refactor.py @@ -21,8 +21,9 @@ import numpy as np ROOT = Path(__file__).resolve().parents[1] -COLORS = ['#96bb70', '#ddc38e', '#5c9cbc', '#b8c4a3', '#e4c34b', '#285838', '#7f8490', '#292936'] -LABELS = ['Grass', 'Sand', 'Water', 'Shore', 'Wheat', 'Wood', 'Stone', 'Buildings'] +COLORS = ['#96bb70', '#ddc38e', '#5c9cbc', '#b8c4a3', '#e4c34b', '#285838', '#7f8490', '#292936', + '#c2413b', '#3fa69a'] +LABELS = ['Grass', 'Sand', 'Water', 'Shore', 'Wheat', 'Wood', 'Stone', 'Buildings', 'Fruit', 'Algae'] def main(): @@ -119,12 +120,12 @@ def main(): success = study[3] == '1' grid = np.loadtxt(dump, skiprows=1, dtype=int) dump.unlink() - ax.imshow(grid, cmap=ListedColormap(COLORS), vmin=0, vmax=7, interpolation='nearest') + ax.imshow(grid, cmap=ListedColormap(COLORS), vmin=0, vmax=len(COLORS) - 1, interpolation='nearest') ax.set_title(f'Seed {seed}'+('\nWeak start example' if index == 3 else '')+(' · FAILED' if not success else ''), fontsize=10) ax.axis('off') preview_records.append(dict(method=method, seed=seed, success=success, hash=study[-1], role='weak-start' if index == 3 else 'fixed')) fig.suptitle(record['name'], fontweight='bold') - fig.legend(handles=[Patch(color=c, label=l) for c, l in zip(COLORS, LABELS)], loc='lower center', ncol=8, frameon=False, fontsize=8) + fig.legend(handles=[Patch(color=c, label=l) for c, l in zip(COLORS, LABELS)], loc='lower center', ncol=len(COLORS), frameon=False, fontsize=8) fig.tight_layout(rect=(0, .06, 1, .94)) fig.savefig(out/f'generator-{method}.png', dpi=145) plt.close(fig) diff --git a/tools/plot_map_generator_study.py b/tools/plot_map_generator_study.py index 063b31f23..8c4c2196e 100644 --- a/tools/plot_map_generator_study.py +++ b/tools/plot_map_generator_study.py @@ -87,7 +87,7 @@ def value(v):return 1< Date: Fri, 11 Sep 2026 14:31:25 -0400 Subject: [PATCH 042/136] Maze: default channel width 4 and loopiness 5 Per playtesting: thicker water beside each wall by default (4 tiles each side instead of 1), and fewer knocked-through walls (5% instead of 10%), which keeps more dead ends and so more fruit treasures. At the default 32-tile cells, passages are now 15 grass tiles across (was 21); maps are about 40% grass and 23% water (was 59% and 6%). Cell size 24 needs channel width 3 or less, so at the new default it is rejected with the existing "channels too wide" message. Treasures at seed 7: 2 at 128x128, 7 at 256x256 (was 5), 24 at 512x512 with 8 colonies. Verified: defaults test passes (including 512x128 and 128x512); every home one exit, walls intact, one treasure per non-home dead end; 0 generation failures across colony counts, map sizes, and cell sizes 32-48; start-quality 0.73-0.76 with fairness 0.96-0.98. Only defaults change, not what a given set of settings generates, so the revision stays 10. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/MAP_GENERATOR_FRAMEWORK.md | 2 +- src/map/generator/generators/MazeGenerator.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 0921f1f03..f65efb37d 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -199,7 +199,7 @@ cul-de-sac. farmable grass. Passage width isn't a control of its own: passages fill whatever the narrowest cell leaves once its walls and channels are taken out, and stay odd so they centre on a tile. - **Walls.** Every closed boundary has a stone spine covering the whole boundary line from corner - to corner, with `channel-width` all-water tiles on either side (default 1). Perpendicular walls + to corner, with `channel-width` all-water tiles on either side (default 4). Perpendicular walls share their corner tile, so a boundary can only be crossed — on foot or swimming — where it's open. STONE only places on a pure-grass tile, which needs grass at all four undermap corners (`Map::regenerateMap`), so each spine sits on a two-wide grass core inside a sand ring. Terrain diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 4d728fa2b..2ebf340a2 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -679,8 +679,8 @@ GeneratorDefinition mazeDefinition() { {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, {24, 32, 40, 48}}, // Open water on each side of a wall's stone line; passages widen to fill the rest. - {"channel-width", "Channel width", 1, 6, 1, 1, ControlGroup::Layout}, - {"loopiness", "Loopiness", 0, 50, 1, 10, ControlGroup::Layout, false, false, + {"channel-width", "Channel width", 1, 6, 1, 4, ControlGroup::Layout}, + {"loopiness", "Loopiness", 0, 50, 1, 5, ControlGroup::Layout, false, false, {0, 5, 10, 20, 35, 50}}, // Densities for the deposits scattered along the passages (per 256 shore tiles); // homes always get the same fixed amounts. Fruit is the size, in tiles, of the From ef1bf6f559c23c86e11717d8f2363cd2221c1338 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 14:45:51 -0400 Subject: [PATCH 043/136] Maze: default channel width 2 Playtesting channel width 4 showed wheat and wood overgrowing too easily (wider water makes the shores more fertile), so the default drops to 2. At the default 32-tile cells passages are now 19 grass tiles across, and maps are about 52% grass and 11% water. Cell size 24 fits channel width 2, so it is valid at the default again, up to 12 colonies at 128x128. Verified: defaults test passes; every home one exit, walls intact, one reachable treasure per non-home dead end (2 at 128x128, 7 at 256x256, 24 at 512x512 with 8 colonies); 0 generation failures across colony counts, map sizes, and all cell sizes; start-quality 0.63-0.66 with fairness 0.97-0.99. Defaults only, so the revision stays 10. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/MAP_GENERATOR_FRAMEWORK.md | 2 +- src/map/generator/generators/MazeGenerator.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index f65efb37d..65d756fe3 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -199,7 +199,7 @@ cul-de-sac. farmable grass. Passage width isn't a control of its own: passages fill whatever the narrowest cell leaves once its walls and channels are taken out, and stay odd so they centre on a tile. - **Walls.** Every closed boundary has a stone spine covering the whole boundary line from corner - to corner, with `channel-width` all-water tiles on either side (default 4). Perpendicular walls + to corner, with `channel-width` all-water tiles on either side (default 2). Perpendicular walls share their corner tile, so a boundary can only be crossed — on foot or swimming — where it's open. STONE only places on a pure-grass tile, which needs grass at all four undermap corners (`Map::regenerateMap`), so each spine sits on a two-wide grass core inside a sand ring. Terrain diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 2ebf340a2..40fa95ba4 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -679,7 +679,7 @@ GeneratorDefinition mazeDefinition() { {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, {24, 32, 40, 48}}, // Open water on each side of a wall's stone line; passages widen to fill the rest. - {"channel-width", "Channel width", 1, 6, 1, 4, ControlGroup::Layout}, + {"channel-width", "Channel width", 1, 6, 1, 2, ControlGroup::Layout}, {"loopiness", "Loopiness", 0, 50, 1, 5, ControlGroup::Layout, false, false, {0, 5, 10, 20, 35, 50}}, // Densities for the deposits scattered along the passages (per 256 shore tiles); From 260b93a7385803ed275c03ffca146fd7f0a7d2cb Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 14:55:48 -0400 Subject: [PATCH 044/136] Maze: sand roads link every cell through ground that can't be closed A three-tile sand road now runs down the middle of every open passage, centre to centre, so every cell and every colony is joined to the rest of the maze by a route that can never be blocked. Map::incResource only seeds a resource on its own terrain, so wheat and wood can't regrow over a road tile, and buildings need pure grass, so nothing can be built on one. The road meets the swarm at each home without covering its footprint, and ends at the centre of every dead end, just short of its treasure. Shore distances for the resource scatter ignore the road, so deposits still hug the water. Verified at 128 and 256 with 4 colonies, 512 with 8, cell size 24 with 8 colonies and cell size 48 with 2: one connected road network touches every swarm and reaches the centre of every non-home cell; swarms stay centred; every home one exit with 32/32 wheat and wood; one treasure per non-home dead end; walls intact. 0 generation failures across colony counts, map sizes, cell sizes, channel widths 1-6, loopiness and fruit extremes; defaults test passes. The road costs building room: 186-264 swarm-sized sites per colony at defaults (was 312-412), fairness 0.97-0.99. Revision 11. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 5 +++ .../generator/generators/MazeGenerator.cpp | 37 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 65d756fe3..6ab9b3c9b 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -198,6 +198,11 @@ cul-de-sac. joining two chambers, so a run of passage reads as one continuous strip of buildable, farmable grass. Passage width isn't a control of its own: passages fill whatever the narrowest cell leaves once its walls and channels are taken out, and stay odd so they centre on a tile. +- **Roads.** A three-tile sand road runs down the middle of every open passage, centre to centre, + so every cell is linked to the rest of the maze by ground that can never be closed: + `Map::incResource` only seeds a resource on its own terrain and buildings need pure grass, so + nothing grows over a road or is built on one. At a home the road stops against the swarm's + footprint. Shore distances for the resource scatter ignore the road. - **Walls.** Every closed boundary has a stone spine covering the whole boundary line from corner to corner, with `channel-width` all-water tiles on either side (default 2). Perpendicular walls share their corner tile, so a boundary can only be crossed — on foot or swimming — where it's diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 40fa95ba4..c39eac81c 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -301,15 +301,16 @@ TileRect wallFor(const MazeGrid &g, int cell, int direction) { return {g.xs[c], g.ys[r + 1], g.xs[c + 1], g.ys[r + 1]}; } -// Distance in 8-neighbour steps from every tile to the nearest tile that isn't pure grass. -std::vector grassDepth(const Map &map) { +// Distance in 8-neighbour steps from every tile to the nearest shore - a tile that isn't pure +// grass - not counting the sand road, which runs down the middle of passages, not their edges. +std::vector shoreDepth(const Map &map, const std::vector &onRoad) { const int w = map.getW(), h = map.getH(); std::vector depth(size_t(w) * h, -1); std::vector queue; queue.reserve(size_t(w) * h); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) - if (!map.isGrass(x, y)) { + if (!map.isGrass(x, y) && !onRoad[size_t(y) * w + x]) { depth[size_t(y) * w + x] = 0; queue.push_back(y * w + x); } @@ -414,10 +415,11 @@ void placeTreasure(Map &map, const MazeGrid &g, int cell, int exit, int half, in // grown over free shore tiles. Only tiles within kShoreBand of a shore are eligible, which is // what keeps each passage's middle clear. Densities are per 256 shore tiles. void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, - const std::vector &isHome, int half, + const std::vector &isHome, + const std::vector &onRoad, int half, const MazeOptions &o) { const int w = map.getW(), h = map.getH(); - const std::vector depth = grassDepth(map); + const std::vector depth = shoreDepth(map, onRoad); const int band = std::min(kShoreBand, half - 1); std::vector free(size_t(w) * h, 0); std::vector pool; @@ -549,7 +551,7 @@ bool generate(Game &game, GenerationContext &context) { // Every cell is a chamber as wide as a passage, and every open boundary a band of that width // joining two chambers, so a run of passage reads as one continuous strip of grass. context.stage = "maze terrain"; - std::vector passages, walls; + std::vector passages, walls, roads; for (int cell = 0; cell < g.cells(); ++cell) { const int cx = g.centerX(cell), cy = g.centerY(cell); passages.push_back({cx - half, cy - half, cx + half, cy + half}); @@ -558,13 +560,20 @@ bool generate(Game &game, GenerationContext &context) { walls.push_back(wallFor(g, cell, d)); continue; } + // A road runs centre to centre, three tiles wide and one tile past each centre so roads + // meeting at a cell overlap. At a home it stops against the swarm's 4x4 footprint (the + // settlement is anchored on the centre), so the swarm still stands on grass. const int next = g.neighbour(cell, d); if (d == East) { const int farX = g.centerX(next) < cx ? g.centerX(next) + g.width : g.centerX(next); passages.push_back({cx - half, cy - half, farX + half, cy + half}); + roads.push_back({isHome[cell] ? cx + 2 : cx - 1, cy - 1, + isHome[next] ? farX - 3 : farX + 1, cy + 1}); } else { const int farY = g.centerY(next) < cy ? g.centerY(next) + g.height : g.centerY(next); passages.push_back({cx - half, cy - half, cx + half, farY + half}); + roads.push_back({cx - 1, isHome[cell] ? cy + 2 : cy - 1, cx + 1, + isHome[next] ? farY - 3 : farY + 1}); } } } @@ -577,6 +586,18 @@ bool generate(Game &game, GenerationContext &context) { stampCore(map, r); for (const TileRect &r : walls) stampCore(map, r); + // Then the roads, cut through the passages' grass: a sand road links every cell to the maze + // and can never be closed. Map::incResource only seeds a resource on its own terrain and + // buildings need pure grass, so nothing can grow over a road tile or be built on one. A road's + // tile rectangle takes undermap sand on (x0, x1] x (y0, y1], giving each of its tiles at least + // one sand corner and leaving every tile around it untouched. + std::vector onRoad(size_t(map.getW()) * map.getH(), 0); + for (const TileRect &r : roads) { + fillUndermap(map, r.x0 + 1, r.y0 + 1, r.x1 - r.x0, r.y1 - r.y0, SAND); + for (int y = r.y0; y <= r.y1; ++y) + for (int x = r.x0; x <= r.x1; ++x) + onRoad[size_t(map.normalizeY(y)) * map.getW() + map.normalizeX(x)] = 1; + } map.rebuildTerrain(); for (const TileRect &r : walls) for (int y = r.y0; y <= r.y1; ++y) @@ -616,7 +637,7 @@ bool generate(Game &game, GenerationContext &context) { placeTreasure(map, g, deadEnds[i], exitOf[deadEnds[i]], half, o.fruit, CHERRY + int((firstType + i) % 3)); } - scatterThroughMaze(map, context, g, isHome, half, o); + scatterThroughMaze(map, context, g, isHome, onRoad, half, o); seedAlgae(map, context, o.algae); return true; } @@ -674,7 +695,7 @@ GeneratorDefinition mazeDefinition() { return {"maze", 11, "Maze", - 10, + 11, false, {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, {24, 32, 40, 48}}, From 919c7446b6670447c7a460c0b8f17d09ebd3dec2 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:03:23 -0400 Subject: [PATCH 045/136] Add the Ring world map generator One continental belt wraps all the way around the map along its longer axis, seamless across the seam: its centre line and width come from whole-number harmonics of the map length, its coast from lattice noise whose cells tile the torus. A dry spine and a shared coast budget keep the belt one landmass and the ocean one band whatever the controls say. Inland lakes, offshore resource islands, colonies in evenly spaced slots at a fixed shore distance, identical starter kits, the shared ambient scatter and reachability backstop, and a minimum-clearing road pass that keeps a walkable loop open around the ring. validateWorld floods from colony 0 with seam winding and requires every colony reached and a loop around the map (resources are walls), and an ocean that wraps beside the belt. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.en.txt | 6 + data/texts.keys.txt | 3 + src/SConscript | 1 + src/map/generator/core/GenerationRequest.h | 3 +- src/map/generator/core/GeneratorRegistry.cpp | 4 +- .../generators/RingWorldGenerator.cpp | 866 ++++++++++++++++++ .../generator/generators/RingWorldGenerator.h | 10 + test/MapGeneratorDefaultsTest.cpp | 2 +- 8 files changed, 892 insertions(+), 3 deletions(-) create mode 100644 src/map/generator/generators/RingWorldGenerator.cpp create mode 100644 src/map/generator/generators/RingWorldGenerator.h diff --git a/data/texts.en.txt b/data/texts.en.txt index 6b68bad6d..ebd1d85a5 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1806,3 +1806,9 @@ Lake size Lake connects to fjords [Resource islands] Resource islands +[Ring world] +Ring world +[Belt width] +Belt width +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +The belt is too short for this many colonies; use a longer map or fewer colonies. diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 629e7cb34..0b3bc77c9 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -901,3 +901,6 @@ [Lake size] [Lake connects to fjords] [Resource islands] +[Ring world] +[Belt width] +[The belt is too short for this many colonies; use a longer map or fewer colonies.] diff --git a/src/SConscript b/src/SConscript index da1ad0345..e9550881a 100644 --- a/src/SConscript +++ b/src/SConscript @@ -183,6 +183,7 @@ map/generator/generators/CraterLakesGenerator.cpp map/generator/generators/FjordContinentGenerator.cpp map/generator/generators/IslandsGenerator.cpp map/generator/generators/IslesGenerator.cpp +map/generator/generators/RingWorldGenerator.cpp map/generator/generators/RiverGenerator.cpp map/generator/generators/RuggedArchipelagoGenerator.cpp map/generator/generators/ShatteredCoastGenerator.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index d0342b85c..0d60acc06 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -28,7 +28,8 @@ struct GenerationRequest { eOLDISLANDS = 8, eCONTESTEDCOMMONS = 9, eMAZE = 11, - eFJORDCONTINENT = 12 + eFJORDCONTINENT = 12, + eRINGWORLD = 16 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 28dddad67..a5d50aeb4 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -7,6 +7,7 @@ #include "IslandsGenerator.h" #include "IslesGenerator.h" #include "MazeGenerator.h" +#include "RingWorldGenerator.h" #include "RiverGenerator.h" #include "RuggedArchipelagoGenerator.h" #include "ShatteredCoastGenerator.h" @@ -75,6 +76,7 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { shatteredCoastDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), mazeDefinition(), uniformDefinition()}); + riverDefinition(), mazeDefinition(), ringWorldDefinition(), + uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp new file mode 100644 index 000000000..355437198 --- /dev/null +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "RingWorldGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Resources.h" +#include "Settlements.h" +#include "Unit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// One continental belt wraps all the way around the map along its longer axis, and the ocean on +// either side of it meets itself across the other axis's wrap. There are no corners and no back +// line: every colony has exactly two land neighbours along the belt. +// +// Everything that shapes the belt is periodic in the map's own size - whole-number harmonics for +// its centre line and width, lattice noise whose cells tile the torus for its coastline - so the +// seam can't be seen. Terrain is written straight to the undermap with an order-independent beach +// pass rather than Map::controlSand(), whose in-place raster scan makes shorelines depend on scan +// order. +namespace { + +constexpr double pi = 3.14159265358979323846; + +// Belt tiles along the ring each colony needs; validateRequest rejects crowding below this. +constexpr int kBeltPerColony = 24; +// A dry spine this many tiles either side of the centre line is never coast or lake, so the belt +// is always one landmass that closes on itself. The coast's budget is measured from it. +constexpr double kSpineHalf = 5.0; +// Half the narrowest ocean allowed between the belt's two coasts, measured across the wrap (or 6% +// of the map's breadth, if that is more). +constexpr double kOceanHalf = 5.0; +// Steepest the belt's centre line and half-width may change per tile along it. +constexpr double kMaxBendSlope = 0.45; +constexpr double kMaxSwingSlope = 0.3; +// Land kept between a lake and the ocean and between two lakes; water kept around an island. +constexpr int kLakeShore = 5; +constexpr int kLakeGap = 5; +constexpr int kIslandMoat = 7; +// Every swarm stands this many tiles from its nearest water, so every start has the same fertile +// shore within reach. +constexpr int kHomeShore = 6; +// Every home starts identical: fixed tile counts, wheat and wood 1:1 as for any guaranteed +// placement, outside a clear ring this wide that keeps the swarm's workers free to walk out. +constexpr int kHomeWheat = 16; +constexpr int kHomeWood = 16; +constexpr int kHomeStone = 10; +constexpr int kSwarmClearance = 2; + +constexpr int kUnreached = INT_MIN; + +double unitDraw(std::mt19937 &rng) { return rng() / 4294967296.0; } + +// Belt coordinates: u runs along the belt and v across it. The belt follows the map's longer +// axis, so a tall map gets one that wraps top to bottom. +struct Axes { + int width, height; + bool alongX; + int length() const { return alongX ? width : height; } + int breadth() const { return alongX ? height : width; } + int u(int x, int y) const { return alongX ? x : y; } + int v(int x, int y) const { return alongX ? y : x; } + int stepU(int dx, int dy) const { return alongX ? dx : dy; } +}; + +Axes axesFor(int width, int height) { return {width, height, width >= height}; } + +int wrapSquare(int width, int height, int ax, int ay, int bx, int by) { + int dx = std::abs(ax - bx), dy = std::abs(ay - by); + dx = std::min(dx, width - dx); + dy = std::min(dy, height - dy); + return dx * dx + dy * dy; +} + +// Breadth-first 8-neighbour steps on the torus from every source tile through open tiles; -1 +// where the flood never arrives. +std::vector stepsFrom(int width, int height, const std::vector &source, + const std::vector &open) { + std::vector dist(size_t(width) * height, -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % width, y = queue[head] / width; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t n = size_t((y + dy + height) % height) * width + (x + dx + width) % width; + if (dist[n] < 0 && open[n]) { + dist[n] = dist[queue[head]] + 1; + queue.push_back(int(n)); + } + } + } + return dist; +} + +// A smooth closed curve along the belt, one sample per tile. Its harmonics are whole numbers of +// cycles per map length, so it meets itself exactly at the seam. Normalised to [-1, 1]; `slope` +// receives its steepest step between neighbouring samples. +std::vector closedCurve(int length, double falloff, std::mt19937 &rng, double &slope) { + const int harmonics = std::clamp(length / 40, 2, 10); + std::vector amplitude(harmonics), phase(harmonics); + for (int k = 0; k < harmonics; ++k) { + amplitude[k] = (0.3 + 0.7 * unitDraw(rng)) / std::pow(k + 1.0, falloff); + phase[k] = 2 * pi * unitDraw(rng); + } + std::vector curve(length, 0.0); + double peak = 0; + for (int u = 0; u < length; ++u) { + for (int k = 0; k < harmonics; ++k) + curve[u] += amplitude[k] * std::sin(2 * pi * (k + 1) * u / length + phase[k]); + peak = std::max(peak, std::abs(curve[u])); + } + slope = 0; + if (peak <= 0) + return curve; + for (double &sample : curve) + sample /= peak; + for (int u = 0; u < length; ++u) + slope = std::max(slope, std::abs(curve[(u + 1) % length] - curve[u])); + return curve; +} + +// Value noise whose lattice cells divide the map exactly (both sides are powers of two), so it +// tiles the torus with no seam in either direction. Normalised to [-1, 1]. +std::vector torusNoise(int width, int height, std::mt19937 &rng) { + static const std::pair octaves[] = {{32, 0.4}, {16, 0.3}, {8, 0.2}, {4, 0.1}}; + std::vector field(size_t(width) * height, 0.0); + const auto ease = [](double t) { return t * t * (3 - 2 * t); }; + for (const auto &octave : octaves) { + const int cell = std::min({octave.first, width, height}); + const int columns = width / cell, rows = height / cell; + std::vector lattice(size_t(columns) * rows); + for (double &value : lattice) + value = 2 * unitDraw(rng) - 1; + for (int y = 0; y < height; ++y) { + const int r0 = y / cell, r1 = (r0 + 1) % rows; + const double ty = ease(double(y % cell) / cell); + for (int x = 0; x < width; ++x) { + const int c0 = x / cell, c1 = (c0 + 1) % columns; + const double tx = ease(double(x % cell) / cell); + const double top = lattice[r0 * columns + c0] * (1 - tx) + lattice[r0 * columns + c1] * tx; + const double bottom = + lattice[r1 * columns + c0] * (1 - tx) + lattice[r1 * columns + c1] * tx; + field[size_t(y) * width + x] += octave.second * (top * (1 - ty) + bottom * ty); + } + } + } + double peak = 0; + for (double value : field) + peak = std::max(peak, std::abs(value)); + std::vector result(field.size(), 0.0f); + if (peak > 0) + for (size_t i = 0; i < field.size(); ++i) + result[i] = float(field[i] / peak); + return result; +} + +struct Belt { + Axes axes; + std::vector centre, halfWidth; // per u + std::vector coast; // per tile, in [-1, 1] + double roughness; // coast displacement, in tiles, where |coast| is 1 + + // Signed distance across the belt from its centre line, wrapped into [-breadth/2, breadth/2]. + double across(int x, int y) const { + return std::remainder(axes.v(x, y) - centre[axes.u(x, y)], double(axes.breadth())); + } + bool land(int x, int y) const { + return std::abs(across(x, y)) < + halfWidth[axes.u(x, y)] + roughness * coast[size_t(y) * axes.width + x]; + } +}; + +// The belt's centre line bends and its half-width swings along its length; the coast is roughened +// on top of that. Swing and roughness share one budget: the coast may bulge out until the ocean is +// at its narrowest and pinch in until it reaches the spine, never further, so the belt can neither +// break nor swallow the ocean whatever the controls say. +Belt shapeBelt(const Axes &axes, GenerationContext &context, const RingWorldOptions &options) { + std::mt19937 &rng = context.stream("belt"); + const int length = axes.length(); + const double breadth = axes.breadth(); + const double meanHalf = options.beltWidth / 100.0 * breadth / 2; + const double oceanHalf = std::max(kOceanHalf, 0.06 * breadth); + const double budget = + std::max(0.0, std::min(breadth / 2 - oceanHalf - meanHalf, meanHalf - kSpineHalf - 1)); + double swing = 0.22 * meanHalf; + double roughness = options.coastRoughness / 100.0 * 0.9 * meanHalf; + if (swing + roughness > budget) { + const double shrink = budget / (swing + roughness); + swing *= shrink; + roughness *= shrink; + } + double bendSlope = 0, swingSlope = 0; + const std::vector bend = closedCurve(length, 1.6, rng, bendSlope); + const std::vector widthCurve = closedCurve(length, 1.2, rng, swingSlope); + double bendAmplitude = 0.11 * breadth; + if (bendSlope > 0) + bendAmplitude = std::min(bendAmplitude, kMaxBendSlope / bendSlope); + if (swingSlope > 0) + swing = std::min(swing, kMaxSwingSlope / swingSlope); + + Belt belt{axes, std::vector(length), std::vector(length), + torusNoise(axes.width, axes.height, rng), roughness}; + for (int u = 0; u < length; ++u) { + belt.centre[u] = breadth / 2 + bendAmplitude * bend[u]; + belt.halfWidth[u] = meanHalf + swing * widthCurve[u]; + } + return belt; +} + +// Inland lakes for fertility. Each keeps kLakeShore tiles of land to the ocean, kLakeGap to other +// lakes, and stays clear of the spine, so no lake or chain of lakes can cut the belt. Density is +// lakes per 4096 tiles of belt. +void carveLakes(std::vector &terrain, const Belt &belt, GenerationContext &context, + int density) { + const int width = belt.axes.width, height = belt.axes.height; + const size_t area = size_t(width) * height; + std::vector water(area), land(area); + std::vector dry; + for (size_t i = 0; i < area; ++i) { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + if (land[i]) + dry.push_back(int(i)); + } + if (density <= 0 || dry.empty()) + return; + const std::vector shore = stepsFrom(width, height, water, land); + const int wanted = int(std::lround(density * double(dry.size()) / 4096.0)); + const double scale = std::clamp(std::sqrt(belt.axes.breadth() / 128.0), 0.8, 1.6); + struct Lake { + int x, y; + double reach; + }; + std::vector lakes; + for (int attempt = 0; int(lakes.size()) < wanted && attempt < wanted * 40; ++attempt) { + const int at = dry[context.bounded("lakes", dry.size())]; + const int x = at % width, y = at / width; + const RadialShape shape((4 + context.bounded("lakes", 5)) * scale, 0.35, context, "lakes"); + const double reach = shape.maximumRadius(); + // The centre line leans at most kMaxBendSlope per tile, so a lake's far side can sit that much + // nearer the spine than its centre. + if (shore[at] < reach + kLakeShore || + std::abs(belt.across(x, y)) < kSpineHalf + (1 + kMaxBendSlope) * reach + 1) + continue; + bool clear = true; + for (const Lake &other : lakes) { + const double gap = reach + other.reach + kLakeGap; + clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; + } + if (!clear) + continue; + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (std::hypot(double(dx), double(dy)) < shape.radiusAt(std::atan2(double(dy), double(dx)))) + terrain[size_t((y + dy + height) % height) * width + (x + dx + width) % width] = WATER; + lakes.push_back({x, y, reach}); + } +} + +struct Island { + int x, y; + double reach; + std::vector tiles; +}; + +// Small islands out in the open ocean, each well clear of the belt and of each other so they are +// only ever reached by swimming. The control counts islands per 128x128 of map. +std::vector raiseIslands(std::vector &terrain, int width, int height, + GenerationContext &context, int perStandardMap) { + std::vector islands; + if (perStandardMap <= 0) + return islands; + const size_t area = size_t(width) * height; + std::vector water(area), land(area); + std::vector sea; + for (size_t i = 0; i < area; ++i) { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + if (water[i]) + sea.push_back(int(i)); + } + if (sea.empty()) + return islands; + const std::vector offshore = stepsFrom(width, height, land, water); + const int wanted = std::max(1, int(std::lround(perStandardMap * double(area) / 16384.0))); + const double scale = std::clamp(std::sqrt(std::min(width, height) / 128.0), 0.8, 1.6); + for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 40; ++attempt) { + const int at = sea[context.bounded("islands", sea.size())]; + const int x = at % width, y = at / width; + const RadialShape shape((4 + context.bounded("islands", 3)) * scale, 0.3, context, "islands"); + const double reach = shape.maximumRadius(); + if (offshore[at] < reach + kIslandMoat) + continue; + bool clear = true; + for (const Island &other : islands) { + const double gap = reach + other.reach + kIslandMoat; + clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; + } + if (!clear) + continue; + Island island{x, y, reach, {}}; + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (std::hypot(double(dx), double(dy)) < + shape.radiusAt(std::atan2(double(dy), double(dx)))) { + const int i = (y + dy + height) % height * width + (x + dx + width) % width; + terrain[i] = GRASS; + island.tiles.push_back(i); + } + islands.push_back(std::move(island)); + } + return islands; +} + +// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), +// so every land corner beside water becomes sand. Unlike Map::controlSand() this reads only the +// original terrain, so the result doesn't depend on scan order and water is never eaten away. +void layBeaches(std::vector &terrain, int width, int height) { + const std::vector original(terrain); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + const size_t i = size_t(y) * width + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[size_t((y + dy + height) % height) * width + (x + dx + width) % width] == + WATER; + if (shore) + terrain[i] = SAND; + } +} + +// A swarm anchored at (x, y) needs pure grass under its 4x4 footprint and dry, unoccupied ground in +// the ring around it where its workers will stand. +bool siteFits(const Map &map, int x, int y) { + for (int dy = -1; dy <= 4; ++dy) + for (int dx = -1; dx <= 4; ++dx) { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const bool footprint = dx >= 0 && dx < 4 && dy >= 0 && dy < 4; + if (footprint ? !map.isGrass(nx, ny) : map.isWater(nx, ny)) + return false; + if (map.getBuilding(nx, ny) != NOGBID || map.getGroundUnit(nx, ny) != NOGUID) + return false; + } + return true; +} + +// Colonies are dealt evenly spaced slots around the ring from a random starting point, with a +// little jitter, and alternate between the belt's two coasts. Within its slot a colony takes the +// site whose nearest water is closest to kHomeShore tiles away, so every start has the same shore. +bool placeColonies(Game &game, GenerationContext &context, const Belt &belt) { + Map &map = game.map; + const Axes &axes = belt.axes; + const int width = axes.width, height = axes.height, length = axes.length(); + const int teams = context.request.nbTeams; + const size_t area = size_t(width) * height; + std::vector water(area), dry(area); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + water[size_t(y) * width + x] = map.isWater(x, y); + dry[size_t(y) * width + x] = !map.isWater(x, y); + } + const std::vector shore = stepsFrom(width, height, water, dry); + // Only the belt itself: an island or a stretch of rough coast cut off at sea can offer the same + // shore, but a colony there would have no land neighbours at all. The spine is always dry, so + // the belt is whatever land is reached from it. + std::vector spine(area, 0); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + spine[size_t(y) * width + x] = + !map.isWater(x, y) && std::abs(belt.across(x, y)) < kSpineHalf - 1; + const std::vector onBelt = stepsFrom(width, height, spine, dry); + const double slot = double(length) / teams; + const double first = context.bounded("colonies", 3600) / 3600.0 * length; + const int firstSide = int(context.bounded("colonies", 2)); + std::vector placed; + for (int team = 0; team < teams; ++team) { + const double jitter = (int(context.bounded("colonies", 2001)) - 1000) / 1000.0 * 0.08 * slot; + const double target = first + team * slot + jitter; + const double side = (firstSide + team) % 2 ? 1.0 : -1.0; + const double spacing = 0.5 * slot; + int bestX = -1, bestY = -1; + double bestScore = std::numeric_limits::max(); + for (double reach : {0.12, 0.25, 0.5}) { + const double window = std::max(3.0, reach * slot); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + const double offset = + std::abs(std::remainder(axes.u(x, y) + 1.5 - target, double(length))); + if (offset > window || onBelt[size_t(y) * width + x] < 0 || !siteFits(map, x, y)) + continue; + bool crowded = false; + for (const MapGeneratorPoint &other : placed) + crowded = crowded || wrapSquare(width, height, x, y, other.x, other.y) < + spacing * spacing; + if (crowded) + continue; + int gap = INT_MAX; + for (int dy = 0; dy < 4; ++dy) + for (int dx = 0; dx < 4; ++dx) + gap = std::min(gap, shore[size_t(map.normalizeY(y + dy)) * width + + map.normalizeX(x + dx)]); + const double across = belt.across(map.normalizeX(x + 2), map.normalizeY(y + 2)); + const double score = + 3.0 * std::abs(gap - kHomeShore) + offset / window + (across * side < 0 ? 6.0 : 0.0); + if (score < bestScore) { + bestScore = score; + bestX = x; + bestY = y; + } + } + if (bestX >= 0) + break; + } + if (bestX < 0) { + context.detail = + "Colony " + std::to_string(team) + ": no room for a swarm on the belt near its slot"; + return false; + } + std::vector home(area, 0); + for (int dy = -3; dy <= 6; ++dy) + for (int dx = -3; dx <= 6; ++dx) { + const int nx = map.normalizeX(bestX + dx), ny = map.normalizeY(bestY + dy); + if (!map.isWater(nx, ny)) + home[size_t(ny) * width + nx] = 1; + } + if (!placeSettlement(game, context, team, home, MapGeneratorPoint(bestX, bestY), "starts")) + return false; + placed.emplace_back(bestX, bestY); + } + return true; +} + +// Grows a compact patch of one resource outward from a seed tile, breadth-first over the four +// cardinal neighbours, onto tiles the predicate allows. Returns how many tiles it placed. +template +int growPatch(Map &map, int seed, int type, int count, Eligible eligible) { + const int width = map.getW(), height = map.getH(); + std::vector queued(size_t(width) * height, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) { + const int i = frontier[head], x = i % width, y = i / width; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) { + const int n = map.normalizeY(y + step[1]) * width + map.normalizeX(x + step[0]); + if (!queued[n]) { + queued[n] = 1; + frontier.push_back(n); + } + } + } + return placed; +} + +// Every home's starter kit: a wheat and a wood patch on the most fertile ground a short walk from +// the swarm, and a little stone further inland. +void furnishHomes(Game &game, GenerationContext &context) { + Map &map = game.map; + const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; + const size_t area = size_t(width) * height; + std::vector water(area), dry(area), reserved(area, 0); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + water[size_t(y) * width + x] = map.isWater(x, y); + dry[size_t(y) * width + x] = !map.isWater(x, y); + } + for (int team = 0; team < teams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + reserved[size_t(map.normalizeY(context.bootY[team] + dy)) * width + + map.normalizeX(context.bootX[team] + dx)] = 1; + const std::vector shore = stepsFrom(width, height, water, dry); + const auto eligible = [&](int i) { + const int x = i % width, y = i / width; + return !reserved[i] && map.isGrass(x, y) && !map.isResource(x, y) && + map.getBuilding(x, y) == NOGBID && map.getGroundUnit(x, y) == NOGUID; + }; + for (int team = 0; team < teams; ++team) { + std::vector footprint(area, 0); + for (int dy = 0; dy < 4; ++dy) + for (int dx = 0; dx < 4; ++dx) + footprint[size_t(map.normalizeY(context.bootY[team] + dy)) * width + + map.normalizeX(context.bootX[team] + dx)] = 1; + const std::vector walk = stepsFrom(width, height, footprint, dry); + // Wettest (or, for stone, driest) eligible tile in a walking band, nearest first on ties. + const auto pick = [&](int nearest, int farthest, bool wet, int avoid) { + int best = -1; + for (int i = 0; i < int(area); ++i) { + if (walk[i] < nearest || walk[i] > farthest || !eligible(i)) + continue; + if (avoid >= 0 && + wrapSquare(width, height, i % width, i / width, avoid % width, avoid / width) < 36) + continue; + const bool better = + best < 0 || (wet ? shore[i] < shore[best] : shore[i] > shore[best]) || + (shore[i] == shore[best] && walk[i] < walk[best]); + if (better) + best = i; + } + return best; + }; + const int wheat = pick(5, 9, true, -1); + if (wheat >= 0) + growPatch(map, wheat, CORN, kHomeWheat, eligible); + const int wood = pick(5, 9, true, wheat); + if (wood >= 0) + growPatch(map, wood, WOOD, kHomeWood, eligible); + const int stone = pick(7, 12, false, -1); + if (stone >= 0) + growPatch(map, stone, STONE, kHomeStone, eligible); + } +} + +// Each island carries one themed prize, as on Fjord continent's outliers. +void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) { + const int width = map.getW(); + for (const Island &island : islands) { + std::vector grass; + for (int i : island.tiles) + if (map.isGrass(i % width, i / width)) + grass.emplace_back(i % width, i / width); + if (grass.empty()) + continue; + MapGeneratorPoint centre(island.x, island.y); + if (!map.isGrass(centre.x, centre.y)) + centre = grass[context.bounded("islands", grass.size())]; + switch (context.bounded("islands", 3)) { + case 0: + placeResourceClump(map, context, centre, STONE, 2); + break; + case 1: + placeResourceClump(map, context, centre, CHERRY + int(context.bounded("islands", 3)), 2); + break; + default: + placeResourceClump(map, context, centre, CORN, 2); + break; + } + } +} + +// Algae in the shallows along every coast, the ocean's and the lakes' alike: close enough to shore +// to regrow and to be harvested. +void seedAlgae(Map &map, GenerationContext &context) { + const int width = map.getW(), height = map.getH(); + const size_t area = size_t(width) * height; + std::vector water(area), dry(area); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + water[size_t(y) * width + x] = map.isWater(x, y); + dry[size_t(y) * width + x] = !map.isWater(x, y); + } + const std::vector offshore = stepsFrom(width, height, dry, water); + std::vector shallows; + for (int i = 0; i < int(area); ++i) + if (offshore[i] >= 2 && offshore[i] <= 5) + shallows.emplace_back(i % width, i / width); + if (shallows.empty()) + return; + for (int clump = 0; clump < int(shallows.size()) / 90; ++clump) + placeResourceClump(map, context, shallows[context.bounded("resources", shallows.size())], + ALGA, 1); +} + +void clearAroundSwarms(Map &map, const GenerationContext &context) { + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) { + const int x = map.normalizeX(context.bootX[team] + dx); + const int y = map.normalizeY(context.bootY[team] + dy); + if (map.isResource(x, y)) + map.setNoResource(x, y, 1); + } +} + +// Deposits may land anywhere, and stone is never cleared in play, so a band of them could wall +// the belt off. This keeps the ring open: for each colony in order around the belt, the cheapest +// walk forward to the next (resources cost one, open ground nothing; water and buildings are +// impassable) is found in the belt's unrolled cover, and only the deposits on it are cleared. The +// last walk crosses the seam back to the first colony, so together they close one loop all the way +// around the map. Almost always nothing is in the way and nothing is cleared. +bool openBeltRoad(Game &game, GenerationContext &context, const Axes &axes) { + Map &map = game.map; + const int width = map.getW(), height = map.getH(), length = axes.length(); + const int teams = context.request.nbTeams; + const int area = width * height; + constexpr int kLayers = 4; // windings -1 to 2 along the belt + std::vector> workers(teams); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); + } + std::vector> order; + for (int team = 0; team < teams; ++team) + order.emplace_back( + std::fmod(axes.u(context.bootX[team], context.bootY[team]) + 2.0, double(length)), team); + std::sort(order.begin(), order.end()); + + std::vector cost(size_t(kLayers) * area), parent(size_t(kLayers) * area); + std::vector done(size_t(kLayers) * area); + std::vector goal(area); + // The winding that puts tile i nearest to a position along the unrolled belt. + const auto windingNear = [&](int i, double position) { + return int(std::lround((position - axes.u(i % width, i / width)) / length)); + }; + for (int k = 0; k < teams; ++k) { + const double fromU = order[k].first; + const double toU = order[(k + 1) % teams].first + (k + 1 == teams ? length : 0); + const int from = order[k].second, to = order[(k + 1) % teams].second; + std::fill(cost.begin(), cost.end(), INT_MAX); + std::fill(parent.begin(), parent.end(), -1); + std::fill(done.begin(), done.end(), 0); + std::fill(goal.begin(), goal.end(), kUnreached); + std::deque queue; + for (int i : workers[from]) { + const int w = windingNear(i, fromU); + if (w >= -1 && w < kLayers - 1) { + cost[(w + 1) * area + i] = 0; + queue.push_back((w + 1) * area + i); + } + } + for (int i : workers[to]) + goal[i] = windingNear(i, toU); + int reached = -1; + while (!queue.empty() && reached < 0) { + const int node = queue.front(); + queue.pop_front(); + if (done[node]) + continue; + done[node] = 1; + const int w = node / area - 1, i = node % area, x = i % width, y = i / width; + if (goal[i] == w) { + reached = node; + break; + } + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID) + continue; + const int step = axes.u(x, y) + axes.stepU(dx, dy); + const int nw = w + (step < 0 ? -1 : step >= length ? 1 : 0); + if (nw < -1 || nw >= kLayers - 1) + continue; + const int next = (nw + 1) * area + ny * width + nx; + const int nextCost = cost[node] + (map.isResource(nx, ny) ? 1 : 0); + if (nextCost < cost[next]) { + cost[next] = nextCost; + parent[next] = node; + if (nextCost == cost[node]) + queue.push_front(next); + else + queue.push_back(next); + } + } + } + if (reached < 0) { + context.detail = "Colony " + std::to_string(from) + " has no way along the belt to colony " + + std::to_string(to); + return false; + } + for (int node = reached; node >= 0; node = parent[node]) { + const int i = node % area; + if (map.isResource(i % width, i / width)) + map.setNoResource(i % width, i / width, 1); + } + } + return true; +} + +bool generate(Game &game, GenerationContext &context) { + context.stage = "ring layout"; + const RingWorldOptions options(context.request); + Map &map = game.map; + const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Axes axes = axesFor(width, height); + if (teams < 1 || axes.length() < kBeltPerColony * teams) { + context.detail = "the belt is too short for this many colonies"; + return false; + } + + context.stage = "ring terrain"; + const Belt belt = shapeBelt(axes, context, options); + std::vector terrain(size_t(width) * height, WATER); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + if (belt.land(x, y)) + terrain[size_t(y) * width + x] = GRASS; + carveLakes(terrain, belt, context, options.lakeDensity); + const std::vector islands = + raiseIslands(terrain, width, height, context, options.resourceIslands); + layBeaches(terrain, width, height); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * width + x])); + map.rebuildTerrain(); + + context.stage = "ring colonies"; + if (!placeColonies(game, context, belt)) + return false; + + context.stage = "ring resources"; + furnishHomes(game, context); + stockIslands(map, context, islands); + // The same ambient layer as Fjord continent: corn:wood 2:1, some stone, rare fruit. Algae is + // seeded along the shallows below instead, since the shared band only scatters over land. + scatterResources(game, context, {/*corn=*/24, /*wood=*/12, /*stone=*/10, /*algae=*/0, + /*fruit=*/3}); + seedAlgae(map, context); + clearAroundSwarms(map, context); + guaranteeStartingResources(game, context, 24, 32, 6); + clearAroundSwarms(map, context); + + context.stage = "ring road"; + return openBeltRoad(game, context, axes); +} + +std::string validate(const GenerationRequest &r) { + const int length = std::max(1 << r.wDec, 1 << r.hDec); + if (r.nbTeams < 1 || length < kBeltPerColony * r.nbTeams) + return "The belt is too short for this many colonies; use a longer map or fewer colonies."; + return ""; +} + +// Floods open tiles from the sources while counting how often each step crosses the seam along the +// belt. Reaching a tile again with a different count closes a walk around the torus, which is +// exactly what it means for the flooded region to wrap. `winding` is shared between floods of +// disjoint regions so each tile is labelled once; `reached` receives this flood's tiles. +template +bool floodAround(const Map &map, const Axes &axes, const std::vector &sources, Open open, + std::vector &winding, std::vector &reached) { + const int width = map.getW(), length = axes.length(); + reached.clear(); + if (sources.empty()) + return false; + const int anchor = axes.u(sources[0] % width, sources[0] / width); + for (int i : sources) { + if (winding[i] != kUnreached) + continue; + // Each source starts at its image nearest the first, so a colony astride the seam is consistent. + winding[i] = int(std::lround(double(anchor - axes.u(i % width, i / width)) / length)); + reached.push_back(i); + } + bool wraps = false; + for (size_t head = 0; head < reached.size(); ++head) { + const int i = reached[head], x = i % width, y = i / width, w = winding[i]; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + if (!open(nx, ny)) + continue; + const int step = axes.u(x, y) + axes.stepU(dx, dy); + const int nw = w + (step < 0 ? -1 : step >= length ? 1 : 0); + const int n = ny * width + nx; + if (winding[n] == kUnreached) { + winding[n] = nw; + reached.push_back(n); + } else if (winding[n] != nw) { + wraps = true; + } + } + } + return wraps; +} + +// The ring's defining guarantees, checked on the finished world rather than trusted. Walking from +// colony 0's workers (water, buildings and every resource block the way; units don't, since they +// move) must reach every other colony and must close a loop around the map along the belt. And the +// seas on either side of the belt must be one ocean: some body of water wraps around the map +// beside it, so the belt can't have swallowed the ocean anywhere. +std::string validateWorld(const Game &game, const GenerationContext &context) { + const Map &map = game.map; + const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; + const Axes axes = axesFor(width, height); + const size_t area = size_t(width) * height; + std::vector> workers(std::max(teams, 1)); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); + } + if (teams < 1 || workers[0].empty()) + return "Colony 0 has no workers to walk the belt."; + std::vector winding(area, kUnreached), reached; + const bool beltWraps = floodAround( + map, axes, workers[0], + [&map](int x, int y) { + return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + }, + winding, reached); + for (int team = 1; team < teams; ++team) { + bool arrived = false; + for (int i : workers[team]) + arrived = arrived || winding[i] != kUnreached; + if (!arrived) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 along the belt."; + } + if (!beltWraps) + return "The walkable belt does not reach all the way around the map."; + + std::fill(winding.begin(), winding.end(), kUnreached); + bool oceanWraps = false; + for (int i = 0; i < int(area) && !oceanWraps; ++i) + if (winding[i] == kUnreached && map.isWater(i % width, i / width)) + oceanWraps = floodAround( + map, axes, {i}, [&map](int x, int y) { return map.isWater(x, y); }, winding, reached); + if (!oceanWraps) + return "The seas beside the belt do not meet around the map."; + return ""; +} +} // namespace + +RingWorldOptions::RingWorldOptions(const GenerationRequest &r) + : beltWidth(r.option("belt-width")), coastRoughness(r.option("coast-roughness")), + lakeDensity(r.option("lake-density")), resourceIslands(r.option("resource-islands")) {} + +GeneratorDefinition ringWorldDefinition() { + return {"ring-world", + 16, + "Ring world", + 1, + false, + // Belt width is the share of the map's breadth the belt covers on average; lake density + // is lakes per 4096 tiles of belt; resource islands are counted per 128x128 of map. + {{"belt-width", "Belt width", 30, 70, 5, 45, ControlGroup::Terrain}, + {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, + {"lake-density", "Lake density", 0, 8, 1, 2, ControlGroup::Terrain}, + {"resource-islands", "Resource islands", 0, 10, 1, 2, ControlGroup::Resources}}, + generate, + true, + validate, + validateWorld}; +} diff --git a/src/map/generator/generators/RingWorldGenerator.h b/src/map/generator/generators/RingWorldGenerator.h new file mode 100644 index 000000000..2758033ec --- /dev/null +++ b/src/map/generator/generators/RingWorldGenerator.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct RingWorldOptions +{ + int beltWidth, coastRoughness, lakeDensity, resourceIslands; + explicit RingWorldOptions(const GenerationRequest &r); +}; +GeneratorDefinition ringWorldDefinition(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index c2405530a..f9182aaf1 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -216,7 +216,7 @@ class MapGeneratorDefaultsTest assert(screen.descriptor.option("test-cell") == 16 && screen.descriptor.option("test-gap") == 5); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 12); + assert(registry.selectionIndex(101) == 13); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); From 44a197c3840c9dfccda59597cf72dc60232026e1 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:06:52 -0400 Subject: [PATCH 046/136] Ring world: keep ocean islands at least 128-map size On maps narrower than 128 tiles the island radius scaled below its 128-tile size, and the beach ring left almost no grass for the island's prize. Floor the scale at 1.0; 128-tile and larger maps are unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- src/map/generator/generators/RingWorldGenerator.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index 355437198..e27c93e15 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -298,7 +298,8 @@ std::vector raiseIslands(std::vector &terrain, int width, return islands; const std::vector offshore = stepsFrom(width, height, land, water); const int wanted = std::max(1, int(std::lround(perStandardMap * double(area) / 16384.0))); - const double scale = std::clamp(std::sqrt(std::min(width, height) / 128.0), 0.8, 1.6); + // Never smaller than on a 128-tile map: any smaller and the beach leaves no grass for a prize. + const double scale = std::clamp(std::sqrt(std::min(width, height) / 128.0), 1.0, 1.6); for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 40; ++attempt) { const int at = sea[context.bounded("islands", sea.size())]; const int x = at % width, y = at / width; From 8733fa3a633f0bf7e4af3070f53fdebfdd722f7e Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:11:55 -0400 Subject: [PATCH 047/136] List Maze second in the generator catalog Playtesting the finished Maze put it among the best generators, so it moves up the product-facing list to sit right after Contested Commons, ahead of Fjord Continent. Contested Commons stays first, so the lobby's default selection is unchanged. Numeric IDs are untouched; only the presentation order changes. The framework doc's catalog table now follows the same order. Verified: catalog order contested-commons, maze, fjord-continent, then the rest as before; defaults test (including lobby default selection and mode memory) and the custom-game setup harness pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkotX2daY8YTuFcRxFf5ba --- docs/map-generators/MAP_GENERATOR_FRAMEWORK.md | 4 ++-- src/map/generator/core/GeneratorRegistry.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 6ab9b3c9b..06d6b937e 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -32,9 +32,9 @@ reused after a generator is retired. | id | legacy id | Display name | Family | |---|---|---|---| -| `fjord-continent` | 12 | Fjord continent | Its own — see below | -| `maze` | 11 | Maze | Its own — see below | | `contested-commons` | 9 | Contested commons | Point dispersion (`shared/Regions`) | +| `maze` | 11 | Maze | Its own — see below | +| `fjord-continent` | 12 | Fjord continent | Its own — see below | | `shattered-coast` | 7 | Old random | Iterative water/sand/grass balancer, own resource search | | `isles` | 6 | Isles | Point dispersion; islands linked by land bridges | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 28dddad67..98ff82995 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -71,10 +71,10 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { // distinctive playable maps first; numeric IDs remain stable compatibility // identifiers and do not determine presentation order. static const GeneratorRegistry registry( - {contestedCommonsDefinition(), fjordContinentDefinition(), + {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), shatteredCoastDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), mazeDefinition(), uniformDefinition()}); + riverDefinition(), uniformDefinition()}); return registry; } From badf29ad2904372aad652bb71b1bd5765ac60983 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:17:48 -0400 Subject: [PATCH 048/136] Add the Symmetric arena map generator An orchard of all three fruits and some stone on an island behind a moat, crossed by causeways, with 2, 4 or 8 colonies whose ground is exactly symmetric: a half turn for two, a quarter turn for four on square maps and the two mirrors on rectangular ones, all eight square symmetries for eight. Symmetry is built in (orbit-summed noise, colony 0's features stamped onto every image, a simultaneous sand pass, equalised deposit amounts) and validateWorld checks it on the finished map, along with equal walking distances from every colony to wheat, wood, each fruit and the orchard. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.en.txt | 20 + data/texts.keys.txt | 10 + src/SConscript | 1 + src/map/generator/core/GenerationRequest.h | 3 +- src/map/generator/core/GeneratorRegistry.cpp | 4 +- .../generators/SymmetricArenaGenerator.cpp | 1010 +++++++++++++++++ .../generators/SymmetricArenaGenerator.h | 9 + test/MapGeneratorDefaultsTest.cpp | 2 +- 8 files changed, 1056 insertions(+), 3 deletions(-) create mode 100644 src/map/generator/generators/SymmetricArenaGenerator.cpp create mode 100644 src/map/generator/generators/SymmetricArenaGenerator.h diff --git a/data/texts.en.txt b/data/texts.en.txt index 6b68bad6d..a0c52cecb 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1806,3 +1806,23 @@ Lake size Lake connects to fjords [Resource islands] Resource islands +[Symmetric arena] +Symmetric arena +[Centre size] +Centre size +[Causeway width] +Causeway width +[Causeways] +Causeways +[Lakes] +Lakes +[Resource richness] +Resource richness +[Symmetric arena needs 2, 4 or 8 colonies.] +Symmetric arena needs 2, 4 or 8 colonies. +[Eight colonies need a square map in the symmetric arena.] +Eight colonies need a square map in the symmetric arena. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +The causeways are too close together; use fewer or narrower causeways, or a bigger centre. diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 629e7cb34..dd7c1c740 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -901,3 +901,13 @@ [Lake size] [Lake connects to fjords] [Resource islands] +[Symmetric arena] +[Centre size] +[Causeway width] +[Causeways] +[Lakes] +[Resource richness] +[Symmetric arena needs 2, 4 or 8 colonies.] +[Eight colonies need a square map in the symmetric arena.] +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] diff --git a/src/SConscript b/src/SConscript index da1ad0345..8822ef4f5 100644 --- a/src/SConscript +++ b/src/SConscript @@ -187,6 +187,7 @@ map/generator/generators/RiverGenerator.cpp map/generator/generators/RuggedArchipelagoGenerator.cpp map/generator/generators/ShatteredCoastGenerator.cpp map/generator/generators/SwampGenerator.cpp +map/generator/generators/SymmetricArenaGenerator.cpp map/generator/generators/UniformGenerator.cpp map/generator/shared/Distances.cpp map/generator/shared/HeightMap.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index d0342b85c..13eaba0f0 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -28,7 +28,8 @@ struct GenerationRequest { eOLDISLANDS = 8, eCONTESTEDCOMMONS = 9, eMAZE = 11, - eFJORDCONTINENT = 12 + eFJORDCONTINENT = 12, + eSYMMETRICARENA = 15 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 28dddad67..26078eea7 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -11,6 +11,7 @@ #include "RuggedArchipelagoGenerator.h" #include "ShatteredCoastGenerator.h" #include "SwampGenerator.h" +#include "SymmetricArenaGenerator.h" #include "UniformGenerator.h" #include #include @@ -75,6 +76,7 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { shatteredCoastDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), mazeDefinition(), uniformDefinition()}); + riverDefinition(), mazeDefinition(), symmetricArenaDefinition(), + uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp new file mode 100644 index 000000000..2a07de070 --- /dev/null +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -0,0 +1,1010 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "SymmetricArenaGenerator.h" +#include "Building.h" +#include "BuildingType.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GlobalContainer.h" +#include "HeightMap.h" +#include "Unit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// A symmetric arena: an orchard of all three fruits and some stone on an island behind a moat, +// crossed by causeways, with the colonies spaced round it so that every colony's ground, route +// and resources are exactly the same as every other's. +// +// Symmetry is enforced by construction. Every decision is either a function of a tile's whole +// orbit (integer noise summed over the orbit, the integer squared distance from the centre) or +// is made once for colony 0 and stamped onto every image of it. Map::controlSand()'s in-place, +// row-order pass is replaced by the same rule applied to every corner at once, per-tile random +// clumps by deterministic growth, and the amounts the engine RNG gives each resource tile are +// equalised over its orbit. validateWorld then checks the invariance on the finished world. +namespace { + +constexpr double kPi = 3.14159265358979323846; + +// Clear grass round every swarm; nothing is planted or dug inside it. +constexpr double kHomeRadius = 6.5; +// Each home's own pond, which keeps its starting farmland growing back. +constexpr double kPondDistance = 11.5, kPondRadius = 3.5; +// Open land kept between the moat and the nearest lake. +constexpr double kApron = 4.0; +// The nearest two homes may be. +constexpr double kMinimumSpacing = 28.0; +// Every home's starting kit, the same whatever the richness control says. +constexpr int kKitFarmland = 16, kKitStone = 4; + +int wrap(int v, int n) { + v %= n; + return v < 0 ? v + n : v; +} + +// A symmetry of the map about its centre, as a signed permutation matrix acting on doubled +// centred coordinates: tile (x, y) is (2x + 1 - W, 2y + 1 - H) and undermap corner (u, v) is +// (2u - W, 2v - H). A tile's terrain comes from its four corners (Map::regenerateMap), and the +// same matrix maps a tile's corners onto its image's corners, so the tile rotation +// (x, y) -> (W-1-y, x) is the corner rotation (u, v) -> (W-v, u). +struct Isometry { + int a, b, c, d; +}; + +struct Symmetry { + int width = 0, height = 0; + // elements[0] is the identity; colony k starts on elements[k]'s image of colony 0's home. + std::vector elements; + + int order() const { return int(elements.size()); } + int tile(int e, int x, int y) const { + const Isometry &g = elements[e]; + const int X = 2 * x + 1 - width, Y = 2 * y + 1 - height; + return wrap((g.c * X + g.d * Y + height - 1) / 2, height) * width + + wrap((g.a * X + g.b * Y + width - 1) / 2, width); + } + int corner(int e, int u, int v) const { + const Isometry &g = elements[e]; + const int U = 2 * u - width, V = 2 * v - height; + return wrap((g.c * U + g.d * V + height) / 2, height) * width + + wrap((g.a * U + g.b * V + width) / 2, width); + } + // The top-left tile of the image of a w x h footprint anchored at (x, y): the image of a + // rectangle is a rectangle, so it is the image's lowest corner on each axis. + std::pair anchor(int e, int x, int y, int w, int h) const { + const Isometry &g = elements[e]; + const int U0 = 2 * x - width, V0 = 2 * y - height; + const int U1 = 2 * (x + w) - width, V1 = 2 * (y + h) - height; + const int X = std::min(g.a * U0 + g.b * V0, g.a * U1 + g.b * V1); + const int Y = std::min(g.c * U0 + g.d * V0, g.c * U1 + g.d * V1); + return {wrap((X + width) / 2, width), wrap((Y + height) / 2, height)}; + } +}; + +// The symmetry that gives every colony identical ground: a half turn for two colonies, a +// quarter turn for four on a square map, the two mirrors for four on a rectangular map (where a +// quarter turn doesn't map the map onto itself), and all eight symmetries of a square for +// eight. Elements are listed in order round the centre, so neighbouring colonies are +// neighbouring team numbers. Empty for colony counts no symmetry serves. +Symmetry symmetryFor(int width, int height, int teams) { + Symmetry s; + s.width = width; + s.height = height; + const bool square = width == height; + if (teams == 2) + s.elements = {{1, 0, 0, 1}, {-1, 0, 0, -1}}; + else if (teams == 4 && square) + s.elements = {{1, 0, 0, 1}, {0, -1, 1, 0}, {-1, 0, 0, -1}, {0, 1, -1, 0}}; + else if (teams == 4) + s.elements = {{1, 0, 0, 1}, {-1, 0, 0, 1}, {-1, 0, 0, -1}, {1, 0, 0, -1}}; + else if (teams == 8 && square) + s.elements = {{1, 0, 0, 1}, {0, 1, 1, 0}, {0, -1, 1, 0}, {-1, 0, 0, 1}, + {-1, 0, 0, -1}, {0, -1, -1, 0}, {0, 1, -1, 0}, {1, 0, 0, -1}}; + return s; +} + +// Continuous positions relative to the map centre, in tiles. The centre is an undermap corner, +// so corners sit on integers and tile centres on half-integers. +struct Point { + double x, y; +}; + +Point cornerPoint(int width, int height, int u, int v) { + return {u - width / 2.0, v - height / 2.0}; +} +Point tilePoint(int width, int height, int x, int y) { + return {x + 0.5 - width / 2.0, y + 0.5 - height / 2.0}; +} +double wrapDelta(double d, double period) { + d = std::fmod(d, period); + if (d >= period / 2) + d -= period; + else if (d < -period / 2) + d += period; + return d; +} +double torusDistance(int width, int height, Point p, Point q) { + return std::hypot(wrapDelta(p.x - q.x, width), wrapDelta(p.y - q.y, height)); +} + +// Squared distance from the centre in doubled units: an integer, so exactly equal across an +// orbit, and comparing it against a radius gives every member of the orbit the same answer. +std::int64_t cornerRadius2(int width, int height, int u, int v) { + const std::int64_t U = 2 * u - width, V = 2 * v - height; + return U * U + V * V; +} +std::int64_t tileRadius2(int width, int height, int x, int y) { + const std::int64_t X = 2 * x + 1 - width, Y = 2 * y + 1 - height; + return X * X + Y * Y; +} +bool within(std::int64_t radius2, double radius) { + return double(radius2) < 4.0 * radius * radius; +} + +// The request's geometry. +struct Arena { + int width, height, teams, causeways; + double centre, moat, causewayWidth; + Symmetry symmetry; +}; + +Arena arenaFor(const GenerationRequest &r) { + const SymmetricArenaOptions o(r); + Arena a; + a.width = 1 << r.wDec; + a.height = 1 << r.hDec; + a.teams = r.nbTeams; + a.causeways = o.causeways; + // centre-size is a share of the shorter side, over a floor that keeps room for all three + // fruits on the smallest maps. + a.centre = 4.0 + o.centreSize / 100.0 * 0.75 * std::min(a.width, a.height); + a.moat = o.moatWidth; + a.causewayWidth = o.causewayWidth; + a.symmetry = symmetryFor(a.width, a.height, a.teams); + return a; +} + +double imageAngle(const Isometry &g, double angle) { + const double x = std::cos(angle), y = std::sin(angle); + return std::atan2(g.c * x + g.d * y, g.a * x + g.b * y); +} + +// Colony 0's causeways, as directions from the centre: one straight towards its home, or two +// flanking it, each a quarter of the way to the nearest neighbour's direction. +std::vector gateAngles(const Arena &a, Point home) { + const double angle = std::atan2(home.y, home.x); + if (a.causeways < 2) + return {angle}; + double gap = 2 * kPi; + for (int e = 1; e < a.symmetry.order(); ++e) { + double d = std::fmod(std::fabs(imageAngle(a.symmetry.elements[e], angle) - angle), 2 * kPi); + gap = std::min(gap, std::min(d, 2 * kPi - d)); + } + return {angle - gap / 4, angle + gap / 4}; +} + +// The smallest angle between two causeways once every image of colony 0's is drawn. +double gateSeparation(const Arena &a, const std::vector &gates) { + std::vector all; + for (const Isometry &g : a.symmetry.elements) + for (double angle : gates) { + double image = std::fmod(imageAngle(g, angle), 2 * kPi); + all.push_back(image < 0 ? image + 2 * kPi : image); + } + std::sort(all.begin(), all.end()); + double gap = all.front() + 2 * kPi - all.back(); + for (size_t i = 1; i < all.size(); ++i) + gap = std::min(gap, all[i] - all[i - 1]); + return gap; +} + +struct Home { + int u, v; + double spacing, radius; +}; + +enum class Fit { Ok, Crowded, Causeways }; + +// Every corner that could be colony 0's home. This depends only on the request, so validation +// and generation agree: the home's clear ground misses the moat, it is well inside half the +// shorter side (so the direction to the centre is unambiguous on the torus), it is far enough +// from every rival, and its causeways leave shore between them. +std::vector homeCandidates(const Arena &a, Fit &fit) { + const int w = a.width, h = a.height, shortSide = std::min(w, h); + const int step = std::max(1, shortSide / 128); + const double inner = a.centre + a.moat + kHomeRadius + 4, outer = 0.45 * shortSide; + std::vector homes; + bool spaced = false; + for (int v = 0; v < h && a.symmetry.order() > 1; v += step) + for (int u = 0; u < w; u += step) { + const Point p = cornerPoint(w, h, u, v); + const double radius = std::hypot(p.x, p.y); + if (radius < inner || radius > outer) + continue; + double spacing = 1e9; + for (int e = 1; e < a.symmetry.order(); ++e) { + const int c = a.symmetry.corner(e, u, v); + spacing = std::min(spacing, torusDistance(w, h, p, cornerPoint(w, h, c % w, c / w))); + } + if (spacing < kMinimumSpacing) + continue; + spaced = true; + if (gateSeparation(a, gateAngles(a, p)) * a.centre < a.causewayWidth + 5) + continue; + homes.push_back({u, v, spacing, radius}); + } + fit = !homes.empty() ? Fit::Ok : spaced ? Fit::Causeways : Fit::Crowded; + return homes; +} + +// The best-spaced homes, and of those the ones nearest the centre, picked from at random. +Home chooseHome(const std::vector &homes, GenerationContext &context, int shortSide) { + double best = 0; + for (const Home &home : homes) + best = std::max(best, home.spacing); + const double floor = best - std::max(2.0, 0.1 * best); + double nearest = 1e9; + for (const Home &home : homes) + if (home.spacing >= floor) + nearest = std::min(nearest, home.radius); + std::vector pool; + for (const Home &home : homes) + if (home.spacing >= floor && home.radius <= nearest + 0.15 * shortSide) + pool.push_back(home); + return pool[context.bounded("layout", pool.size())]; +} + +std::string validate(const GenerationRequest &r) { + if (r.nbTeams != 2 && r.nbTeams != 4 && r.nbTeams != 8) + return "Symmetric arena needs 2, 4 or 8 colonies."; + if (r.nbTeams == 8 && r.wDec != r.hDec) + return "Eight colonies need a square map in the symmetric arena."; + Fit fit; + homeCandidates(arenaFor(r), fit); + if (fit == Fit::Crowded) + return "The arena leaves too little room for every colony; use a bigger map, a smaller " + "centre or a narrower moat."; + if (fit == Fit::Causeways) + return "The causeways are too close together; use fewer or narrower causeways, or a bigger " + "centre."; + return ""; +} + +// Colony 0's choices; everything else is its images. +struct Layout { + int homeU, homeV; + Point home, pond; + double pondAngle; + std::vector gates; + int orchardPhase, orchardPattern; +}; + +// Colony 0's feature stamped onto every image of it: an entry is set when any image of its +// tile (or corner) lies in the feature. A union doesn't depend on the order an orbit is +// visited in, so the result is exactly symmetric. +std::vector stamp(const Symmetry &s, const std::vector &feature, + bool corners) { + std::vector result(feature.size(), 0); + for (int y = 0; y < s.height; ++y) + for (int x = 0; x < s.width; ++x) + for (int e = 0; e < s.order(); ++e) + if (feature[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]) { + result[size_t(y) * s.width + x] = 1; + break; + } + return result; +} + +// Plain noise summed over every orbit, as integers so the sum is exact in any order. +std::vector orbitNoise(GenerationContext &context, const Symmetry &s, + const std::string &stream, float smoothing, bool corners) { + const int w = s.width, h = s.height; + HeightMap noise(w, h, context.stream(stream)); + noise.makePlain(smoothing); + std::vector raw(size_t(w) * h), summed(size_t(w) * h, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + raw[size_t(y) * w + x] = int(noise(x, y) * 4096); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + for (int e = 0; e < s.order(); ++e) + summed[size_t(y) * w + x] += raw[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]; + return summed; +} + +// The share (0 to 1) of eligible entries with the highest (or lowest) values, as a mask. Ties +// at the cut-off are all kept, so the members of an orbit, which have equal values, are never +// split. +std::vector topShare(const std::vector &value, + const std::vector &eligible, double share, + bool highest) { + std::vector pool; + for (size_t i = 0; i < value.size(); ++i) + if (eligible[i]) + pool.push_back(value[i]); + std::vector mask(value.size(), 0); + const size_t count = std::min(pool.size(), size_t(std::lround(std::max(0.0, share) * pool.size()))); + if (!count) + return mask; + std::sort(pool.begin(), pool.end()); + const int threshold = highest ? pool[pool.size() - count] : pool[count - 1]; + for (size_t i = 0; i < value.size(); ++i) + mask[i] = eligible[i] && (highest ? value[i] >= threshold : value[i] <= threshold); + return mask; +} + +// Whether a point lies on one of colony 0's causeways, extended past both shores. +bool onCauseway(const Arena &a, const Layout &l, Point p, double extend, double halfWidth) { + for (double angle : l.gates) { + const double dx = std::cos(angle), dy = std::sin(angle); + const double along = p.x * dx + p.y * dy, across = std::fabs(p.x * dy - p.y * dx); + if (along >= a.centre - extend && along <= a.centre + a.moat + extend && across <= halfWidth) + return true; + } + return false; +} + +// The finished undermap, and the corner masks later steps need. +struct Terrain { + std::vector undermap, moat, ponds, causeways, paths; +}; + +// A walking route for colony 0 from its home to the outer end of each of its causeways: the +// shortest one, where every lake corner crossed costs as much as a long detour, so it keeps to +// land wherever land will do; the moat and the ponds are never crossed. The route's images give +// every colony the same one. Lake water on and beside a route becomes land. +bool carvePaths(const Arena &a, const Layout &l, Terrain &t) { + constexpr int kStraight = 2, kDiagonal = 3, kFord = 60; + const int w = a.width, h = a.height; + const size_t n = size_t(w) * h; + std::vector route(n, 0); + for (double angle : l.gates) { + const double reach = a.centre + a.moat + 3.5; + const int target = wrap(int(std::lround(h / 2.0 + reach * std::sin(angle))), h) * w + + wrap(int(std::lround(w / 2.0 + reach * std::cos(angle))), w); + const int start = l.homeV * w + l.homeU; + std::vector cost(n, INT_MAX), parent(n, -1); + using Entry = std::pair; + std::priority_queue, std::greater> queue; + queue.push({0, start}); + cost[size_t(start)] = 0; + while (!queue.empty()) { + const auto [c, i] = queue.top(); + queue.pop(); + if (c != cost[size_t(i)]) + continue; + if (i == target) + break; + const int u = i % w, v = i / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (!dx && !dy) + continue; + const int j = wrap(v + dy, h) * w + wrap(u + dx, w); + const bool water = t.undermap[size_t(j)] == WATER; + if (water && (t.moat[size_t(j)] || t.ponds[size_t(j)])) + continue; + const int next = c + (dx && dy ? kDiagonal : kStraight) + (water ? kFord : 0); + if (next < cost[size_t(j)]) { + cost[size_t(j)] = next; + parent[size_t(j)] = i; + queue.push({next, j}); + } + } + } + if (cost[size_t(target)] == INT_MAX) + return false; + for (int i = target; i != -1; i = parent[size_t(i)]) + route[size_t(i)] = 1; + } + t.paths = stamp(a.symmetry, route, true); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) { + if (!t.paths[size_t(v) * w + u]) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t j = size_t(wrap(v + dy, h)) * w + wrap(u + dx, w); + if (t.undermap[j] == WATER && !t.moat[j] && !t.ponds[j]) + t.undermap[j] = GRASS; + } + } + return true; +} + +bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const Layout &l, + const SymmetricArenaOptions &o, Terrain &t) { + context.stage = "arena terrain"; + const int w = a.width, h = a.height; + const size_t n = size_t(w) * h; + const Symmetry &s = a.symmetry; + std::vector homeDisc(n, 0), homeZone(n, 0), pond(n, 0), gate(n, 0); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) { + const size_t i = size_t(v) * w + u; + const Point p = cornerPoint(w, h, u, v); + const double toHome = torusDistance(w, h, p, l.home); + homeDisc[i] = toHome <= kHomeRadius; + homeZone[i] = toHome <= kHomeRadius + 4; + pond[i] = torusDistance(w, h, p, l.pond) <= kPondRadius; + gate[i] = onCauseway(a, l, p, 3.0, a.causewayWidth / 2); + } + homeDisc = stamp(s, homeDisc, true); + homeZone = stamp(s, homeZone, true); + t.ponds = stamp(s, pond, true); + t.causeways = stamp(s, gate, true); + + t.moat.assign(n, 0); + std::vector lakeEligible(n, 0); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) { + const size_t i = size_t(v) * w + u; + const std::int64_t r2 = cornerRadius2(w, h, u, v); + t.moat[i] = !within(r2, a.centre) && within(r2, a.centre + a.moat); + lakeEligible[i] = !within(r2, a.centre + a.moat + kApron) && !homeZone[i]; + } + const float smoothing = std::max(12.0f, std::min(w, h) / 8.0f); + const std::vector lakes = + topShare(orbitNoise(context, s, "arena-lakes", smoothing, true), lakeEligible, + o.lakes / 100.0, false); + + // Later layers win: lakes and ponds, then clear home ground, the moat, and causeways of sand. + t.undermap.assign(n, GRASS); + for (size_t i = 0; i < n; ++i) { + if (lakes[i] || t.ponds[i]) + t.undermap[i] = WATER; + if (homeDisc[i]) + t.undermap[i] = GRASS; + if (t.moat[i]) + t.undermap[i] = WATER; + if (t.causeways[i]) + t.undermap[i] = SAND; + } + if (!carvePaths(a, l, t)) { + context.detail = "no route from colony 0's home to its causeway"; + return false; + } + + // Map::controlSand()'s rule - grass touching water becomes sand - applied to every corner at + // once rather than in place in row order, which is what keeps it symmetric. Water is left + // alone, so no moat, pond or channel silts up. + std::vector sanded(t.undermap); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) { + const size_t i = size_t(v) * w + u; + if (t.undermap[i] != GRASS) + continue; + for (int dy = -1; dy <= 1 && sanded[i] == GRASS; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (t.undermap[size_t(wrap(v + dy, h)) * w + wrap(u + dx, w)] == WATER) { + sanded[i] = SAND; + break; + } + } + t.undermap.swap(sanded); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + map.setUMTerrain(u, v, TerrainType(t.undermap[size_t(v) * w + u])); + map.rebuildTerrain(); + return true; +} + +// Walking steps (8-neighbour) from every tile to the nearest source tile: exact integers, so +// symmetric terrain gives symmetric distances. -1 where no source is reachable. +std::vector stepsFrom(int w, int h, const std::vector &source) { + std::vector dist(source.size(), -1), queue; + queue.reserve(source.size()); + for (size_t i = 0; i < source.size(); ++i) + if (source[i]) { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); + if (dist[j] < 0) { + dist[j] = dist[size_t(queue[head])] + 1; + queue.push_back(int(j)); + } + } + } + return dist; +} + +// Every swarm, then every worker, from colony 0's choices mapped by each colony's symmetry: +// its footprint centred on its home corner (the image of a footprint needs its own top-left +// anchor, since a turn or mirror moves which corner is top-left), and a random choice of the +// free tiles touching it. +bool placeColonies(Game &game, GenerationContext &context, const Arena &a, const Layout &l) { + context.stage = "settlement"; + Map &map = game.map; + const Symmetry &s = a.symmetry; + const int w = a.width, h = a.height; + const int type = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + const BuildingType *swarm = globalContainer->buildingsTypes.get(type); + if (!swarm) { + context.detail = "missing swarm type"; + return false; + } + const int x0 = l.homeU - swarm->width / 2, y0 = l.homeV - swarm->height / 2; + std::vector buildings; + for (int team = 0; team < a.teams; ++team) { + const auto [x, y] = s.anchor(team, x0, y0, swarm->width, swarm->height); + Building *building = game.checkRoomForBuilding(x, y, swarm, team, false) + ? game.addBuilding(x, y, type, team, 1, 0) + : nullptr; + if (!building) { + context.detail = "Colony " + std::to_string(team) + ": the swarm does not fit its home"; + return false; + } + buildings.push_back(building); + } + std::vector> ring; + for (int dy = -1; dy <= swarm->height; ++dy) + for (int dx = -1; dx <= swarm->width; ++dx) { + if (dx >= 0 && dx < swarm->width && dy >= 0 && dy < swarm->height) + continue; + const int x = wrap(x0 + dx, w), y = wrap(y0 + dy, h); + if (map.isFreeForGroundUnit(x, y, false, Team::teamNumberToMask(0))) + ring.emplace_back(x, y); + } + const int workers = context.request.nbWorkers; + if (int(ring.size()) < workers) { + context.detail = "need " + std::to_string(workers) + " worker tiles; found " + + std::to_string(ring.size()); + return false; + } + for (int i = 0; i < workers; ++i) + std::swap(ring[size_t(i)], ring[size_t(i) + context.bounded("starts", ring.size() - i)]); + for (int team = 0; team < a.teams; ++team) { + for (int i = 0; i < workers; ++i) { + const int tile = s.tile(team, ring[size_t(i)].first, ring[size_t(i)].second); + if (!game.addUnit(tile % w, tile / w, team, WORKER, 0, 0, 0, 0)) { + context.detail = "Colony " + std::to_string(team) + ": worker placement failed"; + return false; + } + } + Team *colony = game.teams[team]; + colony->startPosX = buildings[size_t(team)]->posX; + colony->startPosY = buildings[size_t(team)]->posY; + colony->startPosSet = Team::START_POS_FROM_SWARM; + context.bootX[team] = colony->startPosX; + context.bootY[team] = colony->startPosY; + colony->createLists(); + } + return true; +} + +// The fruit of the orchard tree at lattice position (ti, tj), for one of three patterns. Each is +// symmetric in ti and tj, so a quarter turn or diagonal mirror, which swaps them, leaves every +// tree's fruit unchanged. +int orchardFruit(int ti, int tj, int pattern) { + const int lo = std::min(ti, tj), hi = std::max(ti, tj); + return CHERRY + (pattern == 0 ? lo + hi : pattern == 1 ? hi : lo + 2 * hi) % 3; +} + +bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layout &l, + const SymmetricArenaOptions &o, const Terrain &t) { + context.stage = "arena resources"; + Map &map = game.map; + const Symmetry &s = a.symmetry; + const int w = a.width, h = a.height; + const size_t n = size_t(w) * h; + const double richness = o.richness / 100.0; + + // Ground kept free of deposits: every home's clear ground, both approaches to every causeway, + // and a lane along every walking route. + std::vector grass(n), water(n), land(n), homeClear(n), landing(n), blocked(n); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + grass[i] = map.isGrass(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; + water[i] = map.isWater(x, y); + land[i] = !water[i]; + const Point p = tilePoint(w, h, x, y); + homeClear[i] = torusDistance(w, h, p, l.home) <= kHomeRadius + 1.5; + landing[i] = onCauseway(a, l, p, 6.0, a.causewayWidth / 2 + 2); + } + homeClear = stamp(s, homeClear, false); + landing = stamp(s, landing, false); + const auto onPath = [&](int u, int v) { return t.paths[size_t(wrap(v, h)) * w + wrap(u, w)] != 0; }; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + blocked[i] = homeClear[i] || landing[i] || onPath(x, y) || onPath(x + 1, y) || + onPath(x, y + 1) || onPath(x + 1, y + 1); + } + const std::vector waterSteps = stepsFrom(w, h, water), landSteps = stepsFrom(w, h, land); + const float coarse = std::max(8.0f, std::min(w, h) / 10.0f); + std::vector plan(n, -1); + + // The ambient layer outside the arena: farmland on lake and pond shores (wheat to wood 2:1), + // stone outcrops away from the water, and algae off the shores. + std::vector farm(n, 0), rock(n, 0), alga(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + const bool open = grass[i] && !blocked[i] && + !within(tileRadius2(w, h, x, y), a.centre + a.moat + 2); + farm[i] = open && waterSteps[i] >= 2 && waterSteps[i] <= 7; + rock[i] = open && waterSteps[i] >= 3; + alga[i] = water[i] && landSteps[i] >= 2; + } + const auto farmland = topShare(orbitNoise(context, s, "arena-farmland", coarse, false), farm, + std::min(0.9, 0.3 * richness), true); + const auto wood = topShare(orbitNoise(context, s, "arena-split", 6.0f, false), farmland, + 1.0 / 3, true); + const auto outcrops = topShare(orbitNoise(context, s, "arena-stone", coarse, false), rock, + 0.012 * richness, true); + const auto algae = topShare(orbitNoise(context, s, "arena-algae", coarse, false), alga, + 0.1 * richness, true); + for (size_t i = 0; i < n; ++i) { + if (farmland[i]) + plan[i] = wood[i] ? WOOD : CORN; + if (outcrops[i]) + plan[i] = STONE; + if (algae[i]) + plan[i] = ALGA; + } + + // The orchard: single trees on a three-tile lattice squared up to the centre, so every tree + // has walkway on all four sides and none can wall another in; a fifth of them are stone. + // Lattice phase and fruit pattern come from the seed. A small centre holds few trees, so if + // that choice leaves a fruit out, the first combination that has all three is used instead. + const std::vector orchardNoise = orbitNoise(context, s, "arena-orchard", 4.0f, false); + bool planted = false; + for (int attempt = -1; attempt < 18 && !planted; ++attempt) { + const int phase = attempt < 0 ? l.orchardPhase : attempt / 6 % 3; + const int pattern = attempt < 0 ? l.orchardPattern : attempt / 2 % 3; + std::vector trees(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + const int col = (std::abs(2 * x + 1 - w) - 1) / 2, row = (std::abs(2 * y + 1 - h) - 1) / 2; + trees[i] = grass[i] && !blocked[i] && within(tileRadius2(w, h, x, y), a.centre - 2) && + col % 3 == phase && row % 3 == phase; + } + const auto stones = attempt < 0 || attempt % 2 == 0 ? topShare(orchardNoise, trees, 0.2, true) + : std::vector(n, 0); + std::vector orchard(n, -1); + bool seen[3] = {false, false, false}; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + if (!trees[i]) + continue; + const int col = (std::abs(2 * x + 1 - w) - 1) / 2, row = (std::abs(2 * y + 1 - h) - 1) / 2; + orchard[i] = stones[i] ? STONE : orchardFruit(col / 3, row / 3, pattern); + if (!stones[i]) + seen[orchard[i] - CHERRY] = true; + } + if (seen[0] && seen[1] && seen[2]) { + planted = true; + for (size_t i = 0; i < n; ++i) + if (orchard[i] >= 0) + plan[i] = orchard[i]; + } + } + if (!planted) { + context.detail = "the centre is too small to hold all three fruits"; + return false; + } + + // Colony 0's starting kit, grown tile by tile from seeds on its pond's banks - wheat on one + // side, wood on the other - with a little stone round the side of its home. Stamped over the + // ambient layer, with a fixed priority wherever two colonies' kits overlap. + std::vector kit(n, -1); + std::vector kitGround(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + kitGround[i] = grass[i] && !blocked[i] && waterSteps[i] >= 2 && + torusDistance(w, h, tilePoint(w, h, x, y), l.home) <= kHomeRadius + 12; + } + const auto grow = [&](Point target, int type, int size) { + int seed = -1; + double nearest = 1e18; + for (size_t i = 0; i < n; ++i) + if (kitGround[i] && kit[i] < 0) { + const double d = torusDistance(w, h, tilePoint(w, h, int(i % w), int(i / w)), target); + if (d < nearest) { + nearest = d; + seed = int(i); + } + } + if (seed < 0) + return 0; + static const int steps[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + std::vector queue{seed}; + std::vector queued(n, 0); + queued[size_t(seed)] = 1; + int placed = 0; + for (size_t head = 0; head < queue.size() && placed < size; ++head, ++placed) { + const int i = queue[head], x = i % w, y = i / w; + kit[size_t(i)] = type; + for (const auto &step : steps) { + const size_t j = size_t(wrap(y + step[1], h)) * w + wrap(x + step[0], w); + if (!queued[j] && kitGround[j] && kit[j] < 0) { + queued[j] = 1; + queue.push_back(int(j)); + } + } + } + return placed; + }; + const Point side{-std::sin(l.pondAngle), std::cos(l.pondAngle)}; + const double stoneAngle = l.pondAngle + 2 * kPi / 3; + const int wheatKit = grow({l.pond.x + 4.5 * side.x, l.pond.y + 4.5 * side.y}, CORN, kKitFarmland); + const int woodKit = grow({l.pond.x - 4.5 * side.x, l.pond.y - 4.5 * side.y}, WOOD, kKitFarmland); + const int stoneKit = grow({l.home.x + (kHomeRadius + 3) * std::cos(stoneAngle), + l.home.y + (kHomeRadius + 3) * std::sin(stoneAngle)}, + STONE, kKitStone); + if (wheatKit < kKitFarmland / 2 || woodKit < kKitFarmland / 2 || stoneKit < 1) { + context.detail = "colony 0's starting kit has no room beside its pond"; + return false; + } + const auto rank = [](int type) { return type == STONE ? 3 : type == WOOD ? 2 : type == CORN ? 1 : 0; }; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + int best = -1; + for (int e = 0; e < s.order(); ++e) { + const int type = kit[size_t(s.tile(e, x, y))]; + if (rank(type) > rank(best)) + best = type; + } + if (best >= 0) + plan[size_t(y) * w + x] = best; + } + + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (plan[size_t(y) * w + x] >= 0) + map.setResource(x, y, plan[size_t(y) * w + x], 1); + // The engine draws each deposit's amount and look from the gameplay RNG. Every orbit takes + // its lowest-indexed tile's, so deposits are symmetric in size as well as type. + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const int i = y * w + x; + int first = i; + for (int e = 1; e < s.order(); ++e) + first = std::min(first, s.tile(e, x, y)); + if (first == i) + continue; + const Resource source = map.getResource(size_t(first)); + Resource &target = map.getResource(size_t(i)); + if (source.type != target.type) { + context.detail = "deposits differ across the orbit of (" + std::to_string(x) + ", " + + std::to_string(y) + ")"; + return false; + } + target = source; + } + return true; +} + +bool generate(Game &game, GenerationContext &context) { + context.stage = "arena layout"; + const SymmetricArenaOptions o(context.request); + const Arena a = arenaFor(context.request); + Map &map = game.map; + map.makeHomogenMap(WATER); + for (int i = 0; i < a.teams; ++i) + game.addTeam(); + if (a.teams < 2 || a.symmetry.order() != a.teams) { + context.detail = "no symmetry gives this many colonies the same ground"; + return false; + } + Fit fit; + const std::vector homes = homeCandidates(a, fit); + if (homes.empty()) { + context.detail = "no home position fits these settings"; + return false; + } + const Home home = chooseHome(homes, context, std::min(a.width, a.height)); + Layout l; + l.homeU = home.u; + l.homeV = home.v; + l.home = cornerPoint(a.width, a.height, home.u, home.v); + l.gates = gateAngles(a, l.home); + // The pond lies away from the centre, turned up to 60 degrees either way. + l.pondAngle = + std::atan2(l.home.y, l.home.x) + (int(context.bounded("layout", 121)) - 60) * kPi / 180; + l.pond = {l.home.x + kPondDistance * std::cos(l.pondAngle), + l.home.y + kPondDistance * std::sin(l.pondAngle)}; + l.orchardPhase = int(context.bounded("layout", 3)); + l.orchardPattern = int(context.bounded("layout", 3)); + Terrain t; + return buildTerrain(map, context, a, l, o, t) && placeColonies(game, context, a, l) && + furnish(game, context, a, l, o, t); +} + +// The class of a tile's graphic (Map::lookup): grass, grass and sand, sand, sand and water, +// water. It depends only on which terrains a tile's corners hold, not on which corner holds +// which, so it is unchanged by any symmetry. +int terrainClass(Uint16 terrain) { + return terrain < 16 ? 0 : terrain < 128 ? 1 : terrain < 144 ? 2 : terrain < 256 ? 3 : 4; +} + +std::string at(int x, int y) { + return " at (" + std::to_string(x) + ", " + std::to_string(y) + ")"; +} + +// The arena's guarantees, checked on the finished world rather than trusted: every symmetry of +// the request maps undermap corners, tile terrain, deposits (type and amount), buildings and +// units onto themselves with the colonies permuted one to one; those permutations carry any +// colony onto any other; and every colony walks to wheat, wood, each fruit and open ground in +// the orchard, all in the same number of steps. Water, buildings and every resource block the +// walk; units don't, since they move. +std::string validateWorld(const Game &game, const GenerationContext &context) { + const Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + const size_t n = size_t(w) * h; + const Arena a = arenaFor(context.request); + const Symmetry &s = a.symmetry; + if (s.order() != teams) + return "No symmetry gives " + std::to_string(teams) + " colonies the same ground."; + std::vector> permutations; + for (int e = 1; e < s.order(); ++e) { + const std::string under = " is not symmetric under symmetry " + std::to_string(e) + "."; + std::vector image(size_t(teams), -1); + const auto relate = [&](int from, int to) { + if (from < 0 || from >= teams || to < 0 || to >= teams) + return false; + if (image[size_t(from)] < 0) + image[size_t(from)] = to; + return image[size_t(from)] == to; + }; + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) { + const int c = s.corner(e, u, v); + if (map.getUMTerrain(u, v) != map.getUMTerrain(c % w, c / w)) + return "Undermap corner" + at(u, v) + under; + } + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const int q = s.tile(e, x, y), qx = q % w, qy = q / w; + if (terrainClass(map.getTerrain(x, y)) != terrainClass(map.getTerrain(qx, qy))) + return "Terrain" + at(x, y) + under; + const Resource &ra = map.getResource(x, y), &rb = map.getResource(qx, qy); + if (ra.type != rb.type || ra.amount != rb.amount) + return "Deposit" + at(x, y) + under; + const Uint16 ba = map.getBuilding(x, y), bb = map.getBuilding(qx, qy); + if ((ba == NOGBID) != (bb == NOGBID)) + return "Building" + at(x, y) + under; + if (ba != NOGBID && + (!relate(Building::GIDtoTeam(ba), Building::GIDtoTeam(bb)) || + game.teams[Building::GIDtoTeam(ba)]->myBuildings[Building::GIDtoID(ba)]->type != + game.teams[Building::GIDtoTeam(bb)]->myBuildings[Building::GIDtoID(bb)]->type)) + return "Building" + at(x, y) + under; + const Uint16 ua = map.getGroundUnit(x, y), ub = map.getGroundUnit(qx, qy); + if ((ua == NOGUID) != (ub == NOGUID)) + return "Unit" + at(x, y) + under; + if (ua != NOGUID && + (!relate(Unit::GIDtoTeam(ua), Unit::GIDtoTeam(ub)) || + game.teams[Unit::GIDtoTeam(ua)]->myUnits[Unit::GIDtoID(ua)]->typeNum != + game.teams[Unit::GIDtoTeam(ub)]->myUnits[Unit::GIDtoID(ub)]->typeNum)) + return "Unit" + at(x, y) + under; + } + std::vector hit(size_t(teams), 0); + for (int team = 0; team < teams; ++team) { + const int to = image[size_t(team)]; + if (to < 0 || hit[size_t(to)]) + return "Symmetry " + std::to_string(e) + " does not map colonies one to one."; + hit[size_t(to)] = 1; + } + permutations.push_back(image); + } + std::vector carried{0}; + std::vector seen(size_t(teams), 0); + seen[0] = 1; + for (size_t head = 0; head < carried.size(); ++head) + for (const auto &image : permutations) + if (!seen[size_t(image[size_t(carried[head])])]) { + seen[size_t(image[size_t(carried[head])])] = 1; + carried.push_back(image[size_t(carried[head])]); + } + if (int(carried.size()) != teams) + return "The symmetries do not carry colony 0 onto every other colony."; + + static const char *const targets[] = {"wheat", "wood", "cherries", "oranges", "prunes", + "the orchard"}; + std::vector walkable(n); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + walkable[size_t(y) * w + x] = !map.isWater(x, y) && !map.isResource(x, y) && + map.getBuilding(x, y) == NOGBID; + std::array first{}; + for (int team = 0; team < teams; ++team) { + std::vector dist(n, -1), queue; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) { + dist[size_t(y) * w + x] = 0; + queue.push_back(y * w + x); + } + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); + if (dist[j] < 0 && walkable[j]) { + dist[j] = dist[size_t(queue[head])] + 1; + queue.push_back(int(j)); + } + } + } + std::array steps; + steps.fill(-1); + const auto reach = [&](int slot, int d) { + if (steps[size_t(slot)] < 0 || d < steps[size_t(slot)]) + steps[size_t(slot)] = d; + }; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const int d = dist[size_t(y) * w + x]; + if (d < 0) + continue; + if (within(tileRadius2(w, h, x, y), a.centre - 2)) + reach(5, d); + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int type = map.getResource(wrap(x + dx, w), wrap(y + dy, h)).type; + if (type == CORN) + reach(0, d + 1); + else if (type == WOOD) + reach(1, d + 1); + else if (type >= CHERRY && type <= CHERRY + 2) + reach(2 + type - CHERRY, d + 1); + } + } + for (size_t k = 0; k < steps.size(); ++k) { + if (steps[k] < 0) + return "Colony " + std::to_string(team) + " cannot walk to " + targets[k] + "."; + if (team > 0 && steps[k] != first[k]) + return "Colony " + std::to_string(team) + " walks " + std::to_string(steps[k]) + + " steps to " + targets[k] + ", colony 0 walks " + std::to_string(first[k]) + "."; + } + if (team == 0) + first = steps; + } + return ""; +} +} // namespace + +SymmetricArenaOptions::SymmetricArenaOptions(const GenerationRequest &r) + : centreSize(r.option("centre-size")), moatWidth(r.option("moat-width")), + causewayWidth(r.option("causeway-width")), causeways(r.option("causeways")), + lakes(r.option("lakes")), richness(r.option("richness")) {} + +GeneratorDefinition symmetricArenaDefinition() { + return {"symmetric-arena", + 15, + "Symmetric arena", + 1, + false, + // centre-size: the orchard island's radius as a share of the shorter side (over a + // small floor); moat and causeway widths are in tiles; causeways per colony, one + // straight towards it or two flanking it; lakes as a share of the open land outside + // the arena; richness scales the ambient deposits, never a home's own kit. + {{"centre-size", "Centre size", 6, 16, 1, 10, ControlGroup::Terrain}, + {"moat-width", "Moat width", 3, 10, 1, 5, ControlGroup::Terrain}, + {"causeway-width", "Causeway width", 2, 8, 1, 4, ControlGroup::Layout}, + {"causeways", "Causeways", 1, 2, 1, 1, ControlGroup::Layout}, + {"lakes", "Lakes", 0, 40, 5, 20, ControlGroup::Terrain}, + {"richness", "Resource richness", 0, 200, 25, 100, ControlGroup::Resources}}, + generate, + true, + validate, + validateWorld}; +} diff --git a/src/map/generator/generators/SymmetricArenaGenerator.h b/src/map/generator/generators/SymmetricArenaGenerator.h new file mode 100644 index 000000000..069b8d6b3 --- /dev/null +++ b/src/map/generator/generators/SymmetricArenaGenerator.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct SymmetricArenaOptions { + int centreSize, moatWidth, causewayWidth, causeways, lakes, richness; + explicit SymmetricArenaOptions(const GenerationRequest &r); +}; +GeneratorDefinition symmetricArenaDefinition(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index c2405530a..f9182aaf1 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -216,7 +216,7 @@ class MapGeneratorDefaultsTest assert(screen.descriptor.option("test-cell") == 16 && screen.descriptor.option("test-gap") == 5); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 12); + assert(registry.selectionIndex(101) == 13); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); From cc471d0cc9e6cf651039e1eecb5be21b09ccf4b3 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:23:46 -0400 Subject: [PATCH 049/136] Symmetric arena: shortest walking routes, orchard groves, roomier home ponds Routes to the causeways are now true shortest paths with lake crossings expensive, rather than any land route, so their deposit-free lanes no longer wander over the orchard and pond banks. The orchard plants two-by-two groves on a four-tile lattice, with stone taking whole groves. Lakes keep clear of each pond's banks, lakes too small to hold open water are dropped, and a starting kit carries on growing from the next free tile when a pocket fills. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generators/SymmetricArenaGenerator.cpp | 125 +++++++++++++----- 1 file changed, 89 insertions(+), 36 deletions(-) diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index 2a07de070..6186ac66e 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -418,21 +418,50 @@ bool carvePaths(const Arena &a, const Layout &l, Terrain &t) { return true; } +// The 8-connected groups of set entries in a mask, on the torus. Which entries share a group is +// a matter of geometry alone, so every symmetry maps groups onto groups of the same size. +std::vector> groupsOf(const std::vector &mask, int w, int h) { + std::vector> groups; + std::vector seen(mask.size(), 0); + for (size_t first = 0; first < mask.size(); ++first) { + if (!mask[first] || seen[first]) + continue; + std::vector group{int(first)}; + seen[first] = 1; + for (size_t head = 0; head < group.size(); ++head) { + const int x = group[head] % w, y = group[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); + if (mask[j] && !seen[j]) { + seen[j] = 1; + group.push_back(int(j)); + } + } + } + groups.push_back(std::move(group)); + } + return groups; +} + bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const Layout &l, const SymmetricArenaOptions &o, Terrain &t) { + // A lake of fewer corners than this is a patch of shoreline with no open water in it. + constexpr size_t kSmallestLake = 16; context.stage = "arena terrain"; const int w = a.width, h = a.height; const size_t n = size_t(w) * h; const Symmetry &s = a.symmetry; + // Lakes keep clear of every home and of its pond's banks, where the starting kit grows. std::vector homeDisc(n, 0), homeZone(n, 0), pond(n, 0), gate(n, 0); for (int v = 0; v < h; ++v) for (int u = 0; u < w; ++u) { const size_t i = size_t(v) * w + u; const Point p = cornerPoint(w, h, u, v); - const double toHome = torusDistance(w, h, p, l.home); + const double toHome = torusDistance(w, h, p, l.home), toPond = torusDistance(w, h, p, l.pond); homeDisc[i] = toHome <= kHomeRadius; - homeZone[i] = toHome <= kHomeRadius + 4; - pond[i] = torusDistance(w, h, p, l.pond) <= kPondRadius; + homeZone[i] = toHome <= kHomeRadius + 4 || toPond <= kPondRadius + 7; + pond[i] = toPond <= kPondRadius; gate[i] = onCauseway(a, l, p, 3.0, a.causewayWidth / 2); } homeDisc = stamp(s, homeDisc, true); @@ -450,9 +479,13 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La lakeEligible[i] = !within(r2, a.centre + a.moat + kApron) && !homeZone[i]; } const float smoothing = std::max(12.0f, std::min(w, h) / 8.0f); - const std::vector lakes = + std::vector lakes = topShare(orbitNoise(context, s, "arena-lakes", smoothing, true), lakeEligible, o.lakes / 100.0, false); + for (const auto &group : groupsOf(lakes, w, h)) + if (group.size() < kSmallestLake) + for (int i : group) + lakes[size_t(i)] = 0; // Later layers win: lakes and ponds, then clear home ground, the moat, and causeways of sand. t.undermap.assign(n, GRASS); @@ -656,24 +689,42 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou plan[i] = ALGA; } - // The orchard: single trees on a three-tile lattice squared up to the centre, so every tree - // has walkway on all four sides and none can wall another in; a fifth of them are stone. - // Lattice phase and fruit pattern come from the seed. A small centre holds few trees, so if - // that choice leaves a fruit out, the first combination that has all three is used instead. + // The orchard: two-by-two groves on a four-tile lattice squared up to the centre, so every + // grove has walkway at least two tiles wide on all four sides and every tree can be picked; + // a fifth of the groves are stone. Lattice phase and fruit pattern come from the seed. A small + // centre holds few groves, so if that choice leaves a fruit out, the first combination that + // has all three is used instead. const std::vector orchardNoise = orbitNoise(context, s, "arena-orchard", 4.0f, false); bool planted = false; for (int attempt = -1; attempt < 18 && !planted; ++attempt) { - const int phase = attempt < 0 ? l.orchardPhase : attempt / 6 % 3; + // Phase 1 gives even walkways, 2 an avenue along each axis, 3 a grove on the centre; 0 + // would join the four groves round the centre into one block with unpickable middle trees. + const int phase = attempt < 0 ? l.orchardPhase : 1 + attempt / 6 % 3; const int pattern = attempt < 0 ? l.orchardPattern : attempt / 2 % 3; + // A tile's grove index along one axis from its doubled centred coordinate, or -1 on a + // walkway. It depends only on the distance from the axis, so turns and mirrors keep it. + const auto grove = [phase](int doubled) { + const int i = (std::abs(doubled) - 1) / 2 + 4 - phase; + return i % 4 < 2 ? i / 4 : -1; + }; std::vector trees(n, 0); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { const size_t i = size_t(y) * w + x; - const int col = (std::abs(2 * x + 1 - w) - 1) / 2, row = (std::abs(2 * y + 1 - h) - 1) / 2; trees[i] = grass[i] && !blocked[i] && within(tileRadius2(w, h, x, y), a.centre - 2) && - col % 3 == phase && row % 3 == phase; + grove(2 * x + 1 - w) >= 0 && grove(2 * y + 1 - h) >= 0; } - const auto stones = attempt < 0 || attempt % 2 == 0 ? topShare(orchardNoise, trees, 0.2, true) + // A grove's value is the highest orbit-noise value among its tiles, which its images share, + // so stone always takes whole groves. + std::vector groveValue(n, INT_MIN); + for (const auto &group : groupsOf(trees, w, h)) { + int best = INT_MIN; + for (int i : group) + best = std::max(best, orchardNoise[size_t(i)]); + for (int i : group) + groveValue[size_t(i)] = best; + } + const auto stones = attempt < 0 || attempt % 2 == 0 ? topShare(groveValue, trees, 0.2, true) : std::vector(n, 0); std::vector orchard(n, -1); bool seen[3] = {false, false, false}; @@ -682,8 +733,8 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou const size_t i = size_t(y) * w + x; if (!trees[i]) continue; - const int col = (std::abs(2 * x + 1 - w) - 1) / 2, row = (std::abs(2 * y + 1 - h) - 1) / 2; - orchard[i] = stones[i] ? STONE : orchardFruit(col / 3, row / 3, pattern); + orchard[i] = stones[i] ? STONE + : orchardFruit(grove(2 * x + 1 - w), grove(2 * y + 1 - h), pattern); if (!stones[i]) seen[orchard[i] - CHERRY] = true; } @@ -710,32 +761,34 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou kitGround[i] = grass[i] && !blocked[i] && waterSteps[i] >= 2 && torusDistance(w, h, tilePoint(w, h, x, y), l.home) <= kHomeRadius + 12; } + // A clump grows outward from the free kit ground nearest its target; if that pocket fills + // first, it carries on from the next nearest free tile, so a cramped bank still gets its kit. const auto grow = [&](Point target, int type, int size) { - int seed = -1; - double nearest = 1e18; + std::vector> nearest; for (size_t i = 0; i < n; ++i) - if (kitGround[i] && kit[i] < 0) { - const double d = torusDistance(w, h, tilePoint(w, h, int(i % w), int(i / w)), target); - if (d < nearest) { - nearest = d; - seed = int(i); - } - } - if (seed < 0) - return 0; + if (kitGround[i] && kit[i] < 0) + nearest.emplace_back( + torusDistance(w, h, tilePoint(w, h, int(i % w), int(i / w)), target), int(i)); + std::sort(nearest.begin(), nearest.end()); static const int steps[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; - std::vector queue{seed}; std::vector queued(n, 0); - queued[size_t(seed)] = 1; int placed = 0; - for (size_t head = 0; head < queue.size() && placed < size; ++head, ++placed) { - const int i = queue[head], x = i % w, y = i / w; - kit[size_t(i)] = type; - for (const auto &step : steps) { - const size_t j = size_t(wrap(y + step[1], h)) * w + wrap(x + step[0], w); - if (!queued[j] && kitGround[j] && kit[j] < 0) { - queued[j] = 1; - queue.push_back(int(j)); + for (const auto &candidate : nearest) { + if (placed >= size) + break; + if (queued[size_t(candidate.second)]) + continue; + std::vector queue{candidate.second}; + queued[size_t(candidate.second)] = 1; + for (size_t head = 0; head < queue.size() && placed < size; ++head, ++placed) { + const int i = queue[head], x = i % w, y = i / w; + kit[size_t(i)] = type; + for (const auto &step : steps) { + const size_t j = size_t(wrap(y + step[1], h)) * w + wrap(x + step[0], w); + if (!queued[j] && kitGround[j] && kit[j] < 0) { + queued[j] = 1; + queue.push_back(int(j)); + } } } } @@ -820,7 +873,7 @@ bool generate(Game &game, GenerationContext &context) { std::atan2(l.home.y, l.home.x) + (int(context.bounded("layout", 121)) - 60) * kPi / 180; l.pond = {l.home.x + kPondDistance * std::cos(l.pondAngle), l.home.y + kPondDistance * std::sin(l.pondAngle)}; - l.orchardPhase = int(context.bounded("layout", 3)); + l.orchardPhase = 1 + int(context.bounded("layout", 3)); l.orchardPattern = int(context.bounded("layout", 3)); Terrain t; return buildTerrain(map, context, a, l, o, t) && placeColonies(game, context, a, l) && From 56d94e17a0cdae6f2cba1e34903d85525750ea08 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:27:27 -0400 Subject: [PATCH 050/136] Add the Stone highlands map generator (legacy id 14) Open grass valleys divided by thin stone ridgelines, joined by passes 3 to 5 tiles wide, with a pond in every basin. Ridges follow the zero contour of a domain-warped cellular noise field; a random spanning tree of passes joins every valley and loopiness opens extra ones. Colonies take the roomiest valleys, one each while they last, beside their valley's pond, with identical 1:1 wheat and wood kits. The design is a pure function of the request, so validateWorld rebuilds it and checks that every ridge keeps its stone, every pass is open and leads through to both of its valleys, and every colony can walk to every other. guaranteeStartingResources gains an optional protected-wall mask so its wall clearing never removes designed walls. Existing call sites are unchanged; dumps and study output of generators 1-9, 11 and 12 for seeds 1-5 at defaults are byte-identical before and after. Controls: valley size 20-44 (32), pass width 3-5 (4), loopiness 0-100 (30), pond size 4-16 (8), fruit 0-12 (4). 50 consecutive default seeds generate, quality mean 0.565, min 0.502. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.en.txt | 12 + data/texts.keys.txt | 6 + src/SConscript | 1 + src/map/generator/core/GenerationRequest.h | 3 +- src/map/generator/core/GeneratorRegistry.cpp | 4 +- .../generators/StoneHighlandsGenerator.cpp | 1255 +++++++++++++++++ .../generators/StoneHighlandsGenerator.h | 9 + src/map/generator/shared/Resources.cpp | 26 +- src/map/generator/shared/Resources.h | 6 +- test/MapGeneratorDefaultsTest.cpp | 2 +- 10 files changed, 1311 insertions(+), 13 deletions(-) create mode 100644 src/map/generator/generators/StoneHighlandsGenerator.cpp create mode 100644 src/map/generator/generators/StoneHighlandsGenerator.h diff --git a/data/texts.en.txt b/data/texts.en.txt index 6b68bad6d..d5639b0d0 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1806,3 +1806,15 @@ Lake size Lake connects to fjords [Resource islands] Resource islands +[Stone highlands] +Stone highlands +[Valley size] +Valley size +[Pass width] +Pass width +[Pond size] +Pond size +[These valleys are too big for the map; use a bigger map or smaller valleys.] +These valleys are too big for the map; use a bigger map or smaller valleys. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +The highlands have too little room for this many colonies; use a bigger map or fewer colonies. diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 629e7cb34..bc2199ea0 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -901,3 +901,9 @@ [Lake size] [Lake connects to fjords] [Resource islands] +[Stone highlands] +[Valley size] +[Pass width] +[Pond size] +[These valleys are too big for the map; use a bigger map or smaller valleys.] +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] diff --git a/src/SConscript b/src/SConscript index da1ad0345..defac66cc 100644 --- a/src/SConscript +++ b/src/SConscript @@ -186,6 +186,7 @@ map/generator/generators/IslesGenerator.cpp map/generator/generators/RiverGenerator.cpp map/generator/generators/RuggedArchipelagoGenerator.cpp map/generator/generators/ShatteredCoastGenerator.cpp +map/generator/generators/StoneHighlandsGenerator.cpp map/generator/generators/SwampGenerator.cpp map/generator/generators/UniformGenerator.cpp map/generator/shared/Distances.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index d0342b85c..d5fa7d76d 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -28,7 +28,8 @@ struct GenerationRequest { eOLDISLANDS = 8, eCONTESTEDCOMMONS = 9, eMAZE = 11, - eFJORDCONTINENT = 12 + eFJORDCONTINENT = 12, + eSTONEHIGHLANDS = 14 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 28dddad67..ed52ede26 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -10,6 +10,7 @@ #include "RiverGenerator.h" #include "RuggedArchipelagoGenerator.h" #include "ShatteredCoastGenerator.h" +#include "StoneHighlandsGenerator.h" #include "SwampGenerator.h" #include "UniformGenerator.h" #include @@ -75,6 +76,7 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { shatteredCoastDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), mazeDefinition(), uniformDefinition()}); + riverDefinition(), mazeDefinition(), stoneHighlandsDefinition(), + uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp new file mode 100644 index 000000000..673a672b1 --- /dev/null +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -0,0 +1,1255 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "StoneHighlandsGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Resources.h" +#include "Settlements.h" +#include "Topology.h" +#include "Unit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Stone highlands: open grass valleys divided by thin ridgelines of stone, joined by narrow +// passes, with a pond in every basin. +// +// The ridgelines are the zero contour of a domain-warped cellular noise field: scattered basin +// sites, each tile labelled with its nearest site after the warp, and a stone tile wherever a +// tile touches a lower label. Stone is eternal and can't be cleared, so ridges never erode. +// Valleys are the walkable components that remain; a random spanning tree of passes joins them +// all, and loopiness opens extra passes for routes to flank along. Colonies take the roomiest +// valleys, one each while they last, and ponds fill the basins around them. The whole design is a +// pure function of the request, so validateWorld rebuilds it and checks the finished world. +namespace { + +// Enclosed pockets smaller than this become part of the ridge: too small to live in, and a pass +// into one would only lead to a dead end a few tiles deep. +constexpr int kPocketTiles = 40; +// A pass needs at least this many tiles of ridge shared by exactly its two valleys, and pass +// centres keep this far apart so two passes never merge into one wide gap. +constexpr int kMinimumBoundary = 3; +constexpr int kPassSpacing = 10; +// Ponds keep this far from any ridge or pass (falling back to the minimum in cramped valleys), so +// the ground along every ridgeline stays walkable and no pond can close a valley or a pass off. +// Below the minimum, a pond's sand would reach the corners of ridge tiles, which need grass. +constexpr int kPondClearance = 6; +constexpr int kPondMinimumClearance = 4; +// Valleys smaller than this get no pond; bigger ones get another pond per kPondArea tiles. +constexpr int kPondMinimumValley = 120; +constexpr int kPondArea = 1600; +constexpr int kPondSeparation = 16; +constexpr int kMinimumPond = 12; +// Farmland, fruit and starter kits keep this far from ridges and passes: the band along every +// ridgeline is the valley's ring road, which is what keeps every pass reachable. +constexpr int kRidgeRoad = 3; +// A swarm centre keeps at least kHomeRidge (or half its valley's depth) from any ridge, sits up +// to kHomeOffset tiles out from its valley's deepest point, and keeps ponds kHomePondGap away; +// its valley's first pond is centred about kHomePondReach from it, towards the interior. +constexpr int kHomeRidge = 5; +constexpr int kHomeOffset = 10; +constexpr int kHomePondGap = 8; +constexpr int kHomePondReach = 11; +// Colonies sharing a valley, once every roomy valley has one, keep this far apart. +constexpr int kHomeSeparation = 16; +// Every colony starts with the same wheat and wood, 1:1, a few steps from the swarm: each patch +// takes the kHomeKit free tiles of the home valley nearest its anchor. +constexpr int kHomeKit = 20; +constexpr int kKitReach = 7; +constexpr int kKitSearch = 16; +// Ambient farmland stays this far from a swarm centre, so no colony is ringed in by it. +constexpr int kHomeClear = 9; +constexpr int kFarmlandPercent = 110; // ambient farmland tiles per 100 pond tiles +constexpr int kAlgaePercent = 12; // of each pond's open water +constexpr int kFruitGrove = 6; +constexpr int kAreaPerColony = 1024; +constexpr int kFar = INT_MAX / 4; + +struct Torus { + int w, h; + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + // Signed shortest offset from a to b. + int offsetX(int a, int b) const { + const int d = x(b - a); + return d > w / 2 ? d - w : d; + } + int offsetY(int a, int b) const { + const int d = y(b - a); + return d > h / 2 ? d - h : d; + } + int dist2(int ax, int ay, int bx, int by) const { + const int dx = offsetX(ax, bx), dy = offsetY(ay, by); + return dx * dx + dy * dy; + } + int chebyshev(int ax, int ay, int bx, int by) const { + return std::max(std::abs(offsetX(ax, bx)), std::abs(offsetY(ay, by))); + } +}; + +template +void shuffle(std::vector &values, GenerationContext &context, const char *stream) { + for (size_t i = values.size(); i > 1; --i) + std::swap(values[i - 1], values[context.bounded(stream, std::uint32_t(i))]); +} + +// Smooth value noise that tiles the map's torus exactly: a lattice of random values about +// `period` tiles apart (a whole number of lattice cells spans the map), blended with a +// smoothstep. Integer arithmetic throughout. Values are 0..65535. +std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng) { + const int gw = std::max(1, (w + period / 2) / std::max(1, period)); + const int gh = std::max(1, (h + period / 2) / std::max(1, period)); + std::vector lattice(size_t(gw) * gh); + for (int &v : lattice) + v = int(rng() >> 16); + const auto smooth = [](int t) { + return int(std::int64_t(t) * t * (3 * 1024 - 2 * t) / (1024 * 1024)); + }; + std::vector field(size_t(w) * h); + for (int y = 0; y < h; ++y) { + const std::int64_t fy = std::int64_t(y) * gh * 1024 / h; + const int y0 = int(fy >> 10), y1 = (y0 + 1) % gh, ty = smooth(int(fy & 1023)); + for (int x = 0; x < w; ++x) { + const std::int64_t fx = std::int64_t(x) * gw * 1024 / w; + const int x0 = int(fx >> 10), x1 = (x0 + 1) % gw, tx = smooth(int(fx & 1023)); + const int a = lattice[size_t(y0) * gw + x0], b = lattice[size_t(y0) * gw + x1]; + const int c = lattice[size_t(y1) * gw + x0], d = lattice[size_t(y1) * gw + x1]; + const int top = a + (b - a) * tx / 1024, bottom = c + (d - c) * tx / 1024; + field[size_t(y) * w + x] = top + (bottom - top) * ty / 1024; + } + } + return field; +} + +// Octaves at period, period/2, ... weighted 2:1 per step. +std::vector fractalNoise(int w, int h, int period, int octaves, std::mt19937 &rng) { + std::vector sum(size_t(w) * h, 0); + int total = 0; + for (int octave = 0; octave < octaves; ++octave) { + const int weight = 1 << (octaves - 1 - octave); + const std::vector layer = periodicNoise(w, h, std::max(2, period >> octave), rng); + for (size_t i = 0; i < sum.size(); ++i) + sum[i] += layer[i] * weight; + total += weight; + } + for (int &v : sum) + v /= total; + return sum; +} + +// The value below which `percent` of the given samples fall. +int percentile(std::vector samples, int percent) { + if (samples.empty()) + return 0; + const size_t k = std::min(samples.size() - 1, samples.size() * size_t(percent) / 100); + std::nth_element(samples.begin(), samples.begin() + k, samples.end()); + return samples[k]; +} + +// Chebyshev steps from every tile to the nearest source tile; kFar with no sources. +std::vector chebyshevDistance(const Torus &t, const std::vector &source) { + std::vector distance(source.size(), kFar); + std::vector queue; + queue.reserve(source.size()); + for (size_t i = 0; i < source.size(); ++i) + if (source[i]) { + distance[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int q = t.at(px + dx, py + dy); + if (distance[q] == kFar) { + distance[q] = distance[p] + 1; + queue.push_back(q); + } + } + } + return distance; +} + +struct Site { + int x, y; +}; + +// Bucket rows or columns to search around bucket c: all of them when a window would wrap onto +// itself. +std::vector bucketWindow(int c, int count, int radius) { + std::vector result; + if (count <= 2 * radius + 1) { + for (int i = 0; i < count; ++i) + result.push_back(i); + } else { + for (int d = -radius; d <= radius; ++d) + result.push_back(((c + d) % count + count) % count); + } + return result; +} + +// Basin sites by dart throwing on the torus. Sites keep 0.83 of the requested spacing apart, +// which, once the map is saturated, leaves one site per spacing-squared tiles on average. +std::vector scatterSites(const Torus &t, int spacing, GenerationContext &context) { + const int minimum = std::max(4, spacing * 83 / 100); + const std::int64_t expected = + std::max(2, std::int64_t(t.w) * t.h / (std::int64_t(spacing) * spacing)); + const int gx = std::max(1, t.w / minimum), gy = std::max(1, t.h / minimum); + std::vector> columns(gx), rows(gy); + for (int c = 0; c < gx; ++c) + columns[c] = bucketWindow(c, gx, 1); + for (int r = 0; r < gy; ++r) + rows[r] = bucketWindow(r, gy, 1); + std::vector> buckets(size_t(gx) * gy); + std::vector sites; + for (std::int64_t attempt = 0; attempt < expected * 60; ++attempt) { + const int x = int(context.bounded("highlands-sites", t.w)); + const int y = int(context.bounded("highlands-sites", t.h)); + const int bx = x * gx / t.w, by = y * gy / t.h; + bool clear = true; + for (int row : rows[by]) { + for (int column : columns[bx]) { + for (int s : buckets[size_t(row) * gx + column]) + if (t.dist2(x, y, sites[s].x, sites[s].y) < minimum * minimum) { + clear = false; + break; + } + if (!clear) + break; + } + if (!clear) + break; + } + if (!clear) + continue; + buckets[size_t(by) * gx + bx].push_back(int(sites.size())); + sites.push_back({x, y}); + } + return sites; +} + +// Every tile's nearest site, measured from a warped copy of the tile so cell borders wander +// instead of running straight. Positions are in sixteenths of a tile. +std::vector labelCells(const Torus &t, const std::vector &sites, int spacing, + GenerationContext &context) { + const int period = spacing * 3 / 2; + const std::vector warpX = fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); + const std::vector warpY = fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); + const std::int64_t amplitude = std::int64_t(spacing) * 16 * 30 / 100; + const int W = t.w * 16, H = t.h * 16; + const int gx = std::max(1, t.w / spacing), gy = std::max(1, t.h / spacing); + std::vector> buckets(size_t(gx) * gy), columns(gx), rows(gy); + for (size_t s = 0; s < sites.size(); ++s) + buckets[size_t(sites[s].y * gy / t.h) * gx + sites[s].x * gx / t.w].push_back(int(s)); + for (int c = 0; c < gx; ++c) + columns[c] = bucketWindow(c, gx, 2); + for (int r = 0; r < gy; ++r) + rows[r] = bucketWindow(r, gy, 2); + std::vector label(size_t(t.w) * t.h, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const size_t i = size_t(y) * t.w + x; + int px = x * 16 + 8 + int((warpX[i] - 32768) * amplitude / 32768); + int py = y * 16 + 8 + int((warpY[i] - 32768) * amplitude / 32768); + px = ((px % W) + W) % W; + py = ((py % H) + H) % H; + const int bx = int(std::int64_t(px) * gx / W), by = int(std::int64_t(py) * gy / H); + std::int64_t best = INT64_MAX; + int nearest = 0; + for (int row : rows[by]) + for (int column : columns[bx]) + for (int s : buckets[size_t(row) * gx + column]) { + int dx = std::abs(px - (sites[s].x * 16 + 8)), dy = std::abs(py - (sites[s].y * 16 + 8)); + dx = std::min(dx, W - dx); + dy = std::min(dy, H - dy); + const std::int64_t d = std::int64_t(dx) * dx + std::int64_t(dy) * dy; + if (d < best || (d == best && s < nearest)) { + best = d; + nearest = s; + } + } + label[i] = nearest; + } + return label; +} + +struct Pass { + int x, y; // centre tile + int lo, hi; // the cleared box is [x + lo, x + hi] by [y + lo, y + hi] + int a, b; // the valleys it joins +}; + +struct Home { + int valley, x, y; // swarm centre +}; + +// The terrain design: a pure function of the request, rebuilt by validateWorld. +struct Layout { + Torus t{1, 1}; + std::vector ridge; // designed stone + std::vector valley; // valley id; -1 on ridge tiles + int valleys = 0; + std::vector passes; + std::vector passTile; // inside a pass's cleared box + std::vector ridgeDistance; // Chebyshev steps to the nearest ridge or pass tile + std::vector> tilesOf; // each valley's tiles outside pass boxes + std::vector pole; // each valley's tile deepest inside it + std::vector homes; + std::vector pond; // undermap water + std::string failure; +}; + +struct Disjoint { + std::vector parent; + explicit Disjoint(int count) : parent(count) { + for (int i = 0; i < count; ++i) + parent[i] = i; + } + int find(int v) { + while (parent[v] != v) { + parent[v] = parent[parent[v]]; + v = parent[v]; + } + return v; + } + bool join(int a, int b) { + a = find(a); + b = find(b); + if (a == b) + return false; + parent[b] = a; + return true; + } +}; + +// Distinct valley ids within Chebyshev radius r of a tile, sorted, at most `limit` of them. +int nearbyValleys(const Layout &L, int x, int y, int r, int ids[], int limit) { + int count = 0; + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) { + const int v = L.valley[L.t.at(x + dx, y + dy)]; + if (v < 0 || std::find(ids, ids + count, v) != ids + count) + continue; + if (count == limit) + return count; + ids[count++] = v; + } + std::sort(ids, ids + count); + return count; +} + +// Whether valleys a and b meet inside the window around (cx, cy), walking through non-ridge +// tiles. +bool joins(const Layout &L, int cx, int cy, int a, int b, int radius) { + const int side = 2 * radius + 1; + std::vector seen(size_t(side) * side, 0); + std::vector queue; + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) + if (L.valley[L.t.at(cx + dx, cy + dy)] == a) { + const int local = (dy + radius) * side + dx + radius; + seen[local] = 1; + queue.push_back(local); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int lx = queue[head] % side, ly = queue[head] / side; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int nx = lx + dx, ny = ly + dy; + if (nx < 0 || ny < 0 || nx >= side || ny >= side || seen[size_t(ny) * side + nx]) + continue; + const int tile = L.t.at(cx + nx - radius, cy + ny - radius); + if (L.ridge[tile]) + continue; + if (L.valley[tile] == b) + return true; + seen[size_t(ny) * side + nx] = 1; + queue.push_back(ny * side + nx); + } + } + return false; +} + +// Cuts a pass of the given width through the ridge between valleys a and b. Sites far along the +// ridge from any junction come first, so a pass sits in the middle of its ridgeline; each try is +// kept only if it really joins the two valleys. +bool carvePass(Layout &L, GenerationContext &context, int a, int b, std::vector candidates, + const std::vector &alongRidge, int width) { + const Torus &t = L.t; + candidates.erase(std::remove_if(candidates.begin(), candidates.end(), + [&](int c) { + if (!L.ridge[c]) + return true; + for (const Pass &p : L.passes) + if (t.chebyshev(c % t.w, c / t.w, p.x, p.y) < kPassSpacing) + return true; + return false; + }), + candidates.end()); + if (candidates.empty()) + return false; + const auto score = [&](int c) { return alongRidge[c] < 0 ? kFar : alongRidge[c]; }; + std::stable_sort(candidates.begin(), candidates.end(), + [&](int p, int q) { return score(p) > score(q); }); + const std::int64_t best = score(candidates.front()); + size_t band = 0; + while (band < candidates.size() && std::int64_t(score(candidates[band])) * 4 >= best * 3) + ++band; + band = std::max(1, band); + const size_t first = context.bounded("highlands-passes", std::uint32_t(band)); + const int lo = -(width - 1) / 2, hi = width / 2; + for (size_t k = 0; k < candidates.size() && k < 12; ++k) { + const int c = candidates[k < band ? (first + k) % band : k]; + const int cx = c % t.w, cy = c / t.w; + std::vector cleared; + for (int dy = lo; dy <= hi; ++dy) + for (int dx = lo; dx <= hi; ++dx) { + const int i = t.at(cx + dx, cy + dy); + if (L.ridge[i]) { + L.ridge[i] = 0; + cleared.push_back(i); + } + } + if (joins(L, cx, cy, a, b, width + 4)) { + L.passes.push_back({cx, cy, lo, hi, a, b}); + for (int dy = lo; dy <= hi; ++dy) + for (int dx = lo; dx <= hi; ++dx) + L.passTile[t.at(cx + dx, cy + dy)] = 1; + return true; + } + for (int i : cleared) + L.ridge[i] = 1; + } + return false; +} + +// Homes by farthest-point spreading over the roomier valleys, one colony per valley while they +// last, then anywhere deep enough and well away from every other home. A valley's home site sits +// out from its deepest point, leaving that interior for the pond beside it. +bool chooseHomes(Layout &L, GenerationContext &context, int teams) { + const Torus &t = L.t; + std::vector site(L.valleys, -1), room(L.valleys, 0), rooms; + for (int v = 0; v < L.valleys; ++v) { + for (int i : L.tilesOf[v]) + if (L.ridgeDistance[i] >= kRidgeRoad) + ++room[v]; + const int pole = L.pole[v]; + if (pole < 0 || L.ridgeDistance[pole] < kHomeRidge) + continue; + // About halfway out from the pole towards the ridge: room to build on the ridge side, and + // the valley's interior left for the pond on the other. + const int depth = std::max(kHomeRidge, L.ridgeDistance[pole] / 2); + const int offset = std::min(kHomeOffset, L.ridgeDistance[pole] - depth); + int bestKey = INT_MAX; + for (int i : L.tilesOf[v]) { + if (L.ridgeDistance[i] < depth) + continue; + const double d = std::sqrt(double(t.dist2(i % t.w, i / t.w, pole % t.w, pole / t.w))); + const int key = std::abs(int(std::lround(d)) - offset) * 64 - L.ridgeDistance[i]; + if (key < bestKey) { + bestKey = key; + site[v] = i; + } + } + rooms.push_back(room[v]); + } + if (rooms.empty()) { + L.failure = "no valley is deep enough for a colony"; + return false; + } + // Colonies take the roomiest valleys, the 2 x colonies largest and none under half the median, + // so no colony starts cramped while a roomier valley goes spare. + std::vector ranked = rooms; + std::sort(ranked.begin(), ranked.end(), std::greater()); + const int roomFloor = + std::max(percentile(rooms, 50) / 2, + ranked[std::min(ranked.size(), size_t(2 * std::max(1, teams))) - 1]); + std::vector primary; + for (int v = 0; v < L.valleys; ++v) + if (site[v] >= 0 && room[v] >= roomFloor) + primary.push_back(v); + std::vector used(L.valleys, 0); + const auto spread = [&](int x, int y) { + int nearest = INT_MAX; + for (const Home &h : L.homes) + nearest = std::min(nearest, t.dist2(x, y, h.x, h.y)); + return nearest; + }; + const auto take = [&](int tile) { + used[L.valley[tile]] = 1; + L.homes.push_back({L.valley[tile], tile % t.w, tile / t.w}); + }; + if (teams > 0 && !primary.empty()) + take(site[primary[context.bounded("highlands-homes", std::uint32_t(primary.size()))]]); + while (int(L.homes.size()) < teams) { + int chosen = -1, chosenSpread = -1; + for (int v : primary) { + if (used[v]) + continue; + const int s = spread(site[v] % t.w, site[v] / t.w); + if (s > chosenSpread || (s == chosenSpread && room[v] > room[L.valley[chosen]])) { + chosen = site[v]; + chosenSpread = s; + } + } + if (chosen < 0) { + for (int v = 0; v < L.valleys; ++v) + for (int i : L.tilesOf[v]) { + const int x = i % t.w, y = i / t.w; + if (((x | y) & 1) || L.ridgeDistance[i] < kHomeRidge) + continue; + const int s = spread(x, y); + if (s > chosenSpread) { + chosen = i; + chosenSpread = s; + } + } + if (chosenSpread < kHomeSeparation * kHomeSeparation) + chosen = -1; + } + if (chosen < 0) { + L.failure = "no room for colony " + std::to_string(L.homes.size()) + " in any valley"; + return false; + } + take(chosen); + } + return true; +} + +// Grows one pond from its seed, always taking the lowest point of the depth field next, so it +// fills its hollow the way water would. Distance from the seed counts as depth too, which keeps +// the outline round rather than following the noise lattice into points. +void growPond(Layout &L, const std::vector &depth, const std::vector &eligible, + int eligibleStamp, std::vector &queued, int queuedStamp, int seed, int target) { + using Entry = std::pair; + const int sx = seed % L.t.w, sy = seed / L.t.w; + const auto key = [&](int tile) { + const double d = std::sqrt(double(L.t.dist2(sx, sy, tile % L.t.w, tile / L.t.w))); + return depth[tile] + int(std::lround(d * 1500)); + }; + std::priority_queue, std::greater> frontier; + frontier.push({key(seed), seed}); + queued[seed] = queuedStamp; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + int grown = 0; + while (!frontier.empty() && grown < target) { + const int tile = frontier.top().second; + frontier.pop(); + if (L.pond[tile]) + continue; + L.pond[tile] = 1; + ++grown; + for (const auto &s : steps) { + const int next = L.t.at(tile % L.t.w + s[0], tile / L.t.w + s[1]); + if (eligible[next] == eligibleStamp && !L.pond[next] && queued[next] != queuedStamp) { + queued[next] = queuedStamp; + frontier.push({key(next), next}); + } + } + } +} + +void placePonds(Layout &L, GenerationContext &context, int spacing, int pondSize) { + const Torus &t = L.t; + const int n = t.w * t.h; + const std::vector depth = + fractalNoise(t.w, t.h, std::max(8, spacing / 2), 2, context.stream("highlands-ponds")); + L.pond.assign(n, 0); + // Ground kept for every colony, and the home each valley's first pond is placed beside. + std::vector reserved(n, 0); + std::vector homeOf(L.valleys, -1); + for (size_t k = 0; k < L.homes.size(); ++k) { + const Home &h = L.homes[k]; + if (homeOf[h.valley] < 0) + homeOf[h.valley] = int(k); + for (int dy = -kHomePondGap; dy <= kHomePondGap; ++dy) + for (int dx = -kHomePondGap; dx <= kHomePondGap; ++dx) + if (dx * dx + dy * dy < kHomePondGap * kHomePondGap) + reserved[t.at(h.x + dx, h.y + dy)] = 1; + // And a five-wide way out, straight down to the band along the ridges, so however the ponds + // grow around a home they can never close it in. + int x = h.x, y = h.y; + while (L.ridgeDistance[t.at(x, y)] > 2) { + int nextX = x, nextY = y; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (L.ridgeDistance[t.at(x + dx, y + dy)] < L.ridgeDistance[t.at(nextX, nextY)]) { + nextX = x + dx; + nextY = y + dy; + } + x = nextX; + y = nextY; + for (int dy = -2; dy <= 2; ++dy) + for (int dx = -2; dx <= 2; ++dx) + reserved[t.at(x + dx, y + dy)] = 1; + } + } + std::vector eligible(n, -1), queued(n, -1); + int queuedStamp = 0; + for (int v = 0; v < L.valleys; ++v) { + const std::vector &tiles = L.tilesOf[v]; + if (int(tiles.size()) < kPondMinimumValley) + continue; + std::vector room; + for (int clearance = kPondClearance; + clearance >= kPondMinimumClearance && int(room.size()) < kMinimumPond; --clearance) { + room.clear(); + for (int i : tiles) + if (L.ridgeDistance[i] >= clearance && !reserved[i]) + room.push_back(i); + } + if (int(room.size()) < kMinimumPond) + continue; + for (int i : room) + eligible[i] = v; + // A colony's pond lies beside it, towards the middle of its valley. + int targetX = -1, targetY = -1; + if (homeOf[v] >= 0) { + const Home &h = L.homes[homeOf[v]]; + const int ox = t.offsetX(h.x, L.pole[v] % t.w), oy = t.offsetY(h.y, L.pole[v] / t.w); + const double length = std::sqrt(double(ox * ox + oy * oy)); + const double ux = length < 1 ? 0.0 : ox / length, uy = length < 1 ? 1.0 : oy / length; + targetX = t.x(h.x + int(std::lround(ux * kHomePondReach))); + targetY = t.y(h.y + int(std::lround(uy * kHomePondReach))); + } + const int area = int(tiles.size()); + const int target = std::max(kMinimumPond, area * pondSize / 100); + const int count = std::max(1, std::min(1 + area / kPondArea, int(room.size()) / (2 * kMinimumPond))); + const int each = std::max(kMinimumPond, target / count); + std::vector seeds; + for (int k = 0; k < count; ++k) { + const bool besideHome = k == 0 && targetX >= 0; + const auto better = [&](int i, int current) { + if (besideHome) { + const int di = t.dist2(i % t.w, i / t.w, targetX, targetY); + const int dc = t.dist2(current % t.w, current / t.w, targetX, targetY); + if (di != dc) + return di < dc; + } + return depth[i] < depth[current]; + }; + // A seed with room all round it, where there is one, so the pond has somewhere to grow. + const auto interior = [&](int i) { + for (int dy = -2; dy <= 2; ++dy) + for (int dx = -2; dx <= 2; ++dx) + if (eligible[t.at(i % t.w + dx, i / t.w + dy)] != v) + return false; + return true; + }; + int seed = -1; + for (int pass = 0; pass < 2 && seed < 0; ++pass) + for (int i : room) { + if (L.pond[i] || (pass == 0 && !interior(i))) + continue; + bool apart = true; + for (int s : seeds) + if (t.dist2(i % t.w, i / t.w, s % t.w, s / t.w) < kPondSeparation * kPondSeparation) { + apart = false; + break; + } + if (apart && (seed < 0 || better(i, seed))) + seed = i; + } + if (seed < 0) + break; + seeds.push_back(seed); + growPond(L, depth, eligible, v, queued, queuedStamp++, seed, each); + } + } + // Fill any ground a pond has closed in: everything still walkable is reachable from the band + // along the ridges, which no pond comes near. + std::vector reached(n, 0); + std::vector queue; + for (int i = 0; i < n; ++i) + if (!L.ridge[i] && !L.pond[i] && L.ridgeDistance[i] <= 2) { + reached[i] = 1; + queue.push_back(i); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int q = t.at(px + dx, py + dy); + if (!reached[q] && !L.ridge[q] && !L.pond[q]) { + reached[q] = 1; + queue.push_back(q); + } + } + } + for (int i = 0; i < n; ++i) + if (!L.ridge[i] && !L.pond[i] && !reached[i]) + L.pond[i] = 1; +} + +Layout design(const GenerationRequest &request, GenerationContext &context) { + const StoneHighlandsOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.w * t.h; + + const std::vector sites = scatterSites(t, o.valleySize, context); + const std::vector label = labelCells(t, sites, o.valleySize, context); + + // A tile is ridge when any of its eight neighbours has a lower label. A tile no ridge was + // painted on then touches only its own label or ridge, so no unit can step, even diagonally, + // from one cell into another. + L.ridge.assign(n, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const int i = y * t.w + x; + for (int dy = -1; dy <= 1 && !L.ridge[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (label[t.at(x + dx, y + dy)] < label[i]) { + L.ridge[i] = 1; + break; + } + } + // Stretches of ridgeline two tiles thick, where a noise field says so. More stone never opens + // a gap. + const std::vector thickness = + periodicNoise(t.w, t.h, std::max(4, o.valleySize / 2), context.stream("highlands-ridge")); + const int thickLevel = percentile(thickness, 55); + std::vector thick = L.ridge; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (int i = 0; i < n; ++i) { + if (!L.ridge[i] || thickness[i] < thickLevel) + continue; + for (const auto &s : steps) { + const int j = t.at(i % t.w + s[0], i / t.w + s[1]); + if (label[j] < label[i]) + thick[j] = 1; + } + } + L.ridge.swap(thick); + + // Valleys are the walkable components left between the ridges; pockets too small to live in + // are filled with stone. + std::vector open(n); + for (int i = 0; i < n; ++i) + open[i] = !L.ridge[i]; + const std::vector region = connectedRegions(open, t.w, t.h, true, GridNeighbors::Eight); + int regions = 0; + for (int r : region) + regions = std::max(regions, r + 1); + std::vector size(regions, 0), id(regions, -1); + for (int r : region) + if (r >= 0) + ++size[r]; + for (int r = 0; r < regions; ++r) + if (size[r] >= kPocketTiles) + id[r] = L.valleys++; + L.valley.assign(n, -1); + for (int i = 0; i < n; ++i) + if (region[i] >= 0) { + if (id[region[i]] < 0) + L.ridge[i] = 1; + else + L.valley[i] = id[region[i]]; + } + if (L.valleys < 1) { + L.failure = "the ridges left no valley to live in"; + return L; + } + + // Pass sites: ridge tiles with exactly two valleys close by, and no third one within reach of + // the cleared box. Junction tiles touch three or more valleys; distance along the ridge from + // the nearest junction ranks how central a site is on its ridgeline. + const int reach = o.passWidth / 2 + 2; + std::map, std::vector> exclusive, anyPair; + std::vector alongRidge(n, -1), queue; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const int i = y * t.w + x; + if (!L.ridge[i]) + continue; + int near[3], far[3]; + const int count = nearbyValleys(L, x, y, 2, near, 3); + if (count >= 3) { + alongRidge[i] = 0; + queue.push_back(i); + } + for (int p = 0; p < count; ++p) + for (int q = p + 1; q < count; ++q) + anyPair[{near[p], near[q]}].push_back(i); + if (count == 2 && nearbyValleys(L, x, y, reach, far, 3) == 2) + exclusive[{near[0], near[1]}].push_back(i); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int q = t.at(px + dx, py + dy); + if (L.ridge[q] && alongRidge[q] < 0) { + alongRidge[q] = alongRidge[p] + 1; + queue.push_back(q); + } + } + } + + // A random spanning tree of passes joins every valley; loopiness then opens that share of the + // remaining shared ridgelines as well. + L.passTile.assign(n, 0); + Disjoint sets(L.valleys); + int groups = L.valleys; + std::vector> edges, spare; + for (const auto &e : exclusive) + if (int(e.second.size()) >= kMinimumBoundary) + edges.push_back(e.first); + shuffle(edges, context, "highlands-passes"); + for (const auto &e : edges) { + if (sets.find(e.first) == sets.find(e.second)) { + spare.push_back(e); + continue; + } + if (carvePass(L, context, e.first, e.second, exclusive[e], alongRidge, o.passWidth)) { + sets.join(e.first, e.second); + --groups; + } + } + if (groups > 1) { + // Some valley only meets its neighbours near a junction; allow a pass there. + std::vector> fallback; + for (const auto &e : anyPair) + fallback.push_back(e.first); + shuffle(fallback, context, "highlands-passes"); + for (const auto &e : fallback) + if (sets.find(e.first) != sets.find(e.second) && + carvePass(L, context, e.first, e.second, anyPair[e], alongRidge, o.passWidth)) { + sets.join(e.first, e.second); + --groups; + } + } + if (groups > 1) { + L.failure = "passes could not join every valley (" + std::to_string(groups) + " groups remain)"; + return L; + } + const int extra = int((std::int64_t(spare.size()) * o.loopiness + 50) / 100); + for (size_t k = 0, added = 0; k < spare.size() && int(added) < extra; ++k) + if (carvePass(L, context, spare[k].first, spare[k].second, exclusive[spare[k]], alongRidge, + o.passWidth)) + ++added; + + std::vector wall(n); + for (int i = 0; i < n; ++i) + wall[i] = L.ridge[i] || L.passTile[i]; + L.ridgeDistance = chebyshevDistance(t, wall); + L.tilesOf.assign(L.valleys, {}); + L.pole.assign(L.valleys, -1); + for (int i = 0; i < n; ++i) { + const int v = L.valley[i]; + if (v < 0 || L.passTile[i]) + continue; + L.tilesOf[v].push_back(i); + if (L.pole[v] < 0 || L.ridgeDistance[i] > L.ridgeDistance[L.pole[v]]) + L.pole[v] = i; + } + if (!chooseHomes(L, context, request.nbTeams)) + return L; + placePonds(L, context, o.valleySize, o.pondSize); + for (size_t k = 0; k < L.homes.size(); ++k) + if (L.pond[t.at(L.homes[k].x, L.homes[k].y)]) + L.failure = "a pond covered the home of colony " + std::to_string(k); + return L; +} + +// Terrain straight from the design: pond water inside a two-tile sand beach, grass everywhere +// else, and stone on every ridge tile. Algae only regrows beside a tile of pure sand, which a +// one-tile beach never makes: every tile takes its terrain from four undermap corners. +bool stampTerrain(Map &map, GenerationContext &context, const Layout &L) { + const Torus &t = L.t; + constexpr int kBeach = 2; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const int i = y * t.w + x; + if (L.pond[i]) { + map.setUMTerrain(x, y, WATER); + continue; + } + bool shore = false; + for (int dy = -kBeach; dy <= kBeach && !shore; ++dy) + for (int dx = -kBeach; dx <= kBeach; ++dx) + if (L.pond[t.at(x + dx, y + dy)]) { + shore = true; + break; + } + map.setUMTerrain(x, y, shore ? SAND : GRASS); + } + map.rebuildTerrain(); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + if (!L.ridge[size_t(y) * t.w + x]) + continue; + if (map.getTerrainType(x, y) != GRASS) { + context.detail = "ridge tile (" + std::to_string(x) + ", " + std::to_string(y) + + ") is not solid grass"; + return false; + } + map.setResource(x, y, STONE, 1); + } + return true; +} + +// Every home gets the same kit: a wheat patch and a wood patch of kHomeKit tiles each, a few +// steps from the swarm on either side of the way down to its pond. +void furnishHome(Map &map, const Layout &L, int bootX, int bootY, int homeValley) { + const Torus &t = L.t; + const int cx = bootX + 2, cy = bootY + 2; + int ox = 0, oy = 1, nearest = INT_MAX; + for (int dy = -24; dy <= 24; ++dy) + for (int dx = -24; dx <= 24; ++dx) + if (L.pond[t.at(cx + dx, cy + dy)] && dx * dx + dy * dy < nearest) { + nearest = dx * dx + dy * dy; + ox = dx; + oy = dy; + } + const double length = std::max(1.0, std::sqrt(double(ox * ox + oy * oy))); + const double ux = ox / length, uy = oy / length; + const double cosine = std::cos(50.0 * 3.14159265358979 / 180.0); + const double sine = std::sin(50.0 * 3.14159265358979 / 180.0); + for (int side : {1, -1}) { + const double rx = ux * cosine - side * uy * sine, ry = side * ux * sine + uy * cosine; + const int ax = cx + int(std::lround(rx * kKitReach)), ay = cy + int(std::lround(ry * kKitReach)); + std::vector> tiles; + for (int dy = -kKitSearch; dy <= kKitSearch; ++dy) + for (int dx = -kKitSearch; dx <= kKitSearch; ++dx) { + const int x = t.x(ax + dx), y = t.y(ay + dy), i = y * t.w + x; + if (L.valley[i] != homeValley || L.passTile[i] || L.ridgeDistance[i] < kRidgeRoad) + continue; + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + // Two clear tiles all round the swarm, where its workers stand and walk out. + const int fx = t.offsetX(bootX, x), fy = t.offsetY(bootY, y); + const int gapX = fx < 0 ? -fx : std::max(0, fx - 3), gapY = fy < 0 ? -fy : std::max(0, fy - 3); + if (std::max(gapX, gapY) <= 2) + continue; + tiles.push_back({dx * dx + dy * dy, i}); + } + std::sort(tiles.begin(), tiles.end()); + for (int k = 0; k < kHomeKit && k < int(tiles.size()); ++k) + map.setResource(tiles[k].second % t.w, tiles[k].second / t.w, side > 0 ? CORN : WOOD, 1); + } +} + +void stampBox(const Torus &t, std::vector &mask, int cx, int cy, int radius) { + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) + mask[t.at(cx + dx, cy + dy)] = 1; +} + +// Ambient farmland: the most fertile ground around each pond, in patches, split 2:1 between +// wheat and wood by an unrelated noise field so the two crops alternate around the shore. +void scatterFarmland(Map &map, GenerationContext &context, const Layout &L, + const std::vector &pondDistance, const std::vector &keepClear) { + const Torus &t = L.t; + const int n = t.w * t.h; + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector split = periodicNoise(t.w, t.h, 8, context.stream("highlands-farmland")); + const std::vector patch = fractalNoise(t.w, t.h, 16, 2, context.stream("highlands-farmland")); + const int patchLevel = percentile(patch, 45); + std::vector> pool(L.valleys); + std::vector pondTiles(L.valleys, 0); + for (int i = 0; i < n; ++i) { + const int v = L.valley[i]; + if (v < 0) + continue; + if (L.pond[i]) { + ++pondTiles[v]; + continue; + } + const int x = i % t.w, y = i / t.w; + if (keepClear[i] || L.ridgeDistance[i] < kRidgeRoad || pondDistance[i] < 2 || + pondDistance[i] > 9 || patch[i] < patchLevel || fertility.at(x, y) == 0) + continue; + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + pool[v].push_back(i); + } + for (int v = 0; v < L.valleys; ++v) { + std::vector &tiles = pool[v]; + const int wanted = std::min(int(std::int64_t(pondTiles[v]) * kFarmlandPercent / 100), + int(tiles.size()) * 2 / 3); + if (wanted <= 0) + continue; + std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { + return fertility.at(a % t.w, a / t.w) > fertility.at(b % t.w, b / t.w); + }); + tiles.resize(wanted); + std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { return split[a] < split[b]; }); + const int wheat = wanted * 2 / 3; + for (int k = 0; k < wanted; ++k) + map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheat ? CORN : WOOD, 1); + } +} + +// Algae in the open water of every pond. +void seedAlgae(Map &map, GenerationContext &context, const Layout &L) { + const Torus &t = L.t; + const std::vector noise = periodicNoise(t.w, t.h, 6, context.stream("highlands-algae")); + std::vector water, levels; + for (int i = 0; i < t.w * t.h; ++i) + if (L.pond[i] && map.isWater(i % t.w, i / t.w)) { + water.push_back(i); + levels.push_back(noise[i]); + } + if (water.empty()) + return; + const int level = percentile(levels, kAlgaePercent); + for (int i : water) + if (noise[i] <= level) + map.setResource(i % t.w, i / t.w, ALGA, 1); +} + +// Fruit is rare: small groves beside the ponds of valleys no colony starts in, a prize for +// whoever holds the passes to them. +void plantFruit(Map &map, GenerationContext &context, const Layout &L, + const std::vector &pondDistance, const std::vector &keepClear, + const std::vector &homeValley, int fruit) { + if (fruit <= 0) + return; + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector> pool(L.valleys); + for (int i = 0; i < n; ++i) { + const int v = L.valley[i], x = i % t.w, y = i / t.w; + if (v < 0 || keepClear[i] || L.pond[i] || L.ridgeDistance[i] < kRidgeRoad + 1 || + pondDistance[i] < 3 || pondDistance[i] > 8) + continue; + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + pool[v].push_back(i); + } + std::vector valleys; + for (int pass = 0; pass < 2 && valleys.empty(); ++pass) + for (int v = 0; v < L.valleys; ++v) + if (!pool[v].empty() && (pass == 1 || !homeValley[v])) + valleys.push_back(v); + if (valleys.empty()) + return; + shuffle(valleys, context, "highlands-fruit"); + const int groves = std::max(1, int(std::int64_t(fruit) * n / 16384)); + const int firstType = int(context.bounded("highlands-fruit", 3)); + for (int g = 0; g < groves; ++g) { + const std::vector &tiles = pool[valleys[g % valleys.size()]]; + const int anchor = tiles[context.bounded("highlands-fruit", std::uint32_t(tiles.size()))]; + const int ax = anchor % t.w, ay = anchor / t.w; + std::vector> grove; + for (int tile : tiles) { + const int d = t.dist2(ax, ay, tile % t.w, tile / t.w); + if (d <= 8 && !map.isResource(tile % t.w, tile / t.w)) + grove.push_back({d, tile}); + } + std::sort(grove.begin(), grove.end()); + for (int k = 0; k < kFruitGrove && k < int(grove.size()); ++k) + map.setResource(grove[k].second % t.w, grove[k].second / t.w, CHERRY + (firstType + g) % 3, 1); + } +} + +bool generate(Game &game, GenerationContext &context) { + context.stage = "highland layout"; + const StoneHighlandsOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(GRASS); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.w * t.h; + + context.stage = "highland terrain"; + if (!stampTerrain(map, context, L)) + return false; + + context.stage = "highland colonies"; + const std::vector pondDistance = chebyshevDistance(t, L.pond); + for (int team = 0; team < teams; ++team) { + const Home &h = L.homes[team]; + std::vector home(n, 0); + for (int dy = -10; dy <= 10; ++dy) + for (int dx = -10; dx <= 10; ++dx) { + const int i = t.at(h.x + dx, h.y + dy); + if (L.valley[i] == h.valley && !L.passTile[i] && L.ridgeDistance[i] >= kRidgeRoad && + pondDistance[i] >= 3) + home[i] = 1; + } + // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. + if (!placeSettlement(game, context, team, home, {h.x - 2, h.y - 2}, "highlands-starts")) + return false; + } + context.stage = "highland resources"; + std::vector keepClear(n, 0), homeValley(L.valleys, 0); + for (const Pass &p : L.passes) + stampBox(t, keepClear, p.x, p.y, p.hi + kRidgeRoad + 1); + for (int team = 0; team < teams; ++team) { + stampBox(t, keepClear, context.bootX[team] + 2, context.bootY[team] + 2, kHomeClear); + homeValley[L.homes[team].valley] = 1; + furnishHome(map, L, context.bootX[team], context.bootY[team], L.homes[team].valley); + } + scatterFarmland(map, context, L, pondDistance, keepClear); + seedAlgae(map, context, L); + plantFruit(map, context, L, pondDistance, keepClear, homeValley, o.fruit); + // The kits above already put wheat and wood a few steps from every swarm; this is only the + // backstop, and the ridges are designed walls it must never clear. + guaranteeStartingResources(game, context, 24, 32, 0, &L.ridge); + // The backstop places what it has to wherever a colony can walk, and that includes passes. + // Passes and their mouths hold nothing but their own ridge stone. + for (const Pass &p : L.passes) + for (int dy = p.lo - 2; dy <= p.hi + 2; ++dy) + for (int dx = p.lo - 2; dx <= p.hi + 2; ++dx) { + const int x = t.x(p.x + dx), y = t.y(p.y + dy); + if (map.isResource(x, y) && map.getResource(x, y).type != STONE) + map.setNoResource(x, y, 1); + } + return true; +} + +std::string validateRequest(const GenerationRequest &r) { + const StoneHighlandsOptions o(r); + const std::int64_t area = std::int64_t(1) << (r.wDec + r.hDec); + if (area < 4 * std::int64_t(o.valleySize) * o.valleySize) + return "These valleys are too big for the map; use a bigger map or smaller valleys."; + if (area < std::int64_t(kAreaPerColony) * r.nbTeams) + return "The highlands have too little room for this many colonies; use a bigger map or fewer " + "colonies."; + return ""; +} + +// Checked on the finished world against the rebuilt design: every ridge still carries its stone, +// every pass is open and reachable on foot, and every colony can walk to every other. Water, +// buildings and every resource tile (farmland included) block the flood; units don't. +std::string validateWorld(const Game &game, const GenerationContext &context) { + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (!L.failure.empty()) + return "The highland design could not be rebuilt: " + L.failure; + const Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (map.getW() != t.w || map.getH() != t.h) + return "The highland design does not match the map size."; + for (int i = 0; i < n; ++i) + if (L.ridge[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "The ridge at (" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + + ") has lost its stone."; + const auto walkable = [&](int x, int y) { + return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + }; + for (size_t k = 0; k < L.passes.size(); ++k) { + const Pass &p = L.passes[k]; + for (int dy = p.lo; dy <= p.hi; ++dy) + for (int dx = p.lo; dx <= p.hi; ++dx) + if (!walkable(t.x(p.x + dx), t.y(p.y + dy))) + return "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + ", " + + std::to_string(p.y) + ") is blocked."; + } + std::vector reached(n, 0); + std::vector queue; + for (int i = 0; i < n; ++i) { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) { + reached[i] = 1; + queue.push_back(i); + } + } + for (size_t head = 0; head < queue.size(); ++head) { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int x = t.x(px + dx), y = t.y(py + dy), q = y * t.w + x; + if (!reached[q] && walkable(x, y)) { + reached[q] = 1; + queue.push_back(q); + } + } + } + std::vector teamReached(teams, 0); + for (int i = 0; i < n; ++i) { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && reached[i] && Unit::GIDtoTeam(gid) < teams) + teamReached[Unit::GIDtoTeam(gid)] = 1; + } + for (int team = 0; team < teams; ++team) + if (!teamReached[team]) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 over the highlands."; + // Each pass must be on the colonies' walkable network and lead straight through: walking out + // of its box, without going far, reaches both valleys it joins. + for (size_t k = 0; k < L.passes.size(); ++k) { + const Pass &p = L.passes[k]; + const std::string where = "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + ", " + + std::to_string(p.y) + ")"; + if (!reached[t.at(p.x, p.y)]) + return where + " cannot be reached on foot."; + const int radius = p.hi + 4, side = 2 * radius + 1; + std::vector seen(size_t(side) * side, 0); + std::vector local; + for (int dy = p.lo; dy <= p.hi; ++dy) + for (int dx = p.lo; dx <= p.hi; ++dx) { + seen[size_t(dy + radius) * side + dx + radius] = 1; + local.push_back((dy + radius) * side + dx + radius); + } + bool sideA = false, sideB = false; + for (size_t head = 0; head < local.size() && !(sideA && sideB); ++head) { + const int lx = local[head] % side, ly = local[head] / side; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int nx = lx + dx, ny = ly + dy; + if (nx < 0 || ny < 0 || nx >= side || ny >= side || seen[size_t(ny) * side + nx]) + continue; + const int x = t.x(p.x + nx - radius), y = t.y(p.y + ny - radius); + if (!walkable(x, y)) + continue; + seen[size_t(ny) * side + nx] = 1; + local.push_back(ny * side + nx); + sideA |= L.valley[y * t.w + x] == p.a; + sideB |= L.valley[y * t.w + x] == p.b; + } + } + if (!sideA || !sideB) + return where + " does not lead through to both of its valleys."; + } + return ""; +} +} // namespace + +StoneHighlandsOptions::StoneHighlandsOptions(const GenerationRequest &r) + : valleySize(r.option("valley-size")), passWidth(r.option("pass-width")), + loopiness(r.option("loopiness")), pondSize(r.option("pond-size")), fruit(r.option("fruit")) {} + +GeneratorDefinition stoneHighlandsDefinition() { + return {"stone-highlands", + 14, + "Stone highlands", + 1, + false, + {// Average spacing between valley centres, in tiles. + {"valley-size", "Valley size", 20, 44, 4, 32, ControlGroup::Layout}, + {"pass-width", "Pass width", 3, 5, 1, 4, ControlGroup::Layout}, + // Share of the ridgelines left over after the spanning tree that get a pass as well. + {"loopiness", "Loopiness", 0, 100, 5, 30, ControlGroup::Layout}, + // Pond area as a percentage of each valley's area. + {"pond-size", "Pond size", 4, 16, 2, 8, ControlGroup::Terrain}, + // Fruit groves per 128x128 tiles of map. + {"fruit", "Fruit", 0, 12, 1, 4, ControlGroup::Resources}}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/StoneHighlandsGenerator.h b/src/map/generator/generators/StoneHighlandsGenerator.h new file mode 100644 index 000000000..652e22a3d --- /dev/null +++ b/src/map/generator/generators/StoneHighlandsGenerator.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct StoneHighlandsOptions { + int valleySize, passWidth, loopiness, pondSize, fruit; + explicit StoneHighlandsOptions(const GenerationRequest &r); +}; +GeneratorDefinition stoneHighlandsDefinition(); diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 626db3023..56a7f87be 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -376,8 +376,10 @@ ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int clo // The same flood, but blocked only by water — as if resources didn't exist. Diffing this // against floodReach's result isolates exactly which tiles a resource wall blocks: reachable // here, not reachable there, adjacent to what is. Clearing only those tiles (rather than an -// entire neighborhood) opens the way while disturbing nothing else nearby. -std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit) { +// entire neighborhood) opens the way while disturbing nothing else nearby. Protected walls are +// part of the terrain, so they block this flood as well. +std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit, + const std::vector *protectedWalls) { const int w = map.getW(), h = map.getH(); std::vector dist(size_t(w) * h, -1); std::vector q(size_t(w) * h); @@ -396,7 +398,7 @@ std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit) { continue; int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); int np = ny * w + nx; - if (dist[np] < 0 && !map.isWater(nx, ny)) { + if (dist[np] < 0 && !map.isWater(nx, ny) && !(protectedWalls && (*protectedWalls)[np])) { dist[np] = dist[p] + 1; q[qTail++] = np; } @@ -410,16 +412,19 @@ std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit) { // tile that is — the wall's inner face — plus, on a wall thick enough to have an outer face // too, that face as well. A real dead end (no meaningfully larger landmass once resources are // ignored) clears nothing, since there is no better tile on the other side to find. Iterated a -// few times by the caller in case a wall is thicker still. +// few times by the caller in case a wall is thicker still. A protected wall tile is never +// cleared. bool clearResourceWall(Map &map, const std::vector &boxedDist, - const std::vector &openDist, int minGain) { + const std::vector &openDist, int minGain, + const std::vector *protectedWalls) { const int w = map.getW(), h = map.getH(); int cleared = 0; std::vector> toClear; for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { int p = y * w + x; - if (boxedDist[p] >= 0 || openDist[p] < 0 || !map.isResource(x, y)) + if (boxedDist[p] >= 0 || openDist[p] < 0 || !map.isResource(x, y) || + (protectedWalls && (*protectedWalls)[p])) continue; bool touchesBoxed = false; for (int dy = -1; dy <= 1 && !touchesBoxed; ++dy) @@ -446,8 +451,11 @@ bool clearResourceWall(Map &map, const std::vector &boxedDist, } // namespace void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, - int woodRange, int clearRadius) { + int woodRange, int clearRadius, + const std::vector *protectedWalls) { Map &map = game.map; + if (protectedWalls && protectedWalls->size() != size_t(map.getW()) * map.getH()) + throw GenerationFailure("Protected wall mask does not match the map size"); const int exploreLimit = std::max(wheatRange, woodRange) * 5 / 2; const int closeRange = wheatRange / 2; // Below this many reached tiles a team is badly boxed in, but that has two very different @@ -470,8 +478,8 @@ void guaranteeStartingResources(Game &game, GenerationContext &context, int whea bool underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || reach.woodDist < 0 || reach.woodDist > woodRange; for (int attempt = 0; underServed && pocketSize() < minPocketTiles && attempt < 6; ++attempt) { - std::vector open = terrainOnlyReach(map, bootX, bootY, exploreLimit); - if (!clearResourceWall(map, reach.dist, open, /*minGain=*/1)) + std::vector open = terrainOnlyReach(map, bootX, bootY, exploreLimit, protectedWalls); + if (!clearResourceWall(map, reach.dist, open, /*minGain=*/1, protectedWalls)) break; reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || reach.woodDist < 0 || diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h index 28f275e56..efe5d9016 100644 --- a/src/map/generator/shared/Resources.h +++ b/src/map/generator/shared/Resources.h @@ -19,6 +19,10 @@ void scatterResources(Game &, GenerationContext &, const ResourceDensities &); // range are left untouched. clearRadius skips a ring around the boot tile that a caller still // has to carve out for the swarm/workers after this runs (StartingPositions.cpp's setNoResource // calls) — pass 0 when this runs after that carving has already happened. +// protectedWalls, when given, is a row-major width*height mask of resource tiles that belong to +// the map's design (a generator's stone ridgelines, say). They are treated like terrain: never +// cleared, and never looked past when deciding whether a colony is walled into a pocket. void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, - int woodRange, int clearRadius = 0); + int woodRange, int clearRadius = 0, + const std::vector *protectedWalls = nullptr); } // namespace MapGeneration diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index c2405530a..f9182aaf1 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -216,7 +216,7 @@ class MapGeneratorDefaultsTest assert(screen.descriptor.option("test-cell") == 16 && screen.descriptor.option("test-gap") == 5); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 12); + assert(registry.selectionIndex(101) == 13); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); From 8cffde4ed454e72853f7ac4b5e60d23991e9803a Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:33:22 -0400 Subject: [PATCH 051/136] Symmetric arena: orchard dealt by orbit, tidier causeways, scale caps Groves are dealt fruit an orbit at a time from the centre outwards, with stone capped so at least three orbits keep fruit, and requests whose centre is too small to guarantee that are rejected cleanly. Causeway sand reaches less far past each shore, farmland keeps off the apron round the moat, and lake and farmland noise scales are capped so large maps get more shoreline. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.en.txt | 2 + data/texts.keys.txt | 1 + .../generators/SymmetricArenaGenerator.cpp | 158 +++++++++++------- 3 files changed, 96 insertions(+), 65 deletions(-) diff --git a/data/texts.en.txt b/data/texts.en.txt index a0c52cecb..354864604 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1822,6 +1822,8 @@ Resource richness Symmetric arena needs 2, 4 or 8 colonies. [Eight colonies need a square map in the symmetric arena.] Eight colonies need a square map in the symmetric arena. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +The centre is too small for an orchard on this map; use a bigger centre or a bigger map. [The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat. [The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index dd7c1c740..a69c520e2 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -909,5 +909,6 @@ [Resource richness] [Symmetric arena needs 2, 4 or 8 colonies.] [Eight colonies need a square map in the symmetric arena.] +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] [The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] [The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index 6186ac66e..2984632ab 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,9 @@ constexpr double kHomeRadius = 6.5; constexpr double kPondDistance = 11.5, kPondRadius = 3.5; // Open land kept between the moat and the nearest lake. constexpr double kApron = 4.0; +// The smallest orchard island radius that always holds three orbits of groves (one lattice +// phase has them within five tiles of the centre), wherever the causeways come ashore. +constexpr double kMinimumCentre = 8.0; // The nearest two homes may be. constexpr double kMinimumSpacing = 28.0; // Every home's starting kit, the same whatever the richness control says. @@ -265,8 +269,12 @@ std::string validate(const GenerationRequest &r) { return "Symmetric arena needs 2, 4 or 8 colonies."; if (r.nbTeams == 8 && r.wDec != r.hDec) return "Eight colonies need a square map in the symmetric arena."; + const Arena a = arenaFor(r); + if (a.centre < kMinimumCentre) + return "The centre is too small for an orchard on this map; use a bigger centre or a bigger " + "map."; Fit fit; - homeCandidates(arenaFor(r), fit); + homeCandidates(a, fit); if (fit == Fit::Crowded) return "The arena leaves too little room for every colony; use a bigger map, a smaller " "centre or a narrower moat."; @@ -329,7 +337,8 @@ std::vector topShare(const std::vector &value, if (eligible[i]) pool.push_back(value[i]); std::vector mask(value.size(), 0); - const size_t count = std::min(pool.size(), size_t(std::lround(std::max(0.0, share) * pool.size()))); + const size_t count = + std::min(pool.size(), size_t(std::lround(std::max(0.0, share) * pool.size()))); if (!count) return mask; std::sort(pool.begin(), pool.end()); @@ -339,12 +348,14 @@ std::vector topShare(const std::vector &value, return mask; } -// Whether a point lies on one of colony 0's causeways, extended past both shores. -bool onCauseway(const Arena &a, const Layout &l, Point p, double extend, double halfWidth) { +// Whether a point lies on one of colony 0's causeways, extended past the island's shore by +// `inner` and past the outer shore by `outer`. +bool onCauseway(const Arena &a, const Layout &l, Point p, double inner, double outer, + double halfWidth) { for (double angle : l.gates) { const double dx = std::cos(angle), dy = std::sin(angle); const double along = p.x * dx + p.y * dy, across = std::fabs(p.x * dy - p.y * dx); - if (along >= a.centre - extend && along <= a.centre + a.moat + extend && across <= halfWidth) + if (along >= a.centre - inner && along <= a.centre + a.moat + outer && across <= halfWidth) return true; } return false; @@ -458,11 +469,12 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La for (int u = 0; u < w; ++u) { const size_t i = size_t(v) * w + u; const Point p = cornerPoint(w, h, u, v); - const double toHome = torusDistance(w, h, p, l.home), toPond = torusDistance(w, h, p, l.pond); + const double toHome = torusDistance(w, h, p, l.home); + const double toPond = torusDistance(w, h, p, l.pond); homeDisc[i] = toHome <= kHomeRadius; homeZone[i] = toHome <= kHomeRadius + 4 || toPond <= kPondRadius + 7; pond[i] = toPond <= kPondRadius; - gate[i] = onCauseway(a, l, p, 3.0, a.causewayWidth / 2); + gate[i] = onCauseway(a, l, p, 1.5, 1.5, a.causewayWidth / 2); } homeDisc = stamp(s, homeDisc, true); homeZone = stamp(s, homeZone, true); @@ -478,7 +490,8 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La t.moat[i] = !within(r2, a.centre) && within(r2, a.centre + a.moat); lakeEligible[i] = !within(r2, a.centre + a.moat + kApron) && !homeZone[i]; } - const float smoothing = std::max(12.0f, std::min(w, h) / 8.0f); + // Capped so big maps get more lakes rather than bigger ones: more shore, more farmland. + const float smoothing = std::min(32.0f, std::max(12.0f, std::min(w, h) / 10.0f)); std::vector lakes = topShare(orbitNoise(context, s, "arena-lakes", smoothing, true), lakeEligible, o.lakes / 100.0, false); @@ -616,14 +629,6 @@ bool placeColonies(Game &game, GenerationContext &context, const Arena &a, const return true; } -// The fruit of the orchard tree at lattice position (ti, tj), for one of three patterns. Each is -// symmetric in ti and tj, so a quarter turn or diagonal mirror, which swaps them, leaves every -// tree's fruit unchanged. -int orchardFruit(int ti, int tj, int pattern) { - const int lo = std::min(ti, tj), hi = std::max(ti, tj); - return CHERRY + (pattern == 0 ? lo + hi : pattern == 1 ? hi : lo + 2 * hi) % 3; -} - bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layout &l, const SymmetricArenaOptions &o, const Terrain &t) { context.stage = "arena resources"; @@ -645,11 +650,13 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou land[i] = !water[i]; const Point p = tilePoint(w, h, x, y); homeClear[i] = torusDistance(w, h, p, l.home) <= kHomeRadius + 1.5; - landing[i] = onCauseway(a, l, p, 6.0, a.causewayWidth / 2 + 2); + landing[i] = onCauseway(a, l, p, 3.0, 4.0, a.causewayWidth / 2 + 2); } homeClear = stamp(s, homeClear, false); landing = stamp(s, landing, false); - const auto onPath = [&](int u, int v) { return t.paths[size_t(wrap(v, h)) * w + wrap(u, w)] != 0; }; + const auto onPath = [&](int u, int v) { + return t.paths[size_t(wrap(v, h)) * w + wrap(u, w)] != 0; + }; for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { const size_t i = size_t(y) * w + x; @@ -657,17 +664,17 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou onPath(x, y + 1) || onPath(x + 1, y + 1); } const std::vector waterSteps = stepsFrom(w, h, water), landSteps = stepsFrom(w, h, land); - const float coarse = std::max(8.0f, std::min(w, h) / 10.0f); + const float coarse = std::min(24.0f, std::max(8.0f, std::min(w, h) / 10.0f)); std::vector plan(n, -1); - // The ambient layer outside the arena: farmland on lake and pond shores (wheat to wood 2:1), - // stone outcrops away from the water, and algae off the shores. + // The ambient layer beyond the open apron round the moat: farmland on lake and pond shores + // (wheat to wood 2:1), stone outcrops away from the water, and algae off every shore. std::vector farm(n, 0), rock(n, 0), alga(n, 0); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { const size_t i = size_t(y) * w + x; const bool open = grass[i] && !blocked[i] && - !within(tileRadius2(w, h, x, y), a.centre + a.moat + 2); + !within(tileRadius2(w, h, x, y), a.centre + a.moat + kApron + 2); farm[i] = open && waterSteps[i] >= 2 && waterSteps[i] <= 7; rock[i] = open && waterSteps[i] >= 3; alga[i] = water[i] && landSteps[i] >= 2; @@ -690,60 +697,77 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou } // The orchard: two-by-two groves on a four-tile lattice squared up to the centre, so every - // grove has walkway at least two tiles wide on all four sides and every tree can be picked; - // a fifth of the groves are stone. Lattice phase and fruit pattern come from the seed. A small - // centre holds few groves, so if that choice leaves a fruit out, the first combination that - // has all three is used instead. + // grove has walkway at least two tiles wide on all four sides and every tree can be picked. + // Groves are dealt out an orbit at a time - the groves the symmetries carry onto each other - + // from the centre outwards: cherries, oranges and prunes in turn from a seeded start, with + // about a fifth of the orbits turned to stone but never fewer than three left for fruit. The + // lattice phase comes from the seed; if it leaves a small centre fewer than three orbits, the + // other phases are tried. + struct Orbit { + std::int64_t radius2 = INT64_MAX; + int key = 0, value = INT_MIN; + bool stone = false; + std::vector tiles; + }; const std::vector orchardNoise = orbitNoise(context, s, "arena-orchard", 4.0f, false); bool planted = false; - for (int attempt = -1; attempt < 18 && !planted; ++attempt) { + for (int attempt = 0; attempt < 4 && !planted; ++attempt) { // Phase 1 gives even walkways, 2 an avenue along each axis, 3 a grove on the centre; 0 // would join the four groves round the centre into one block with unpickable middle trees. - const int phase = attempt < 0 ? l.orchardPhase : 1 + attempt / 6 % 3; - const int pattern = attempt < 0 ? l.orchardPattern : attempt / 2 % 3; - // A tile's grove index along one axis from its doubled centred coordinate, or -1 on a - // walkway. It depends only on the distance from the axis, so turns and mirrors keep it. - const auto grove = [phase](int doubled) { - const int i = (std::abs(doubled) - 1) / 2 + 4 - phase; - return i % 4 < 2 ? i / 4 : -1; + const int phase = attempt == 0 ? l.orchardPhase : attempt; + // Whether a tile's doubled centred coordinate falls in a grove row. It depends only on the + // distance from the axis, so turns and mirrors keep it. + const auto inGrove = [phase](int doubled) { + return ((std::abs(doubled) - 1) / 2 + 4 - phase) % 4 < 2; }; std::vector trees(n, 0); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { const size_t i = size_t(y) * w + x; trees[i] = grass[i] && !blocked[i] && within(tileRadius2(w, h, x, y), a.centre - 2) && - grove(2 * x + 1 - w) >= 0 && grove(2 * y + 1 - h) >= 0; + inGrove(2 * x + 1 - w) && inGrove(2 * y + 1 - h); } - // A grove's value is the highest orbit-noise value among its tiles, which its images share, - // so stone always takes whole groves. - std::vector groveValue(n, INT_MIN); + // An orbit is named by the lowest tile index any symmetry carries one of its tiles to, and + // ranked by its nearest tile to the centre and its highest noise value; all three are the + // same whichever of its groves they are read from. + std::map orbits; for (const auto &group : groupsOf(trees, w, h)) { - int best = INT_MIN; - for (int i : group) - best = std::max(best, orchardNoise[size_t(i)]); - for (int i : group) - groveValue[size_t(i)] = best; - } - const auto stones = attempt < 0 || attempt % 2 == 0 ? topShare(groveValue, trees, 0.2, true) - : std::vector(n, 0); - std::vector orchard(n, -1); - bool seen[3] = {false, false, false}; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - if (!trees[i]) - continue; - orchard[i] = stones[i] ? STONE - : orchardFruit(grove(2 * x + 1 - w), grove(2 * y + 1 - h), pattern); - if (!stones[i]) - seen[orchard[i] - CHERRY] = true; + int key = INT_MAX, value = INT_MIN; + std::int64_t radius2 = INT64_MAX; + for (int i : group) { + value = std::max(value, orchardNoise[size_t(i)]); + radius2 = std::min(radius2, tileRadius2(w, h, i % w, i / w)); + for (int e = 0; e < s.order(); ++e) + key = std::min(key, s.tile(e, i % w, i / w)); } - if (seen[0] && seen[1] && seen[2]) { - planted = true; - for (size_t i = 0; i < n; ++i) - if (orchard[i] >= 0) - plan[i] = orchard[i]; + Orbit &orbit = orbits[key]; + orbit.key = key; + orbit.radius2 = radius2; + orbit.value = value; + orbit.tiles.insert(orbit.tiles.end(), group.begin(), group.end()); } + if (orbits.size() < 3) + continue; + std::vector ranked; + for (auto &entry : orbits) + ranked.push_back(&entry.second); + std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) { + return p->value != q->value ? p->value > q->value : p->key < q->key; + }); + const size_t stones = + std::min(ranked.size() - 3, size_t(std::lround(0.2 * double(ranked.size())))); + for (size_t k = 0; k < stones; ++k) + ranked[k]->stone = true; + std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) { + return p->radius2 != q->radius2 ? p->radius2 < q->radius2 : p->key < q->key; + }); + int fruit = l.orchardPattern; + for (const Orbit *orbit : ranked) { + const int type = orbit->stone ? STONE : CHERRY + fruit++ % 3; + for (int i : orbit->tiles) + plan[size_t(i)] = type; + } + planted = true; } if (!planted) { context.detail = "the centre is too small to hold all three fruits"; @@ -796,8 +820,10 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou }; const Point side{-std::sin(l.pondAngle), std::cos(l.pondAngle)}; const double stoneAngle = l.pondAngle + 2 * kPi / 3; - const int wheatKit = grow({l.pond.x + 4.5 * side.x, l.pond.y + 4.5 * side.y}, CORN, kKitFarmland); - const int woodKit = grow({l.pond.x - 4.5 * side.x, l.pond.y - 4.5 * side.y}, WOOD, kKitFarmland); + const int wheatKit = + grow({l.pond.x + 4.5 * side.x, l.pond.y + 4.5 * side.y}, CORN, kKitFarmland); + const int woodKit = + grow({l.pond.x - 4.5 * side.x, l.pond.y - 4.5 * side.y}, WOOD, kKitFarmland); const int stoneKit = grow({l.home.x + (kHomeRadius + 3) * std::cos(stoneAngle), l.home.y + (kHomeRadius + 3) * std::sin(stoneAngle)}, STONE, kKitStone); @@ -805,7 +831,9 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou context.detail = "colony 0's starting kit has no room beside its pond"; return false; } - const auto rank = [](int type) { return type == STONE ? 3 : type == WOOD ? 2 : type == CORN ? 1 : 0; }; + const auto rank = [](int type) { + return type == STONE ? 3 : type == WOOD ? 2 : type == CORN ? 1 : 0; + }; for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { int best = -1; From 825bd455c3c5277e32a4d479cef63613d2a1922f Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:38:41 -0400 Subject: [PATCH 052/136] Add the Watershed map generator A branching river system draining to a sea along one side of the map. Springs join a trunk that splits into a delta of one to three mouths. Every channel keeps a core of open water, so sand fords are the only crossings on foot. Floodplains carry ribbons of wheat and wood, and uplands far from water dry out to sand. validateWorld plans the layout again from the request and checks every ford and channel on the finished map, as well as colony walkability and wheat and wood reach. Registered as legacy id 13 between Maze and uniform terrain, with River density, River width, Dryness and Fords controls. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.en.txt | 10 + data/texts.keys.txt | 5 + src/SConscript | 1 + src/map/generator/core/GenerationRequest.h | 3 +- src/map/generator/core/GeneratorRegistry.cpp | 4 +- .../generators/WatershedGenerator.cpp | 1818 +++++++++++++++++ .../generator/generators/WatershedGenerator.h | 9 + test/MapGeneratorDefaultsTest.cpp | 2 +- 8 files changed, 1849 insertions(+), 3 deletions(-) create mode 100644 src/map/generator/generators/WatershedGenerator.cpp create mode 100644 src/map/generator/generators/WatershedGenerator.h diff --git a/data/texts.en.txt b/data/texts.en.txt index 6b68bad6d..d2f5ec16b 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1806,3 +1806,13 @@ Lake size Lake connects to fjords [Resource islands] Resource islands +[Watershed] +Watershed +[River density] +River density +[Dryness] +Dryness +[Fords] +Fords +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies. diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 629e7cb34..b564a9a33 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -901,3 +901,8 @@ [Lake size] [Lake connects to fjords] [Resource islands] +[Watershed] +[River density] +[Dryness] +[Fords] +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] diff --git a/src/SConscript b/src/SConscript index da1ad0345..cfaf5b745 100644 --- a/src/SConscript +++ b/src/SConscript @@ -188,6 +188,7 @@ map/generator/generators/RuggedArchipelagoGenerator.cpp map/generator/generators/ShatteredCoastGenerator.cpp map/generator/generators/SwampGenerator.cpp map/generator/generators/UniformGenerator.cpp +map/generator/generators/WatershedGenerator.cpp map/generator/shared/Distances.cpp map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index d0342b85c..522bba2a4 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -28,7 +28,8 @@ struct GenerationRequest { eOLDISLANDS = 8, eCONTESTEDCOMMONS = 9, eMAZE = 11, - eFJORDCONTINENT = 12 + eFJORDCONTINENT = 12, + eWATERSHED = 13 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 28dddad67..953c61733 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -12,6 +12,7 @@ #include "ShatteredCoastGenerator.h" #include "SwampGenerator.h" #include "UniformGenerator.h" +#include "WatershedGenerator.h" #include #include #include @@ -75,6 +76,7 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { shatteredCoastDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), mazeDefinition(), uniformDefinition()}); + riverDefinition(), mazeDefinition(), watershedDefinition(), + uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp new file mode 100644 index 000000000..97b7646b0 --- /dev/null +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -0,0 +1,1818 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "WatershedGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Resources.h" +#include "Settlements.h" +#include "Unit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// A branching river system draining into a sea along one side of the map. Tributaries rise at +// springs in the uplands and join a trunk that splits into a delta at the coast. Every channel is +// wide enough to keep a core of open water, so on foot a river can only be crossed at a ford: a +// strip of sand laid across it. Wheat and wood only regrow near water, so farmland follows the +// rivers in long ribbons, while ground far from any river dries out to sand. +// +// The coast, rivers and fords are planned in a canonical frame with the sea along the bottom, +// then turned so the sea lies along one side of the map. Planning draws only from its +// own named streams and never reads the map, so validateWorld can plan the same layout again and +// check every ford and channel against the finished world. +// +// Terrain is stamped on the undermap with a symmetric version of Map::controlSand's shoreline +// rule, then controlSand itself is run and must change nothing: the shores are already what it +// would make of them, without its row-order raster pass silting narrow channels unevenly. +namespace { + +constexpr double kPi = 3.14159265358979323846; +constexpr const char *kLayoutStream = "watershed-layout"; +constexpr const char *kRiverStream = "watershed-rivers"; +constexpr const char *kStartStream = "watershed-starts"; + +// Channel radius, in undermap vertices, before the shores are sanded. Sanding takes up to about +// 1.5 vertices off a diagonal channel, and a tile is water only when all four corners are, so this +// is the narrowest channel whose water tiles still form a 4-connected core in every direction: a +// line no unit can step across. +constexpr double kMinimumChannelRadius = 3.2; +// Distance between consecutive points of a river's centreline. +constexpr double kSpacing = 0.5; +// A ford is sand this far either side of its centre line along the river - three rows of pure +// sand tiles, five rows walkable - reaching this far past the channel's radius across it. +constexpr double kFordHalfWidth = 2.0; +constexpr double kFordReach = 2.5; +// A tributary joins a point downstream of its spring by at least this much per tile of sideways +// travel, so rivers run towards the sea rather than along it. +constexpr double kSlope = 0.3; +// Tiles of map per colony that validateRequest allows. +constexpr int kTilesPerColony = 1024; +// Distance along a river between fords, for each setting of the fords control. +constexpr int kFordSpacing[5] = {80, 60, 44, 34, 26}; +// Colonies start between these many steps from open water: on the floodplain, not its shore. +constexpr int kColonyWaterNear = 5, kColonyWaterFar = 11; +constexpr double kColonySpacing = 14; + +double sq(double v) { return v * v; } + +struct Vec { + double x, y; +}; +Vec operator+(Vec a, Vec b) { return {a.x + b.x, a.y + b.y}; } +Vec operator-(Vec a, Vec b) { return {a.x - b.x, a.y - b.y}; } +Vec operator*(Vec a, double s) { return {a.x * s, a.y * s}; } +double dot(Vec a, Vec b) { return a.x * b.x + a.y * b.y; } +double length(Vec a) { return std::sqrt(dot(a, a)); } +Vec normalized(Vec a) { + const double l = length(a); + return l > 1e-9 ? a * (1.0 / l) : Vec{0, 1}; +} +double wrapped(double value, double period) { return value - period * std::floor(value / period); } +double centred(double delta, double period) { return delta - period * std::round(delta / period); } +int wrapIndex(int value, int period) { return ((value % period) + period) % period; } + +double unit(GenerationContext &context, const char *stream) { + return context.bounded(stream, 1u << 20) / double(1u << 20); +} +double between(GenerationContext &context, const char *stream, double lo, double hi) { + return lo + (hi - lo) * unit(context, stream); +} + +double channelRadius(int riverWidth) { return kMinimumChannelRadius + 0.4 * (riverWidth - 1); } + +// A coastline: a sum of sinusoids whose frequencies are whole cycles of the coast's length, so it +// joins itself seamlessly across the torus seam. +struct Ripple { + double base = 0, period = 1; + std::array amplitude{}, phase{}, frequency{}; + // A smooth bulge, such as a delta building land out into the sea. + double lobeCentre = 0, lobeHeight = 0, lobeWidth = 1; + double at(double u) const { + double value = base; + for (size_t k = 0; k < amplitude.size(); ++k) + value += amplitude[k] * std::sin(2 * kPi * frequency[k] * u / period + phase[k]); + if (lobeHeight != 0) + value += lobeHeight * std::exp(-sq(centred(u - lobeCentre, period) / lobeWidth)); + return value; + } +}; + +Ripple makeRipple(GenerationContext &context, double base, double amplitude, int period) { + static const double harmonics[4] = {1, 3, 5, 9}, weights[4] = {0.35, 0.3, 0.2, 0.15}; + Ripple ripple; + ripple.base = base; + ripple.period = period; + const int scale = std::max(1, period / 128); + for (size_t k = 0; k < 4; ++k) { + ripple.frequency[k] = harmonics[k] * scale; + ripple.amplitude[k] = amplitude * weights[k] * between(context, kLayoutStream, 0.5, 1.0); + ripple.phase[k] = between(context, kLayoutStream, 0, 2 * kPi); + } + return ripple; +} + +// Smooth value noise on a lattice that tiles the map, so it wraps like the map does. +class PeriodicNoise { +public: + PeriodicNoise(int width, int height, double cell, std::mt19937 &random) + : width(width), height(height), columns(std::max(2, int(std::lround(width / cell)))), + rows(std::max(2, int(std::lround(height / cell)))), lattice(size_t(columns) * rows) { + for (double &value : lattice) + value = random() / 4294967296.0; + } + double at(double x, double y) const { + const double fx = x * columns / width, fy = y * rows / height; + const double x0 = std::floor(fx), y0 = std::floor(fy); + double tx = fx - x0, ty = fy - y0; + tx = tx * tx * (3 - 2 * tx); + ty = ty * ty * (3 - 2 * ty); + const int ix = wrapIndex(int(x0), columns), iy = wrapIndex(int(y0), rows); + const int jx = (ix + 1) % columns, jy = (iy + 1) % rows; + const double top = lattice[iy * columns + ix] * (1 - tx) + lattice[iy * columns + jx] * tx; + const double bottom = lattice[jy * columns + ix] * (1 - tx) + lattice[jy * columns + jx] * tx; + return top * (1 - ty) + bottom * ty; + } + +private: + int width, height, columns, rows; + std::vector lattice; +}; + +// The canonical frame: u runs along the coast, v across it with the sea at large v. The map is +// the frame turned so the sea lies along one of its sides: orientations 0 and 1 put it south or +// north, 2 and 3 east or west. +struct Frame { + int width = 0, height = 0; // the map + int along = 0, across = 0; // the frame + int orientation = 0; + Vec toMap(Vec c) const { + switch (orientation) { + case 0: + return {c.x, c.y}; + case 1: + return {c.x, across - c.y}; + case 2: + return {c.y, c.x}; + default: + return {across - c.y, c.x}; + } + } + Vec toCanonical(Vec m) const { + const double x = m.x, y = m.y; + Vec c{x, y}; + switch (orientation) { + case 1: + c = {x, across - y}; + break; + case 2: + c = {y, x}; + break; + case 3: + c = {y, across - x}; + break; + default: + break; + } + return {wrapped(c.x, along), wrapped(c.y, across)}; + } +}; + +// One stretch of river, from its upstream end (a spring, or the delta's apex for a distributary) +// to where it ends: joining its parent at joinIndex, at the apex for the trunk, or in the sea. +struct Reach { + std::vector points; + std::vector radius; + // Distance across the frame to the nearer coast; small or negative where a reach meets the sea. + std::vector coastGap; + int parent = -1, joinIndex = -1; + bool distributary = false; + double length() const { return (points.size() - 1) * kSpacing; } +}; + +struct Ford { + Vec center, along, across; + double span; +}; + +struct Layout { + Frame frame; + Ripple north, south; + int trunk = 0; + // Everything below is in map coordinates. + std::vector reaches; + std::vector> joins; + // Lines from every spring straight upstream to the sea: they split the land into banks. + std::vector> divides; + std::vector mouths, junctions; + std::vector fords; +}; + +// River centreline points in map coordinates, bucketed so a ford can check what is near it. +class ChannelIndex { +public: + explicit ChannelIndex(const Layout &layout) + : layout(layout), w(layout.frame.width), h(layout.frame.height), + columns(std::max(1, w / 16)), rows(std::max(1, h / 16)), cells(size_t(columns) * rows) { + for (int r = 0; r < int(layout.reaches.size()); ++r) + for (int i = 0; i < int(layout.reaches[r].points.size()); ++i) + cells[cellOf(layout.reaches[r].points[i])].push_back({r, i}); + } + + // Whether p lies at least `margin` outside every channel, ignoring reach r within `skip` tiles + // of point i along it. + bool clear(Vec p, double margin, int r, int i, double skip) const { + const int cx = column(p), cy = row(p); + int xs[3], ys[3], nx = 0, ny = 0; + for (int d = -1; d <= 1; ++d) { + const int x = wrapIndex(cx + d, columns), y = wrapIndex(cy + d, rows); + if (std::find(xs, xs + nx, x) == xs + nx) + xs[nx++] = x; + if (std::find(ys, ys + ny, y) == ys + ny) + ys[ny++] = y; + } + for (int a = 0; a < ny; ++a) + for (int b = 0; b < nx; ++b) + for (const auto &entry : cells[size_t(ys[a]) * columns + xs[b]]) { + if (entry.first == r && std::abs(entry.second - i) * kSpacing <= skip) + continue; + const Reach &reach = layout.reaches[entry.first]; + double radius = reach.radius[entry.second]; + if (entry.second == 0 && !reach.distributary) + radius += 1.3; // the spring's pond + const Vec q = reach.points[entry.second]; + if (sq(centred(p.x - q.x, w)) + sq(centred(p.y - q.y, h)) < sq(radius + margin)) + return false; + } + return true; + } + +private: + int column(Vec p) const { return std::min(columns - 1, int(wrapped(p.x, w) / 16)); } + int row(Vec p) const { return std::min(rows - 1, int(wrapped(p.y, h) / 16)); } + size_t cellOf(Vec p) const { return size_t(row(p)) * columns + column(p); } + const Layout &layout; + int w, h, columns, rows; + std::vector>> cells; +}; + +std::vector resample(const std::vector &line) { + std::vector out{line.front()}; + double carried = 0; + for (size_t i = 1; i < line.size(); ++i) { + Vec a = line[i - 1]; + const Vec b = line[i]; + double segment = length(b - a); + while (segment > 0 && carried + segment >= kSpacing) { + a = a + (b - a) * ((kSpacing - carried) / segment); + out.push_back(a); + segment = length(b - a); + carried = 0; + } + carried += segment; + } + if (out.size() > 1 && length(line.back() - out.back()) < kSpacing / 2) + out.back() = line.back(); + else + out.push_back(line.back()); + return out; +} + +// Grows the river network in the canonical frame. Springs join the nearest point downstream of +// them on the network so far, nearest first, which grows a branching tree the way a spanning tree +// does. A path that would run out into the sea is refused; one that runs into another river joins +// that river instead. +class RiverPlanner { +public: + struct Attachment { + double distance2; + int reach, index; + }; + + RiverPlanner(const Frame &frame, const Ripple &north, const Ripple &south, double clearance, + double northMargin, double southMargin) + : along(frame.along), across(frame.across), north(north), south(south), + clearance(clearance), northMargin(northMargin), southMargin(southMargin) { + columns = std::max(1, along / 24); + rows = std::max(1, across / 24); + cellW = double(along) / columns; + cellH = double(across) / rows; + cells.resize(size_t(columns) * rows); + } + + std::vector reaches; + // Per reach: where each tributary joins it, and from which side. + std::vector>> joins; + int trunk = -1; + + int sideOf(int r, int i, Vec source) const { + const Vec t = tangent(r, i), p = reaches[r].points[i]; + const Vec d{centred(source.x - p.x, along), source.y - p.y}; + return t.x * d.y - t.y * d.x >= 0 ? 1 : -1; + } + + double distance2(Vec a, Vec b) const { return sq(centred(a.x - b.x, along)) + sq(a.y - b.y); } + + int add(Reach reach) { + reaches.push_back(std::move(reach)); + joins.emplace_back(); + const int id = int(reaches.size()) - 1; + const auto &points = reaches[id].points; + for (size_t i = 0; i < points.size(); i += 2) + cells[cellOf(points[i])].push_back({id, int(i)}); + return id; + } + + // Squared distance to the nearest indexed river point within about one cell, or the largest + // double when there is none that close; `only` limits the search to one reach. + double nearest2(Vec p, int ignore = -1, int only = -1, int *nearestReach = nullptr) const { + const int cx = column(p), cy = row(p); + int xs[3], count = 0; + for (int dx = -1; dx <= 1; ++dx) { + const int x = wrapIndex(cx + dx, columns); + if (std::find(xs, xs + count, x) == xs + count) + xs[count++] = x; + } + double best = std::numeric_limits::max(); + for (int dy = -1; dy <= 1; ++dy) { + const int y = cy + dy; + if (y < 0 || y >= rows) + continue; + for (int k = 0; k < count; ++k) + for (const auto &entry : cells[size_t(y) * columns + xs[k]]) + if (entry.first != ignore && (only < 0 || entry.first == only)) { + const double d = distance2(p, reaches[entry.first].points[entry.second]); + if (d < best) { + best = d; + if (nearestReach) + *nearestReach = entry.first; + } + } + } + return best; + } + + // The point of reach r nearest to p where a spring at `source` may join it, if one is close. + int nearestAttachable(int r, Vec p, Vec source) const { + int best = -1; + double bestDistance = sq(2 * clearance); + for (int i = 0; i < int(reaches[r].points.size()); i += 2) { + if (!attachable(r, i, source)) + continue; + const Vec q = reaches[r].points[i]; + const double du = centred(q.x - source.x, along), dv = q.y - source.y; + if (dv < std::max(4.0, kSlope * std::abs(du))) + continue; + const double d = distance2(p, q); + if (d < bestDistance) { + bestDistance = d; + best = i; + } + } + return best; + } + + bool inland(Vec p, double northGap, double southGap) const { + return p.y >= north.at(p.x) + northGap && p.y <= south.at(p.x) - southGap; + } + + Vec tangent(int r, int i) const { + const auto &points = reaches[r].points; + const int a = std::max(0, i - 2), b = std::min(int(points.size()) - 1, i + 2); + return normalized(Vec{centred(points[b].x - points[a].x, along), points[b].y - points[a].y}); + } + + // A cubic curve that leaves `from` towards `to` and arrives leaning into the direction the + // river it joins is flowing, with a meander that fades to nothing at both ends. + std::vector path(Vec from, Vec to, Vec endDirection, double meander, + GenerationContext &context) const { + const Vec delta{centred(to.x - from.x, along), to.y - from.y}; + const Vec end = from + delta; + const double span = std::max(1.0, length(delta)); + const Vec blend = normalized(endDirection * 0.4 + normalized(delta) * 0.6); + const Vec c1 = from + delta * 0.33, c2 = end - blend * (0.33 * span); + const int steps = std::max(16, int(span * 3)); + std::vector raw; + raw.reserve(steps + 1); + for (int k = 0; k <= steps; ++k) { + const double t = double(k) / steps, s = 1 - t; + raw.push_back(from * (s * s * s) + c1 * (3 * s * s * t) + c2 * (3 * s * t * t) + + end * (t * t * t)); + } + const std::vector base = resample(raw); + const double amplitude = std::min(6.5, 0.1 * span) * meander; + const double waves = std::max(1.0, span / 34.0); + const double phase1 = between(context, kRiverStream, 0, 2 * kPi); + const double phase2 = between(context, kRiverStream, 0, 2 * kPi); + const double last = double(base.size() - 1); + std::vector bent(base.size()); + for (size_t i = 0; i < base.size(); ++i) { + const double t = last > 0 ? i / last : 0; + const Vec direction = normalized(base[std::min(base.size() - 1, i + 1)] - base[i ? i - 1 : 0]); + const double offset = amplitude * std::sin(kPi * t) * + (0.75 * std::sin(2 * kPi * waves * t + phase1) + + 0.25 * std::sin(2 * kPi * 2.3 * waves * t + phase2)); + bent[i] = base[i] + Vec{-direction.y, direction.x} * offset; + } + return resample(bent); + } + + // Where along the network a tributary may join: not on the delta, not at a spring or a + // junction, and not beside another tributary's mouth. + bool attachable(int r, int i, Vec source) const { + const Reach &reach = reaches[r]; + if (reach.distributary) + return false; + const double s = i * kSpacing; + if (s < 8 || reach.length() - s < 8) + return false; + // Junctions on the same bank well apart leave straight stretches between them to ford, and + // send later springs to join tributaries instead of all lining up along the trunk; one from + // the opposite bank only needs room for its own mouth. + const int side = sideOf(r, i, source); + for (const auto &join : joins[r]) + if (std::abs(i - join.first) * kSpacing < (join.second == side ? 20 : 8)) + return false; + return true; + } + + std::vector attachments(Vec source, int firstReach = 0) const { + std::vector out; + for (int r = firstReach; r < int(reaches.size()); ++r) + for (int i = 0; i < int(reaches[r].points.size()); i += 2) { + if (!attachable(r, i, source)) + continue; + const Vec p = reaches[r].points[i]; + const double du = centred(p.x - source.x, along), dv = p.y - source.y; + if (dv < std::max(4.0, kSlope * std::abs(du))) + continue; + out.push_back({du * du + dv * dv, r, i}); + } + std::sort(out.begin(), out.end(), [](const Attachment &a, const Attachment &b) { + if (a.distance2 != b.distance2) + return a.distance2 < b.distance2; + return a.reach != b.reach ? a.reach < b.reach : a.index < b.index; + }); + return out; + } + + struct Verdict { + bool ok = false; + int blocking = -1; // the other river the path ran into, when that is why it was refused + Vec where{0, 0}; + }; + + Verdict review(const std::vector &points, int parent) const { + Verdict verdict; + const size_t n = points.size(); + if ((n - 1) * kSpacing < 12) + return verdict; + const double joinZone = clearance + 4; + for (size_t i = 0; i < n; i += 2) { + const double toEnd = (n - 1 - i) * kSpacing; + const Vec p = points[i]; + if (toEnd > 4 && !inland(p, northMargin, southMargin)) + return verdict; + if (toEnd <= 6) + continue; + // Clear of every other river; and while it swings in to join its parent, arriving at an + // angle rather than running alongside it. + int near = -1; + if (nearest2(p, parent, -1, &near) < sq(toEnd > joinZone ? clearance : 0.75 * clearance)) { + verdict.blocking = near; + verdict.where = p; + return verdict; + } + if (nearest2(p, -1, parent) < sq(std::min(clearance, 0.35 * toEnd))) + return verdict; + } + verdict.ok = true; + return verdict; + } + + void attachSources(const std::vector &sources, GenerationContext &context) { + const double none = std::numeric_limits::max(); + std::vector cached(sources.size(), none); + std::vector alive(sources.size(), 1); + for (size_t k = 0; k < sources.size(); ++k) { + const auto list = attachments(sources[k]); + if (!list.empty()) + cached[k] = list.front().distance2; + } + while (true) { + int pick = -1; + for (size_t k = 0; k < sources.size(); ++k) + if (alive[k] && cached[k] < none && (pick < 0 || cached[k] < cached[pick])) + pick = int(k); + if (pick < 0) + return; + const auto list = attachments(sources[pick]); + if (list.empty()) { + alive[pick] = 0; + continue; + } + // A cached distance can only be optimistic (junctions since added rule points out), so a + // spring whose real best is further than cached waits its proper turn. + if (list.front().distance2 > cached[pick] + 1e-9) { + cached[pick] = list.front().distance2; + continue; + } + alive[pick] = 0; + std::vector tried; + int added = -1; + for (const Attachment &a : list) { + if (tried.size() >= 12 || added >= 0) + break; + const Vec p = reaches[a.reach].points[a.index]; + bool repeat = false; + for (Vec t : tried) + repeat = repeat || distance2(t, p) < 36; + if (repeat) + continue; + tried.push_back(p); + // A full meander first, then a gentler one. A river joins whatever river it runs into: + // a path refused for passing too near another river is tried again as its tributary. + int target = a.reach, index = a.index; + for (int hop = 0; hop < 3 && added < 0; ++hop) { + Verdict last; + for (double meander : {1.0, 0.35}) { + Reach reach; + reach.points = path(sources[pick], reaches[target].points[index], + tangent(target, index), meander, context); + last = review(reach.points, target); + if (!last.ok) + continue; + reach.parent = target; + reach.joinIndex = index; + const int side = sideOf(target, index, sources[pick]); + added = add(std::move(reach)); + joins[target].push_back({index, side}); + break; + } + if (added >= 0 || last.blocking < 0) + break; + // Nothing joins the delta itself: a path that runs into it joins the trunk above it. + const int blocking = reaches[last.blocking].distributary ? trunk : last.blocking; + const int next = blocking >= 0 ? nearestAttachable(blocking, last.where, sources[pick]) : -1; + if (next < 0) + break; + target = blocking; + index = next; + } + } + if (added < 0) + continue; + for (size_t k = 0; k < sources.size(); ++k) { + if (!alive[k]) + continue; + if (nearest2(sources[k]) < sq(clearance)) { + alive[k] = 0; // it would only add a stub beside the river that just passed it + continue; + } + const auto fresh = attachments(sources[k], added); + if (!fresh.empty()) + cached[k] = std::min(cached[k], fresh.front().distance2); + } + } + } + +private: + int column(Vec p) const { return std::min(columns - 1, int(wrapped(p.x, along) / cellW)); } + int row(Vec p) const { return std::clamp(int(std::floor(p.y / cellH)), 0, rows - 1); } + size_t cellOf(Vec p) const { return size_t(row(p)) * columns + column(p); } + + int along, across; + const Ripple &north, &south; + double clearance, northMargin, southMargin; + int columns, rows; + double cellW, cellH; + std::vector>> cells; +}; + +// Fords go where a river can be crossed cleanly: the middle of every straight, uncluttered run of +// river, longest runs first, about one per `spacing` tiles of each reach. +void chooseFords(Layout &layout, const WatershedOptions &o) { + const double spacing = kFordSpacing[std::clamp(o.fords, 1, 5) - 1]; + const Frame &frame = layout.frame; + auto distance2 = [&](Vec a, Vec b) { + return sq(centred(a.x - b.x, frame.width)) + sq(centred(a.y - b.y, frame.height)); + }; + const int count = int(layout.reaches.size()); + const ChannelIndex channels(layout); + std::vector order; + for (int r = layout.trunk; r < count; ++r) + order.push_back(r); + for (int r = 0; r < layout.trunk; ++r) + order.push_back(r); + for (int r : order) { + const Reach &reach = layout.reaches[r]; + const int n = int(reach.points.size()); + const double len = reach.length(); + if (len < std::max(20.0, spacing / 2)) + continue; + auto valid = [&](int i) { + if (i < 16 || i >= n - 16) + return false; + const double radius = reach.radius[i], span = radius + kFordReach; + if (reach.coastGap[i] < span + 6) + return false; + for (int j : layout.joins[r]) + if (std::abs(i - j) * kSpacing < radius + 8) + return false; + // Only on a straight stretch, eight tiles either way, where the channel runs on in line + // beyond both sides of the ford and is no wider across than its radius. + const Vec before = reach.points[i] - reach.points[i - 16]; + const Vec after = reach.points[i + 16] - reach.points[i]; + if (dot(normalized(before), normalized(after)) < std::cos(25 * kPi / 180)) + return false; + // The crossing, and dry ground a little past both of its ends, touch no other channel: + // a ford crosses one river, not a junction, a spring's pond or a neighbouring bend. + const Vec along = normalized(reach.points[i + 2] - reach.points[i - 2]); + const Vec across{-along.y, along.x}; + for (double s = -(span + 1); s <= span + 1 + 1e-9; s += 1.0) + for (double a : {-kFordHalfWidth, 0.0, kFordHalfWidth}) + if (!channels.clear(reach.points[i] + across * s + along * a, + std::abs(s) <= span ? 1.5 : 0.5, r, i, span + 6)) + return false; + return true; + }; + std::vector> runs; // first and last fordable point, a tile apart + for (int i = 12; i < n - 12; i += 2) { + if (!valid(i)) + continue; + if (!runs.empty() && runs.back().second == i - 2) + runs.back().second = i; + else + runs.push_back({i, i}); + } + struct Spot { + int index; + int run; + }; + std::vector spots; + for (const auto &run : runs) { + const int steps = (run.second - run.first) / 2; + const int extra = int(steps / spacing); + for (int k = 0; k <= extra; ++k) + spots.push_back({run.first + 2 * int(steps * (k + 0.5) / (extra + 1)), steps}); + } + std::stable_sort(spots.begin(), spots.end(), + [](const Spot &a, const Spot &b) { return a.run > b.run; }); + const int limit = std::max(1, int(std::lround(len / spacing))); + std::vector chosen; + for (const Spot &spot : spots) { + if (int(chosen.size()) >= limit) + break; + const double span = reach.radius[spot.index] + kFordReach; + bool apart = true; + for (int c : chosen) + apart = apart && std::abs(c - spot.index) * kSpacing >= 0.75 * spacing; + for (const Ford &f : layout.fords) + apart = apart && distance2(reach.points[spot.index], f.center) >= sq(f.span + span + 10); + if (!apart) + continue; + chosen.push_back(spot.index); + const Vec along = normalized(reach.points[spot.index + 2] - reach.points[spot.index - 2]); + layout.fords.push_back({reach.points[spot.index], along, Vec{-along.y, along.x}, span}); + } + } +} + +// The whole plan: a pure function of the request's size, options and seed. +Layout planLayout(int width, int height, const WatershedOptions &o, GenerationContext &context) { + Layout layout; + Frame &frame = layout.frame; + frame.width = width; + frame.height = height; + // Any of the four sides on a square map. On a rectangular one the rivers run the long way, with + // the sea across one of its short ends: one network cannot gather a coast twice as long as the + // land is deep, and would leave most of the map a desert. + frame.orientation = int(context.bounded(kLayoutStream, width == height ? 4 : 2)); + if (width > height) + frame.orientation += 2; + frame.along = frame.orientation < 2 ? width : height; + frame.across = frame.orientation < 2 ? height : width; + const double along = frame.along, across = frame.across; + + // One band of sea along the map's edge: deep off the delta, and a narrow strait along the + // uplands on the far side of the torus seam. + const double sea = std::clamp(std::round(8 + 0.14 * across), 16.0, 64.0); + layout.north = makeRipple(context, 0.35 * sea, 0.08 * sea + 1.0, frame.along); + layout.south = makeRipple(context, across - 0.65 * sea, 0.2 * sea, frame.along); + + const double minimumRadius = channelRadius(o.riverWidth); + const double gain = 0.35 + 0.1 * o.riverWidth; + const double estimate = minimumRadius + 0.5; + const double clearance = 2 * estimate + 5; + const double northMargin = estimate + 8, southMargin = estimate + 6; + RiverPlanner planner(frame, layout.north, layout.south, clearance, northMargin, southMargin); + + // The delta: one to three distributaries fanning out from an apex to the coast. + const double deltaLength = std::clamp(0.12 * across + 6, 12.0, 44.0); + const double spread = std::clamp(0.16 * along, 18.0, 44.0); + const int mouthLimit = std::max(1, int(0.7 * along / spread) + 1); + const int mouthCount = std::min(mouthLimit, 1 + int(context.bounded(kLayoutStream, 3))); + const double u0 = between(context, kLayoutStream, 0, along); + layout.south.lobeCentre = u0; + layout.south.lobeHeight = 0.3 * sea; + layout.south.lobeWidth = 1.1 * spread + 6; + const Vec apex{u0, layout.south.at(u0) - deltaLength}; + static const double shares[3][3] = {{1, 0, 0}, {0.5, 0.5, 0}, {0.32, 0.36, 0.32}}; + std::vector mouths; + for (int m = 0; m < mouthCount; ++m) { + const double u = u0 + (m - (mouthCount - 1) / 2.0) * spread * + between(context, kLayoutStream, 0.85, 1.15); + mouths.push_back({u, layout.south.at(u)}); + Reach reach; + reach.points = + planner.path(apex, {u, layout.south.at(u) + std::min(8.0, 0.25 * sea)}, {0, 1}, 0.5, context); + reach.distributary = true; + planner.add(std::move(reach)); + } + + // The trunk rises well up in the uplands; every other spring joins the network it starts. + const double headU = u0 + between(context, kLayoutStream, -0.15, 0.15) * along; + const double top = layout.north.at(headU) + northMargin; + const Vec head{headU, top + std::max(0.0, apex.y - top) * between(context, kLayoutStream, 0.1, 0.3)}; + { + Reach reach; + reach.points = planner.path(head, apex, {0, 1}, 1.0, context); + layout.trunk = planner.add(std::move(reach)); + planner.trunk = layout.trunk; + } + const int trunk = layout.trunk; + + const double landArea = along * (across - sea); + const int wanted = int(std::lround(landArea * o.riverDensity * 1.5e-4)); + const double sourceSpacing = 0.7 * std::sqrt(landArea / (wanted + 1.0)); + // A spring that finds no way onto the network is dropped, so springs are sampled again wherever + // there is still room until enough have joined it. + for (int round = 0; round < 6; ++round) { + const int missing = wanted - (int(planner.reaches.size()) - trunk - 1); + if (missing <= 0) + break; + std::vector sources; + for (int attempt = 0; attempt < 60 * missing && int(sources.size()) < missing; ++attempt) { + const double u = between(context, kRiverStream, 0, along); + // Upstream of the delta's apex: nearer the sea than that, there is no river to run down to. + const double lo = layout.north.at(u) + northMargin; + const double hi = std::min(layout.south.at(u) - (deltaLength + southMargin), apex.y - 6); + const double v = between(context, kRiverStream, 0, 1); + if (hi <= lo) + continue; + const Vec s{u, lo + (hi - lo) * v}; + bool clear = planner.nearest2(s) >= sq(clearance); + for (size_t k = 0; clear && k < sources.size(); ++k) + clear = planner.distance2(s, sources[k]) >= sq(sourceSpacing); + for (int r = trunk + 1; clear && r < int(planner.reaches.size()); ++r) + clear = planner.distance2(s, planner.reaches[r].points.front()) >= sq(sourceSpacing); + if (clear) + sources.push_back(s); + } + planner.attachSources(sources, context); + } + + // Width grows with the number of springs upstream. + const int count = int(planner.reaches.size()); + std::vector outflow(count, 1.0); + std::vector> children(count); + for (int r = count - 1; r > trunk; --r) { + outflow[planner.reaches[r].parent] += outflow[r]; + children[planner.reaches[r].parent].push_back(r); + } + auto radiusFor = [&](double flow) { + return std::min(minimumRadius + 3.5, minimumRadius + gain * (std::sqrt(flow) - 1)); + }; + for (int r = trunk; r < count; ++r) { + Reach &reach = planner.reaches[r]; + std::vector> inflows; + for (int c : children[r]) + inflows.push_back({planner.reaches[c].joinIndex, outflow[c]}); + std::sort(inflows.begin(), inflows.end()); + double flow = 1; + size_t next = 0; + reach.radius.resize(reach.points.size()); + for (size_t i = 0; i < reach.points.size(); ++i) { + while (next < inflows.size() && inflows[next].first <= int(i)) + flow += inflows[next++].second; + reach.radius[i] = radiusFor(flow); + } + } + for (int m = 0; m < mouthCount; ++m) { + Reach &reach = planner.reaches[m]; + const double radius = radiusFor(std::max(1.0, outflow[trunk] * shares[mouthCount - 1][m])); + const double last = double(reach.points.size() - 1); + reach.radius.resize(reach.points.size()); + for (size_t i = 0; i < reach.points.size(); ++i) + reach.radius[i] = radius + 0.8 * (i / last); + } + + // Into map coordinates. + for (Reach &reach : planner.reaches) { + reach.coastGap.resize(reach.points.size()); + for (size_t i = 0; i < reach.points.size(); ++i) { + const Vec c = reach.points[i]; + reach.coastGap[i] = std::min(c.y - layout.north.at(c.x), layout.south.at(c.x) - c.y); + } + } + for (int r = trunk + 1; r < count; ++r) { + const Reach &reach = planner.reaches[r]; + layout.junctions.push_back(frame.toMap(planner.reaches[reach.parent].points[reach.joinIndex])); + } + layout.junctions.push_back(frame.toMap(apex)); + for (int r = trunk; r < count; ++r) { + std::vector line; + Vec p = planner.reaches[r].points.front(); + for (int k = 0; k < frame.across * 2; ++k) { + line.push_back(frame.toMap(p)); + if (p.y < layout.north.at(p.x) - 1) + break; + p.y -= kSpacing; + } + layout.divides.push_back(std::move(line)); + } + for (Vec m : mouths) + layout.mouths.push_back(frame.toMap(m)); + for (Reach &reach : planner.reaches) + for (Vec &p : reach.points) + p = frame.toMap(p); + layout.reaches = std::move(planner.reaches); + layout.joins.resize(planner.joins.size()); + for (size_t r = 0; r < planner.joins.size(); ++r) + for (const auto &join : planner.joins[r]) + layout.joins[r].push_back(join.first); + chooseFords(layout, o); + return layout; +} + +// Distance in fifths of a tile (5 across, 7 diagonally) from every vertex to the nearest water. +std::vector waterDistance(const std::vector &terrain, int w, int h) { + const int unreached = std::numeric_limits::max(); + std::vector distance(terrain.size(), unreached); + std::array, 8> buckets; + size_t pending = 0; + for (size_t i = 0; i < terrain.size(); ++i) + if (terrain[i] == WATER) { + distance[i] = 0; + buckets[0].push_back(int(i)); + ++pending; + } + for (int current = 0; pending > 0; ++current) { + std::vector items; + items.swap(buckets[current % 8]); + pending -= items.size(); + for (int p : items) { + if (distance[p] != current) + continue; + const int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (!dx && !dy) + continue; + const int q = wrapIndex(y + dy, h) * w + wrapIndex(x + dx, w); + const int next = current + (dx && dy ? 7 : 5); + if (next < distance[q]) { + distance[q] = next; + buckets[next % 8].push_back(q); + ++pending; + } + } + } + } + return distance; +} + +void stampFord(std::vector &terrain, int w, int h, const Ford &f) { + const int reach = int(std::ceil(f.span + kFordHalfWidth)) + 1; + const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) { + const Vec d{cx + dx - f.center.x, cy + dy - f.center.y}; + if (std::abs(dot(d, f.along)) > kFordHalfWidth || std::abs(dot(d, f.across)) > f.span) + continue; + unsigned char &t = terrain[size_t(wrapIndex(cy + dy, h)) * w + wrapIndex(cx + dx, w)]; + if (t == WATER) + t = SAND; + } +} + +std::vector stampTerrain(const Layout &layout, const WatershedOptions &o, + GenerationContext &context) { + const Frame &frame = layout.frame; + const int w = frame.width, h = frame.height; + std::vector terrain(size_t(w) * h, GRASS); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const Vec c = frame.toCanonical({double(x), double(y)}); + if (c.y < layout.north.at(c.x) || c.y > layout.south.at(c.x)) + terrain[size_t(y) * w + x] = WATER; + } + auto disc = [&](Vec centre, double radius) { + const int x0 = int(std::floor(centre.x - radius)), x1 = int(std::ceil(centre.x + radius)); + const int y0 = int(std::floor(centre.y - radius)), y1 = int(std::ceil(centre.y + radius)); + for (int y = y0; y <= y1; ++y) + for (int x = x0; x <= x1; ++x) + if (sq(x - centre.x) + sq(y - centre.y) <= radius * radius) + terrain[size_t(wrapIndex(y, h)) * w + wrapIndex(x, w)] = WATER; + }; + for (const Reach &reach : layout.reaches) { + for (size_t i = 0; i < reach.points.size(); ++i) + disc(reach.points[i], reach.radius[i]); + if (!reach.distributary) + disc(reach.points.front(), reach.radius.front() + 1.3); // the spring's pond + } + + // Uplands far from any water dry out to sand. The floor keeps a floodplain of grass wide enough + // to farm and build on beside every river, whatever the setting. + if (o.dryness > 0) { + const std::vector distance = waterDistance(terrain, w, h); + std::mt19937 &random = context.stream("watershed-terrain"); + const PeriodicNoise coarse(w, h, 28, random), fine(w, h, 9, random); + const double reach = 24.0 - 1.3 * o.dryness; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + if (terrain[i] != GRASS) + continue; + const double n = 0.7 * coarse.at(x, y) + 0.3 * fine.at(x, y); + if (distance[i] > 5 * std::max(11.0, reach + 12.0 * (n - 0.5))) + terrain[i] = SAND; + } + } + + // Map::controlSand's rule, applied to every vertex at once instead of in raster order: water + // touching grass and grass touching water both become sand, so every shore gets the same sand + // on both sides and the result is already a fixed point of controlSand. + const std::vector before = terrain; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + if (before[i] == SAND) + continue; + const unsigned char other = before[i] == WATER ? GRASS : WATER; + for (int dy = -1; dy <= 1 && terrain[i] != SAND; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (before[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] == other) { + terrain[i] = SAND; + break; + } + } + for (const Ford &f : layout.fords) + stampFord(terrain, w, h, f); + return terrain; +} + +bool vertexTileWater(const std::vector &t, int w, int h, int x, int y) { + const int x1 = (x + 1) % w, y1 = (y + 1) % h; + return t[size_t(y) * w + x] == WATER && t[size_t(y) * w + x1] == WATER && + t[size_t(y1) * w + x] == WATER && t[size_t(y1) * w + x1] == WATER; +} + +// Every ford interrupts a real channel - open water runs on in line just upstream and downstream +// of it - is open from bank to bank along three parallel lines, and reaches dry land at both +// ends. `water(x, y)` answers for a tile. +template std::string checkFords(const Layout &layout, Water water) { + const int w = layout.frame.width, h = layout.frame.height; + auto wet = [&](Vec p) { + return water(wrapIndex(int(std::floor(p.x)), w), wrapIndex(int(std::floor(p.y)), h)); + }; + auto where = [&](const Ford &f) { + return " at (" + std::to_string(wrapIndex(int(f.center.x), w)) + ", " + + std::to_string(wrapIndex(int(f.center.y), h)) + ")"; + }; + // Open water within a tile of a point: a thin or diagonal channel's water core need not cover + // any one exact tile once its shores are sanded. + auto wetNear = [&](Vec p) { + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (wet(p + Vec{double(dx), double(dy)})) + return true; + return false; + }; + for (const Ford &f : layout.fords) { + for (int side : {-1, 1}) + if (!wetNear(f.center + f.along * (side * (kFordHalfWidth + 2.5)))) + return "A ford" + where(f) + " does not cross a channel."; + for (int a = -1; a <= 1; ++a) + for (double s = -f.span; s <= f.span + 1e-9; s += kSpacing) + if (wet(f.center + f.along * a + f.across * s)) + return "A ford" + where(f) + " is cut by open water."; + for (int side : {-1, 1}) + if (wet(f.center + f.across * (side * (f.span + 1.0)))) + return "A ford" + where(f) + " does not reach its bank."; + } + return ""; +} + +// Every channel keeps a 4-connected core of open water from its spring to where it ends, except +// where a ford crosses it - so the fords are the only way over on foot. +template std::string checkChannels(const Layout &layout, Water water) { + const int w = layout.frame.width, h = layout.frame.height; + for (const Reach &reach : layout.reaches) { + bool previous = false; + int px = 0, py = 0; + for (size_t i = 0; i < reach.points.size(); ++i) { + const Vec p = reach.points[i]; + bool skip = reach.coastGap[i] < 2.0; + for (size_t k = 0; !skip && k < layout.fords.size(); ++k) { + const Ford &f = layout.fords[k]; + const Vec d{centred(p.x - f.center.x, w), centred(p.y - f.center.y, h)}; + skip = std::abs(dot(d, f.along)) <= kFordHalfWidth + 2 && + std::abs(dot(d, f.across)) <= f.span + 1; + } + if (skip) { + previous = false; + continue; + } + const int x = wrapIndex(int(std::floor(p.x)), w), y = wrapIndex(int(std::floor(p.y)), h); + if (!water(x, y)) + return "A river channel silts up at (" + std::to_string(x) + ", " + std::to_string(y) + ")."; + if (previous && x != px && y != py && !water(x, py) && !water(px, y)) + return "A river channel can be stepped across at (" + std::to_string(x) + ", " + + std::to_string(y) + ")."; + previous = true; + px = x; + py = y; + } + } + return ""; +} + +// Labels 8-connected components of open tiles; closed tiles get -1. +std::vector components(const std::vector &open, int w, int h, int &count) { + std::vector label(open.size(), -1); + std::vector queue; + queue.reserve(open.size()); + count = 0; + for (size_t start = 0; start < open.size(); ++start) { + if (!open[start] || label[start] >= 0) + continue; + queue.clear(); + queue.push_back(int(start)); + label[start] = count; + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); + if (open[q] && label[q] < 0) { + label[q] = count; + queue.push_back(int(q)); + } + } + } + ++count; + } + return label; +} + +// The finished terrain seen tile by tile. +struct Tiles { + int w = 0, h = 0; + std::vector water, grass; + std::vector waterSteps; // 8-neighbour steps to the nearest water tile + std::vector grassDepth; // 8-neighbour steps to the nearest tile that isn't pure grass + std::vector component; // walkable component, -1 on water + int mainComponent = -1; +}; + +// Steps from every tile to the nearest tile where `seed` holds. +std::vector stepsFrom(const std::vector &seed, int w, int h) { + std::vector steps(seed.size(), -1), queue; + queue.reserve(seed.size()); + for (size_t i = 0; i < seed.size(); ++i) + if (seed[i]) { + steps[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); + if (steps[q] < 0) { + steps[q] = steps[queue[head]] + 1; + queue.push_back(int(q)); + } + } + } + return steps; +} + +Tiles tileView(const std::vector &terrain, int w, int h) { + Tiles t; + t.w = w; + t.h = h; + const size_t n = size_t(w) * h; + t.water.assign(n, 0); + t.grass.assign(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + const int x1 = (x + 1) % w, y1 = (y + 1) % h; + const unsigned char a = terrain[i], b = terrain[size_t(y) * w + x1]; + const unsigned char c = terrain[size_t(y1) * w + x], d = terrain[size_t(y1) * w + x1]; + t.water[i] = a == WATER && b == WATER && c == WATER && d == WATER; + t.grass[i] = a == GRASS && b == GRASS && c == GRASS && d == GRASS; + } + t.waterSteps = stepsFrom(t.water, w, h); + std::vector land(n), notGrass(n); + for (size_t i = 0; i < n; ++i) { + land[i] = !t.water[i]; + notGrass[i] = !t.grass[i]; + } + t.grassDepth = stepsFrom(notGrass, w, h); + int count = 0; + t.component = components(land, w, h, count); + std::vector sizes(count, 0); + for (int c : t.component) + if (c >= 0) + ++sizes[c]; + if (count) + t.mainComponent = int(std::max_element(sizes.begin(), sizes.end()) - sizes.begin()); + return t; +} + +// The banks: land split by the rivers, by every ford as if it were still water, and by a line +// from each spring upstream to the sea, so the ground either side of a river counts as two banks +// even though a colony could walk around its spring. +std::vector bankRegions(const Layout &layout, const Tiles &t) { + const int w = t.w, h = t.h; + std::vector open(t.water.size()); + for (size_t i = 0; i < open.size(); ++i) + open[i] = !t.water[i]; + auto close = [&](int x, int y) { open[size_t(wrapIndex(y, h)) * w + wrapIndex(x, w)] = 0; }; + for (const Ford &f : layout.fords) { + const int reach = int(std::ceil(f.span + kFordHalfWidth)) + 2; + const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) { + const Vec d{cx + dx + 0.5 - f.center.x, cy + dy + 0.5 - f.center.y}; + if (std::abs(dot(d, f.along)) <= kFordHalfWidth + 1 && + std::abs(dot(d, f.across)) <= f.span + 1) + close(cx + dx, cy + dy); + } + } + for (const auto &line : layout.divides) + for (Vec p : line) + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + close(int(std::floor(p.x)) + dx, int(std::floor(p.y)) + dy); + int count = 0; + return components(open, w, h, count); +} + +// Swarm anchors on the floodplain of the main landmass, clear of the fords, spread as far apart +// as possible and, where the map allows, each on a bank of its own. +std::vector chooseSites(const Layout &layout, const Tiles &t, + const std::vector ®ions, + const Fertility::Field &fertility, int teams, + GenerationContext &context, std::string &detail) { + const int w = t.w, h = t.h; + struct Site { + int x, y, region; + double fertility; + }; + std::vector candidates; + for (int y = 0; y < h; y += 2) + for (int x = 0; x < w; x += 2) { + const size_t c = size_t((y + 2) % h) * w + (x + 2) % w; + if (t.component[c] != t.mainComponent || regions[c] < 0 || + t.waterSteps[c] < kColonyWaterNear || t.waterSteps[c] > kColonyWaterFar) + continue; + bool open = true; + for (int dy = -2; dy <= 5 && open; ++dy) + for (int dx = -2; dx <= 5 && open; ++dx) + open = t.grass[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] != 0; + const Vec centre{x + 2.0, y + 2.0}; + for (size_t k = 0; open && k < layout.fords.size(); ++k) { + const Ford &f = layout.fords[k]; + open = sq(centred(centre.x - f.center.x, w)) + sq(centred(centre.y - f.center.y, h)) >= + sq(f.span + 12); + } + if (!open) + continue; + // How fertile the ground a young colony works is, around the swarm. + double sum = 0; + int samples = 0; + for (int dy = -10; dy <= 13; dy += 3) + for (int dx = -10; dx <= 13; dx += 3, ++samples) + sum += fertility.at(wrapIndex(x + dx, w), wrapIndex(y + dy, h)); + candidates.push_back({x, y, regions[c], sum / samples}); + } + if (int(candidates.size()) < teams) { + detail = "only " + std::to_string(candidates.size()) + " floodplain sites for " + + std::to_string(teams) + " colonies"; + return {}; + } + // Every colony starts on the more fertile half of the floodplain, so none is handed the dry + // fringe while another gets the river bank; spacing is then chosen within that half. + std::stable_sort(candidates.begin(), candidates.end(), + [](const Site &a, const Site &b) { return a.fertility > b.fertility; }); + const std::vector all = candidates; + candidates.resize(std::max(size_t(teams) * 6, candidates.size() / 2)); + if (candidates.size() > all.size()) + candidates = all; + auto distance2 = [&](const Site &a, const Site &b) { + return sq(centred(double(a.x - b.x), w)) + sq(centred(double(a.y - b.y), h)); + }; + std::vector best; + double bestScore = -1; + std::vector nearest(candidates.size()); + for (int trial = 0; trial < 32; ++trial) { + // Half the trials in the fertile half; if no spacing fits there, the rest use every site. + if (trial == 16) { + if (!best.empty()) + break; + candidates = all; + nearest.assign(candidates.size(), 0); + } + std::vector chosen{int(context.bounded(kStartStream, candidates.size()))}; + std::fill(nearest.begin(), nearest.end(), std::numeric_limits::max()); + auto claim = [&](int k) { + for (size_t c = 0; c < candidates.size(); ++c) + nearest[c] = std::min(nearest[c], distance2(candidates[c], candidates[k])); + }; + claim(chosen.front()); + while (int(chosen.size()) < teams) { + int pick = -1; + double pickScore = -1; + for (size_t c = 0; c < candidates.size(); ++c) { + if (nearest[c] < sq(kColonySpacing)) + continue; + bool shared = false; + for (int k : chosen) + shared = shared || candidates[k].region == candidates[c].region; + const double score = nearest[c] * (shared ? 0.3 : 1.0); + if (score > pickScore) { + pickScore = score; + pick = int(c); + } + } + if (pick < 0) + break; + chosen.push_back(pick); + claim(pick); + } + if (int(chosen.size()) < teams) + continue; + double closest = std::numeric_limits::max(); + int shared = 0; + for (size_t a = 0; a < chosen.size(); ++a) + for (size_t b = a + 1; b < chosen.size(); ++b) { + closest = std::min(closest, distance2(candidates[chosen[a]], candidates[chosen[b]])); + shared += candidates[chosen[a]].region == candidates[chosen[b]].region; + } + const double score = (teams > 1 ? closest : 1.0) * std::pow(0.5, shared); + if (score > bestScore) { + bestScore = score; + best = chosen; + } + } + if (best.empty()) { + detail = "the floodplain sites are too close together for " + std::to_string(teams) + + " colonies"; + return {}; + } + std::vector sites; + for (int k : best) + sites.emplace_back(candidates[k].x, candidates[k].y); + return sites; +} + +// Walking steps from the given tiles over land that is free of resources and buildings. +std::vector walk(const Map &map, const std::vector &from) { + const int w = map.getW(), h = map.getH(); + std::vector distance(size_t(w) * h, -1); + std::vector queue; + queue.reserve(distance.size()); + for (int p : from) + if (distance[p] < 0) { + distance[p] = 0; + queue.push_back(p); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const size_t q = size_t(ny) * w + nx; + if (distance[q] < 0 && !map.isWater(nx, ny) && !map.isResource(nx, ny) && + map.getBuilding(nx, ny) == NOGBID) { + distance[q] = distance[queue[head]] + 1; + queue.push_back(int(q)); + } + } + } + return distance; +} + +std::vector> workersByTeam(const Map &map, int teams) { + std::vector> workers(teams); + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid == NOGUID) + continue; + const int team = Unit::GIDtoTeam(gid); + if (team >= 0 && team < teams) + workers[team].push_back(y * map.getW() + x); + } + return workers; +} + +// Clumps keep a gap of open ground between them, so no two can join into a wall. +class Reservations { +public: + Reservations(int w, int h) : w(w), h(h), taken(size_t(w) * h, 0) {} + bool free(int x, int y, int radius) const { + const int reach = radius + 2; + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + if (dx * dx + dy * dy <= reach * reach && + taken[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]) + return false; + return true; + } + void reserve(int x, int y, int radius) { + const int reach = radius + 1; + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + if (dx * dx + dy * dy <= reach * reach) + taken[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] = 1; + } + +private: + int w, h; + std::vector taken; +}; + +// Every colony gets the same kit on its own floodplain: wheat and wood where the ground is most +// fertile, on different sides of the swarm, and a little stone on the drier side. +void placeStarterKits(Game &game, GenerationContext &context, int teams, const Tiles &t, + const Fertility::Field &fertility, Reservations &reservations) { + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const auto workers = workersByTeam(map, teams); + for (int team = 0; team < teams; ++team) { + const std::vector distance = walk(map, workers[team]); + const int bx = context.bootX[team], by = context.bootY[team]; + const Vec centre{bx + 2.0, by + 2.0}; + struct Spot { + int x, y, steps; + Vec offset; + std::uint32_t fertility; + }; + std::vector spots; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const int steps = distance[size_t(y) * w + x]; + if (steps < 6 || steps > 22 || !map.isGrass(x, y) || !map.isResourceAllowed(x, y, CORN)) + continue; + const Vec offset{centred(x - centre.x, w), centred(y - centre.y, h)}; + if (std::max(std::abs(offset.x), std::abs(offset.y)) < 7.5) + continue; + spots.push_back({x, y, steps, offset, fertility.at(x, y)}); + } + // On the preferred side at full size if possible; then on any side; then a smaller clump a + // little further out that may sit beside another, for a colony on a narrow strip of + // floodplain. Any route a kit closes is opened again by connectColonies. + auto pick = [&](int nearSteps, int farSteps, bool fertile, const std::vector &avoid, + double minimumAngle, int radius) -> std::pair { + for (int pass = 0; pass < 3; ++pass) { + const int size = pass < 2 ? radius : 1, reach = pass < 2 ? farSteps : farSteps + 8; + const Spot *chosen = nullptr; + for (const Spot &s : spots) { + if (s.steps < nearSteps || s.steps > reach || + t.grassDepth[size_t(s.y) * w + s.x] <= size || + (pass < 2 && !reservations.free(s.x, s.y, size))) + continue; + bool apart = true; + for (Vec a : avoid) + apart = apart && (pass > 0 || dot(normalized(a), normalized(s.offset)) <= + std::cos(minimumAngle * kPi / 180)); + if (!apart) + continue; + if (!chosen || (fertile ? s.fertility > chosen->fertility : s.fertility < chosen->fertility)) + chosen = &s; + } + if (chosen) + return {chosen, size}; + } + return {nullptr, 0}; + }; + std::vector used; + for (const auto &kit : {std::pair{CORN, 2}, std::pair{WOOD, 2}, + std::pair{STONE, 1}}) { + const bool stone = kit.first == STONE; + const auto [spot, size] = pick(stone ? 10 : 6, stone ? 14 : 10, !stone, used, + stone ? 60 : 100, kit.second); + if (!spot) + continue; + placeResourceClump(map, context, MapGeneratorPoint(spot->x, spot->y), kit.first, size); + reservations.reserve(spot->x, spot->y, size); + used.push_back(spot->offset); + } + } +} + +// Farmland in ribbons along the rivers: clumps seeded on the most fertile free ground, 2:1 wheat +// to wood, with the crop chosen by a coarse field so stretches of bank favour one or the other. +void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, + const Fertility::Field &fertility, const std::vector &keepClear, + Reservations &reservations) { + Map &map = game.map; + const int w = t.w, h = t.h; + std::mt19937 &random = context.stream("watershed-farmland"); + const PeriodicNoise patch(w, h, 6, random), crop(w, h, 20, random); + struct Candidate { + double score; + int index; + }; + std::vector order; + const std::uint32_t floor = Fertility::kScale / 20; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + const std::uint32_t f = fertility.at(x, y); + if (t.grassDepth[i] < 3 || keepClear[i] || f < floor || !map.isResourceAllowed(x, y, CORN) || + map.isResource(x, y)) + continue; + order.push_back({f * (0.5 + patch.at(x + 0.5, y + 0.5)), int(i)}); + } + std::sort(order.begin(), order.end(), [](const Candidate &a, const Candidate &b) { + return a.score != b.score ? a.score > b.score : a.index < b.index; + }); + int budget = int(order.size()) / 5; + for (const Candidate &c : order) { + if (budget <= 0) + break; + const int x = c.index % w, y = c.index / w; + const int radius = context.bounded("resources", 5) < 2 && t.grassDepth[c.index] > 3 ? 3 : 2; + if (!reservations.free(x, y, radius)) + continue; + const int type = crop.at(x + 0.5, y + 0.5) < 0.62 ? CORN : WOOD; + budget -= placeResourceClump(map, context, MapGeneratorPoint(x, y), type, radius); + reservations.reserve(x, y, radius); + } +} + +// Stone outcrops at the edge of the dry uplands, well away from the rivers. +void placeStone(Game &game, GenerationContext &context, const Tiles &t, + const std::vector &keepClear, Reservations &reservations) { + Map &map = game.map; + const int w = t.w, h = t.h; + std::vector candidates; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + if (!t.grass[i] || keepClear[i] || t.waterSteps[i] < 9) + continue; + bool edge = false; + for (int dy = -2; dy <= 2 && !edge; ++dy) + for (int dx = -2; dx <= 2 && !edge; ++dx) + edge = !t.grass[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]; + if (edge || t.waterSteps[i] >= 14) + candidates.push_back(int(i)); + } + const int outcrops = std::max(2, w * h / 3000); + std::vector placed; + for (int attempt = 0; attempt < outcrops * 20 && int(placed.size()) < outcrops && !candidates.empty(); + ++attempt) { + const int i = candidates[context.bounded("resources", candidates.size())]; + const int x = i % w, y = i / w, radius = 1 + int(context.bounded("resources", 2)); + bool apart = reservations.free(x, y, radius) && map.isResourceAllowed(x, y, STONE); + for (const auto &p : placed) + apart = apart && sq(centred(double(x - p.x), w)) + sq(centred(double(y - p.y), h)) >= sq(20); + if (!apart) + continue; + placeResourceClump(map, context, MapGeneratorPoint(x, y), STONE, radius); + reservations.reserve(x, y, radius); + placed.emplace_back(x, y); + } +} + +// Fruit is rare: a small grove in the fertile wedge where two rivers meet, one kind at a time. +void placeFruit(Game &game, GenerationContext &context, const Layout &layout, const Tiles &t, + const Fertility::Field &fertility, const std::vector &keepClear, + Reservations &reservations) { + Map &map = game.map; + const int w = t.w, h = t.h; + std::vector junctions = layout.junctions; + for (size_t i = junctions.size(); i > 1; --i) + std::swap(junctions[i - 1], junctions[context.bounded("resources", i)]); + const int groves = std::max(3, w * h / 6000); + int type = int(context.bounded("resources", 3)), placed = 0; + for (size_t j = 0; j < junctions.size() && placed < groves; ++j) { + const int jx = int(std::floor(junctions[j].x)), jy = int(std::floor(junctions[j].y)); + int bestX = 0, bestY = 0; + std::uint32_t bestFertility = 0; + for (int dy = -12; dy <= 12; ++dy) + for (int dx = -12; dx <= 12; ++dx) { + const int d2 = dx * dx + dy * dy; + const int x = wrapIndex(jx + dx, w), y = wrapIndex(jy + dy, h); + const size_t i = size_t(y) * w + x; + if (d2 < 25 || d2 > 144 || !t.grass[i] || keepClear[i] || fertility.at(x, y) <= bestFertility || + !map.isResourceAllowed(x, y, CHERRY) || !reservations.free(x, y, 1)) + continue; + bestFertility = fertility.at(x, y); + bestX = x; + bestY = y; + } + if (!bestFertility) + continue; + placeResourceClump(map, context, MapGeneratorPoint(bestX, bestY), CHERRY + type, 1); + reservations.reserve(bestX, bestY, 1); + type = (type + 1) % 3; + ++placed; + } +} + +// Algae off every mouth and in the sea's shallows, where there is sand near enough for it to +// grow back. +void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, const Tiles &t) { + Map &map = game.map; + const int w = t.w, h = t.h; + for (Vec mouth : layout.mouths) { + std::vector water; + const int mx = int(std::floor(mouth.x)), my = int(std::floor(mouth.y)); + for (int dy = -10; dy <= 10; ++dy) + for (int dx = -10; dx <= 10; ++dx) { + const int x = wrapIndex(mx + dx, w), y = wrapIndex(my + dy, h); + const size_t i = size_t(y) * w + x; + if (dx * dx + dy * dy <= 100 && t.water[i] && t.waterSteps[i] == 0) + water.push_back(int(i)); + } + for (int clump = 0; clump < 3 && !water.empty(); ++clump) { + const int i = water[context.bounded("resources", water.size())]; + placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 2); + } + } + std::vector shallows; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const size_t i = size_t(y) * w + x; + if (!t.water[i]) + continue; + bool shore = false; + for (int dy = -3; dy <= 3 && !shore; ++dy) + for (int dx = -3; dx <= 3 && !shore; ++dx) + shore = !t.water[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]; + if (!shore) + continue; + const Vec c = layout.frame.toCanonical({x + 0.5, y + 0.5}); + if (c.y < layout.north.at(c.x) - 1 || c.y > layout.south.at(c.x) + 1) + shallows.push_back(int(i)); + } + for (int clump = 0; clump < int(shallows.size()) / 60; ++clump) { + const int i = shallows[context.bounded("resources", shallows.size())]; + placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 1); + } +} + +// Clears the fewest resource tiles that let every colony walk to colony 0: a 0-1 search where +// stepping onto a resource costs one and open ground nothing. +bool connectColonies(Game &game, int teams, std::string &detail) { + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const auto workers = workersByTeam(map, teams); + for (int team = 1; team < teams; ++team) { + std::vector reach = walk(map, workers[0]); + bool connected = false; + for (int p : workers[team]) + connected = connected || reach[p] >= 0; + if (connected) + continue; + std::vector cost(size_t(w) * h, std::numeric_limits::max()), from(size_t(w) * h, -1); + std::vector target(size_t(w) * h, 0); + for (int p : workers[team]) + target[p] = 1; + std::deque queue; + for (int p : workers[0]) { + cost[p] = 0; + queue.push_back(p); + } + int reached = -1; + while (!queue.empty() && reached < 0) { + const int p = queue.front(); + queue.pop_front(); + if (target[p]) { + reached = p; + break; + } + const int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const int q = ny * w + nx; + if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID) + continue; + const int step = map.isResource(nx, ny) ? 1 : 0; + if (cost[p] + step < cost[q]) { + cost[q] = cost[p] + step; + from[q] = p; + if (step) + queue.push_back(q); + else + queue.push_front(q); + } + } + } + if (reached < 0) { + detail = "colony " + std::to_string(team) + " has no land route to colony 0"; + return false; + } + for (int p = reached; p >= 0; p = from[p]) + if (map.isResource(p % w, p / w)) + map.setNoResource(p % w, p / w, 1); + } + return true; +} + +bool generate(Game &game, GenerationContext &context) { + context.stage = "watershed layout"; + const WatershedOptions o(context.request); + Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + const Layout layout = planLayout(w, h, o, context); + + context.stage = "watershed terrain"; + const std::vector terrain = stampTerrain(layout, o, context); + const auto stampedWater = [&](int x, int y) { return vertexTileWater(terrain, w, h, x, y); }; + context.detail = checkFords(layout, stampedWater); + if (context.detail.empty()) + context.detail = checkChannels(layout, stampedWater); + if (!context.detail.empty()) + return false; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * w + x])); + map.controlSand(); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (map.getUMTerrain(x, y) != terrain[size_t(y) * w + x]) { + context.detail = "Map::controlSand changed the stamped shoreline"; + return false; + } + map.rebuildTerrain(); + + context.stage = "watershed colonies"; + const Tiles tiles = tileView(terrain, w, h); + const std::vector regions = bankRegions(layout, tiles); + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector sites = + chooseSites(layout, tiles, regions, fertility, teams, context, context.detail); + if (int(sites.size()) != teams) + return false; + for (int team = 0; team < teams; ++team) + game.addTeam(); + for (int team = 0; team < teams; ++team) { + std::vector home(size_t(w) * h, 0); + for (int dy = -6; dy <= 9; ++dy) + for (int dx = -6; dx <= 9; ++dx) { + const int x = wrapIndex(sites[team].x + dx, w), y = wrapIndex(sites[team].y + dy, h); + home[size_t(y) * w + x] = !tiles.water[size_t(y) * w + x]; + } + if (!placeSettlement(game, context, team, home, sites[team], "starts")) + return false; + } + + context.stage = "watershed resources"; + std::vector keepClear(size_t(w) * h, 0); + for (const Ford &f : layout.fords) { + const int reach = int(std::ceil(f.span + 5)); + const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + if (sq(cx + dx + 0.5 - f.center.x) + sq(cy + dy + 0.5 - f.center.y) <= sq(f.span + 5)) + keepClear[size_t(wrapIndex(cy + dy, h)) * w + wrapIndex(cx + dx, w)] = 1; + } + for (int team = 0; team < teams; ++team) + for (int dy = -3; dy <= 6; ++dy) + for (int dx = -3; dx <= 6; ++dx) + keepClear[size_t(wrapIndex(context.bootY[team] + dy, h)) * w + + wrapIndex(context.bootX[team] + dx, w)] = 1; + Reservations reservations(w, h); + placeStarterKits(game, context, teams, tiles, fertility, reservations); + placeFruit(game, context, layout, tiles, fertility, keepClear, reservations); + placeStone(game, context, tiles, keepClear, reservations); + placeFarmland(game, context, tiles, fertility, keepClear, reservations); + placeAlgae(game, context, layout, tiles); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (keepClear[size_t(y) * w + x] && map.isResource(x, y) && !map.isWater(x, y)) + map.setNoResource(x, y, 1); + context.stage = "watershed connectivity"; + if (!connectColonies(game, teams, context.detail)) + return false; + // Opening a route can clear part of a starter kit, so the kits are topped up afterwards - held + // to the kit's own distance rather than the shared defaults, so a colony whose kit could not be + // placed still gets wheat and wood as near as its rivals do - and the routes checked once more + // in case a top-up closed one. + guaranteeStartingResources(game, context, 16, 16); + return connectColonies(game, teams, context.detail); +} + +std::string validate(const GenerationRequest &r) { + if (r.nbTeams > (1 << (r.wDec + r.hDec)) / kTilesPerColony) + return "The watershed has too little floodplain for this many colonies; use a bigger map or " + "fewer colonies."; + return ""; +} + +// Checked on the finished world rather than trusted. Walking treats water, buildings and every +// resource tile as walls (units move, so they don't count): every colony must reach colony 0 and +// stand beside wheat and wood that way. The layout is planned again from the request to find the +// fords and channels: every ford must interrupt a real channel, be open from bank to bank and have +// walkable land at both ends, and every channel must keep its core of open water everywhere else. +std::string validateWorld(const Game &game, const GenerationContext &context) { + const Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + const auto workers = workersByTeam(map, teams); + for (int team = 0; team < teams; ++team) + if (workers[team].empty()) + return "Colony " + std::to_string(team) + " has no workers."; + std::vector fromFirst; + for (int team = 0; team < teams; ++team) { + const std::vector reach = walk(map, workers[team]); + if (team == 0) + fromFirst = reach; + bool met = false; + for (int p : workers[team]) + met = met || fromFirst[p] >= 0; + if (!met) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 without swimming."; + bool wheat = false, wood = false; + for (int y = 0; y < h && !(wheat && wood); ++y) + for (int x = 0; x < w; ++x) { + const int type = map.getResource(x, y).type; + if (type != CORN && type != WOOD) + continue; + bool beside = false; + for (int dy = -1; dy <= 1 && !beside; ++dy) + for (int dx = -1; dx <= 1 && !beside; ++dx) + beside = reach[size_t(map.normalizeY(y + dy)) * w + map.normalizeX(x + dx)] >= 0; + (type == CORN ? wheat : wood) = (type == CORN ? wheat : wood) || beside; + } + if (!wheat || !wood) + return "Colony " + std::to_string(team) + " cannot walk to " + (wheat ? "wood." : "wheat."); + } + + const WatershedOptions o(context.request); + GenerationContext replay(context.request); + const Layout layout = planLayout(w, h, o, replay); + const auto water = [&](int x, int y) { return map.isWater(x, y); }; + std::string error = checkFords(layout, water); + if (!error.empty()) + return error; + for (const Ford &f : layout.fords) + for (int side : {-1, 1}) { + const Vec end = f.center + f.across * (side * (f.span + 1.0)); + bool walkable = false; + for (int dy = -1; dy <= 1 && !walkable; ++dy) + for (int dx = -1; dx <= 1 && !walkable; ++dx) { + const int x = map.normalizeX(int(std::floor(end.x)) + dx); + const int y = map.normalizeY(int(std::floor(end.y)) + dy); + walkable = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + } + if (!walkable) + return "A ford at (" + std::to_string(map.normalizeX(int(f.center.x))) + ", " + + std::to_string(map.normalizeY(int(f.center.y))) + + ") has no walkable land on one bank."; + } + return checkChannels(layout, water); +} +} // namespace + +WatershedOptions::WatershedOptions(const GenerationRequest &r) + : riverDensity(r.option("river-density")), riverWidth(r.option("river-width")), + dryness(r.option("dryness")), fords(r.option("fords")) {} + +GeneratorDefinition watershedDefinition() { + return {"watershed", + 13, + "Watershed", + 1, + false, + {// Springs per area of land; each one that finds its way to the network is a tributary. + {"river-density", "River density", 1, 10, 1, 5, ControlGroup::Terrain}, + // The narrowest channel, at a spring; rivers widen downstream as tributaries join. + {"river-width", "River width", 1, 5, 1, 2, ControlGroup::Terrain}, + // How near to the rivers the uplands dry out to sand; 0 leaves them grass, and no + // setting dries ground within 11 tiles of water, so every river keeps a floodplain. + {"dryness", "Dryness", 0, 10, 1, 5, ControlGroup::Terrain}, + // How often a ford crosses a river: about one per 80 tiles of river at 1, one per 26 + // at 5, and at least one on every reach long enough to hold one. + {"fords", "Fords", 1, 5, 1, 3, ControlGroup::Layout}}, + generate, + true, + validate, + validateWorld}; +} diff --git a/src/map/generator/generators/WatershedGenerator.h b/src/map/generator/generators/WatershedGenerator.h new file mode 100644 index 000000000..a9fb30516 --- /dev/null +++ b/src/map/generator/generators/WatershedGenerator.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct WatershedOptions { + int riverDensity, riverWidth, dryness, fords; + explicit WatershedOptions(const GenerationRequest &r); +}; +GeneratorDefinition watershedDefinition(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index c2405530a..f9182aaf1 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -216,7 +216,7 @@ class MapGeneratorDefaultsTest assert(screen.descriptor.option("test-cell") == 16 && screen.descriptor.option("test-gap") == 5); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 12); + assert(registry.selectionIndex(101) == 13); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); From 014fd1be4e198bbac6c0eab27d2d04236936727d Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 15:50:11 -0400 Subject: [PATCH 053/136] Document the Watershed, Stone highlands, Symmetric arena and Ring world generators Adds the four new generators to the catalog table, gives each its own section, and records guaranteeStartingResources' optional protected-wall mask along with the current callers of it and of scatterResources. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 118 +++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 06d6b937e..a7704d8d2 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -43,6 +43,10 @@ reused after a generator is retired. | `islands` | 3 | Islands | Height-field noise; organic islands with no inter-island passage | | `swamp` | 1 | Swamp | Height-field noise; water interleaved with land | | `river` | 2 | River | Height-field noise; a winding river through connected land | +| `watershed` | 13 | Watershed | Its own — see below | +| `stone-highlands` | 14 | Stone highlands | Its own — see below | +| `symmetric-arena` | 15 | Symmetric arena | Its own — see below | +| `ring-world` | 16 | Ring world | Its own — see below | | `uniform` | 0 | uniform terrain | Editor-only; one terrain type, unstructured | Swamp, River, Islands and Crater Lakes ("the height-field generators") shape their terrain and @@ -79,20 +83,25 @@ fixed resource pass: - `scatterBand` is the same per-component noise-threshold technique for stone and algae, which have no growth rule to prefer and so use a plain noise field rather than fertility. - `scatterResources(game, context, densities)` is the shared ambient layer — ordinary, unclaimed - deposits filling the interior between a generator's deliberate placements — used by Fjord. + deposits filling the interior between a generator's deliberate placements — used by Fjord and + Ring world. It runs after every colony's swarm, workers and guaranteed clumps already exist: `isResourceAllowed` refuses any tile already holding a building or unit, so scattering earlier can claim a tile a swarm footprint needed; scattering last only ever loses a clump's own tile to whatever was already there, the same low-stakes trade every generator's own resource layering already makes. -- `guaranteeStartingResources(game, context, wheatRange, woodRange, clearRadius=0)` is a +- `guaranteeStartingResources(game, context, wheatRange, woodRange, clearRadius=0, + protectedWalls=nullptr)` is a reachability backstop, not a placement pass: it compares a resource-respecting flood from each boot tile against the same flood with resources ignored, and where the two disagree — a colony's own tile is on well-connected land, just walled off from wheat or wood by a resource tile a ground unit can't stand on — it clears exactly the wall tiles responsible before topping up whichever resource is still out of range. A colony genuinely isolated on its own small spot (nothing reachable even through terrain alone) is left untouched, since there's nothing on the - other side of a wall that isn't there. RuggedArchipelago, ShatteredCoast and Fjord call this. + other side of a wall that isn't there. `protectedWalls` is an optional width×height mask of + resource tiles that belong to the map's design, such as Stone highlands' ridgelines; they are + treated like terrain, never cleared and never looked past. RuggedArchipelago, ShatteredCoast, + Fjord, Watershed, Stone highlands and Ring world call this. Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane down every passage, and its `validateWorld` confirms every colony can still walk to every other. @@ -226,6 +235,109 @@ cul-de-sac. resource, including wall spines, block it) from colony 0's workers, and fails the candidate if any colony isn't reached. +## Watershed + +A branching river system draining into a sea along one side of the map: fertile floodplains, +dry sandy uplands, and sand fords across the channels. + +- **Layout.** The coast, springs, flow tree and fords are planned in a canonical frame from named + streams, then oriented: the sea can face any side of a square map but faces a short end of a + rectangular one, so the rivers run the long way. Each map has one river network. +- **Rivers.** Springs are sampled by land area (`river-density`) and join the network downstream + of them nearest-first; a path that runs into another river becomes its tributary. Width grows + with the square root of the springs upstream (`river-width`), and the trunk splits into one to + three distributaries on a delta lobe. +- **Channels that keep their water.** Channels are stamped wide enough to keep a four-connected + water core under an all-at-once version of `controlSand`'s grass-to-sand rule; + `Map::controlSand()` is then required to change nothing. +- **Fords and uplands.** `fords` sets the spacing of sand strips laid across straight stretches + clear of every other channel. Ground farther from water than `dryness` allows turns to sand, + never within 11 tiles of water. Springs rise inland, so a ford is the shortest crossing between + banks, not the only route. +- **Resources and colonies.** Colonies take fertile floodplain sites spread across banks, each + with an identical wheat, wood and stone kit. Farmland ribbons follow the rivers at 2:1 wheat to + wood, with stone at the upland edge, fruit at confluences and algae at the mouths. + `guaranteeStartingResources` runs with 16-step ranges. `validateRequest` allows one colony per + 1,024 map tiles. +- **Checked, not assumed.** `validateWorld` plans the layout again from the request and checks + that every ford interrupts a real channel, is open across its width and has walkable land at + both ends; that every channel keeps its water core outside fords; and that every colony can walk + to colony 0 and stand beside wheat and wood (water, buildings and every resource block). + +## Stone highlands + +Open grass valleys divided by thin stone ridgelines, joined by passes, with a pond in every basin. +Stone is never used up and players can't clear it, so the passes are the only ground routes for +the whole game. + +- **Ridges.** Basin sites spaced about `valley-size` apart are scattered on the torus, and every + tile takes its nearest site after a periodic warp. A tile becomes ridge when any of its eight + neighbours has a lower label, so no unit can slip through diagonally; noise thickens some + stretches to two tiles, and enclosed pockets under 40 tiles are filled. +- **Passes.** A random spanning tree of `pass-width` openings, cut mid-ridge, joins every valley; + `loopiness` opens that share of the remaining shared ridges. +- **Homes and ponds.** Colonies take the roomiest valleys by farthest-point spreading, one per + valley while they last. Ponds (`pond-size`, a percentage of each valley) then grow in basin + interiors away from ridges, passes and homes, with two-tile beaches so algae can regrow; valleys + under 120 tiles get none. +- **Resources.** Identical 1:1 wheat and wood kits beside every home, 2:1 farmland ringing the + ponds, algae in the water, and `fruit` groves (per 128×128) only in valleys no colony starts in. + `guaranteeStartingResources` runs with the ridges as protected walls, and any resource left in + or beside a pass is removed. `validateRequest` rejects maps smaller than four valleys or with + fewer than 1,024 tiles per colony. +- **Checked, not assumed.** The layout is a pure function of the request, so `validateWorld` + rebuilds it and checks that every ridge tile holds stone, every pass is open and leads to both + its valleys, and every colony can walk to colony 0. + +## Symmetric arena + +An orchard island behind a moat at the exact centre, crossed by sand causeways, giving 2, 4 or 8 +colonies identical starting ground. + +- **Symmetry.** A half turn for 2 colonies; a quarter turn for 4 on square maps and both mirrors + for 4 on rectangular ones; all eight square symmetries for 8, on square maps only. + `validateRequest` rejects other colony counts and layouts that leave too little room. +- **Built, not copied.** Every decision is either a function of a tile's whole orbit (integer + noise summed over the orbit, the integer squared radius) or is made once for colony 0 (home, + pond, starting kit, shortest route to its causeways, swarm and workers) and stamped onto every + image, with the swarm's top-left anchor recomputed per image. Terrain is written with + `controlSand`'s rule applied to every corner at once, because the engine's row-order pass is + order dependent, and resource amounts drawn from the engine RNG are equalised across each orbit. +- **Centre.** `centre-size` sets the island's radius and `moat-width` the water around it; + `causeways` and `causeway-width` choose one gate straight towards each colony or two flanking it. + The orchard deals two-by-two fruit groves out an orbit at a time, turns some orbits to stone and + always keeps all three fruits. It is the map's only fruit. +- **Outside the ring.** Symmetric lakes (`lakes`), shore farmland, stone outcrops and algae scaled + by `richness`. Each home gets a pond and a fixed kit, and each colony's shortest route to its + causeways is kept as clear land. `scatterResources` is not used. +- **Checked, not assumed.** `validateWorld` requires corner, terrain, deposit, building and unit + invariance under every symmetry, with a consistent colony permutation that reaches every colony, + and equal walking distances from every colony to wheat, wood, each fruit and the orchard. Only + the starting state is symmetric: in-game growth uses the synced RNG. `scoreStarts` counts + building sites by top-left anchor, so its fairness reads just under 1 on this map. + +## Ring world + +One continental belt wraps the map along its longer axis (horizontally on square and wide maps), +and the seas either side meet across the other wrap: there are no corners, and every colony has +exactly two land neighbours. + +- **Belt.** The centre line and width are whole-number harmonics of the map length and the coast + is lattice noise whose cells tile the torus, so the seam can't be seen. A dry spine beside the + centre line and one shared coast budget keep the belt a single landmass and the ocean a band + whatever `belt-width` (a percentage of the map's breadth) and `coast-roughness` ask for. Terrain + is stamped with an order-independent beach pass rather than `Map::controlSand()`. +- **Lakes and islands.** `lake-density` adds inland lakes that keep land between them and the + ocean and stay off the spine; `resource-islands` (per 128×128) adds themed islands out at sea. +- **Colonies and resources.** Evenly spaced, jittered slots alternate coasts and sit on the + spine-connected belt at one fixed distance from water, with identical starter patches; then + `scatterResources`, a shallows algae pass and `guaranteeStartingResources`. A final road pass + clears only the deposits on the cheapest walk from each colony to the next, closing the loop. + `validateRequest` needs at least 24 tiles of belt length per colony. +- **Checked, not assumed.** `validateWorld` floods from colony 0 while counting seam crossings, + with water, buildings and every resource blocking, and requires every colony reached, a walkable + loop around the map, and a body of water that wraps beside the belt. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort From e9b200a0c3f59ee242a4f68bc761e8e8b762b609 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 16:00:18 -0400 Subject: [PATCH 054/136] Scatter algae over water bodies, not land components scatterResources grouped every band's candidates by land component, and land components never label water, so an algae density placed nothing at all. Components can now be built from water as well as land, and algae is shared out between water bodies the way stone is shared between landmasses. Fjord and Ring world pass no algae, so no generator's output changes: the 55 existing generator dumps and the new generators' dumps and quality scores are identical. A contract test now checks that a sea and a lake inside an island both get algae. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 6 ++-- src/map/generator/shared/Resources.cpp | 23 ++++++++---- test/MapGeneratorDefaultsTest.cpp | 35 +++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index a7704d8d2..198a127f7 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -67,7 +67,7 @@ fixed resource pass: - `placeResourceClump` / `placeResourceClumpInArea` grow a resource outward from a point to a requested tile count — the primitive every guaranteed placement (starter kits, bank deposits) is built from. -- `computeLandComponents` flood-fills the map into its connected non-water components. A +- `computeComponents` flood-fills the map into connected components of land (or of water). A generator whose landmasses are separate (an island generator) needs this so that a map-wide "take the best tiles first" pass can't spend an entire resource band on whichever one or two islands score highest, starving the rest — every subsequent per-component pass below @@ -81,7 +81,9 @@ fixed resource pass: share and a wood share so the two alternate along the region instead of forming two concentric rings sorted by fertility. - `scatterBand` is the same per-component noise-threshold technique for stone and algae, which - have no growth rule to prefer and so use a plain noise field rather than fertility. + have no growth rule to prefer and so use a plain noise field rather than fertility. Stone is + shared out between landmasses and algae between water bodies (a lake apart from the sea), since + algae only places on water. - `scatterResources(game, context, densities)` is the shared ambient layer — ordinary, unclaimed deposits filling the interior between a generator's deliberate placements — used by Fjord and Ring world. diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 56a7f87be..5b5be29e0 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -84,7 +84,12 @@ namespace { // own share of the candidate pool, keeps every landmass' band proportional to how much eligible // ground it has - on a single connected map this is one group covering everything, identical to // before. -std::vector computeLandComponents(const Map &map, int &numComponents) { +// +// water chooses what the components are made of: false groups land into landmasses, true groups +// water into separate bodies (a lake apart from the sea). Algae only places on water, so an algae +// band has to be shared out between water bodies the same way a stone band is shared out between +// landmasses; drawn over land components it would never find a single candidate. +std::vector computeComponents(const Map &map, bool water, int &numComponents) { const int w = map.getW(), h = map.getH(); std::vector component(size_t(w) * h, -1); // Bounded by the component[np] < 0 guard below to at most w*h enqueues per component, and @@ -95,7 +100,7 @@ std::vector computeLandComponents(const Map &map, int &numComponents) { for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { const int start = y * w + x; - if (component[start] != -1 || map.isWater(x, y)) + if (component[start] != -1 || map.isWater(x, y) != water) continue; size_t qHead = 0, qTail = 0; component[start] = nextId; @@ -109,7 +114,7 @@ std::vector computeLandComponents(const Map &map, int &numComponents) { continue; const int nx = map.normalizeX(px + dx), ny = map.normalizeY(py + dy); const int np = ny * w + nx; - if (component[np] == -1 && !map.isWater(nx, ny)) { + if (component[np] == -1 && map.isWater(nx, ny) == water) { component[np] = nextId; queue[qTail++] = np; } @@ -122,7 +127,7 @@ std::vector computeLandComponents(const Map &map, int &numComponents) { } void scatterBand(Map &map, HeightMap &noise, int resourceType, int targetTiles, - const std::vector &landComponent, int numComponents) { + const std::vector &component, int numComponents) { if (targetTiles <= 0 || numComponents <= 0) return; const int width = map.getW(), height = map.getH(); @@ -137,7 +142,7 @@ void scatterBand(Map &map, HeightMap &noise, int resourceType, int targetTiles, continue; if (!map.isResourceAllowed(x, y, resourceType)) continue; - const int comp = landComponent[y * width + x]; + const int comp = component[y * width + x]; if (comp < 0) continue; const unsigned lvl = noise.uiLevel(x, y, kBuckets); @@ -289,12 +294,16 @@ void scatterResources(Game &game, GenerationContext &context, HeightMap splitNoise(width, height, context.stream("scatter-split")); splitNoise.makePlain(6); int numComponents = 0; - const std::vector landComponent = computeLandComponents(map, numComponents); + const std::vector landComponent = computeComponents(map, false, numComponents); scatterFarmland(map, fertility, splitNoise, density.corn * area / 1600, density.wood * area / 1600, landComponent, numComponents); scatterBand(map, noise, STONE, density.stone * area / 3000, landComponent, numComponents); - scatterBand(map, noise, ALGA, density.algae * area / 800, landComponent, numComponents); + if (density.algae > 0) { + int numWaterBodies = 0; + const std::vector waterBody = computeComponents(map, true, numWaterBodies); + scatterBand(map, noise, ALGA, density.algae * area / 800, waterBody, numWaterBodies); + } // Fruit stays a rare, discrete find rather than a background band - "a distinct little // prize", the same role it already plays elsewhere in these generators - so it keeps the diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 6b3ef2b41..02d3bef48 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -10,6 +10,7 @@ #include "MapGeneratorFrameworkChecks.h" #include "NewMapScreen.h" #include "Race.h" +#include "Resources.h" #include "StartingPositions.h" #include "Unit.h" #include "Utilities.h" @@ -255,6 +256,39 @@ class MapGeneratorDefaultsTest puts("PASS registration extension, explicit seeds, interleaved repeatability, RNG " "isolation, errors and legacy sentinels"); } + // scatterResources used to group algae candidates by land component, and land components + // never label water, so no algae density ever placed a single tile. Algae is now shared out + // between water bodies: a sea with an island, and a lake inside the island, must both get some. + static void scatterAlgaeOnWater() + { + Game game(nullptr); + game.map.setSize(6, 6); + game.map.setGame(&game); + game.map.makeHomogenMap(WATER); + for (int y = 12; y < 52; ++y) + for (int x = 12; x < 52; ++x) + game.map.setUMTerrain(x, y, GRASS); + for (int y = 24; y < 40; ++y) + for (int x = 24; x < 40; ++x) + game.map.setUMTerrain(x, y, WATER); + game.map.controlSand(); + game.map.rebuildTerrain(); + D request; + request.seed = 7; + GenerationContext context(request); + MapGeneration::scatterResources(game, context, {0, 0, 0, 50, 0}); + int sea = 0, lake = 0; + for (int y = 0; y < game.map.getH(); ++y) + for (int x = 0; x < game.map.getW(); ++x) + { + if (game.map.getResource(x, y).type != ALGA) + continue; + assert(game.map.isWater(x, y)); + (x >= 24 && x < 40 && y >= 24 && y < 40 ? lake : sea) += 1; + } + assert(sea > 0 && lake > 0); + puts("PASS scatterResources places algae on water, in every water body"); + } static D DWithDefaults(int method) { D r; @@ -270,6 +304,7 @@ class MapGeneratorDefaultsTest static void run(const char *output) { generationContracts(); + scatterAlgaeOnWater(); NewMapScreen s; s.gfx = globalContainer->gfx; s.dispatchInit(); From 119419f0d6e9d55f55e2d3954fd01e6291fc7541 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 16:00:18 -0400 Subject: [PATCH 055/136] List the new generators mid-catalog Moves Watershed, Stone highlands, Symmetric arena and Ring world from the end of the catalog to just after Isles, and matches the framework doc's catalog table. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/MAP_GENERATOR_FRAMEWORK.md | 8 ++++---- src/map/generator/core/GeneratorRegistry.cpp | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 198a127f7..cbc8fac6a 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -37,16 +37,16 @@ reused after a generator is retired. | `fjord-continent` | 12 | Fjord continent | Its own — see below | | `shattered-coast` | 7 | Old random | Iterative water/sand/grass balancer, own resource search | | `isles` | 6 | Isles | Point dispersion; islands linked by land bridges | +| `watershed` | 13 | Watershed | Its own — see below | +| `stone-highlands` | 14 | Stone highlands | Its own — see below | +| `symmetric-arena` | 15 | Symmetric arena | Its own — see below | +| `ring-world` | 16 | Ring world | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | | `islands` | 3 | Islands | Height-field noise; organic islands with no inter-island passage | | `swamp` | 1 | Swamp | Height-field noise; water interleaved with land | | `river` | 2 | River | Height-field noise; a winding river through connected land | -| `watershed` | 13 | Watershed | Its own — see below | -| `stone-highlands` | 14 | Stone highlands | Its own — see below | -| `symmetric-arena` | 15 | Symmetric arena | Its own — see below | -| `ring-world` | 16 | Ring world | Its own — see below | | `uniform` | 0 | uniform terrain | Editor-only; one terrain type, unstructured | Swamp, River, Islands and Crater Lakes ("the height-field generators") shape their terrain and diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index d7e7cbd6a..475b7f113 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -76,10 +76,10 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { // identifiers and do not determine presentation order. static const GeneratorRegistry registry( {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), - shatteredCoastDefinition(), islesDefinition(), - ruggedArchipelagoDefinition(), concreteIslandsDefinition(), - craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), watershedDefinition(), stoneHighlandsDefinition(), - symmetricArenaDefinition(), ringWorldDefinition(), uniformDefinition()}); + shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), + stoneHighlandsDefinition(), symmetricArenaDefinition(), + ringWorldDefinition(), ruggedArchipelagoDefinition(), + concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), + swampDefinition(), riverDefinition(), uniformDefinition()}); return registry; } From 0e69a3102924d372067ef19fbb692ad493c92ef8 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 16:19:26 -0400 Subject: [PATCH 056/136] Translate new map generator labels in all language tables Add translations to the 32 non-English string tables for the Watershed, Stone highlands, Symmetric arena and Ring world generator labels and errors, plus four older keys that had never been translated: the three Maze errors and [Lake connects to fjords]. Entries follow texts.keys.txt order, using each table's existing generator terminology and register. Also translate [Randomize] and [Reset to defaults], placed after [Reroll]; their keys and English values land separately. Remove the duplicate [Lake size] key from texts.keys.txt and texts.en.txt. Both copies had the identical value "Lake size". check_translations.py now reports 0 structural errors and passes --strict; test_translations.py and test_font_coverage.py pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.ar.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.br.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ca.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.cz.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.de.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.dk.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.en.txt | 2 -- data/texts.eo.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.es.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.eu.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.fa.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.fi.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.fr.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.gr.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.hu.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.id.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.it.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ja.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.keys.txt | 1 - data/texts.ko.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.nl.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.pl.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.pt.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ro.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ru.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.si.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.sk.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.sr.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.sv.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.tr.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.uk.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.vi.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.zh-cn.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.zh-tw.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++ 34 files changed, 1984 insertions(+), 3 deletions(-) diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 6a018d6a5..a0e700e4d 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1540,6 +1540,10 @@ Cortex معركة سريعة: سرعة مضاعفة؛ تبدأ الخرائط التي تم إنشاؤها بـ 8 عمال. [Reroll] إنشاء خريطة أخرى +[Randomize] +إنشاء عشوائي +[Reset to defaults] +استعادة الإعدادات الافتراضية [Ruleset] مجموعة القواعد [Select a map from the library.] @@ -1790,6 +1794,12 @@ OpenGL غير متوفر في هذا الإصدار. عرض الممر [Loopiness] كثافة الحلقات +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +تحتاج المتاهة إلى خليتين على الأقل أفقيًا ورأسيًا؛ استخدم خريطة أكبر أو خلايا أصغر. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +القنوات بهذا العرض تترك عشبًا قليلًا جدًا في كل خلية؛ استخدم قنوات أضيق أو خلايا أكبر. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +لا تحتوي المتاهة على طرق مسدودة كافية لهذا العدد من المستعمرات؛ استخدم خريطة أكبر أو خلايا أصغر. [Home island size] حجم جزيرة البداية [Commons size] @@ -1804,5 +1814,57 @@ OpenGL غير متوفر في هذا الإصدار. وعورة الساحل [Fjord width] عرض المضيق البحري +[Lake connects to fjords] +البحيرة متصلة بالمضائق البحرية [Resource islands] جزر الموارد +[Watershed] +مستجمع المياه +[River density] +كثافة الأنهار +[Dryness] +الجفاف +[Fords] +المخاضات +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +لا يحتوي مستجمع المياه على سهول فيضية كافية لهذا العدد من المستعمرات؛ استخدم خريطة أكبر أو عددًا أقل من المستعمرات. +[Stone highlands] +المرتفعات الحجرية +[Valley size] +حجم الوادي +[Pass width] +عرض الممر الجبلي +[Pond size] +حجم البركة +[These valleys are too big for the map; use a bigger map or smaller valleys.] +هذه الوديان أكبر من أن تتسع لها الخريطة؛ استخدم خريطة أكبر أو وديانًا أصغر. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +لا تتسع المرتفعات لهذا العدد من المستعمرات؛ استخدم خريطة أكبر أو عددًا أقل من المستعمرات. +[Symmetric arena] +الساحة المتناظرة +[Centre size] +حجم المركز +[Causeway width] +عرض الممر المرتفع +[Causeways] +الممرات المرتفعة +[Lakes] +البحيرات +[Resource richness] +وفرة الموارد +[Symmetric arena needs 2, 4 or 8 colonies.] +تحتاج الساحة المتناظرة إلى 2 أو 4 أو 8 مستعمرات. +[Eight colonies need a square map in the symmetric arena.] +تحتاج ثماني مستعمرات إلى خريطة مربعة في الساحة المتناظرة. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +المركز أصغر من أن يتسع لبستان في هذه الخريطة؛ استخدم مركزًا أكبر أو خريطة أكبر. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +لا تترك الساحة مساحة كافية لكل مستعمرة؛ استخدم خريطة أكبر أو مركزًا أصغر أو خندقًا مائيًا أضيق. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +الممرات المرتفعة متقاربة جدًا؛ استخدم ممرات مرتفعة أقل أو أضيق، أو مركزًا أكبر. +[Ring world] +العالم الحلقي +[Belt width] +عرض الحزام +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +الحزام أقصر من أن يتسع لهذا العدد من المستعمرات؛ استخدم خريطة أطول أو عددًا أقل من المستعمرات. diff --git a/data/texts.br.txt b/data/texts.br.txt index 6e8077fbf..4f292c8c9 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1538,6 +1538,10 @@ Nenhuma visualização atual Batalha rápida: velocidade dupla; os mapas gerados começam com 8 trabalhadores. [Reroll] Gere outro mapa +[Randomize] +Gerar aleatoriamente +[Reset to defaults] +Restaurar padrões [Ruleset] Conjunto de regras [Select a map from the library.] @@ -1788,6 +1792,12 @@ Tamanho da sala Largura do corredor [Loopiness] Densidade de ciclos +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +O labirinto precisa de pelo menos duas células na horizontal e na vertical; use um mapa maior ou células menores. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Canais tão largos deixam pouca grama em cada célula; use canais mais estreitos ou células maiores. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +O labirinto tem poucos becos sem saída para tantas colônias; use um mapa maior ou células menores. [Home island size] Tamanho da ilha inicial [Commons size] @@ -1802,5 +1812,57 @@ Tamanho do continente Irregularidade da costa [Fjord width] Largura do fiorde +[Lake connects to fjords] +Lago ligado aos fiordes [Resource islands] Ilhas de recursos +[Watershed] +Bacia hidrográfica +[River density] +Densidade de rios +[Dryness] +Aridez +[Fords] +Vaus +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +A bacia hidrográfica tem pouca planície aluvial para tantas colônias; use um mapa maior ou menos colônias. +[Stone highlands] +Planaltos de pedra +[Valley size] +Tamanho do vale +[Pass width] +Largura da passagem +[Pond size] +Tamanho da lagoa +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Estes vales são grandes demais para o mapa; use um mapa maior ou vales menores. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Os planaltos têm pouco espaço para tantas colônias; use um mapa maior ou menos colônias. +[Symmetric arena] +Arena simétrica +[Centre size] +Tamanho do centro +[Causeway width] +Largura dos caminhos elevados +[Causeways] +Caminhos elevados +[Lakes] +Lagos +[Resource richness] +Riqueza de recursos +[Symmetric arena needs 2, 4 or 8 colonies.] +A arena simétrica precisa de 2, 4 ou 8 colônias. +[Eight colonies need a square map in the symmetric arena.] +Oito colônias precisam de um mapa quadrado na arena simétrica. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +O centro é pequeno demais para um pomar neste mapa; use um centro maior ou um mapa maior. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +A arena deixa pouco espaço para cada colônia; use um mapa maior, um centro menor ou um fosso mais estreito. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Os caminhos elevados estão próximos demais; use menos caminhos elevados ou mais estreitos, ou um centro maior. +[Ring world] +Mundo anel +[Belt width] +Largura da faixa +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +A faixa é curta demais para tantas colônias; use um mapa mais longo ou menos colônias. diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 717726f77..4496c48fe 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1550,6 +1550,10 @@ No hi ha previsualització actual Batalla ràpida: velocitat doble; els mapes generats comencen amb 8 treballadors. [Reroll] Genera un altre mapa +[Randomize] +Genera aleatòriament +[Reset to defaults] +Restaura els valors per defecte [Ruleset] Conjunt de regles [Select a map from the library.] @@ -1800,6 +1804,12 @@ Mida de la sala Amplada del passadís [Loopiness] Densitat de bucles +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +El laberint necessita almenys dues cel·les d'amplada i d'alçada; feu servir un mapa més gran o cel·les més petites. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Uns canals tan amples deixen massa poca herba a cada cel·la; feu servir canals més estrets o cel·les més grans. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +El laberint té massa pocs atzucacs per a tantes colònies; feu servir un mapa més gran o cel·les més petites. [Home island size] Mida de l'illa inicial [Commons size] @@ -1814,5 +1824,57 @@ Mida del continent Irregularitat de la costa [Fjord width] Amplada del fiord +[Lake connects to fjords] +Llac connectat als fiords [Resource islands] Illes de recursos +[Watershed] +Conca hidrogràfica +[River density] +Densitat de rius +[Dryness] +Aridesa +[Fords] +Guals +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +La conca té massa poca plana al·luvial per a tantes colònies; feu servir un mapa més gran o menys colònies. +[Stone highlands] +Terres altes de pedra +[Valley size] +Mida de la vall +[Pass width] +Amplada del pas +[Pond size] +Mida de la bassa +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Aquestes valls són massa grans per al mapa; feu servir un mapa més gran o valls més petites. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Les terres altes tenen massa poc espai per a tantes colònies; feu servir un mapa més gran o menys colònies. +[Symmetric arena] +Arena simètrica +[Centre size] +Mida del centre +[Causeway width] +Amplada de la calçada +[Causeways] +Calçades +[Lakes] +Llacs +[Resource richness] +Riquesa de recursos +[Symmetric arena needs 2, 4 or 8 colonies.] +L'arena simètrica necessita 2, 4 o 8 colònies. +[Eight colonies need a square map in the symmetric arena.] +Vuit colònies necessiten un mapa quadrat a l'arena simètrica. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +El centre és massa petit per a un fruiterar en aquest mapa; feu servir un centre més gran o un mapa més gran. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +L'arena deixa massa poc espai per a cada colònia; feu servir un mapa més gran, un centre més petit o un fossat més estret. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Les calçades són massa a prop entre elles; feu servir menys calçades o més estretes, o un centre més gran. +[Ring world] +Món anell +[Belt width] +Amplada del cinturó +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +El cinturó és massa curt per a tantes colònies; feu servir un mapa més llarg o menys colònies. diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 9f8a075d1..b7c1801d5 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1542,6 +1542,10 @@ Formát hry Rychlá bitva: dvojnásobná rychlost; generované mapy začínají s 8 pracovníky. [Reroll] Vygenerujte další mapu +[Randomize] +Vygenerovat náhodně +[Reset to defaults] +Obnovit výchozí hodnoty [Ruleset] Pravidla [Select a map from the library.] @@ -1792,6 +1796,12 @@ Velikost místnosti Šířka chodby [Loopiness] Propojenost +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Bludiště potřebuje alespoň dvě buňky na šířku i na výšku; použijte větší mapu nebo menší buňky. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Takto široké průlivy nechávají v každé buňce příliš málo trávy; použijte užší průlivy nebo větší buňky. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Bludiště má pro tolik kolonií příliš málo slepých uliček; použijte větší mapu nebo menší buňky. [Home island size] Velikost domovského ostrova [Commons size] @@ -1806,5 +1816,57 @@ Velikost kontinentu Členitost pobřeží [Fjord width] Šířka fjordu +[Lake connects to fjords] +Jezero spojené s fjordy [Resource islands] Surovinové ostrovy +[Watershed] +Povodí +[River density] +Hustota řek +[Dryness] +Vyprahlost +[Fords] +Brody +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Povodí má pro tolik kolonií příliš málo záplavové nivy; použijte větší mapu nebo méně kolonií. +[Stone highlands] +Kamenná vysočina +[Valley size] +Velikost údolí +[Pass width] +Šířka průsmyku +[Pond size] +Velikost tůně +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Tato údolí jsou pro mapu příliš velká; použijte větší mapu nebo menší údolí. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Vysočina má pro tolik kolonií příliš málo místa; použijte větší mapu nebo méně kolonií. +[Symmetric arena] +Symetrická aréna +[Centre size] +Velikost středu +[Causeway width] +Šířka hráze +[Causeways] +Hráze +[Lakes] +Jezera +[Resource richness] +Bohatost surovin +[Symmetric arena needs 2, 4 or 8 colonies.] +Symetrická aréna vyžaduje 2, 4 nebo 8 kolonií. +[Eight colonies need a square map in the symmetric arena.] +Osm kolonií vyžaduje v symetrické aréně čtvercovou mapu. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Střed je na této mapě příliš malý pro ovocný sad; použijte větší střed nebo větší mapu. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Aréna nechává příliš málo místa pro každou kolonii; použijte větší mapu, menší střed nebo užší příkop. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Hráze jsou příliš blízko sebe; použijte méně nebo užší hráze, případně větší střed. +[Ring world] +Prstencový svět +[Belt width] +Šířka pásu +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Pás je pro tolik kolonií příliš krátký; použijte delší mapu nebo méně kolonií. diff --git a/data/texts.de.txt b/data/texts.de.txt index 2959a80e1..c0c2550d5 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1542,6 +1542,10 @@ Keine aktuelle Vorschau Schneller Kampf: doppelte Geschwindigkeit; Generierte Karten beginnen mit 8 Arbeitern. [Reroll] Erzeugen Sie eine weitere Karte +[Randomize] +Zufällig generieren +[Reset to defaults] +Auf Standardwerte zurücksetzen [Ruleset] Regelsatz [Select a map from the library.] @@ -1792,6 +1796,12 @@ Raumgröße Korridorbreite [Loopiness] Vernetzungsgrad +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Das Labyrinth braucht mindestens zwei Zellen in der Breite und in der Höhe; verwenden Sie eine größere Karte oder kleinere Zellen. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +So breite Kanäle lassen in jeder Zelle zu wenig Gras übrig; verwenden Sie schmalere Kanäle oder größere Zellen. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Das Labyrinth hat zu wenige Sackgassen für so viele Kolonien; verwenden Sie eine größere Karte oder kleinere Zellen. [Home island size] Größe der Startinsel [Commons size] @@ -1806,5 +1816,57 @@ Kontinentgröße Küstenzerklüftung [Fjord width] Fjordbreite +[Lake connects to fjords] +See mit Fjorden verbunden [Resource islands] Rohstoffinseln +[Watershed] +Flusseinzugsgebiet +[River density] +Dichte der Flüsse +[Dryness] +Trockenheit +[Fords] +Furten +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Das Flusseinzugsgebiet hat zu wenig Auenland für so viele Kolonien; verwenden Sie eine größere Karte oder weniger Kolonien. +[Stone highlands] +Steinhochland +[Valley size] +Talgröße +[Pass width] +Passbreite +[Pond size] +Teichgröße +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Diese Täler sind zu groß für die Karte; verwenden Sie eine größere Karte oder kleinere Täler. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Das Hochland bietet zu wenig Platz für so viele Kolonien; verwenden Sie eine größere Karte oder weniger Kolonien. +[Symmetric arena] +Symmetrische Arena +[Centre size] +Größe des Zentrums +[Causeway width] +Breite der Dammwege +[Causeways] +Dammwege +[Lakes] +Seen +[Resource richness] +Rohstoffreichtum +[Symmetric arena needs 2, 4 or 8 colonies.] +Die symmetrische Arena benötigt 2, 4 oder 8 Kolonien. +[Eight colonies need a square map in the symmetric arena.] +Acht Kolonien benötigen in der symmetrischen Arena eine quadratische Karte. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Das Zentrum ist auf dieser Karte zu klein für einen Obstgarten; verwenden Sie ein größeres Zentrum oder eine größere Karte. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Die Arena lässt zu wenig Platz für jede Kolonie; verwenden Sie eine größere Karte, ein kleineres Zentrum oder einen schmaleren Graben. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Die Dammwege liegen zu dicht beieinander; verwenden Sie weniger oder schmalere Dammwege oder ein größeres Zentrum. +[Ring world] +Ringwelt +[Belt width] +Gürtelbreite +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Der Gürtel ist zu kurz für so viele Kolonien; verwenden Sie eine längere Karte oder weniger Kolonien. diff --git a/data/texts.dk.txt b/data/texts.dk.txt index cffe067bb..ccd5dce47 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1618,6 +1618,10 @@ Ingen aktuel forhåndsvisning Hurtig kamp: dobbelt hastighed; genererede kort starter med 8 arbejdere. [Reroll] Generer endnu et kort +[Randomize] +Generer tilfældigt +[Reset to defaults] +Nulstil til standard [Ruleset] Regelsæt [Select a map from the library.] @@ -1868,6 +1872,12 @@ Rumstørrelse Korridorbredde [Loopiness] Løkketæthed +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labyrinten skal have mindst to celler på tværs og på langs; brug et større kort eller mindre celler. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Så brede kanaler efterlader for lidt græs i hver celle; brug smallere kanaler eller større celler. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labyrinten har for få blindgyder til så mange kolonier; brug et større kort eller mindre celler. [Home island size] Størrelse på startø [Commons size] @@ -1882,5 +1892,57 @@ Kontinentstørrelse Kystens ujævnhed [Fjord width] Fjordbredde +[Lake connects to fjords] +Sø forbundet med fjorde [Resource islands] Ressourceøer +[Watershed] +Afvandingsområde +[River density] +Flodtæthed +[Dryness] +Tørhed +[Fords] +Vadesteder +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Afvandingsområdet har for lidt flodslette til så mange kolonier; brug et større kort eller færre kolonier. +[Stone highlands] +Stenhøjland +[Valley size] +Dalstørrelse +[Pass width] +Pasbredde +[Pond size] +Damstørrelse +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Disse dale er for store til kortet; brug et større kort eller mindre dale. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Højlandet har for lidt plads til så mange kolonier; brug et større kort eller færre kolonier. +[Symmetric arena] +Symmetrisk arena +[Centre size] +Centrets størrelse +[Causeway width] +Dæmningsvejsbredde +[Causeways] +Dæmningsveje +[Lakes] +Søer +[Resource richness] +Ressourcerigdom +[Symmetric arena needs 2, 4 or 8 colonies.] +Symmetrisk arena kræver 2, 4 eller 8 kolonier. +[Eight colonies need a square map in the symmetric arena.] +Otte kolonier kræver et kvadratisk kort i den symmetriske arena. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Centret er for lille til en frugtlund på dette kort; brug et større center eller et større kort. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arenaen giver for lidt plads til hver koloni; brug et større kort, et mindre center eller en smallere voldgrav. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Dæmningsvejene ligger for tæt; brug færre eller smallere dæmningsveje, eller et større center. +[Ring world] +Ringverden +[Belt width] +Bæltebredde +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Bæltet er for kort til så mange kolonier; brug et længere kort eller færre kolonier. diff --git a/data/texts.en.txt b/data/texts.en.txt index 0602b9aaf..b7364ad3e 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1800,8 +1800,6 @@ Continent size Coast roughness [Fjord width] Fjord width -[Lake size] -Lake size [Lake connects to fjords] Lake connects to fjords [Resource islands] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index a62fd49bd..fd6957291 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1540,6 +1540,10 @@ Neniu aktuala antaŭvido Rapida batalo: duobla rapido; generitaj mapoj komenciĝas per 8 laboristoj. [Reroll] Generu alian mapon +[Randomize] +Generu hazarde +[Reset to defaults] +Restarigi defaŭltajn valorojn [Ruleset] Regularo [Select a map from the library.] @@ -1790,6 +1794,12 @@ Grandeco de ĉambro Larĝo de koridoro [Loopiness] Bukleco +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +La labirinto bezonas almenaŭ du ĉelojn laŭlarĝe kaj laŭalte; uzu pli grandan mapon aŭ pli malgrandajn ĉelojn. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Tiel larĝaj kanaloj lasas tro malmulte da herbo en ĉiu ĉelo; uzu pli mallarĝajn kanalojn aŭ pli grandajn ĉelojn. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +La labirinto havas tro malmultajn sakstratojn por tiom da kolonioj; uzu pli grandan mapon aŭ pli malgrandajn ĉelojn. [Home island size] Grandeco de hejminsulo [Commons size] @@ -1804,5 +1814,57 @@ Grandeco de kontinento Malglateco de marbordo [Fjord width] Larĝo de fjordo +[Lake connects to fjords] +Lago ligita al fjordoj [Resource islands] Rimedinsuloj +[Watershed] +Riverbaseno +[River density] +Denseco de riveroj +[Dryness] +Sekeco +[Fords] +Travadejoj +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +La riverbaseno havas tro malmulte da inundebenaĵo por tiom da kolonioj; uzu pli grandan mapon aŭ malpli da kolonioj. +[Stone highlands] +Ŝtonaj altebenaĵoj +[Valley size] +Grandeco de valo +[Pass width] +Larĝo de montpasejo +[Pond size] +Grandeco de lageto +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Ĉi tiuj valoj estas tro grandaj por la mapo; uzu pli grandan mapon aŭ pli malgrandajn valojn. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +La altebenaĵoj havas tro malmulte da spaco por tiom da kolonioj; uzu pli grandan mapon aŭ malpli da kolonioj. +[Symmetric arena] +Simetria areno +[Centre size] +Grandeco de centro +[Causeway width] +Larĝo de digvojo +[Causeways] +Digvojoj +[Lakes] +Lagoj +[Resource richness] +Riĉeco de rimedoj +[Symmetric arena needs 2, 4 or 8 colonies.] +Simetria areno bezonas 2, 4 aŭ 8 koloniojn. +[Eight colonies need a square map in the symmetric arena.] +Ok kolonioj bezonas kvadratan mapon en la simetria areno. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +La centro estas tro malgranda por fruktoĝardeno sur ĉi tiu mapo; uzu pli grandan centron aŭ pli grandan mapon. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +La areno lasas tro malmulte da spaco por ĉiu kolonio; uzu pli grandan mapon, pli malgrandan centron aŭ pli mallarĝan fosaĵon. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +La digvojoj estas tro proksimaj unu al la alia; uzu malpli multajn aŭ pli mallarĝajn digvojojn, aŭ pli grandan centron. +[Ring world] +Ringa mondo +[Belt width] +Larĝo de zono +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +La zono estas tro mallonga por tiom da kolonioj; uzu pli longan mapon aŭ malpli da kolonioj. diff --git a/data/texts.es.txt b/data/texts.es.txt index a6270f2c6..46e3c476d 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1542,6 +1542,10 @@ No hay vista previa actual Batalla rápida: doble velocidad; Los mapas generados comienzan con 8 trabajadores. [Reroll] Generar otro mapa +[Randomize] +Generar aleatoriamente +[Reset to defaults] +Restaurar valores predeterminados [Ruleset] Conjunto de reglas [Select a map from the library.] @@ -1792,6 +1796,12 @@ Tamaño de la sala Anchura del pasillo [Loopiness] Densidad de bucles +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +El laberinto necesita al menos dos celdas a lo ancho y a lo alto; use un mapa más grande o celdas más pequeñas. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Unos canales tan anchos dejan muy poca hierba en cada celda; use canales más estrechos o celdas más grandes. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +El laberinto tiene muy pocos callejones sin salida para tantas colonias; use un mapa más grande o celdas más pequeñas. [Home island size] Tamaño de la isla inicial [Commons size] @@ -1806,5 +1816,57 @@ Tamaño del continente Irregularidad de la costa [Fjord width] Anchura del fiordo +[Lake connects to fjords] +Lago conectado a los fiordos [Resource islands] Islas de recursos +[Watershed] +Cuenca hidrográfica +[River density] +Densidad de ríos +[Dryness] +Aridez +[Fords] +Vados +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +La cuenca tiene muy poca llanura aluvial para tantas colonias; use un mapa más grande o menos colonias. +[Stone highlands] +Tierras altas de piedra +[Valley size] +Tamaño del valle +[Pass width] +Anchura del paso +[Pond size] +Tamaño del estanque +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Estos valles son demasiado grandes para el mapa; use un mapa más grande o valles más pequeños. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Las tierras altas tienen muy poco espacio para tantas colonias; use un mapa más grande o menos colonias. +[Symmetric arena] +Arena simétrica +[Centre size] +Tamaño del centro +[Causeway width] +Anchura de la calzada +[Causeways] +Calzadas +[Lakes] +Lagos +[Resource richness] +Riqueza de recursos +[Symmetric arena needs 2, 4 or 8 colonies.] +La arena simétrica necesita 2, 4 u 8 colonias. +[Eight colonies need a square map in the symmetric arena.] +Ocho colonias necesitan un mapa cuadrado en la arena simétrica. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +El centro es demasiado pequeño para un huerto frutal en este mapa; use un centro más grande o un mapa más grande. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +La arena deja muy poco espacio para cada colonia; use un mapa más grande, un centro más pequeño o un foso más estrecho. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Las calzadas están demasiado juntas; use menos calzadas o más estrechas, o un centro más grande. +[Ring world] +Mundo anillo +[Belt width] +Anchura del cinturón +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +El cinturón es demasiado corto para tantas colonias; use un mapa más largo o menos colonias. diff --git a/data/texts.eu.txt b/data/texts.eu.txt index bebd90675..348ef74b6 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1552,6 +1552,10 @@ Ez dago oraingo aurrebistarik Borroka azkarra: abiadura bikoitza; sortutako mapak 8 langilerekin hasten dira. [Reroll] Sortu beste mapa bat +[Randomize] +Sortu ausaz +[Reset to defaults] +Leheneratu balio lehenetsiak [Ruleset] Arau multzoa [Select a map from the library.] @@ -1802,6 +1806,12 @@ Gelaren tamaina Korridorearen zabalera [Loopiness] Begizta-maila +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labirintoak gutxienez bi gelaxka behar ditu zabaleran eta altueran; erabili mapa handiagoa edo gelaxka txikiagoak. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Hain kanal zabalek belar gutxiegi uzten dute gelaxka bakoitzean; erabili kanal estuagoak edo gelaxka handiagoak. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labirintoak kale itsu gutxiegi ditu hainbeste koloniarentzat; erabili mapa handiagoa edo gelaxka txikiagoak. [Home island size] Hasierako uhartearen tamaina [Commons size] @@ -1816,5 +1826,57 @@ Kontinentearen tamaina Kostaldearen irregulartasuna [Fjord width] Fiordoaren zabalera +[Lake connects to fjords] +Aintzira fiordoekin lotuta [Resource islands] Baliabide-uharteak +[Watershed] +Ibai-arroa +[River density] +Ibaien dentsitatea +[Dryness] +Lehortasuna +[Fords] +Ibiak +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Ibai-arroak uholde-lautada gutxiegi du hainbeste koloniarentzat; erabili mapa handiagoa edo kolonia gutxiago. +[Stone highlands] +Harrizko goi-lurrak +[Valley size] +Haranaren tamaina +[Pass width] +Mendatearen zabalera +[Pond size] +Urmaelaren tamaina +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Haran hauek handiegiak dira maparako; erabili mapa handiagoa edo haran txikiagoak. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Goi-lurrek leku gutxiegi dute hainbeste koloniarentzat; erabili mapa handiagoa edo kolonia gutxiago. +[Symmetric arena] +Arena simetrikoa +[Centre size] +Erdigunearen tamaina +[Causeway width] +Galtzadaren zabalera +[Causeways] +Galtzadak +[Lakes] +Aintzirak +[Resource richness] +Baliabideen ugaritasuna +[Symmetric arena needs 2, 4 or 8 colonies.] +Arena simetrikoak 2, 4 edo 8 kolonia behar ditu. +[Eight colonies need a square map in the symmetric arena.] +Zortzi koloniarentzat mapa karratua behar da arena simetrikoan. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Erdigunea txikiegia da mapa honetan fruta-baratze baterako; erabili erdigune handiagoa edo mapa handiagoa. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arenak leku gutxiegi uzten du kolonia bakoitzarentzat; erabili mapa handiagoa, erdigune txikiagoa edo lubanarro estuagoa. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Galtzadak elkarrengandik hurbilegi daude; erabili galtzada gutxiago edo estuagoak, edo erdigune handiagoa. +[Ring world] +Eraztun-mundua +[Belt width] +Gerrikoaren zabalera +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Gerrikoa laburregia da hainbeste koloniarentzat; erabili mapa luzeagoa edo kolonia gutxiago. diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 8ffd47448..90b69016c 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1540,6 +1540,10 @@ Cortex نبرد سریع: سرعت دو برابر. نقشه های تولید شده با 8 کارگر شروع می شود. [Reroll] نقشه دیگری ایجاد کنید +[Randomize] +ایجاد تصادفی +[Reset to defaults] +بازگرداندن مقادیر پیش‌فرض [Ruleset] مجموعه قوانین [Select a map from the library.] @@ -1790,6 +1794,12 @@ OpenGL در این بیلد موجود نیست. پهنای راهرو [Loopiness] میزان حلقه‌دار بودن +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +هزارتو دست‌کم به دو سلول در عرض و در طول نیاز دارد؛ از نقشهٔ بزرگ‌تر یا سلول‌های کوچک‌تر استفاده کنید. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +آبراه‌هایی با این پهنا در هر سلول چمن بسیار کمی باقی می‌گذارند؛ از آبراه‌های باریک‌تر یا سلول‌های بزرگ‌تر استفاده کنید. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +هزارتو برای این تعداد مستعمره بن‌بست‌های بسیار کمی دارد؛ از نقشهٔ بزرگ‌تر یا سلول‌های کوچک‌تر استفاده کنید. [Home island size] اندازهٔ جزیرهٔ آغازین [Commons size] @@ -1804,5 +1814,57 @@ OpenGL در این بیلد موجود نیست. ناهمواری ساحل [Fjord width] پهنای آبدره +[Lake connects to fjords] +اتصال دریاچه به آبدره‌ها [Resource islands] جزایر منابع +[Watershed] +حوضهٔ آبریز +[River density] +تراکم رودخانه‌ها +[Dryness] +خشکی +[Fords] +گدارها +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +حوضهٔ آبریز برای این تعداد مستعمره دشت سیلابی بسیار کمی دارد؛ از نقشهٔ بزرگ‌تر یا مستعمره‌های کمتر استفاده کنید. +[Stone highlands] +ارتفاعات سنگی +[Valley size] +اندازهٔ دره +[Pass width] +پهنای گردنه +[Pond size] +اندازهٔ برکه +[These valleys are too big for the map; use a bigger map or smaller valleys.] +این دره‌ها برای نقشه بیش از حد بزرگ‌اند؛ از نقشهٔ بزرگ‌تر یا دره‌های کوچک‌تر استفاده کنید. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +ارتفاعات برای این تعداد مستعمره جای بسیار کمی دارند؛ از نقشهٔ بزرگ‌تر یا مستعمره‌های کمتر استفاده کنید. +[Symmetric arena] +میدان متقارن +[Centre size] +اندازهٔ مرکز +[Causeway width] +پهنای گذرگاه مرتفع +[Causeways] +گذرگاه‌های مرتفع +[Lakes] +دریاچه‌ها +[Resource richness] +فراوانی منابع +[Symmetric arena needs 2, 4 or 8 colonies.] +میدان متقارن به 2، 4 یا 8 مستعمره نیاز دارد. +[Eight colonies need a square map in the symmetric arena.] +هشت مستعمره در میدان متقارن به نقشهٔ مربعی نیاز دارند. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +مرکز در این نقشه برای یک باغ میوه بیش از حد کوچک است؛ از مرکز بزرگ‌تر یا نقشهٔ بزرگ‌تر استفاده کنید. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +میدان برای هر مستعمره جای بسیار کمی باقی می‌گذارد؛ از نقشهٔ بزرگ‌تر، مرکز کوچک‌تر یا خندق آبی باریک‌تر استفاده کنید. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +گذرگاه‌های مرتفع بیش از حد به هم نزدیک‌اند؛ از گذرگاه‌های مرتفع کمتر یا باریک‌تر، یا مرکز بزرگ‌تر استفاده کنید. +[Ring world] +جهان حلقه‌ای +[Belt width] +پهنای کمربند +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +کمربند برای این تعداد مستعمره بیش از حد کوتاه است؛ از نقشهٔ بلندتر یا مستعمره‌های کمتر استفاده کنید. diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 4143649b2..ab8caf845 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1542,6 +1542,10 @@ Ei nykyistä esikatselua Nopea taistelu: kaksinkertainen nopeus; luodut kartat alkavat 8 työntekijällä. [Reroll] Luo toinen kartta +[Randomize] +Satunnaista +[Reset to defaults] +Palauta oletusarvot [Ruleset] Säännöstö [Select a map from the library.] @@ -1792,6 +1796,12 @@ Huoneen koko Käytävän leveys [Loopiness] Silmukoiden määrä +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Sokkelo tarvitsee vähintään kaksi solua sekä vaaka- että pystysuunnassa; käytä suurempaa karttaa tai pienempiä soluja. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Näin leveät kanavat jättävät jokaiseen soluun liian vähän ruohoa; käytä kapeampia kanavia tai suurempia soluja. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Sokkelossa on liian vähän umpikujia näin monelle siirtokunnalle; käytä suurempaa karttaa tai pienempiä soluja. [Home island size] Kotisaaren koko [Commons size] @@ -1806,5 +1816,57 @@ Mantereen koko Rannikon rikkonaisuus [Fjord width] Vuonon leveys +[Lake connects to fjords] +Järvi yhdistyy vuonoihin [Resource islands] Luonnonvarasaaret +[Watershed] +Valuma-alue +[River density] +Jokien tiheys +[Dryness] +Kuivuus +[Fords] +Kahlaamot +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Valuma-alueella on liian vähän tulvatasankoa näin monelle siirtokunnalle; käytä suurempaa karttaa tai vähemmän siirtokuntia. +[Stone highlands] +Kiviylänkö +[Valley size] +Laakson koko +[Pass width] +Solan leveys +[Pond size] +Lammen koko +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Nämä laaksot ovat liian suuria kartalle; käytä suurempaa karttaa tai pienempiä laaksoja. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Ylängöllä on liian vähän tilaa näin monelle siirtokunnalle; käytä suurempaa karttaa tai vähemmän siirtokuntia. +[Symmetric arena] +Symmetrinen areena +[Centre size] +Keskialueen koko +[Causeway width] +Pengertien leveys +[Causeways] +Pengertiet +[Lakes] +Järvet +[Resource richness] +Luonnonvarojen runsaus +[Symmetric arena needs 2, 4 or 8 colonies.] +Symmetrinen areena vaatii 2, 4 tai 8 siirtokuntaa. +[Eight colonies need a square map in the symmetric arena.] +Kahdeksan siirtokuntaa vaatii symmetrisessä areenassa neliönmuotoisen kartan. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Keskialue on tällä kartalla liian pieni hedelmätarhalle; käytä suurempaa keskialuetta tai suurempaa karttaa. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Areenalle ei jää tarpeeksi tilaa jokaiselle siirtokunnalle; käytä suurempaa karttaa, pienempää keskialuetta tai kapeampaa vallihautaa. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Pengertiet ovat liian lähellä toisiaan; käytä vähemmän tai kapeampia pengerteitä tai suurempaa keskialuetta. +[Ring world] +Rengasmaailma +[Belt width] +Vyöhykkeen leveys +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Vyöhyke on liian lyhyt näin monelle siirtokunnalle; käytä pidempää karttaa tai vähemmän siirtokuntia. diff --git a/data/texts.fr.txt b/data/texts.fr.txt index c56158645..df98ddc51 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1552,6 +1552,10 @@ Aucun aperçu actuel Bataille rapide : double vitesse ; les cartes générées commencent avec 8 ouvrières. [Reroll] Générer une autre carte +[Randomize] +Générer aléatoirement +[Reset to defaults] +Restaurer les valeurs par défaut [Ruleset] Ensemble de règles [Select a map from the library.] @@ -1802,6 +1806,12 @@ Taille des salles Largeur des couloirs [Loopiness] Taux de boucles +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Le labyrinthe doit compter au moins deux cellules en largeur et en hauteur ; utilisez une carte plus grande ou des cellules plus petites. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Des chenaux aussi larges laissent trop peu d'herbe dans chaque cellule ; utilisez des chenaux plus étroits ou des cellules plus grandes. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Le labyrinthe compte trop peu de culs-de-sac pour autant de colonies ; utilisez une carte plus grande ou des cellules plus petites. [Home island size] Taille de l'île de départ [Commons size] @@ -1816,5 +1826,57 @@ Taille du continent Irrégularité des côtes [Fjord width] Largeur des fjords +[Lake connects to fjords] +Lac relié aux fjords [Resource islands] Îles de ressources +[Watershed] +Bassin versant +[River density] +Densité des rivières +[Dryness] +Aridité +[Fords] +Gués +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Le bassin versant a trop peu de plaines inondables pour autant de colonies ; utilisez une carte plus grande ou moins de colonies. +[Stone highlands] +Hautes terres de pierre +[Valley size] +Taille des vallées +[Pass width] +Largeur des cols +[Pond size] +Taille des étangs +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Ces vallées sont trop grandes pour la carte ; utilisez une carte plus grande ou des vallées plus petites. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Les hautes terres manquent de place pour autant de colonies ; utilisez une carte plus grande ou moins de colonies. +[Symmetric arena] +Arène symétrique +[Centre size] +Taille du centre +[Causeway width] +Largeur des chaussées +[Causeways] +Chaussées +[Lakes] +Lacs +[Resource richness] +Abondance des ressources +[Symmetric arena needs 2, 4 or 8 colonies.] +L'arène symétrique nécessite 2, 4 ou 8 colonies. +[Eight colonies need a square map in the symmetric arena.] +Huit colonies nécessitent une carte carrée dans l'arène symétrique. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Le centre est trop petit pour un verger sur cette carte ; utilisez un centre plus grand ou une carte plus grande. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +L'arène laisse trop peu de place à chaque colonie ; utilisez une carte plus grande, un centre plus petit ou des douves plus étroites. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Les chaussées sont trop rapprochées ; utilisez des chaussées moins nombreuses ou plus étroites, ou un centre plus grand. +[Ring world] +Monde-anneau +[Belt width] +Largeur de la ceinture +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +La ceinture est trop courte pour autant de colonies ; utilisez une carte plus longue ou moins de colonies. diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 81931633f..0db8d5d30 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1612,6 +1612,10 @@ Cortex Γρήγορη μάχη: διπλή ταχύτητα. οι χάρτες που δημιουργούνται ξεκινούν με 8 εργαζόμενους. [Reroll] Δημιουργήστε έναν άλλο χάρτη +[Randomize] +Τυχαία δημιουργία +[Reset to defaults] +Επαναφορά προεπιλογών [Ruleset] Σύνολο κανόνων [Select a map from the library.] @@ -1862,6 +1866,12 @@ Cortex Πλάτος διαδρόμου [Loopiness] Βαθμός διασύνδεσης +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Ο λαβύρινθος χρειάζεται τουλάχιστον δύο κελιά σε πλάτος και ύψος. Χρησιμοποιήστε μεγαλύτερο χάρτη ή μικρότερα κελιά. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Τόσο φαρδιά κανάλια αφήνουν πολύ λίγο γρασίδι σε κάθε κελί. Χρησιμοποιήστε στενότερα κανάλια ή μεγαλύτερα κελιά. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Ο λαβύρινθος έχει πολύ λίγα αδιέξοδα για τόσες αποικίες. Χρησιμοποιήστε μεγαλύτερο χάρτη ή μικρότερα κελιά. [Home island size] Μέγεθος αρχικού νησιού [Commons size] @@ -1876,5 +1886,57 @@ Cortex Τραχύτητα ακτογραμμής [Fjord width] Πλάτος φιόρδ +[Lake connects to fjords] +Η λίμνη συνδέεται με τα φιόρδ [Resource islands] Νησιά πόρων +[Watershed] +Λεκάνη απορροής +[River density] +Πυκνότητα ποταμών +[Dryness] +Ξηρασία +[Fords] +Ρηχά περάσματα +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Η λεκάνη απορροής έχει πολύ λίγη πεδιάδα πλημμυρών για τόσες αποικίες. Χρησιμοποιήστε μεγαλύτερο χάρτη ή λιγότερες αποικίες. +[Stone highlands] +Πετρώδη υψίπεδα +[Valley size] +Μέγεθος κοιλάδας +[Pass width] +Πλάτος διάσελου +[Pond size] +Μέγεθος λιμνούλας +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Αυτές οι κοιλάδες είναι πολύ μεγάλες για τον χάρτη. Χρησιμοποιήστε μεγαλύτερο χάρτη ή μικρότερες κοιλάδες. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Τα υψίπεδα έχουν πολύ λίγο χώρο για τόσες αποικίες. Χρησιμοποιήστε μεγαλύτερο χάρτη ή λιγότερες αποικίες. +[Symmetric arena] +Συμμετρική αρένα +[Centre size] +Μέγεθος κέντρου +[Causeway width] +Πλάτος αναχώματος +[Causeways] +Αναχώματα +[Lakes] +Λίμνες +[Resource richness] +Αφθονία πόρων +[Symmetric arena needs 2, 4 or 8 colonies.] +Η συμμετρική αρένα χρειάζεται 2, 4 ή 8 αποικίες. +[Eight colonies need a square map in the symmetric arena.] +Οκτώ αποικίες χρειάζονται τετράγωνο χάρτη στη συμμετρική αρένα. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Το κέντρο είναι πολύ μικρό για οπωρώνα σε αυτόν τον χάρτη. Χρησιμοποιήστε μεγαλύτερο κέντρο ή μεγαλύτερο χάρτη. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Η αρένα αφήνει πολύ λίγο χώρο για κάθε αποικία. Χρησιμοποιήστε μεγαλύτερο χάρτη, μικρότερο κέντρο ή στενότερη τάφρο. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Τα αναχώματα είναι πολύ κοντά μεταξύ τους. Χρησιμοποιήστε λιγότερα ή στενότερα αναχώματα ή μεγαλύτερο κέντρο. +[Ring world] +Δακτυλιοειδής κόσμος +[Belt width] +Πλάτος ζώνης +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Η ζώνη είναι πολύ κοντή για τόσες αποικίες. Χρησιμοποιήστε μακρύτερο χάρτη ή λιγότερες αποικίες. diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 9f36e6172..f8c6a331e 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1542,6 +1542,10 @@ Nincs aktuális előnézet Gyors csata: dupla sebesség; A generált térképek 8 munkással kezdődnek. [Reroll] Hozzon létre egy másik térképet +[Randomize] +Véletlenszerű generálás +[Reset to defaults] +Alapértelmezett értékek visszaállítása [Ruleset] Szabálykészlet [Select a map from the library.] @@ -1792,6 +1796,12 @@ Szoba mérete Folyosó szélessége [Loopiness] Huroksűrűség +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +A labirintusnak szélességben és magasságban is legalább két cellára van szüksége; használjon nagyobb térképet vagy kisebb cellákat. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Ilyen széles csatornák mellett minden cellában túl kevés fű marad; használjon keskenyebb csatornákat vagy nagyobb cellákat. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +A labirintusban túl kevés a zsákutca ennyi kolóniához; használjon nagyobb térképet vagy kisebb cellákat. [Home island size] Kezdősziget mérete [Commons size] @@ -1806,5 +1816,57 @@ Kontinens mérete Partvonal tagoltsága [Fjord width] Fjord szélessége +[Lake connects to fjords] +A tó kapcsolódik a fjordokhoz [Resource islands] Nyersanyagszigetek +[Watershed] +Vízgyűjtő +[River density] +Folyók sűrűsége +[Dryness] +Szárazság +[Fords] +Gázlók +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +A vízgyűjtőn túl kevés az ártér ennyi kolóniához; használjon nagyobb térképet vagy kevesebb kolóniát. +[Stone highlands] +Köves felföld +[Valley size] +Völgy mérete +[Pass width] +Hágó szélessége +[Pond size] +Tavacska mérete +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Ezek a völgyek túl nagyok a térképhez; használjon nagyobb térképet vagy kisebb völgyeket. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +A felföldön túl kevés a hely ennyi kolóniához; használjon nagyobb térképet vagy kevesebb kolóniát. +[Symmetric arena] +Szimmetrikus aréna +[Centre size] +Középrész mérete +[Causeway width] +Töltésút szélessége +[Causeways] +Töltésutak +[Lakes] +Tavak +[Resource richness] +Nyersanyagbőség +[Symmetric arena needs 2, 4 or 8 colonies.] +A szimmetrikus arénához 2, 4 vagy 8 kolónia kell. +[Eight colonies need a square map in the symmetric arena.] +Nyolc kolóniához négyzet alakú térkép kell a szimmetrikus arénában. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +A középrész túl kicsi egy gyümölcsöshöz ezen a térképen; használjon nagyobb középrészt vagy nagyobb térképet. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Az aréna túl kevés helyet hagy minden kolóniának; használjon nagyobb térképet, kisebb középrészt vagy keskenyebb várárkot. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +A töltésutak túl közel vannak egymáshoz; használjon kevesebb vagy keskenyebb töltésutat, vagy nagyobb középrészt. +[Ring world] +Gyűrűvilág +[Belt width] +Öv szélessége +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Az öv túl rövid ennyi kolóniához; használjon hosszabb térképet vagy kevesebb kolóniát. diff --git a/data/texts.id.txt b/data/texts.id.txt index cfd0f39da..68a9fb52a 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1538,6 +1538,10 @@ Tidak ada pratinjau saat ini Pertarungan cepat: kecepatan ganda; peta yang dihasilkan dimulai dengan 8 pekerja. [Reroll] Hasilkan peta lain +[Randomize] +Hasilkan secara acak +[Reset to defaults] +Pulihkan nilai baku [Ruleset] Aturan [Select a map from the library.] @@ -1788,6 +1792,12 @@ Ukuran ruangan Lebar koridor [Loopiness] Kepadatan jalur melingkar +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labirin memerlukan setidaknya dua sel secara horizontal dan vertikal; gunakan peta yang lebih besar atau sel yang lebih kecil. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Kanal selebar ini menyisakan terlalu sedikit rumput di setiap sel; gunakan kanal yang lebih sempit atau sel yang lebih besar. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labirin memiliki terlalu sedikit jalan buntu untuk koloni sebanyak ini; gunakan peta yang lebih besar atau sel yang lebih kecil. [Home island size] Ukuran pulau awal [Commons size] @@ -1802,5 +1812,57 @@ Ukuran benua Ketidakteraturan pantai [Fjord width] Lebar fjord +[Lake connects to fjords] +Danau terhubung ke fjord [Resource islands] Pulau sumber daya +[Watershed] +Daerah aliran sungai +[River density] +Kepadatan sungai +[Dryness] +Kekeringan +[Fords] +Penyeberangan dangkal +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Daerah aliran sungai memiliki terlalu sedikit dataran banjir untuk koloni sebanyak ini; gunakan peta yang lebih besar atau lebih sedikit koloni. +[Stone highlands] +Dataran tinggi batu +[Valley size] +Ukuran lembah +[Pass width] +Lebar celah gunung +[Pond size] +Ukuran kolam +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Lembah-lembah ini terlalu besar untuk peta; gunakan peta yang lebih besar atau lembah yang lebih kecil. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Dataran tinggi terlalu sempit untuk koloni sebanyak ini; gunakan peta yang lebih besar atau lebih sedikit koloni. +[Symmetric arena] +Arena simetris +[Centre size] +Ukuran pusat +[Causeway width] +Lebar jalan tambak +[Causeways] +Jalan tambak +[Lakes] +Danau +[Resource richness] +Kelimpahan sumber daya +[Symmetric arena needs 2, 4 or 8 colonies.] +Arena simetris memerlukan 2, 4, atau 8 koloni. +[Eight colonies need a square map in the symmetric arena.] +Delapan koloni memerlukan peta persegi di arena simetris. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Pusat terlalu kecil untuk kebun buah di peta ini; gunakan pusat yang lebih besar atau peta yang lebih besar. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arena menyisakan terlalu sedikit ruang untuk setiap koloni; gunakan peta yang lebih besar, pusat yang lebih kecil, atau parit air yang lebih sempit. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Jalan tambak terlalu berdekatan; gunakan lebih sedikit jalan tambak atau yang lebih sempit, atau pusat yang lebih besar. +[Ring world] +Dunia cincin +[Belt width] +Lebar sabuk +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Sabuk terlalu pendek untuk koloni sebanyak ini; gunakan peta yang lebih panjang atau lebih sedikit koloni. diff --git a/data/texts.it.txt b/data/texts.it.txt index fc9a792b5..c0af5e0f0 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1604,6 +1604,10 @@ Nessuna anteprima corrente Battaglia veloce: doppia velocità; le mappe generate iniziano con 8 operai. [Reroll] Genera un'altra mappa +[Randomize] +Genera casualmente +[Reset to defaults] +Ripristina valori predefiniti [Ruleset] Set di regole [Select a map from the library.] @@ -1854,6 +1858,12 @@ Dimensione delle stanze Larghezza dei corridoi [Loopiness] Densità dei cicli +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Il labirinto richiede almeno due celle in larghezza e in altezza; usa una mappa più grande o celle più piccole. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Canali così larghi lasciano troppo poca erba in ogni cella; usa canali più stretti o celle più grandi. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Il labirinto ha troppo pochi vicoli ciechi per così tante colonie; usa una mappa più grande o celle più piccole. [Home island size] Dimensione dell'isola iniziale [Commons size] @@ -1868,5 +1878,57 @@ Dimensione del continente Irregolarità della costa [Fjord width] Larghezza del fiordo +[Lake connects to fjords] +Lago collegato ai fiordi [Resource islands] Isole di risorse +[Watershed] +Bacino idrografico +[River density] +Densità dei fiumi +[Dryness] +Aridità +[Fords] +Guadi +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Il bacino idrografico ha troppo poca pianura alluvionale per così tante colonie; usa una mappa più grande o meno colonie. +[Stone highlands] +Altopiani di pietra +[Valley size] +Dimensione della valle +[Pass width] +Larghezza del passo +[Pond size] +Dimensione dello stagno +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Queste valli sono troppo grandi per la mappa; usa una mappa più grande o valli più piccole. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Gli altopiani hanno troppo poco spazio per così tante colonie; usa una mappa più grande o meno colonie. +[Symmetric arena] +Arena simmetrica +[Centre size] +Dimensione del centro +[Causeway width] +Larghezza della strada rialzata +[Causeways] +Strade rialzate +[Lakes] +Laghi +[Resource richness] +Abbondanza di risorse +[Symmetric arena needs 2, 4 or 8 colonies.] +L'arena simmetrica richiede 2, 4 o 8 colonie. +[Eight colonies need a square map in the symmetric arena.] +Otto colonie richiedono una mappa quadrata nell'arena simmetrica. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Il centro è troppo piccolo per un frutteto su questa mappa; usa un centro più grande o una mappa più grande. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +L'arena lascia troppo poco spazio a ogni colonia; usa una mappa più grande, un centro più piccolo o un fossato più stretto. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Le strade rialzate sono troppo vicine tra loro; usa meno strade rialzate o più strette, oppure un centro più grande. +[Ring world] +Mondo ad anello +[Belt width] +Larghezza della fascia +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +La fascia è troppo corta per così tante colonie; usa una mappa più lunga o meno colonie. diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 4a8db7a2d..131bcddeb 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1538,6 +1538,10 @@ AIの命令なし クイックバトル:2倍速。生成されたマップは 8 人のワーカーから始まります。 [Reroll] 別のマップを生成する +[Randomize] +ランダム生成 +[Reset to defaults] +デフォルトに戻す [Ruleset] ルールセット [Select a map from the library.] @@ -1788,6 +1792,12 @@ Globulation 2 のカーソルを表示します。 通路の幅 [Loopiness] ループの多さ +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +迷路には縦横それぞれ2マス以上が必要です。より大きなマップを使うか、マスを小さくしてください。 +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +水路がこの幅だと各マスに残る草地が少なすぎます。水路を狭くするか、マスを大きくしてください。 +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +このコロニー数に対して迷路の行き止まりが少なすぎます。より大きなマップを使うか、マスを小さくしてください。 [Home island size] 開始地点の島の大きさ [Commons size] @@ -1802,5 +1812,57 @@ Globulation 2 のカーソルを表示します。 海岸線の複雑さ [Fjord width] フィヨルドの幅 +[Lake connects to fjords] +湖をフィヨルドにつなげる [Resource islands] 資源島 +[Watershed] +流域 +[River density] +川の密度 +[Dryness] +乾燥度 +[Fords] +浅瀬 +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +このコロニー数に対して流域の氾濫原が少なすぎます。より大きなマップを使うか、コロニーを減らしてください。 +[Stone highlands] +岩の高地 +[Valley size] +谷の大きさ +[Pass width] +峠の幅 +[Pond size] +池の大きさ +[These valleys are too big for the map; use a bigger map or smaller valleys.] +谷がマップに対して大きすぎます。より大きなマップを使うか、谷を小さくしてください。 +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +このコロニー数に対して高地の広さが足りません。より大きなマップを使うか、コロニーを減らしてください。 +[Symmetric arena] +対称アリーナ +[Centre size] +中央の大きさ +[Causeway width] +土手道の幅 +[Causeways] +土手道 +[Lakes] +湖 +[Resource richness] +資源の豊富さ +[Symmetric arena needs 2, 4 or 8 colonies.] +対称アリーナにはコロニーが2つ、4つ、または8つ必要です。 +[Eight colonies need a square map in the symmetric arena.] +対称アリーナでコロニーを8つにするには、正方形のマップが必要です。 +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +このマップでは中央が狭すぎて果樹園を置けません。中央を大きくするか、より大きなマップを使ってください。 +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +アリーナに各コロニー分の十分な広さがありません。より大きなマップを使うか、中央を小さくするか、堀を狭くしてください。 +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +土手道同士が近すぎます。土手道を減らすか狭くするか、中央を大きくしてください。 +[Ring world] +環状世界 +[Belt width] +帯の幅 +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +このコロニー数に対して帯が短すぎます。より長いマップを使うか、コロニーを減らしてください。 diff --git a/data/texts.keys.txt b/data/texts.keys.txt index d4bb1eddc..ed1c92c1e 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -898,7 +898,6 @@ [Continent size] [Coast roughness] [Fjord width] -[Lake size] [Lake connects to fjords] [Resource islands] [Watershed] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 0d10a3b93..636368a6e 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1538,6 +1538,10 @@ AI 명령 없음 빠른 전투: 속도 2배; 생성된 맵은 8명의 작업자로 시작합니다. [Reroll] 다른 지도 생성 +[Randomize] +무작위 생성 +[Reset to defaults] +기본값 복원 [Ruleset] 규칙 세트 [Select a map from the library.] @@ -1788,6 +1792,12 @@ Globulation 2 커서를 표시합니다. 통로 너비 [Loopiness] 순환로 밀도 +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +미로에는 가로와 세로로 각각 칸이 2개 이상 필요합니다. 더 큰 지도를 사용하거나 칸을 더 작게 하세요. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +수로가 이렇게 넓으면 각 칸에 남는 초지가 너무 적습니다. 수로를 더 좁게 하거나 칸을 더 크게 하세요. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +이 식민지 수에 비해 미로의 막다른 길이 너무 적습니다. 더 큰 지도를 사용하거나 칸을 더 작게 하세요. [Home island size] 시작 섬 크기 [Commons size] @@ -1802,5 +1812,57 @@ Globulation 2 커서를 표시합니다. 해안선 굴곡도 [Fjord width] 피오르 너비 +[Lake connects to fjords] +호수와 피오르 연결 [Resource islands] 자원 섬 +[Watershed] +유역 +[River density] +강 밀도 +[Dryness] +건조도 +[Fords] +여울 +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +이 식민지 수에 비해 유역의 범람원이 너무 적습니다. 더 큰 지도를 사용하거나 식민지 수를 줄이세요. +[Stone highlands] +바위 고원 +[Valley size] +골짜기 크기 +[Pass width] +고갯길 너비 +[Pond size] +연못 크기 +[These valleys are too big for the map; use a bigger map or smaller valleys.] +골짜기가 지도에 비해 너무 큽니다. 더 큰 지도를 사용하거나 골짜기를 더 작게 하세요. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +이 식민지 수에 비해 고원의 공간이 너무 부족합니다. 더 큰 지도를 사용하거나 식민지 수를 줄이세요. +[Symmetric arena] +대칭 경기장 +[Centre size] +중앙 크기 +[Causeway width] +둑길 너비 +[Causeways] +둑길 +[Lakes] +호수 +[Resource richness] +자원 풍부도 +[Symmetric arena needs 2, 4 or 8 colonies.] +대칭 경기장에는 식민지가 2개, 4개 또는 8개 필요합니다. +[Eight colonies need a square map in the symmetric arena.] +대칭 경기장에서 식민지를 8개로 하려면 정사각형 지도가 필요합니다. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +이 지도에서는 중앙이 너무 작아 과수원을 둘 수 없습니다. 중앙을 더 크게 하거나 더 큰 지도를 사용하세요. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +경기장에 식민지마다 둘 공간이 너무 부족합니다. 더 큰 지도를 사용하거나, 중앙을 더 작게 하거나, 해자를 더 좁게 하세요. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +둑길끼리 너무 가깝습니다. 둑길 수를 줄이거나 더 좁게 하거나, 중앙을 더 크게 하세요. +[Ring world] +고리 세계 +[Belt width] +띠 너비 +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +이 식민지 수에 비해 띠가 너무 짧습니다. 더 긴 지도를 사용하거나 식민지 수를 줄이세요. diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 2959fe917..78d77aae7 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1566,6 +1566,10 @@ Geen actueel voorbeeld Snelle strijd: dubbele snelheid; gegenereerde kaarten beginnen met 8 werkers. [Reroll] Genereer nog een kaart +[Randomize] +Willekeurig genereren +[Reset to defaults] +Standaardwaarden herstellen [Ruleset] Regelset [Select a map from the library.] @@ -1816,6 +1820,12 @@ Kamergrootte Gangbreedte [Loopiness] Aantal lussen +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Het doolhof heeft minstens twee cellen in de breedte en de hoogte nodig; gebruik een grotere kaart of kleinere cellen. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Zulke brede kanalen laten te weinig gras in elke cel over; gebruik smallere kanalen of grotere cellen. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Het doolhof heeft te weinig doodlopende gangen voor zoveel kolonies; gebruik een grotere kaart of kleinere cellen. [Home island size] Grootte van het starteiland [Commons size] @@ -1830,5 +1840,57 @@ Continentgrootte Grilligheid van de kust [Fjord width] Fjordbreedte +[Lake connects to fjords] +Meer verbonden met fjorden [Resource islands] Grondstofeilanden +[Watershed] +Stroomgebied +[River density] +Rivierdichtheid +[Dryness] +Droogte +[Fords] +Doorwaadplaatsen +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Het stroomgebied heeft te weinig overstromingsvlakte voor zoveel kolonies; gebruik een grotere kaart of minder kolonies. +[Stone highlands] +Stenen hooglanden +[Valley size] +Dalgrootte +[Pass width] +Pasbreedte +[Pond size] +Vijvergrootte +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Deze dalen zijn te groot voor de kaart; gebruik een grotere kaart of kleinere dalen. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +De hooglanden hebben te weinig ruimte voor zoveel kolonies; gebruik een grotere kaart of minder kolonies. +[Symmetric arena] +Symmetrische arena +[Centre size] +Grootte van het centrum +[Causeway width] +Dijkwegbreedte +[Causeways] +Dijkwegen +[Lakes] +Meren +[Resource richness] +Rijkdom aan grondstoffen +[Symmetric arena needs 2, 4 or 8 colonies.] +De symmetrische arena heeft 2, 4 of 8 kolonies nodig. +[Eight colonies need a square map in the symmetric arena.] +Acht kolonies hebben in de symmetrische arena een vierkante kaart nodig. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Het centrum is te klein voor een boomgaard op deze kaart; gebruik een groter centrum of een grotere kaart. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +De arena laat te weinig ruimte voor elke kolonie; gebruik een grotere kaart, een kleiner centrum of een smallere slotgracht. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +De dijkwegen liggen te dicht bij elkaar; gebruik minder of smallere dijkwegen, of een groter centrum. +[Ring world] +Ringwereld +[Belt width] +Gordelbreedte +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +De gordel is te kort voor zoveel kolonies; gebruik een langere kaart of minder kolonies. diff --git a/data/texts.pl.txt b/data/texts.pl.txt index a88a98b7b..7c91c9e5a 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1542,6 +1542,10 @@ Brak aktualnego podglądu Szybka bitwa: podwójna prędkość; wygenerowane mapy zaczynają się od 8 pracowników. [Reroll] Wygeneruj kolejną mapę +[Randomize] +Wygeneruj losowo +[Reset to defaults] +Przywróć ustawienia domyślne [Ruleset] Zestaw reguł [Select a map from the library.] @@ -1792,6 +1796,12 @@ Rozmiar pomieszczenia Szerokość korytarza [Loopiness] Liczba pętli +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labirynt wymaga co najmniej dwóch komórek w poziomie i w pionie; użyj większej mapy lub mniejszych komórek. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Tak szerokie kanały zostawiają w każdej komórce za mało trawy; użyj węższych kanałów lub większych komórek. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labirynt ma za mało ślepych zaułków dla tylu kolonii; użyj większej mapy lub mniejszych komórek. [Home island size] Rozmiar wyspy startowej [Commons size] @@ -1806,5 +1816,57 @@ Rozmiar kontynentu Urozmaicenie wybrzeża [Fjord width] Szerokość fiordu +[Lake connects to fjords] +Jezioro połączone z fiordami [Resource islands] Wyspy z surowcami +[Watershed] +Dorzecze +[River density] +Gęstość rzek +[Dryness] +Suchość +[Fords] +Brody +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Dorzecze ma za mało terenów zalewowych dla tylu kolonii; użyj większej mapy lub mniejszej liczby kolonii. +[Stone highlands] +Kamienne wyżyny +[Valley size] +Rozmiar doliny +[Pass width] +Szerokość przełęczy +[Pond size] +Rozmiar stawu +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Te doliny są za duże na tę mapę; użyj większej mapy lub mniejszych dolin. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Wyżyny mają za mało miejsca dla tylu kolonii; użyj większej mapy lub mniejszej liczby kolonii. +[Symmetric arena] +Symetryczna arena +[Centre size] +Rozmiar centrum +[Causeway width] +Szerokość grobli +[Causeways] +Groble +[Lakes] +Jeziora +[Resource richness] +Zasobność surowców +[Symmetric arena needs 2, 4 or 8 colonies.] +Symetryczna arena wymaga 2, 4 lub 8 kolonii. +[Eight colonies need a square map in the symmetric arena.] +Osiem kolonii wymaga kwadratowej mapy na symetrycznej arenie. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Centrum jest za małe na sad na tej mapie; użyj większego centrum lub większej mapy. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arena zostawia za mało miejsca dla każdej kolonii; użyj większej mapy, mniejszego centrum lub węższej fosy. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Groble są zbyt blisko siebie; użyj mniej licznych lub węższych grobli albo większego centrum. +[Ring world] +Świat pierścieniowy +[Belt width] +Szerokość pasa +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Pas jest za krótki dla tylu kolonii; użyj dłuższej mapy lub mniejszej liczby kolonii. diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 56f531e2b..acede099f 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1548,6 +1548,10 @@ Nenhuma visualização atual Batalha rápida: velocidade dupla; os mapas gerados começam com 8 trabalhadores. [Reroll] Gere outro mapa +[Randomize] +Gerar aleatoriamente +[Reset to defaults] +Restaurar predefinições [Ruleset] Conjunto de regras [Select a map from the library.] @@ -1798,6 +1802,12 @@ Tamanho da sala Largura do corredor [Loopiness] Densidade de ciclos +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +O labirinto precisa de pelo menos duas células na horizontal e na vertical; use um mapa maior ou células mais pequenas. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Canais tão largos deixam muito pouca erva em cada célula; use canais mais estreitos ou células maiores. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +O labirinto tem poucos becos sem saída para tantas colónias; use um mapa maior ou células mais pequenas. [Home island size] Tamanho da ilha inicial [Commons size] @@ -1812,5 +1822,57 @@ Tamanho do continente Irregularidade da costa [Fjord width] Largura do fiorde +[Lake connects to fjords] +Lago ligado aos fiordes [Resource islands] Ilhas de recursos +[Watershed] +Bacia hidrográfica +[River density] +Densidade de rios +[Dryness] +Aridez +[Fords] +Vaus +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +A bacia hidrográfica tem pouca planície aluvial para tantas colónias; use um mapa maior ou menos colónias. +[Stone highlands] +Planaltos de pedra +[Valley size] +Tamanho do vale +[Pass width] +Largura da passagem +[Pond size] +Tamanho da lagoa +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Estes vales são demasiado grandes para o mapa; use um mapa maior ou vales mais pequenos. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Os planaltos têm pouco espaço para tantas colónias; use um mapa maior ou menos colónias. +[Symmetric arena] +Arena simétrica +[Centre size] +Tamanho do centro +[Causeway width] +Largura dos caminhos elevados +[Causeways] +Caminhos elevados +[Lakes] +Lagos +[Resource richness] +Riqueza de recursos +[Symmetric arena needs 2, 4 or 8 colonies.] +A arena simétrica precisa de 2, 4 ou 8 colónias. +[Eight colonies need a square map in the symmetric arena.] +Oito colónias precisam de um mapa quadrado na arena simétrica. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +O centro é demasiado pequeno para um pomar neste mapa; use um centro maior ou um mapa maior. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +A arena deixa pouco espaço para cada colónia; use um mapa maior, um centro mais pequeno ou um fosso mais estreito. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Os caminhos elevados estão demasiado próximos; use menos caminhos elevados ou mais estreitos, ou um centro maior. +[Ring world] +Mundo anel +[Belt width] +Largura da faixa +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +A faixa é demasiado curta para tantas colónias; use um mapa mais longo ou menos colónias. diff --git a/data/texts.ro.txt b/data/texts.ro.txt index b43125632..f4ff1e847 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1542,6 +1542,10 @@ Nicio previzualizare curentă Luptă rapidă: viteză dublă; hărțile generate încep cu 8 lucrători. [Reroll] Generați o altă hartă +[Randomize] +Generați aleatoriu +[Reset to defaults] +Restabilește valorile implicite [Ruleset] Set de reguli [Select a map from the library.] @@ -1792,6 +1796,12 @@ Mărimea camerei Lățimea coridorului [Loopiness] Densitatea buclelor +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labirintul are nevoie de cel puțin două celule pe orizontală și pe verticală; folosiți o hartă mai mare sau celule mai mici. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Canalele atât de late lasă prea puțină iarbă în fiecare celulă; folosiți canale mai înguste sau celule mai mari. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labirintul are prea puține fundături pentru atâtea colonii; folosiți o hartă mai mare sau celule mai mici. [Home island size] Mărimea insulei de pornire [Commons size] @@ -1806,5 +1816,57 @@ Mărimea continentului Neregularitatea coastei [Fjord width] Lățimea fiordului +[Lake connects to fjords] +Lac conectat la fiorduri [Resource islands] Insule cu resurse +[Watershed] +Bazin hidrografic +[River density] +Densitatea râurilor +[Dryness] +Ariditate +[Fords] +Vaduri +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Bazinul hidrografic are prea puțină luncă pentru atâtea colonii; folosiți o hartă mai mare sau mai puține colonii. +[Stone highlands] +Podișuri de piatră +[Valley size] +Mărimea văii +[Pass width] +Lățimea trecătorii +[Pond size] +Mărimea iazului +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Aceste văi sunt prea mari pentru hartă; folosiți o hartă mai mare sau văi mai mici. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Podișurile au prea puțin spațiu pentru atâtea colonii; folosiți o hartă mai mare sau mai puține colonii. +[Symmetric arena] +Arenă simetrică +[Centre size] +Mărimea centrului +[Causeway width] +Lățimea digului +[Causeways] +Diguri +[Lakes] +Lacuri +[Resource richness] +Bogăția resurselor +[Symmetric arena needs 2, 4 or 8 colonies.] +Arena simetrică necesită 2, 4 sau 8 colonii. +[Eight colonies need a square map in the symmetric arena.] +Opt colonii necesită o hartă pătrată în arena simetrică. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Centrul este prea mic pentru o livadă pe această hartă; folosiți un centru mai mare sau o hartă mai mare. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arena lasă prea puțin spațiu pentru fiecare colonie; folosiți o hartă mai mare, un centru mai mic sau un șanț cu apă mai îngust. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Digurile sunt prea apropiate; folosiți mai puține diguri sau diguri mai înguste, ori un centru mai mare. +[Ring world] +Lume inelară +[Belt width] +Lățimea centurii +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Centura este prea scurtă pentru atâtea colonii; folosiți o hartă mai lungă sau mai puține colonii. diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 699abc405..6571f8ee9 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1540,6 +1540,10 @@ Cortex Быстрый бой: двойная скорость; сгенерированные карты начинаются с 8 рабочих. [Reroll] Создать еще одну карту +[Randomize] +Случайная генерация +[Reset to defaults] +Восстановить значения по умолчанию [Ruleset] Набор правил [Select a map from the library.] @@ -1790,6 +1794,12 @@ OpenGL недоступен в этой сборке. Ширина коридора [Loopiness] Количество петель +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Лабиринту нужно не меньше двух ячеек по горизонтали и по вертикали; используйте карту побольше или ячейки поменьше. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +При такой ширине проливов в каждой ячейке остаётся слишком мало травы; используйте проливы поуже или ячейки побольше. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +В лабиринте слишком мало тупиков для такого числа колоний; используйте карту побольше или ячейки поменьше. [Home island size] Размер стартового острова [Commons size] @@ -1804,5 +1814,57 @@ OpenGL недоступен в этой сборке. Изрезанность побережья [Fjord width] Ширина фьорда +[Lake connects to fjords] +Озеро соединено с фьордами [Resource islands] Острова с ресурсами +[Watershed] +Речной бассейн +[River density] +Плотность рек +[Dryness] +Засушливость +[Fords] +Броды +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +В речном бассейне слишком мало пойменных земель для такого числа колоний; используйте карту побольше или меньше колоний. +[Stone highlands] +Каменное нагорье +[Valley size] +Размер долины +[Pass width] +Ширина перевала +[Pond size] +Размер пруда +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Эти долины слишком велики для карты; используйте карту побольше или долины поменьше. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +На нагорье слишком мало места для такого числа колоний; используйте карту побольше или меньше колоний. +[Symmetric arena] +Симметричная арена +[Centre size] +Размер центра +[Causeway width] +Ширина дамбы +[Causeways] +Дамбы +[Lakes] +Озёра +[Resource richness] +Богатство ресурсов +[Symmetric arena needs 2, 4 or 8 colonies.] +Симметричной арене нужно 2, 4 или 8 колоний. +[Eight colonies need a square map in the symmetric arena.] +Для восьми колоний симметричной арене нужна квадратная карта. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Центр слишком мал для фруктового сада на этой карте; используйте центр побольше или карту побольше. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +На арене слишком мало места для каждой колонии; используйте карту побольше, центр поменьше или ров поуже. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Дамбы расположены слишком близко друг к другу; используйте меньше дамб, более узкие дамбы или центр побольше. +[Ring world] +Кольцевой мир +[Belt width] +Ширина пояса +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Пояс слишком короткий для такого числа колоний; используйте карту подлиннее или меньше колоний. diff --git a/data/texts.si.txt b/data/texts.si.txt index a68b19c87..23359e903 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1542,6 +1542,10 @@ Ni trenutnega predogleda Hitra bitka: dvojna hitrost; generirani zemljevidi se začnejo z 8 delavci. [Reroll] Ustvarite drug zemljevid +[Randomize] +Naključno ustvari +[Reset to defaults] +Obnovi privzete vrednosti [Ruleset] Nabor pravil [Select a map from the library.] @@ -1792,6 +1796,12 @@ Velikost sobe Širina hodnika [Loopiness] Gostota zank +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labirint potrebuje vsaj dve celici v širino in v višino; uporabite večji zemljevid ali manjše celice. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Tako široki kanali v vsaki celici pustijo premalo trave; uporabite ožje kanale ali večje celice. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labirint ima premalo slepih ulic za toliko kolonij; uporabite večji zemljevid ali manjše celice. [Home island size] Velikost začetnega otoka [Commons size] @@ -1806,5 +1816,57 @@ Velikost celine Razgibanost obale [Fjord width] Širina fjorda +[Lake connects to fjords] +Jezero povezano s fjordi [Resource islands] Otoki z viri +[Watershed] +Porečje +[River density] +Gostota rek +[Dryness] +Sušnost +[Fords] +Brodi +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Porečje ima premalo poplavne ravnice za toliko kolonij; uporabite večji zemljevid ali manj kolonij. +[Stone highlands] +Kamnito višavje +[Valley size] +Velikost doline +[Pass width] +Širina prelaza +[Pond size] +Velikost mlake +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Te doline so prevelike za zemljevid; uporabite večji zemljevid ali manjše doline. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Višavje ima premalo prostora za toliko kolonij; uporabite večji zemljevid ali manj kolonij. +[Symmetric arena] +Simetrična arena +[Centre size] +Velikost središča +[Causeway width] +Širina nasipa +[Causeways] +Nasipi +[Lakes] +Jezera +[Resource richness] +Bogastvo virov +[Symmetric arena needs 2, 4 or 8 colonies.] +Simetrična arena potrebuje 2, 4 ali 8 kolonij. +[Eight colonies need a square map in the symmetric arena.] +Osem kolonij potrebuje v simetrični areni kvadraten zemljevid. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Središče je na tem zemljevidu premajhno za sadovnjak; uporabite večje središče ali večji zemljevid. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arena pušča premalo prostora za vsako kolonijo; uporabite večji zemljevid, manjše središče ali ožji vodni jarek. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Nasipi so preblizu skupaj; uporabite manj nasipov, ožje nasipe ali večje središče. +[Ring world] +Obročasti svet +[Belt width] +Širina pasu +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Pas je prekratek za toliko kolonij; uporabite daljši zemljevid ali manj kolonij. diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 528b3cdfc..0f1ddd36c 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1550,6 +1550,10 @@ Formát hry Rýchla bitka: dvojnásobná rýchlosť; generované mapy začínajú s 8 pracovníkmi. [Reroll] Vytvorte ďalšiu mapu +[Randomize] +Vygenerovať náhodne +[Reset to defaults] +Obnoviť predvolené hodnoty [Ruleset] Pravidlá [Select a map from the library.] @@ -1800,6 +1804,12 @@ Veľkosť miestnosti Šírka chodby [Loopiness] Hustota slučiek +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Bludisko potrebuje aspoň dve bunky na šírku aj na výšku; použite väčšiu mapu alebo menšie bunky. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Takéto široké prielivy nechávajú v každej bunke príliš málo trávy; použite užšie prielivy alebo väčšie bunky. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Bludisko má pre toľko kolónií príliš málo slepých uličiek; použite väčšiu mapu alebo menšie bunky. [Home island size] Veľkosť štartovacieho ostrova [Commons size] @@ -1814,5 +1824,57 @@ Veľkosť kontinentu Členitosť pobrežia [Fjord width] Šírka fjordu +[Lake connects to fjords] +Jazero spojené s fjordmi [Resource islands] Ostrovy so surovinami +[Watershed] +Povodie +[River density] +Hustota riek +[Dryness] +Vyprahnutosť +[Fords] +Brody +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Povodie má pre toľko kolónií príliš málo záplavovej nivy; použite väčšiu mapu alebo menej kolónií. +[Stone highlands] +Kamenná vysočina +[Valley size] +Veľkosť údolia +[Pass width] +Šírka priesmyku +[Pond size] +Veľkosť jazierka +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Tieto údolia sú pre mapu príliš veľké; použite väčšiu mapu alebo menšie údolia. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Vysočina má pre toľko kolónií príliš málo miesta; použite väčšiu mapu alebo menej kolónií. +[Symmetric arena] +Symetrická aréna +[Centre size] +Veľkosť stredu +[Causeway width] +Šírka hrádze +[Causeways] +Hrádze +[Lakes] +Jazerá +[Resource richness] +Bohatstvo surovín +[Symmetric arena needs 2, 4 or 8 colonies.] +Symetrická aréna vyžaduje 2, 4 alebo 8 kolónií. +[Eight colonies need a square map in the symmetric arena.] +Osem kolónií vyžaduje v symetrickej aréne štvorcovú mapu. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Stred je na tejto mape príliš malý na ovocný sad; použite väčší stred alebo väčšiu mapu. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Aréna ponecháva príliš málo miesta pre každú kolóniu; použite väčšiu mapu, menší stred alebo užšiu vodnú priekopu. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Hrádze sú príliš blízko pri sebe; použite menej alebo užšie hrádze, prípadne väčší stred. +[Ring world] +Prstencový svet +[Belt width] +Šírka pásu +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Pás je pre toľko kolónií príliš krátky; použite dlhšiu mapu alebo menej kolónií. diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 38110eea0..d083d6450 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1542,6 +1542,10 @@ Cortex Брза битка: двострука брзина; генерисане карте почињу са 8 радника. [Reroll] Генеришите другу мапу +[Randomize] +Генеришите насумично +[Reset to defaults] +Врати подразумеване вредности [Ruleset] Скуп правила [Select a map from the library.] @@ -1792,6 +1796,12 @@ OpenGL није доступан у овој верзији. Ширина ходника [Loopiness] Учесталост петљи +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Лавиринту су потребне најмање две ћелије по ширини и по висини; користите већу мапу или мање ћелије. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Овако широки канали остављају премало траве у свакој ћелији; користите уже канале или веће ћелије. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Лавиринт има премало ћорсокака за толико колонија; користите већу мапу или мање ћелије. [Home island size] Величина почетног острва [Commons size] @@ -1806,5 +1816,57 @@ OpenGL није доступан у овој верзији. Разуђеност обале [Fjord width] Ширина фјорда +[Lake connects to fjords] +Језеро повезано са фјордовима [Resource islands] Острва са ресурсима +[Watershed] +Речни слив +[River density] +Густина река +[Dryness] +Сушност +[Fords] +Газови +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Речни слив има премало плавне равнице за толико колонија; користите већу мапу или мање колонија. +[Stone highlands] +Камене висоравни +[Valley size] +Величина долине +[Pass width] +Ширина превоја +[Pond size] +Величина баре +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Ове долине су превелике за мапу; користите већу мапу или мање долине. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +На висоравнима има премало места за толико колонија; користите већу мапу или мање колонија. +[Symmetric arena] +Симетрична арена +[Centre size] +Величина средишта +[Causeway width] +Ширина насипа +[Causeways] +Насипи +[Lakes] +Језера +[Resource richness] +Богатство ресурса +[Symmetric arena needs 2, 4 or 8 colonies.] +Симетричној арени су потребне 2, 4 или 8 колонија. +[Eight colonies need a square map in the symmetric arena.] +За осам колонија симетричној арени је потребна квадратна мапа. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Средиште је премало за воћњак на овој мапи; користите веће средиште или већу мапу. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Арена оставља премало места за сваку колонију; користите већу мапу, мање средиште или ужи водени ров. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Насипи су преблизу један другом; користите мање насипа, уже насипе или веће средиште. +[Ring world] +Прстенасти свет +[Belt width] +Ширина појаса +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Појас је прекратак за толико колонија; користите дужу мапу или мање колонија. diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 25ff8cb10..1b6dcd64a 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1552,6 +1552,10 @@ Ingen aktuell förhandsvisning Snabb strid: dubbel hastighet; genererade kartor börjar med 8 arbetare. [Reroll] Skapa en annan karta +[Randomize] +Slumpa +[Reset to defaults] +Återställ standardvärden [Ruleset] Regeluppsättning [Select a map from the library.] @@ -1802,6 +1806,12 @@ Rumsstorlek Korridorbredd [Loopiness] Antal slingor +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labyrinten behöver minst två celler på bredden och på höjden; använd en större karta eller mindre celler. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Så breda kanaler lämnar för lite gräs i varje cell; använd smalare kanaler eller större celler. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labyrinten har för få återvändsgränder för så många kolonier; använd en större karta eller mindre celler. [Home island size] Startöns storlek [Commons size] @@ -1816,5 +1826,57 @@ Kontinentstorlek Kustlinjens oregelbundenhet [Fjord width] Fjordbredd +[Lake connects to fjords] +Sjön ansluter till fjordarna [Resource islands] Resursöar +[Watershed] +Avrinningsområde +[River density] +Flodtäthet +[Dryness] +Torrhet +[Fords] +Vadställen +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Avrinningsområdet har för lite flodslätt för så många kolonier; använd en större karta eller färre kolonier. +[Stone highlands] +Stenhögland +[Valley size] +Dalstorlek +[Pass width] +Passbredd +[Pond size] +Dammstorlek +[These valleys are too big for the map; use a bigger map or smaller valleys.] +De här dalarna är för stora för kartan; använd en större karta eller mindre dalar. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Höglandet har för lite utrymme för så många kolonier; använd en större karta eller färre kolonier. +[Symmetric arena] +Symmetrisk arena +[Centre size] +Centrumstorlek +[Causeway width] +Vägbankarnas bredd +[Causeways] +Vägbankar +[Lakes] +Sjöar +[Resource richness] +Resursrikedom +[Symmetric arena needs 2, 4 or 8 colonies.] +Symmetrisk arena kräver 2, 4 eller 8 kolonier. +[Eight colonies need a square map in the symmetric arena.] +Åtta kolonier kräver en kvadratisk karta i den symmetriska arenan. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Centrum är för litet för en fruktträdgård på den här kartan; använd ett större centrum eller en större karta. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arenan lämnar för lite utrymme för varje koloni; använd en större karta, ett mindre centrum eller en smalare vallgrav. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Vägbankarna ligger för tätt; använd färre eller smalare vägbankar, eller ett större centrum. +[Ring world] +Ringvärld +[Belt width] +Bältets bredd +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Bältet är för kort för så många kolonier; använd en längre karta eller färre kolonier. diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 812784e39..6f9d1c512 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1550,6 +1550,10 @@ Geçerli önizleme yok Hızlı savaş: iki kat hız; oluşturulan haritalar 8 işçiyle başlar. [Reroll] Başka bir harita oluştur +[Randomize] +Rastgele oluştur +[Reset to defaults] +Varsayılanları geri yükle [Ruleset] Kural seti [Select a map from the library.] @@ -1800,6 +1804,12 @@ Oda boyutu Koridor genişliği [Loopiness] Döngü yoğunluğu +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Labirent yatayda ve dikeyde en az iki hücre gerektirir; daha büyük bir harita veya daha küçük hücreler kullanın. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Bu kadar geniş kanallar her hücrede çok az çimen bırakır; daha dar kanallar veya daha büyük hücreler kullanın. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Labirentte bu kadar koloni için çok az çıkmaz sokak var; daha büyük bir harita veya daha küçük hücreler kullanın. [Home island size] Başlangıç adası boyutu [Commons size] @@ -1814,5 +1824,57 @@ Kıta boyutu Kıyı girintiliği [Fjord width] Fiyort genişliği +[Lake connects to fjords] +Göl fiyortlara bağlı [Resource islands] Kaynak adaları +[Watershed] +Havza +[River density] +Nehir yoğunluğu +[Dryness] +Kuraklık +[Fords] +Sığ geçitler +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Havzada bu kadar koloni için çok az taşkın ovası var; daha büyük bir harita veya daha az koloni kullanın. +[Stone highlands] +Taşlı yaylalar +[Valley size] +Vadi boyutu +[Pass width] +Geçit genişliği +[Pond size] +Gölet boyutu +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Bu vadiler harita için çok büyük; daha büyük bir harita veya daha küçük vadiler kullanın. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Yaylalarda bu kadar koloni için çok az yer var; daha büyük bir harita veya daha az koloni kullanın. +[Symmetric arena] +Simetrik arena +[Centre size] +Merkez boyutu +[Causeway width] +Set yolu genişliği +[Causeways] +Set yolları +[Lakes] +Göller +[Resource richness] +Kaynak zenginliği +[Symmetric arena needs 2, 4 or 8 colonies.] +Simetrik arena 2, 4 veya 8 koloni gerektirir. +[Eight colonies need a square map in the symmetric arena.] +Simetrik arenada sekiz koloni için kare bir harita gerekir. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Merkez bu haritada bir meyve bahçesi için çok küçük; daha büyük bir merkez veya daha büyük bir harita kullanın. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Arena her koloniye çok az yer bırakıyor; daha büyük bir harita, daha küçük bir merkez veya daha dar bir su hendeği kullanın. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Set yolları birbirine çok yakın; daha az veya daha dar set yolları ya da daha büyük bir merkez kullanın. +[Ring world] +Halka dünya +[Belt width] +Kuşak genişliği +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Kuşak bu kadar koloni için çok kısa; daha uzun bir harita veya daha az koloni kullanın. diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 32461e4c8..e70eeb5b6 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1538,6 +1538,10 @@ Ysagoon Online Gaming Швидкий бій: подвійна швидкість; створені карти починаються з 8 робітників. [Reroll] Створіть іншу карту +[Randomize] +Випадкова генерація +[Reset to defaults] +Відновити типові значення [Ruleset] Набір правил [Select a map from the library.] @@ -1788,6 +1792,12 @@ OpenGL недоступний у цій збірці. Ширина коридору [Loopiness] Кількість петель +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Лабіринту потрібно щонайменше дві комірки по горизонталі й вертикалі; використайте більшу карту або менші комірки. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +За такої ширини проток у кожній комірці лишається замало трави; використайте вужчі протоки або більші комірки. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +У лабіринті замало тупиків для такої кількості колоній; використайте більшу карту або менші комірки. [Home island size] Розмір стартового острова [Commons size] @@ -1802,5 +1812,57 @@ OpenGL недоступний у цій збірці. Порізаність узбережжя [Fjord width] Ширина фіорду +[Lake connects to fjords] +Озеро з’єднане з фіордами [Resource islands] Острови з ресурсами +[Watershed] +Річковий басейн +[River density] +Щільність річок +[Dryness] +Посушливість +[Fords] +Броди +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +У річковому басейні замало заплавних земель для такої кількості колоній; використайте більшу карту або менше колоній. +[Stone highlands] +Кам’яне нагір’я +[Valley size] +Розмір долини +[Pass width] +Ширина перевалу +[Pond size] +Розмір ставка +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Ці долини завеликі для карти; використайте більшу карту або менші долини. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +На нагір’ї замало місця для такої кількості колоній; використайте більшу карту або менше колоній. +[Symmetric arena] +Симетрична арена +[Centre size] +Розмір центру +[Causeway width] +Ширина дамби +[Causeways] +Дамби +[Lakes] +Озера +[Resource richness] +Багатство ресурсів +[Symmetric arena needs 2, 4 or 8 colonies.] +Симетричній арені потрібно 2, 4 або 8 колоній. +[Eight colonies need a square map in the symmetric arena.] +Для восьми колоній симетрична арена потребує квадратної карти. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Центр замалий для фруктового саду на цій карті; використайте більший центр або більшу карту. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Арена залишає замало місця для кожної колонії; використайте більшу карту, менший центр або вужчий рів. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Дамби розташовані надто близько одна до одної; використайте менше дамб, вужчі дамби або більший центр. +[Ring world] +Кільцевий світ +[Belt width] +Ширина поясу +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Пояс закороткий для такої кількості колоній; використайте довшу карту або менше колоній. diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 48fe16196..1218b31af 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1538,6 +1538,10 @@ Không có bản xem trước hiện tại Trận chiến nhanh: tốc độ gấp đôi; bản đồ được tạo bắt đầu với 8 công nhân. [Reroll] Tạo một bản đồ khác +[Randomize] +Tạo ngẫu nhiên +[Reset to defaults] +Khôi phục mặc định [Ruleset] Bộ quy tắc [Select a map from the library.] @@ -1788,6 +1792,12 @@ Kích thước phòng Độ rộng hành lang [Loopiness] Mật độ vòng lặp +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +Mê cung cần ít nhất hai ô theo chiều ngang và chiều dọc; hãy dùng bản đồ lớn hơn hoặc ô nhỏ hơn. +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +Kênh rộng như vậy để lại quá ít cỏ trong mỗi ô; hãy dùng kênh hẹp hơn hoặc ô lớn hơn. +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +Mê cung có quá ít ngõ cụt cho số thuộc địa này; hãy dùng bản đồ lớn hơn hoặc ô nhỏ hơn. [Home island size] Kích thước đảo khởi đầu [Commons size] @@ -1802,5 +1812,57 @@ Kích thước lục địa Độ khúc khuỷu của bờ biển [Fjord width] Độ rộng vịnh hẹp +[Lake connects to fjords] +Hồ nối với vịnh hẹp [Resource islands] Đảo tài nguyên +[Watershed] +Lưu vực sông +[River density] +Mật độ sông +[Dryness] +Độ khô hạn +[Fords] +Bãi cạn +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +Lưu vực sông có quá ít đồng bằng ngập lũ cho số thuộc địa này; hãy dùng bản đồ lớn hơn hoặc ít thuộc địa hơn. +[Stone highlands] +Cao nguyên đá +[Valley size] +Kích thước thung lũng +[Pass width] +Độ rộng đèo +[Pond size] +Kích thước ao +[These valleys are too big for the map; use a bigger map or smaller valleys.] +Các thung lũng này quá lớn so với bản đồ; hãy dùng bản đồ lớn hơn hoặc thung lũng nhỏ hơn. +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +Cao nguyên có quá ít chỗ cho số thuộc địa này; hãy dùng bản đồ lớn hơn hoặc ít thuộc địa hơn. +[Symmetric arena] +Đấu trường đối xứng +[Centre size] +Kích thước trung tâm +[Causeway width] +Độ rộng đường đắp cao +[Causeways] +Đường đắp cao +[Lakes] +Hồ +[Resource richness] +Độ phong phú tài nguyên +[Symmetric arena needs 2, 4 or 8 colonies.] +Đấu trường đối xứng cần 2, 4 hoặc 8 thuộc địa. +[Eight colonies need a square map in the symmetric arena.] +Tám thuộc địa cần bản đồ hình vuông trong đấu trường đối xứng. +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +Trung tâm quá nhỏ để có vườn cây ăn quả trên bản đồ này; hãy dùng trung tâm lớn hơn hoặc bản đồ lớn hơn. +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +Đấu trường để lại quá ít chỗ cho mỗi thuộc địa; hãy dùng bản đồ lớn hơn, trung tâm nhỏ hơn hoặc hào nước hẹp hơn. +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +Các đường đắp cao quá gần nhau; hãy dùng ít đường đắp cao hơn hoặc hẹp hơn, hoặc trung tâm lớn hơn. +[Ring world] +Thế giới hình vòng +[Belt width] +Độ rộng vành đai +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +Vành đai quá ngắn cho số thuộc địa này; hãy dùng bản đồ dài hơn hoặc ít thuộc địa hơn. diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 44d06c149..09a33cc20 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1538,6 +1538,10 @@ AI策略与对抗 快速战斗:双倍速度;生成的地图以 8 个工人开始。 [Reroll] 生成另一张地图 +[Randomize] +随机生成 +[Reset to defaults] +恢复默认设置 [Ruleset] 规则集 [Select a map from the library.] @@ -1788,6 +1792,12 @@ OpenGL 在此版本中不可用。 通道宽度 [Loopiness] 回环程度 +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +迷宫在横向和纵向上都至少需要两个单元格;请使用更大的地图或更小的单元格。 +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +水道这么宽,每个单元格里剩下的草地太少;请使用更窄的水道或更大的单元格。 +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +迷宫中的死胡同太少,不足以容纳这么多殖民地;请使用更大的地图或更小的单元格。 [Home island size] 起始岛屿大小 [Commons size] @@ -1802,5 +1812,57 @@ OpenGL 在此版本中不可用。 海岸线曲折度 [Fjord width] 峡湾宽度 +[Lake connects to fjords] +湖泊连通峡湾 [Resource islands] 资源岛屿 +[Watershed] +流域 +[River density] +河流密度 +[Dryness] +干旱程度 +[Fords] +浅滩 +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +流域中的泛滥平原太少,容纳不下这么多殖民地;请使用更大的地图或减少殖民地数量。 +[Stone highlands] +石质高地 +[Valley size] +山谷大小 +[Pass width] +山口宽度 +[Pond size] +池塘大小 +[These valleys are too big for the map; use a bigger map or smaller valleys.] +这些山谷对于地图来说太大;请使用更大的地图或更小的山谷。 +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +高地空间太少,容纳不下这么多殖民地;请使用更大的地图或减少殖民地数量。 +[Symmetric arena] +对称竞技场 +[Centre size] +中心大小 +[Causeway width] +堤道宽度 +[Causeways] +堤道 +[Lakes] +湖泊 +[Resource richness] +资源丰富度 +[Symmetric arena needs 2, 4 or 8 colonies.] +对称竞技场需要2、4或8个殖民地。 +[Eight colonies need a square map in the symmetric arena.] +在对称竞技场中,八个殖民地需要正方形地图。 +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +在这张地图上,中心太小,放不下果园;请使用更大的中心或更大的地图。 +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +竞技场留给每个殖民地的空间太少;请使用更大的地图、更小的中心或更窄的护城河。 +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +堤道之间靠得太近;请减少堤道数量或使用更窄的堤道,或者使用更大的中心。 +[Ring world] +环形世界 +[Belt width] +环带宽度 +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +环带太短,容纳不下这么多殖民地;请使用更长的地图或减少殖民地数量。 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 1b89bb199..f2231b979 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1614,6 +1614,10 @@ AI策略與對抗 快速戰鬥:雙倍速度;生成的地圖以 8 個工人開始。 [Reroll] 生成另一張地圖 +[Randomize] +隨機生成 +[Reset to defaults] +還原預設值 [Ruleset] 規則集 [Select a map from the library.] @@ -1864,6 +1868,12 @@ OpenGL 在此版本中不可用。 通道寬度 [Loopiness] 迴路密度 +[The maze needs at least two cells across and down; use a bigger map or smaller cells.] +迷宮在橫向和縱向上都至少需要兩個單元格;請使用更大的地圖或更小的單元格。 +[Channels this wide leave too little grass in each cell; use narrower channels or bigger cells.] +水道這麼寬,每個單元格裡剩下的草地太少;請使用更窄的水道或更大的單元格。 +[The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller cells.] +迷宮中的死路太少,不足以容納這麼多殖民地;請使用更大的地圖或更小的單元格。 [Home island size] 起始島嶼大小 [Commons size] @@ -1878,5 +1888,57 @@ OpenGL 在此版本中不可用。 海岸線曲折度 [Fjord width] 峽灣寬度 +[Lake connects to fjords] +湖泊連通峽灣 [Resource islands] 資源島嶼 +[Watershed] +流域 +[River density] +河流密度 +[Dryness] +乾旱程度 +[Fords] +淺灘 +[The watershed has too little floodplain for this many colonies; use a bigger map or fewer colonies.] +流域中的氾濫平原太少,容納不下這麼多殖民地;請使用更大的地圖或減少殖民地數量。 +[Stone highlands] +石質高地 +[Valley size] +山谷大小 +[Pass width] +隘口寬度 +[Pond size] +池塘大小 +[These valleys are too big for the map; use a bigger map or smaller valleys.] +這些山谷對於地圖來說太大;請使用更大的地圖或更小的山谷。 +[The highlands have too little room for this many colonies; use a bigger map or fewer colonies.] +高地空間太少,容納不下這麼多殖民地;請使用更大的地圖或減少殖民地數量。 +[Symmetric arena] +對稱競技場 +[Centre size] +中心大小 +[Causeway width] +堤道寬度 +[Causeways] +堤道 +[Lakes] +湖泊 +[Resource richness] +資源豐富度 +[Symmetric arena needs 2, 4 or 8 colonies.] +對稱競技場需要2、4或8個殖民地。 +[Eight colonies need a square map in the symmetric arena.] +在對稱競技場中,八個殖民地需要正方形地圖。 +[The centre is too small for an orchard on this map; use a bigger centre or a bigger map.] +在這張地圖上,中心太小,放不下果園;請使用更大的中心或更大的地圖。 +[The arena leaves too little room for every colony; use a bigger map, a smaller centre or a narrower moat.] +競技場留給每個殖民地的空間太少;請使用更大的地圖、更小的中心或更窄的護城河。 +[The causeways are too close together; use fewer or narrower causeways, or a bigger centre.] +堤道之間靠得太近;請減少堤道數量或使用更窄的堤道,或者使用更大的中心。 +[Ring world] +環形世界 +[Belt width] +環帶寬度 +[The belt is too short for this many colonies; use a longer map or fewer colonies.] +環帶太短,容納不下這麼多殖民地;請使用更長的地圖或減少殖民地數量。 From 6b2e05e7b69abf344cee449dc3f7d8fd989b71a8 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 16:25:36 -0400 Subject: [PATCH 057/136] Stop computeDistances overrunning its queue on repeated sources computeDistances sized its flat FIFO for one visit per tile but queued every source, repeats included. Isles passes its land-bridge lines as sources, and crossing lines repeat tiles, so each repeat overran the queue and the flood read tile indices back from past its end. The finished map then depended on leftover heap memory: at 256x256, an Isles seed generated after another map differed from the same seed generated first, which the contract test's repeatability check caught. Repeated sources are now skipped; they add nothing to the flood. Isles moves to revision 2: its 128x128 seed 5 changes with this fix. The other callers pass distinct tiles. Every other sampled 128x128 dump (62) and all 45 sampled 256x256 dumps across the 15 generators are unchanged. The contract test now names the generator when repeatability fails. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- src/map/generator/generators/IslesGenerator.cpp | 2 +- src/map/generator/shared/Distances.cpp | 11 +++++++++-- test/MapGeneratorDefaultsTest.cpp | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index 5655539ec..005756f92 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -231,7 +231,7 @@ GeneratorDefinition islesDefinition() return {"isles", 6, "Isles", - 1, + 2, false, {{"island-size", "Island size", 45, 65, 5, 60, ControlGroup::Terrain, false}, {"bridge-width", "Land bridge width", 3, 6, 1, 4, ControlGroup::Terrain, false}}, diff --git a/src/map/generator/shared/Distances.cpp b/src/map/generator/shared/Distances.cpp index 4bd53c536..714b9d56c 100644 --- a/src/map/generator/shared/Distances.cpp +++ b/src/map/generator/shared/Distances.cpp @@ -57,8 +57,15 @@ void computeDistances(Map &map, std::vector &sources, heightmap.resize(mapSize, 0); for (unsigned int i = 0; i < sources.size(); ++i) { - heightmap[sources[i].y * map.getW() + sources[i].x] = 1; - places[tail++] = sources[i].y * map.getW() + sources[i].x; + // Callers can list a tile more than once (Isles' land-bridge lines cross each other). Queueing + // a repeat takes a slot the w*h bound above never counted, so each one overran `places`, and + // the flood then read tile indices back from past its end. A repeated source adds nothing + // to the flood, so skip it. + const int index = sources[i].y * map.getW() + sources[i].x; + if (heightmap[index] == 1) + continue; + heightmap[index] = 1; + places[tail++] = index; } for (unsigned int i = 0; i < obstacles.size(); ++i) { diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 02d3bef48..87517c60c 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -109,6 +109,9 @@ class MapGeneratorDefaultsTest service.generate(other, intervening); Game repeat(nullptr); auto b = service.generate(repeat, request); + if (bool(a) != bool(b) || a.stage != b.stage || hash != fingerprint(repeat)) + std::fprintf(stderr, "Not repeatable after an intervening map: generator %d (%s, %s)\n", + method, a.diagnostic().c_str(), b.diagnostic().c_str()); assert(bool(a) == bool(b) && a.stage == b.stage && hash == fingerprint(repeat)); assert(checksum == repeat.checkSum(nullptr, nullptr, nullptr, true)); assert(request.seed == 22001 && request.options == DWithDefaults(method).options); From c3fa53b02ce50e23e3e2aaad738be3a5e03914b0 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 16:25:36 -0400 Subject: [PATCH 058/136] Default generated maps to 256x256 Width and height now default to 256 in the shared generator controls, so the custom game lobby's random maps, the editor's New Map screen and Reset to defaults all start at 256x256. Saved lobby settings keep their own size. Every playable generator generated at the new default with no failures (10 seeds each), and a lobby preview's five rolls take about 0.1-0.35 s. The contract test's all-water swamp failure is pinned to 128x128, since that swamp fits its colonies on a larger map. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/custom-game-setup/README.md | 3 ++- src/map/generator/core/GeneratorControls.cpp | 4 ++-- test/MapGeneratorDefaultsTest.cpp | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/custom-game-setup/README.md b/docs/custom-game-setup/README.md index 6aa517ae0..91613b375 100644 --- a/docs/custom-game-setup/README.md +++ b/docs/custom-game-setup/README.md @@ -8,7 +8,8 @@ keeps the match summary and launch action available while dense content scrolls. - Start with FourSquares1 and four-player FFA: you plus three Numbi AIs. - Unix map libraries separate installed and user roots. Windows/shared-root installations show one combined library so shipped maps remain accessible. -- Select a premade map or generate a random world. Random previews appear +- Select a premade map or generate a random world. Random maps start at + 256×256 unless saved settings say otherwise. Random previews appear automatically after a 500 ms edit debounce, with generation deferred during a drag or open choice menu. The displayed snapshot is the map that launches. - Expand Terrain, Resources and Layout to tune the applicable generator controls. diff --git a/src/map/generator/core/GeneratorControls.cpp b/src/map/generator/core/GeneratorControls.cpp index b4d45be28..b4196a5ad 100644 --- a/src/map/generator/core/GeneratorControls.cpp +++ b/src/map/generator/core/GeneratorControls.cpp @@ -65,8 +65,8 @@ void GeneratorControl::set(GenerationRequest &r, int value) const const std::vector &sharedGeneratorControls() { static const std::vector controls = { - {"width", "Width", 6, 9, 1, 7, ControlGroup::Shared, true}, - {"height", "Height", 6, 9, 1, 7, ControlGroup::Shared, true}, + {"width", "Width", 6, 9, 1, 8, ControlGroup::Shared, true}, + {"height", "Height", 6, 9, 1, 8, ControlGroup::Shared, true}, {"teams", "Colonies", 1, Team::MAX_COUNT, 1, 4, ControlGroup::Shared}, {"workers", "Starting workers", 1, 8, 1, 4, ControlGroup::Shared}}; return controls; diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 87517c60c..5b5a0bd91 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -158,6 +158,9 @@ class MapGeneratorDefaultsTest invalid.wDec = 31; assert(service.generate(fresh, invalid).error == GenerationError::InvalidRequest); invalid = DWithDefaults(D::eSWAMP); + // An all-water swamp can still fit its colonies on a big map, so pin this failure to 128x128 + // whatever the default size is. + invalid.wDec = invalid.hDec = 7; invalid.options["water"] = 100; invalid.options["grass"] = 0; auto failed = service.generate(fresh, invalid); From 52dcef642ad2eb48815896b93f90f73fbf0bb4e3 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 16:41:58 -0400 Subject: [PATCH 059/136] Add Randomize and Reset to defaults to the custom game Map tab Randomize, under the random map preview, rolls the same settings again with a new seed through the normal preview path; in random mode the preview's height limit leaves a row for it above the footer. Reset to defaults, below the generator fields, returns width, height, colony count and the landscape's own controls to their registered defaults, keeping the landscape and the Game Rules tab's starting workers, and is disabled when nothing differs. Both appear only for random maps. The custom setup harness clicks both through the production controls: Randomize replaces and releases the snapshot with settings and map revision unchanged; Reset restores the registered values and disables itself. Both are captured at 640x480 and 1000x700. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.en.txt | 4 +++ data/texts.keys.txt | 2 ++ docs/custom-game-setup/README.md | 3 +++ src/CustomGameScreen.cpp | 46 +++++++++++++++++++++++++++++--- test/CustomGameSetupHarness.cpp | 46 ++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 3 deletions(-) diff --git a/data/texts.en.txt b/data/texts.en.txt index 0602b9aaf..9266fae60 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1540,6 +1540,10 @@ No current preview Quick clash: 2x speed; generated maps start with 8 workers. [Reroll] Reroll +[Randomize] +Randomize +[Reset to defaults] +Reset to defaults [Ruleset] Ruleset [Select a map from the library.] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index d4bb1eddc..dc0e27484 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -768,6 +768,8 @@ [No current preview] [Quick clash: 2x speed; generated maps start with 8 workers.] [Reroll] +[Randomize] +[Reset to defaults] [Ruleset] [Select a map from the library.] [Starting workers affects generated maps only.] diff --git a/docs/custom-game-setup/README.md b/docs/custom-game-setup/README.md index 91613b375..3bc29bd26 100644 --- a/docs/custom-game-setup/README.md +++ b/docs/custom-game-setup/README.md @@ -12,7 +12,10 @@ keeps the match summary and launch action available while dense content scrolls. 256×256 unless saved settings say otherwise. Random previews appear automatically after a 500 ms edit debounce, with generation deferred during a drag or open choice menu. The displayed snapshot is the map that launches. + Randomize, under the preview, rolls the same settings again with a new seed. - Expand Terrain, Resources and Layout to tune the applicable generator controls. + Reset to defaults, below them, returns width, height, colony count and the + landscape's own controls to their defaults, keeping the landscape itself. Starting workers belong to Game Rules; premade maps retain authored units. - Each colony has a numbered color swatch, controller, AI/difficulty and team. You, AI, shared You + AI, and Closed are explicit choices. Shared control uses diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 9074e06dd..03ebf4d74 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -962,6 +962,28 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) : "Dimensions and colony count are set above.")) + 8; } + // Reset keeps the chosen landscape and the Game Rules tab's starting workers, and returns + // every other field in this column to the landscape's registered defaults. + GenerationRequest defaults; + defaults.setMethodDefaults(g.method); + const bool atDefaults = g.options == defaults.options && g.wDec == defaults.wDec && + g.hDec == defaults.hDec && setup.capacity == defaults.nbTeams; + ui.button( + "generator/reset", {x, yy, std::min(230, leftW - 16), 30}, tr("Reset to defaults"), + [this] + { + GenerationRequest reset; + reset.setMethodDefaults(setup.generator.method); + reset.nbWorkers = setup.generator.nbWorkers; + reset.terrainType = setup.generator.terrainType; + reset.seed = setup.generator.seed; + setup.generator = reset; + setup.setCapacity(reset.nbTeams); + ++setup.mapRevision; + invalidate(); + }, + false, !atDefaults, true); + yy += 38; ui.endRegion(yy - startY + 8); } ui.text(rightX, top, @@ -976,15 +998,17 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) ui.text(rightX, top + 29, dimensions + " / " + std::to_string(setup.capacity) + " " + tr("colonies"), "little", rightW, true); - int size = std::min(rightW, h - 126); + // A random map keeps a row under its preview for the Randomize button. + const int previewLimit = h - (setup.random ? 162 : 126); + int size = std::min(rightW, previewLimit); int previewW = size, previewH = size; if (validMap) { previewW = rightW; previewH = previewW * preview->getLastHeight() / preview->getLastWidth(); - if (previewH > h - 126) + if (previewH > previewLimit) { - previewH = h - 126; + previewH = previewLimit; previewW = previewH * preview->getLastWidth() / preview->getLastHeight(); } } @@ -1004,4 +1028,20 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) ? "Updating map preview..." : "Preview unavailable. Adjust settings or start to retry.")); } + if (setup.random) + { + const int buttonW = std::min(rightW, 160); + ui.button( + "map/randomize", + {rightX + (rightW - buttonW) / 2, py + (validMap ? previewH : size) + 10, buttonW, 30}, + tr("Randomize"), + [this] + { + // Same settings, new seed: generateMap draws a fresh root seed on every run, so this + // only has to ask for the preview now instead of after the edit debounce. + invalidate(); + previewDue = SDL_GetTicks(); + }, + false, setup.validation().empty() && !previewPending); + } } diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index b550e0f9b..f76733830 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -494,6 +494,11 @@ struct CustomGameSetupHarness globalContainer->gfx->printScreen(output + "/" + name + ".bmp"); }; capture("map-640"); + // Randomize and Reset to defaults only apply to random maps. + assert(std::none_of(screen.controls->hits.begin(), screen.controls->hits.end(), + [](const auto &h) { + return h.id == "map/randomize" || h.id == "generator/reset"; + })); screen.activateGroup(screen.groups[1]); capture("players-640"); screen.setup.colonies[1].ai = AI::CORTEX; @@ -551,6 +556,47 @@ struct CustomGameSetupHarness assert(screen.generateMap()); assert(screen.snapshot != first && !std::filesystem::exists(first)); + // Randomize rolls the same settings again with a new seed, through the + // normal preview path, and releases the snapshot it replaces. + { + const auto settings = screen.setup.generator; + const auto mapRevision = screen.setup.mapRevision; + const auto replaced = screen.snapshot; + clickControl("map/randomize"); + assert(!screen.validMap && screen.previewPending); + screen.onTimer(screen.previewDue); + assert(screen.validMap && screen.snapshot != replaced && + !std::filesystem::exists(replaced)); + assert(screen.setup.generator.method == settings.method && + screen.setup.generator.options == settings.options && + screen.setup.mapRevision == mapRevision); + capture("randomize-640"); + } + // Reset to defaults restores this landscape's registered values, keeping + // the landscape and the Game Rules tab's starting workers. + { + GenerationRequest expected; + expected.setMethodDefaults(screen.setup.generator.method); + const auto method = screen.setup.generator.method; + const auto workers = screen.setup.generator.nbWorkers; + assert(screen.setup.generator.options != expected.options); + clickControl("generator/reset"); + assert(screen.setup.generator.method == method && + screen.setup.generator.nbWorkers == workers && + screen.setup.generator.options == expected.options && + screen.setup.generator.wDec == expected.wDec && + screen.setup.generator.hDec == expected.hDec && + screen.setup.capacity == expected.nbTeams && !screen.validMap && + screen.previewPending); + paint(); + const auto reset = std::find_if( + screen.controls->hits.begin(), screen.controls->hits.end(), + [](const auto &h) { return h.id == "generator/reset"; }); + assert(reset != screen.controls->hits.end() && !reset->enabled); + screen.onTimer(screen.previewDue); + assert(screen.validMap); + capture("reset-640"); + } screen.setup.presetRules(1); screen.invalidate(); assert(!screen.validMap); From d010554d614fddebf862ca1939ae55700d6380b5 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:30:36 -0400 Subject: [PATCH 060/136] Add a map fairness tournament harness Measure whether generated maps favour start positions in real games, not only by the generator's start-quality score. - MapGeneratorStudy: candidates=N picks the lobby's best-of-N roll; save= rotations=N writes playable maps with the generator's colony t relabelled as team (t + r) mod N. Each rotation is re-checked from its saved bytes, and N rotations must reproduce rotation 0 exactly. - Headless engine: GLOB2_TEAM_RESULTS prints one GLOB2_TEAM_RESULT line per team (outcome, elimination tick, start, prestige, units, buildings); GLOB2_TEST_MAX_TICKS overrides the -test-games-nox 90,000-tick cap. Both only read state or decide when the driver stops; neither changes how a tick is simulated, and default output is unchanged. - tools/map_fairness_tournament.py with smoke and standard presets, and docs/map-generators/FAIRNESS_TOURNAMENT.md (cost section still to be measured). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/headless-replays.md | 26 +- docs/map-generators/FAIRNESS_TOURNAMENT.md | 157 +++ docs/map-generators/README.md | 2 + src/Engine.h | 11 + src/EngineRun.cpp | 70 ++ src/Glob2.cpp | 7 +- test/MapGeneratorStudy.cpp | 297 ++++- tools/map-fairness/smoke.json | 15 + tools/map-fairness/standard.json | 15 + tools/map_fairness_tournament.py | 1130 ++++++++++++++++++++ 10 files changed, 1725 insertions(+), 5 deletions(-) create mode 100644 docs/map-generators/FAIRNESS_TOURNAMENT.md create mode 100644 tools/map-fairness/smoke.json create mode 100644 tools/map-fairness/standard.json create mode 100644 tools/map_fairness_tournament.py diff --git a/docs/headless-replays.md b/docs/headless-replays.md index f802fe55e..212946708 100644 --- a/docs/headless-replays.md +++ b/docs/headless-replays.md @@ -20,12 +20,13 @@ To create a `.game` file with specific AI players: start the game with GUI, set ### `-test-games-nox [count]` -Runs random AI-vs-AI games headlessly. Each game auto-ends at 90,000 ticks (~60 minutes of game time at 25 ticks/sec). An optional `count` parameter controls how many games to run (default: infinite). +Runs random AI-vs-AI games headlessly. Each game auto-ends at 90,000 ticks (~60 minutes of game time at 25 ticks/sec), or after `GLOB2_TEST_MAX_TICKS` ticks when that environment variable is a positive integer; a game stopped at the cap reports `winner_team=-1`. The cap only decides when the driver stops the game, not how ticks are simulated. An optional `count` parameter controls how many games to run (default: infinite). ```bash ./glob2 -test-games-nox 1 # run one game and exit ./glob2 -test-games-nox 5 # run five games and exit ./glob2 -test-games-nox # run forever (kill with Ctrl+C) +GLOB2_TEST_MAX_TICKS=30000 ./glob2 -test-games-nox 1 # stop at 30,000 ticks ``` The random game setup (`Engine::createRandomGame`) creates one local player + N AI players with randomly chosen AI types from the map's team count. @@ -171,6 +172,29 @@ GLOB2_GAME_END ticks=2483 winner_team=1 seed=1777219846 map="Playground" orders= The format is intended for grep/regex consumption — fields are space-separated key=value, with `map` quoted. +### Per-team results (`GLOB2_TEAM_RESULTS`) + +With `GLOB2_TEAM_RESULTS` set, the engine also prints one line per team +after `GLOB2_GAME_END`: + +``` +GLOB2_TEAM_RESULT team=0 result=undecided alive=1 eliminated_tick=-1 start=17,29 prestige=0 units=4 workers=4 explorers=0 warriors=0 buildings=1 sites=1 +``` + +- `result` — `won`, `lost` or `undecided`: the team's win-condition + state when the game stopped (`undecided` at a tick cap) +- `alive` — `Team::isAlive` at the end +- `eliminated_tick` — the tick at which `isAlive` was cleared, counted + like `ticks`; `-1` for a team still alive +- `start` — the team's start position from the map +- `prestige` — final prestige; `units`, `workers`, `explorers` and + `warriors` count live units; `buildings` counts finished buildings and + `sites` building sites (virtual flags excluded) + +The lines only read game state. `tools/map_fairness_tournament.py` uses +them together with `GLOB2_TEST_MAX_TICKS`; see +[Map fairness tournament](map-generators/FAIRNESS_TOURNAMENT.md). + For cross-codebase testing, the canonical baselines live in `glob2/tests/baselines/`. See [`docs/replay-verification.md`](../../docs/replay-verification.md) at the workspace root for the full verification workflows and the regeneration procedure. The `ReplayWriter` records live during gameplay: diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md new file mode 100644 index 000000000..652b16366 --- /dev/null +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -0,0 +1,157 @@ +# Map fairness tournament + +A generator's start-quality score (`MapGeneration::scoreStarts`) says how evenly a map shares +out wood, wheat, room and distance to rivals. It does not say who wins. The fairness tournament +measures that directly: it plays free-for-all games on generated maps with the same AI in every +slot and asks whether some start positions keep winning, and by how much. + +## Running it + +Build the optimized client and the study tool first: + +```bash +scons release=1 -j3 +scons release=1 -j3 map-generator-study +``` + +Then run a preset: + +```bash +python3 tools/map_fairness_tournament.py run smoke # quick end-to-end check +python3 tools/map_fairness_tournament.py run standard # the standard measurement +``` + +Results go to `artifacts/map-fairness//` (ignored by Git). A run resumes where it stopped: +finished maps and games are kept, so re-running the same command after an interruption only plays +what is missing. `--fresh` discards the run's earlier outputs. A changed configuration refuses to +mix into an old directory unless `--fresh`, `--name` or `--out` is given. `summarize ` recomputes the report from the stored results. + +Presets live in `tools/map-fairness/*.json`. Most keys can also be set on the command line +(`--generators 15,9 --map-seeds 1001-1004 --games-per-rotation 2 --size 128 --colonies 4 +--tick-cap 90000 --jobs 3 --verify 2`): + +| Key | Meaning | +| --- | --- | +| `generators` | Generator ids; `controls` (`{"": {"control": value}}`) overrides their defaults. | +| `map_seeds` | Map seeds, e.g. `"2001-2012"`. One map per generator and seed. | +| `candidates` | Rolls per map. The lobby keeps the best-scoring of 5 rolls derived from its seed, so the default reproduces the maps players get. `0` uses the seed as is. | +| `size`, `colonies`, `workers` | Map side in tiles (a power of two), colonies, starting workers (generator default when absent). | +| `rotations` | `"all"` plays every cyclic rotation of team numbers over starts (see below). | +| `games_per_rotation` | Engine seeds per rotation. Games per map = rotations x this. | +| `ai` | The AI in every slot (default `nicowar`). | +| `tick_cap` | Ticks after which an unfinished game is stopped and adjudicated. | +| `jobs` | Concurrent processes. Results do not depend on it. | +| `verify_games` | Finished games re-run from scratch at the end; the report says whether they matched. | + +Everything is seeded, so the same preset reproduces the same maps, games and report. + +## What a run does + +1. **Map production.** `MapGeneratorStudy w= h= teams= quality candidates=5 + save= rotations=N` generates the map through `GenerationService`, choosing the roll + exactly as `CustomGameScreen::generateMap()` does, and saves it as a playable `.map`. Next to the + map files, `map.json` records the generator id and revision, the request (size, colonies, + workers, every control value), the map seed and chosen roll, the start of every colony, the + start-quality report and the Git revision. +2. **Rotations.** The same file is written once per rotation `r`, with the generator's colony `t` + relabelled as team `(t + r) mod N`. Terrain and every colony object keep their exact state; only + the team number each object answers to changes (unit and building ids, per-tile team masks, team + masks). The tool checks every rotation from its saved bytes: the terrain is unchanged, each + colony sits under its new team number, every tile reference resolves, and relabelling N times + reproduces rotation 0 byte for byte. In the games, each team's reported start must match too. +3. **Matches.** Each rotation is played `games_per_rotation` times with + `glob2 -test-games-nox 1 --map --matchup nicowar,...`, with the engine seed pinned by + `GLOB2_TEST_SEED` and nothing else varied. Every game runs in its own `GLOB2_USER_DIR`, which + holds the map, so it never touches `~/.glob2`. Winning conditions are the lobby's free-for-all + defaults, prestige victory included. `GLOB2_TEAM_RESULTS=1` makes the engine print one + `GLOB2_TEAM_RESULT` line per team (outcome, elimination tick, start, prestige, units, buildings), + and `GLOB2_TEST_MAX_TICKS` sets the tick cap. See [headless replays](../headless-replays.md). + +Per game, `games.csv` has the winner's start and team, the winning start's coordinates, how the +game ended (`elimination`, `prestige` or `cap`), elimination order with ticks, placements, wall time +and the checksum at the cap. `colonies.csv` has one row per colony per game with its final state and +start-quality factors; `maps.csv` has one row per map. + +## Separating the start from the team index + +In a headless game the team index is not neutral. Teams step in index order, the AIs issue orders +in player order, and `-test-games-nox` adds a passive local player on team 0, whose AI therefore +polls last. On a single map, start `t` is always team `t`, so a start that wins more often cannot be +told apart from a team index that does. + +Rotations break that link. Over the N rotations every team index plays every start exactly once, so: + +- **wins by start**, pooled over rotations, carry no team-index advantage, because each start had + every index equally often; and +- **wins by team index**, pooled over rotations, carry no start advantage. + +Both tallies come from the same games. Rotation balancing makes each test conservative when the +other effect is present: it removes that effect's mean and slightly lowers the variance. + +Symmetric arena (generator 15) is the baseline. Its colonies have exactly the same ground by +construction, so its wins by start should be uniform, and its wins by team index show the engine's +own team-index bias with no map effect at all. + +## Adjudication + +A game ends when one colony has defeated every other, when total prestige reaches the lobby +threshold (the highest-prestige colony wins), or at the tick cap. At the cap, the surviving colony +with the most prestige wins, then the one with more finished buildings, then more units. That is +the engine's own prestige victory applied to an unfinished game. An exact tie stays unresolved and +is left out of win counts. The report gives the share of games decided at the cap. Placements rank +the winner first, other survivors by prestige, then eliminated colonies, the last eliminated first. + +## Reading the summary + +`summary.md` (and `summary.json` with every number) has a headline row per generator: + +- **Position bias (pp)** is the headline unfairness number. It is the root-mean-square gap between + each start's true win probability and the fair `1/N`, in percentage points, averaged over the + generator's maps. Raw win shares scatter around `1/N` even on a perfectly fair map, so the + estimate is corrected for chance: with `n` decided games and chi-square statistic `X` against + uniform, `(X - (N - 1)) / ((n - 1) N)` is an unbiased estimate of `sum_s (p_s - 1/N)^2`. Averaged + over maps, clipped at zero and converted to an RMS, it reads 0 for fair maps. With four colonies, + one start winning 40% and the others 20% is 8.7 pp. The bracket is a 95% bootstrap interval over + maps, so it covers both map-to-map variation and the game-to-game noise within each map. +- **Biased maps** counts maps whose wins by start reject a uniform split at p < 0.05, raw and after + Benjamini-Hochberg across that generator's maps (false discovery rate 5%). "Chance" is how many + raw rejections a generator with only fair maps would expect. Holm-adjusted counts are in the + per-generator section. The p-value is an exact multinomial test (probability of an outcome no + more likely than the observed one under a uniform split), or a seeded Monte Carlo version when + there are too many outcomes to enumerate. +- **Best start / fair** is the most-winning start's share times `N`, as a median over maps: 1 is + fair, 2 means that start won twice its fair share. Picking the best start inflates this on small + samples, so read it next to the p-value and the position bias. Per map, the table also gives that + start's share with a Wilson 95% interval. +- **Any bias p** tests whether any of the generator's maps is biased: the sum of the per-map + chi-square statistics, calibrated by simulating every map's winners under a fair split. +- **Scorer rho** is the rank correlation between a colony's start-quality total and its win share + on the same map, pooled over maps after centring both within each map, with a bootstrap interval + over maps. Near +1, the scorer ranks starts the way games do. Near 0, it does not predict who + wins. The per-generator section adds the correlation with placement (which uses every colony's + finish, not just the winner's), per scoring factor, and how often the start the scorer rated best + actually won. Symmetric arena scores every colony alike, so it has no scorer check. + +The engine team-index table reports wins by team index for the baseline and for all generators +pooled, with an exact test and Wilson intervals. + +Per map, the generator section lists wins by start, p and BH q values, the best start's share, the +bias estimate and the start-quality score of each start. Two pooled lines follow: wins by generator +colony index (does the generator favour a colony it places early or late?) and wins by team index. + +## Cost + + + +## Limits + +- The AI is the measuring instrument. A map that is fair for Nicowar can still be unfair for a + person, and Nicowar's own habits (for example how it expands over water) shape what counts as a + good start. +- Few games per map give wide intervals. Per-map tests only detect large effects; the generator + headline, pooled over maps, is the number to act on. +- Cyclic rotations separate the start and team-index main effects, not every interaction between + them. +- Games stopped at the cap are judged on prestige, which rewards building over military position. + Check the cap share before reading much into a generator whose games mostly hit it. diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index db114f328..b17452ecc 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -9,6 +9,8 @@ Normal lobby generation rolls a fresh random map; the preview snapshot is the ma Build the study executable with `scons release=1 map-generator-study`. `build/src/MapGeneratorStudy --catalog` exports the current control definitions. Use `python3 tools/map_generator_study.py --help` for seeded generation studies. +Use `python3 tools/map_fairness_tournament.py run smoke` to measure whether generated maps favour +some start positions in real games; see [Map fairness tournament](FAIRNESS_TOURNAMENT.md). Study results and screenshots belong in ignored `artifacts/`, not in this directory. Record investigation findings and validation results in the relevant pull request. diff --git a/src/Engine.h b/src/Engine.h index d69cd44eb..0e0f378a6 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "Campaign.h" #include "MapHeader.h" #include "GameHeader.h" @@ -186,6 +187,14 @@ class Engine /// AIs' trajectories after a single headless game. void printTeamTimeline(); + /// Remember the tick at which each team was eliminated (Team::isAlive cleared). Called from + /// pollAutomaticEndingConditions under automaticEndingGame; only reads game state. + void trackTeamEliminations(); + + /// One GLOB2_TEAM_RESULT line per team: outcome, elimination tick, start position and final + /// prestige and forces. Gated by GLOB2_TEAM_RESULTS; tools/map_fairness_tournament.py scrapes it. + void printTeamResults(); + /// Tell the YOG multiplayer session how this match ended (won, lost, /// quit). Caller checks `multiplayer` is non-null. void reportMultiplayerResult(); @@ -212,6 +221,8 @@ class Engine shared_ptr multiplayer; Uint64 automaticGameStartTick, automaticGameEndTick; + //! Tick at which each team was eliminated, -1 while alive (see trackTeamEliminations). + std::vector teamEliminatedTick; static const bool verbose = false; }; diff --git a/src/EngineRun.cpp b/src/EngineRun.cpp index 3a735247e..7e0dca5a2 100644 --- a/src/EngineRun.cpp +++ b/src/EngineRun.cpp @@ -17,6 +17,7 @@ #include "team/Team.h" #include "TeamStat.h" #include "building/IntBuildingType.h" +#include "unit/Unit.h" #include "unit/UnitConsts.h" #include @@ -61,6 +62,8 @@ void Engine::pollAutomaticEndingConditions() if (!globalContainer->automaticEndingGame) return; + trackTeamEliminations(); + auto endGame = [this](const char* reason) { printf("nox::%s\n", reason); gui.isRunning = false; @@ -318,6 +321,72 @@ void Engine::printAutomaticEndingSummary() // compared side by side after a single game. if (getenv("GLOB2_TEAM_TIMELINE")) printTeamTimeline(); + + // Optional per-team outcome lines for tooling (tools/map_fairness_tournament.py): who won or + // lost, when each team was eliminated, where it started and what it had left. Reads state + // only, and is gated by GLOB2_TEAM_RESULTS so normal headless output is unchanged. + if (getenv("GLOB2_TEAM_RESULTS")) + printTeamResults(); +} + +void Engine::trackTeamEliminations() +{ + const int nbTeams = gui.game.mapHeader.getNumberOfTeams(); + if ((int)teamEliminatedTick.size() != nbTeams) + teamEliminatedTick.assign(nbTeams, -1); + for (int t = 0; t < nbTeams; t++) + { + const Team* team = gui.game.teams[t]; + // stepCounter already counts the step that cleared isAlive, like GLOB2_GAME_END's ticks. + if (team && !team->isAlive && teamEliminatedTick[t] < 0) + teamEliminatedTick[t] = (Sint32)gui.game.stepCounter; + } +} + +void Engine::printTeamResults() +{ + trackTeamEliminations(); + Game& game = gui.game; + for (int t = 0; t < game.mapHeader.getNumberOfTeams(); t++) + { + const Team* team = game.teams[t]; + if (!team) + continue; + int units = 0, workers = 0, explorers = 0, warriors = 0, buildings = 0, sites = 0; + for (int i = 0; i < Unit::MAX_COUNT; i++) + { + const Unit* unit = team->myUnits[i]; + if (!unit) + continue; + units++; + workers += unit->typeNum == WORKER; + explorers += unit->typeNum == EXPLORER; + warriors += unit->typeNum == WARRIOR; + } + for (int i = 0; i < Building::MAX_COUNT; i++) + { + const Building* building = team->myBuildings[i]; + if (!building || building->type->isVirtual) + continue; + if (building->type->isBuildingSite) + sites++; + else + buildings++; + } + std::cout << "GLOB2_TEAM_RESULT team=" << t + << " result=" << (team->hasWon ? "won" : team->hasLost ? "lost" : "undecided") + << " alive=" << (team->isAlive ? 1 : 0) + << " eliminated_tick=" << teamEliminatedTick[t] + << " start=" << team->startPosX << "," << team->startPosY + << " prestige=" << team->prestige + << " units=" << units + << " workers=" << workers + << " explorers=" << explorers + << " warriors=" << warriors + << " buildings=" << buildings + << " sites=" << sites + << std::endl; + } } // Per-team timeline dump (see GLOB2_TEAM_TIMELINE in printAutomaticEndingSummary). @@ -501,6 +570,7 @@ void Engine::runOneGameSession(bool& doRunOnceAgain) st.needToBeTime = 0; st.startTime = SDL_GetTicks64(); st.frameNumber = 0; + teamEliminatedTick.clear(); while (gui.isRunning) { diff --git a/src/Glob2.cpp b/src/Glob2.cpp index 05c64297f..ee42b9c16 100644 --- a/src/Glob2.cpp +++ b/src/Glob2.cpp @@ -131,7 +131,12 @@ int Glob2::runNoX() int Glob2::runTestGames() { - globalContainer->automaticEndingSteps=90000; + // GLOB2_TEST_MAX_TICKS overrides the 90,000-tick cap for tooling that + // trades game length for throughput (tools/map_fairness_tournament.py). + // The cap only decides when the driver stops the game; it never changes + // how a tick is simulated. + const char* envMaxTicks = getenv("GLOB2_TEST_MAX_TICKS"); + globalContainer->automaticEndingSteps = (envMaxTicks && atoi(envMaxTicks) > 0) ? atoi(envMaxTicks) : 90000; int maxRuns = globalContainer->runTestGamesCount; int run = 0; while(maxRuns == 0 || run < maxRuns) diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 577654b18..61309f225 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -11,18 +11,290 @@ #include "Unit.h" #include "StartQuality.h" #include "Utilities.h" +#include +#include #include #include #include #include #include #include +#include #include #include #include #include GlobalContainer *globalContainer = nullptr; + +namespace +{ +constexpr std::uint64_t kFnvOffset = 14695981039346656037ULL; +std::uint64_t fnv(std::uint64_t hash, std::uint64_t value) +{ + hash ^= value; + return hash * 1099511628211ULL; +} +std::uint64_t fnvBytes(const std::string &bytes) +{ + std::uint64_t hash = kFnvOffset; + for (unsigned char c : bytes) + hash = fnv(hash, c); + return hash; +} + +// A playable map file's bytes, saved the way CustomGameScreen::generateMap() saves the lobby's +// generated map. +std::string saveMapBytes(Game &game, const std::string &name) +{ + auto *backend = new GAGCore::MemoryStreamBackend(); + GAGCore::BinaryOutputStream stream(backend); // owns the backend + game.save(&stream, true, name); + stream.flush(); + backend->seekFromEnd(0); + return std::string(backend->getBuffer(), backend->getPosition()); +} + +bool loadMapBytes(Game &game, const std::string &bytes) +{ + // The backend's constructor copies the bytes in by writing them, which leaves it at the end. + auto *backend = new GAGCore::MemoryStreamBackend(bytes.data(), bytes.size()); + backend->seekFromStart(0); + GAGCore::BinaryInputStream stream(backend); // owns the backend + return game.load(&stream); +} + +// Everything on the map that belongs to no colony. +std::uint64_t worldHash(const Game &game) +{ + std::uint64_t hash = kFnvOffset; + for (const auto &tile : game.map.tiles) + { + hash = fnv(hash, tile.terrain); + hash = fnv(hash, tile.resource.getUint32()); + hash = fnv(hash, tile.fertility); + hash = fnv(hash, tile.canResourcesGrow); + } + return hash; +} + +// One colony described without its team number: its start and every object it owns, with that +// object's state. Equal signatures mean the same colony, whichever team it plays as. +std::uint64_t colonySignature(const Game &game, int team) +{ + const Team *colony = game.teams[team]; + std::uint64_t hash = kFnvOffset; + for (std::int64_t v : {std::int64_t(colony->startPosX), std::int64_t(colony->startPosY), + std::int64_t(colony->startPosSet)}) + hash = fnv(hash, std::uint64_t(v)); + for (int i = 0; i < Unit::MAX_COUNT; ++i) + if (const Unit *u = colony->myUnits[i]) + for (std::int64_t v : {std::int64_t(i), std::int64_t(Unit::GIDtoID(u->gid)), + std::int64_t(u->typeNum), std::int64_t(u->posX), + std::int64_t(u->posY), std::int64_t(u->hp), + std::int64_t(u->hungry), std::int64_t(u->direction)}) + hash = fnv(hash, std::uint64_t(v)); + for (int i = 0; i < Building::MAX_COUNT; ++i) + if (const Building *b = colony->myBuildings[i]) + for (std::int64_t v : {std::int64_t(i), std::int64_t(Building::GIDtoID(b->gid)), + std::int64_t(b->typeNum), std::int64_t(b->posX), + std::int64_t(b->posY), std::int64_t(b->hp)}) + hash = fnv(hash, std::uint64_t(v)); + for (unsigned r = 0; r < MAX_NB_RESOURCES; ++r) + hash = fnv(hash, colony->teamResources[r]); + return hash; +} + +// Every unit or building a tile names must be the one that team keeps under that id, and every +// team must answer to its own index. +bool mapReferencesConsistent(const Game &game) +{ + const auto &map = game.map; + const int teams = game.mapHeader.getNumberOfTeams(); + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + { + for (Uint16 gid : {map.getGroundUnit(x, y), map.getAirUnit(x, y)}) + { + if (gid == NOGUID) + continue; + const int t = Unit::GIDtoTeam(gid); + const Unit *u = t < teams ? game.teams[t]->myUnits[Unit::GIDtoID(gid)] : nullptr; + if (!u || u->gid != gid || u->posX != x || u->posY != y) + return false; + } + const Uint16 gbid = map.getBuilding(x, y); + if (gbid == NOGBID) + continue; + const int t = Building::GIDtoTeam(gbid); + const Building *b = + t < teams ? game.teams[t]->myBuildings[Building::GIDtoID(gbid)] : nullptr; + if (!b || b->gid != gbid) + return false; + } + for (int t = 0; t < teams; ++t) + if (game.teams[t]->teamNumber != t || game.teams[t]->me != Team::teamNumberToMask(t)) + return false; + return true; +} + +// Relabels colonies so the colony that was team t becomes team (t + shift) mod N, without +// touching terrain or any colony object's state: only the team number each one answers to +// changes. That lets a fairness tournament rotate colony-to-start assignments on one generated +// map, separating the start position from the team index the engine processes it under. +// +// A team slot keeps what belongs to the seat rather than the colony (colour, controller +// bookkeeping), so a rotated map looks like any other. This only runs on a map freshly loaded at +// tick 0 that is saved straight afterwards; runtime caches a map file does not carry (gradients, +// explored areas, clearing claims) are never simulated from and are left alone. +void shiftTeams(Game &game, int shift) +{ + const int n = game.mapHeader.getNumberOfTeams(); + assert(n > 0 && n < 32 && game.stepCounter == 0 && game.gameHeader.getNumberOfPlayers() == 0); + shift = ((shift % n) + n) % n; + auto to = [n, shift](int t) { return (t + shift) % n; }; + const Uint32 low = (Uint32(1) << n) - 1; + auto mask = [&](Uint32 bits) + { + Uint32 out = bits & ~low; + for (int t = 0; t < n; ++t) + if (bits & (Uint32(1) << t)) + out |= Uint32(1) << to(t); + return out; + }; + auto unitGid = [&](Uint16 gid) + { + return gid == NOGUID ? gid + : Uint16(Unit::GIDfrom(Unit::GIDtoID(gid), to(Unit::GIDtoTeam(gid)))); + }; + auto buildingGid = [&](Uint16 gid) + { + return gid == NOGBID + ? gid + : Uint16(Building::GIDfrom(Building::GIDtoID(gid), to(Building::GIDtoTeam(gid)))); + }; + struct Seat + { + BaseTeam::TeamType type; + Sint32 numberOfPlayer; + GAGCore::Color color; + Uint32 playersMask; + }; + std::vector seats; + const std::vector colonies(game.teams, game.teams + n); + for (const Team *team : colonies) + seats.push_back({team->type, team->numberOfPlayer, team->color, team->playersMask}); + for (int t = 0; t < n; ++t) + game.teams[to(t)] = colonies[t]; + for (int t = 0; t < n; ++t) + { + Team &team = *game.teams[t]; + team.teamNumber = t; + team.type = seats[t].type; + team.numberOfPlayer = seats[t].numberOfPlayer; + team.color = seats[t].color; + team.playersMask = seats[t].playersMask; + for (Uint32 *bits : {&team.me, &team.allies, &team.enemies, &team.sharedVisionExchange, + &team.sharedVisionFood, &team.sharedVisionOther}) + *bits = mask(*bits); + for (int i = 0; i < Unit::MAX_COUNT; ++i) + if (Unit *unit = team.myUnits[i]) + unit->gid = unitGid(unit->gid); + for (int i = 0; i < Building::MAX_COUNT; ++i) + if (Building *building = team.myBuildings[i]) + { + building->gid = buildingGid(building->gid); + building->seenByMask = mask(building->seenByMask); + } + } + for (auto &tile : game.map.tiles) + { + tile.building = buildingGid(tile.building); + tile.groundUnit = unitGid(tile.groundUnit); + tile.airUnit = unitGid(tile.airUnit); + tile.forbidden = mask(tile.forbidden); + tile.guardArea = mask(tile.guardArea); + tile.clearArea = mask(tile.clearArea); + } + for (auto &bits : game.map.mapDiscovered) + bits = mask(bits); +} + +// Writes -r.map for k = 0..rotations-1, where colony t (the generator's team t, which +// the START and COLONY lines describe) plays as team (t + k) mod N. Each rotation is checked from +// its saved bytes, and going all the way round must reproduce rotation 0 byte for byte. +int saveRotatedMaps(Game &game, const GenerationResult &result, const GenerationRequest &request, + const std::string &prefix, const std::string &name, int rotations) +{ + if (!result) + return 4; + const int n = game.mapHeader.getNumberOfTeams(); + if (n < 1 || n >= 32 || rotations < 1 || rotations > n) + return 2; + std::printf("REQUEST,%s,%u,%u,%d,%d,%d,%d\n", result.generatorId.c_str(), result.revision, + result.seed, request.wDec, request.hDec, request.nbTeams, request.nbWorkers); + for (const auto &option : request.options) + std::printf("OPTION,%s,%d\n", option.first.c_str(), option.second); + for (int t = 0; t < n; ++t) + std::printf("START,%d,%d,%d\n", t, game.teams[t]->startPosX, game.teams[t]->startPosY); + + // No players: a headless driver assigns every colony's controller when it launches the map. + GameHeader header; + header.setRandomSeed(result.seed); + game.setGameHeader(header); + auto label = [&](int k) { return name + "-r" + std::to_string(k); }; + const std::string generated = saveMapBytes(game, label(0)); + + Game canonical(nullptr); + if (!loadMapBytes(canonical, generated)) + return 5; + const std::string first = saveMapBytes(canonical, label(0)); + const bool stable = first == generated; + const std::uint64_t world = worldHash(canonical); + std::vector colonies; + for (int t = 0; t < n; ++t) + colonies.push_back(colonySignature(canonical, t)); + bool consistent = mapReferencesConsistent(canonical), placed = true, roundTrip = false; + + auto write = [&](int k, const std::string &bytes) + { + const std::string path = prefix + "-r" + std::to_string(k) + ".map"; + std::ofstream out(path, std::ios::binary); + out.write(bytes.data(), std::streamsize(bytes.size())); + if (!out) + return false; + std::printf("MAPFILE,%d,%s,%zu,%llu\n", k, path.c_str(), bytes.size(), + (unsigned long long)fnvBytes(bytes)); + return true; + }; + if (!write(0, first)) + return 3; + std::string previous = first; + for (int k = 1; k <= n; ++k) + { + Game shifted(nullptr); + if (!loadMapBytes(shifted, previous)) + return 5; + shiftTeams(shifted, 1); + const std::string bytes = saveMapBytes(shifted, label(k % n)); + Game check(nullptr); + if (!loadMapBytes(check, bytes)) + return 5; + consistent = consistent && mapReferencesConsistent(check); + placed = placed && worldHash(check) == world; + for (int t = 0; t < n; ++t) + placed = placed && colonySignature(check, (t + k) % n) == colonies[t]; + if (k == n) + roundTrip = bytes == first; + else if (k < rotations && !write(k, bytes)) + return 3; + previous = bytes; + } + std::printf("ROTATIONS,%d,%d,%d,%d,%d,%d\n", n, rotations, int(stable), int(consistent), + int(placed), int(roundTrip)); + return consistent && placed && roundTrip ? 0 : 6; +} +} // namespace int main(int argc, char **argv) { if (argc == 2 && std::string(argv[1]) == "--catalog") @@ -82,7 +354,8 @@ int main(int argc, char **argv) bool tuning = false; bool headroom = false; bool quality = false; - std::string dump; + std::string dump, savePrefix, mapName; + int candidates = 0, rotations = 1; std::map aliases = {{"smooth", "smoothing"}, {"craters", "lake-density"}, {"extra", "extra-islands"}, @@ -112,9 +385,14 @@ int main(int argc, char **argv) if (eq == std::string::npos) return 2; std::string id = arg.substr(0, eq); - if (id == "dump") + if (id == "dump" || id == "save" || id == "name") { - dump = arg.substr(eq + 1); + (id == "dump" ? dump : id == "save" ? savePrefix : mapName) = arg.substr(eq + 1); + continue; + } + if (id == "candidates" || id == "rotations") + { + (id == "candidates" ? candidates : rotations) = std::stoi(arg.substr(eq + 1)); continue; } if (aliases.count(id)) @@ -132,6 +410,13 @@ int main(int argc, char **argv) descriptor.options[id] = value; // Service validates; never silently clamp studies. } } + if (candidates > 0) + { + // The lobby's roll: the best-scoring of `candidates` seeds derived from the root seed + // (CustomGameScreen::generateMap), so studied maps are the maps players actually get. + descriptor.seed = GenerationService().bestSeed(descriptor, seed, candidates); + std::printf("SAMPLED,%u,%u,%d\n", seed, descriptor.seed, candidates); + } const auto start = std::chrono::steady_clock::now(); const auto result = GenerationService().generate(game, descriptor); const bool success = bool(result); @@ -461,5 +746,11 @@ int main(int argc, char **argv) } std::fclose(f); } + if (!savePrefix.empty()) + return saveRotatedMaps(game, result, descriptor, savePrefix, + mapName.empty() ? "study-" + std::to_string(method) + "-" + + std::to_string(seed) + : mapName, + rotations); return 0; } diff --git a/tools/map-fairness/smoke.json b/tools/map-fairness/smoke.json new file mode 100644 index 000000000..bfd96f0e0 --- /dev/null +++ b/tools/map-fairness/smoke.json @@ -0,0 +1,15 @@ +{ + "name": "smoke", + "description": "Quick check that the tournament runs end to end: the Symmetric arena baseline and three other generators, two maps each, every rotation once.", + "generators": [15, 9, 13, 4], + "map_seeds": "1001-1002", + "games_per_rotation": 1, + "rotations": "all", + "size": 128, + "colonies": 4, + "candidates": 5, + "ai": "nicowar", + "tick_cap": 90000, + "jobs": 3, + "verify_games": 2 +} diff --git a/tools/map-fairness/standard.json b/tools/map-fairness/standard.json new file mode 100644 index 000000000..b1a0249af --- /dev/null +++ b/tools/map-fairness/standard.json @@ -0,0 +1,15 @@ +{ + "name": "standard", + "description": "Standard fairness tournament: every playable generator, twelve maps each, every rotation played by three engine seeds.", + "generators": [15, 9, 11, 12, 7, 6, 13, 14, 16, 8, 5, 4, 3, 1, 2], + "map_seeds": "2001-2012", + "games_per_rotation": 3, + "rotations": "all", + "size": 128, + "colonies": 4, + "candidates": 5, + "ai": "nicowar", + "tick_cap": 90000, + "jobs": 3, + "verify_games": 4 +} diff --git a/tools/map_fairness_tournament.py b/tools/map_fairness_tournament.py new file mode 100644 index 000000000..ea92bb418 --- /dev/null +++ b/tools/map_fairness_tournament.py @@ -0,0 +1,1130 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-or-later +"""Map fairness tournament for Globulation 2's random map generators. + +Asks whether generated maps are fair in real games, not only by the generator's own +start-quality score. For every generator and map seed the tournament: + +1. generates one playable map the way the custom-game lobby does (the best-scoring of five + rolls derived from the map seed) and writes one copy per cyclic rotation, in which the + generator's colony t plays as team (t + r) mod N (MapGeneratorStudy save= rotations=); +2. plays free-for-all games with the same AI in every slot on every rotation, differing only in + the engine seed (GLOB2_TEST_SEED), headlessly through glob2 -test-games-nox; +3. tallies wins by start position and, separately, by team index. Every team index plays every + start equally often, so a start that keeps winning is the map's doing and a team index that + keeps winning is the engine's own processing-order bias. + +See docs/map-generators/FAIRNESS_TOURNAMENT.md for the metrics and how to read the report. + + python3 tools/map_fairness_tournament.py run smoke + python3 tools/map_fairness_tournament.py run standard --jobs 3 + python3 tools/map_fairness_tournament.py summarize artifacts/map-fairness/smoke +""" +import argparse +import concurrent.futures +import csv +import datetime +import json +import math +import os +import random +import re +import shutil +import statistics +import subprocess +import sys +import threading +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PRESET_DIR = ROOT / 'tools' / 'map-fairness' +BASELINE_METHOD = 15 # Symmetric arena: exactly symmetric starts, the fairness baseline. +Z95 = 1.959963984540054 +FACTORS = ['wheat', 'wood', 'fertility', 'depth', 'room', 'isolation'] +COLONY_FIELDS = ['wheat_distance', 'wood_distance', 'catchment_tiles', 'build_sites', + 'resource_amount', 'rival_distance', 'rivals_within_threat', 'mean_fertility', + *FACTORS, 'total'] +RUN_FILES = ['config.json', 'summary.json', 'summary.md', 'games.csv', 'colonies.csv', 'maps.csv', + 'verification.json'] +RUN_DIRECTORIES = ['maps', 'games', 'profiles', 'verify'] + +DEFAULTS = { + 'name': 'custom', + 'description': '', + 'generators': [BASELINE_METHOD], + 'map_seeds': [1001], + 'games_per_rotation': 1, + 'engine_seed_base': 1, + 'rotations': 'all', + 'size': 128, + 'colonies': 4, + 'workers': None, + 'controls': {}, + 'candidates': 5, + 'ai': 'nicowar', + 'tick_cap': 90000, + 'jobs': 3, + 'game_timeout_seconds': 3600, + 'keep_replays': False, + 'timeline': False, + 'verify_games': 0, + 'bootstrap_draws': 4000, +} + + +def remove_tree(path, inside): + """Delete a directory, but only one strictly inside `inside` (the run directory).""" + target, root = Path(path).resolve(), Path(inside).resolve() + if target == root or not target.is_relative_to(root): + raise RuntimeError(f'refusing to delete {target}: not inside {root}') + shutil.rmtree(target, ignore_errors=True) + + +# ---------------------------------------------------------------------------- configuration + +def parse_int_list(spec): + """'1001-1004', '1001,1005', a JSON list or {"start", "count"} as a list of ints.""" + if isinstance(spec, dict): + start = int(spec['start']) + return list(range(start, start + int(spec['count']))) + if isinstance(spec, (list, tuple)): + return [int(s) for s in spec] + values = [] + for part in str(spec).split(','): + part = part.strip() + if not part: + continue + if '-' in part: + first, last = part.split('-', 1) + values.extend(range(int(first), int(last) + 1)) + else: + values.append(int(part)) + return values + + +def load_config(preset, overrides): + path = Path(preset) + if path.suffix != '.json': + path = PRESET_DIR / f'{preset}.json' + if not path.exists(): + raise SystemExit(f'error: preset not found: {path}') + config = dict(DEFAULTS) + config.update(json.loads(path.read_text())) + for key, value in overrides.items(): + if value is not None: + config[key] = value + resolved = path.resolve() + config['preset_file'] = str(resolved.relative_to(ROOT)) if resolved.is_relative_to(ROOT) else str(resolved) + config['map_seeds'] = parse_int_list(config['map_seeds']) + generators = config['generators'] + if isinstance(generators, str): + generators = parse_int_list(generators) + config['generators'] = [g if isinstance(g, dict) else {'method': int(g)} for g in generators] + size = config['size'] + width, height = (size, size) if isinstance(size, int) else (int(size[0]), int(size[1])) + for side in (width, height): + if side < 16 or side & (side - 1): + raise SystemExit(f'error: map sides must be powers of two >= 16 (got {width}x{height})') + config['width'], config['height'] = width, height + colonies = int(config['colonies']) + rotations = colonies if config['rotations'] == 'all' else int(config['rotations']) + if not 1 <= rotations <= colonies: + raise SystemExit(f'error: rotations must be "all" or 1..{colonies}') + config['rotation_count'] = rotations + return config + + +def generator_controls(config, generator): + controls = dict(config.get('controls', {}).get(str(generator['method']), {})) + controls.update(generator.get('controls', {})) + return controls + + +def git_revision(): + def git(*args): + return subprocess.run(['git', *args], cwd=ROOT, capture_output=True, text=True).stdout.strip() + revision = git('rev-parse', 'HEAD') + dirty = bool(git('status', '--porcelain', '--untracked-files=no')) + return revision + ('-dirty' if dirty else '') + + +# ---------------------------------------------------------------------------- map production + +def map_key(method, seed): + return f'g{method}-s{seed}' + + +def parse_study(stdout): + record = {'colonies': [], 'starts': [], 'files': [], 'options': {}} + for line in stdout.splitlines(): + parts = line.strip().split(',') + tag = parts[0] + if tag == 'STUDY': + record['generated'] = parts[3] == '1' + record['generation_seconds'] = float(parts[14]) + elif tag == 'SAMPLED': + record['root_seed'], record['chosen_seed'], record['candidates'] = map(int, parts[1:4]) + elif tag == 'QUALITY': + record['quality'] = {'measured': parts[1] == '1', 'score': float(parts[2]), + 'fairness': float(parts[3]), 'worst': float(parts[4]), + 'best': float(parts[5])} + elif tag == 'COLONY': + record['colonies'].append(dict(zip(COLONY_FIELDS, (float(v) for v in parts[2:])))) + elif tag == 'REQUEST': + record['request'] = {'generator_id': parts[1], 'revision': int(parts[2]), + 'seed': int(parts[3]), 'wDec': int(parts[4]), 'hDec': int(parts[5]), + 'teams': int(parts[6]), 'workers': int(parts[7])} + elif tag == 'OPTION': + record['options'][parts[1]] = int(parts[2]) + elif tag == 'START': + record['starts'].append([int(parts[2]), int(parts[3])]) + elif tag == 'MAPFILE': + record['files'].append({'rotation': int(parts[1]), 'path': ','.join(parts[2:-2]), + 'bytes': int(parts[-2]), 'fnv1a64': parts[-1]}) + elif tag == 'ROTATIONS': + record['rotation_checks'] = dict(zip( + ['teams', 'rotations', 'save_load_stable', 'references_consistent', + 'colonies_placed', 'round_trip'], map(int, parts[1:7]))) + return record + + +def produce_map(config, paths, generator, seed): + method = generator['method'] + key = map_key(method, seed) + directory = paths['maps'] / key + manifest = directory / 'map.json' + if manifest.exists(): + record = json.loads(manifest.read_text()) + if record.get('ok') or record.get('failure') == 'generation': + return record + remove_tree(directory, paths['out']) + directory.mkdir(parents=True) + profile = paths['profiles'] / f'study-{key}' + remove_tree(profile, paths['out']) + profile.mkdir(parents=True) + colonies = int(config['colonies']) + command = [str(paths['study']), str(method), str(seed), 'glob2-fairness', + f'w={int(math.log2(config["width"]))}', f'h={int(math.log2(config["height"]))}', + f'teams={colonies}', 'quality', f'candidates={int(config["candidates"])}', + f'save={directory / "map"}', f'rotations={config["rotation_count"]}', + f'name=fairness-{key}'] + if config.get('workers'): + command.append(f'workers={int(config["workers"])}') + controls = generator_controls(config, generator) + command += [f'{k}={v}' for k, v in controls.items()] + env = dict(os.environ, GLOB2_USER_DIR=str(profile)) + begun = time.monotonic() + try: + result = subprocess.run(command, cwd=ROOT, env=env, capture_output=True, text=True, timeout=1800) + stdout, exit_code = result.stdout, result.returncode + (directory / 'study.log').write_text(result.stdout + result.stderr) + except subprocess.TimeoutExpired: + stdout, exit_code = '', 'timeout' + remove_tree(profile, paths['out']) + record = parse_study(stdout) + record.update({ + 'key': key, 'method': method, 'map_seed': seed, 'colonies': colonies, + 'width': config['width'], 'height': config['height'], 'controls': controls, + 'study_command': [str(Path(c).relative_to(ROOT)) if c.startswith(str(ROOT)) else c + for c in command], + 'exit_code': exit_code, 'wall_seconds': round(time.monotonic() - begun, 3), + 'git_revision': paths['git_revision'], + }) + record.setdefault('chosen_seed', seed) + checks = record.get('rotation_checks', {}) + if exit_code == 4 or (exit_code == 0 and not record.get('generated', False)): + record.update(ok=False, failure='generation') + elif exit_code != 0 or not checks or not all( + checks[k] for k in ('references_consistent', 'colonies_placed', 'round_trip')): + record.update(ok=False, failure=f'study exit {exit_code}') + elif len(record['starts']) != colonies or len(record['files']) != config['rotation_count']: + record.update(ok=False, failure='incomplete study output') + else: + record['ok'] = True + manifest.write_text(json.dumps(record, indent=2)) + return record + + +# ---------------------------------------------------------------------------- matches + +END_RE = re.compile(r'^GLOB2_GAME_END ticks=(\d+) winner_team=(-?\d+) seed=(\d+) ' + r'map="([^"]*)" orders=(\d+)') + + +def parse_game_log(text): + parsed = {'end': None, 'teams': {}, 'reason': None, 'cap_checksum': None} + for line in text.splitlines(): + if line.startswith('GLOB2_GAME_END'): + match = END_RE.match(line) + if match: + parsed['end'] = {'ticks': int(match[1]), 'winner_team': int(match[2]), + 'seed': int(match[3]), 'map': match[4], 'orders': int(match[5])} + elif line.startswith('GLOB2_TEAM_RESULT '): + fields = dict(item.split('=', 1) for item in line.split()[1:]) + team = int(fields.pop('team')) + start = [int(v) for v in fields.pop('start').split(',')] + entry = {k: (v if k == 'result' else int(v)) for k, v in fields.items()} + entry['start'] = start + parsed['teams'][team] = entry + elif line.startswith('nox::gui.game.totalPrestigeReached'): + parsed['reason'] = parsed['reason'] or 'prestige' + elif line.startswith('nox::gui.game.isGameEnded'): + parsed['reason'] = parsed['reason'] or 'elimination' + elif line.startswith('nox::gui.game.checkSum() = '): + parsed['cap_checksum'] = line.split('=', 1)[1].strip() + return parsed + + +def adjudicate(game, record, config): + """Winner, placement and elimination order by start position, from a parsed game.""" + n = record['colonies'] + rotation = game['rotation'] + teams = {int(t): e for t, e in game['parsed']['teams'].items()} + end = game['parsed']['end'] + slot_of = lambda team: (team - rotation) % n + outcome = {'status': game['status'], 'end_reason': None, 'ticks': None, 'winner_team': None, + 'winner_slot': None, 'adjudication': 'none', 'start_mismatch': False, + 'elimination_order': [], 'placement': {}} + if game['status'] != 'ok' or end is None or len(teams) != n: + outcome['status'] = game['status'] if game['status'] != 'ok' else 'incomplete' + return outcome + outcome['ticks'] = end['ticks'] + for team, entry in teams.items(): + if entry['start'] != record['starts'][slot_of(team)]: + outcome['start_mismatch'] = True + cap = int(config['tick_cap']) + winners = [t for t, e in teams.items() if e['result'] == 'won'] + alive = [t for t, e in teams.items() if e['alive']] + strength = lambda t: (teams[t]['prestige'], teams[t]['buildings'], teams[t]['units']) + if end['winner_team'] >= 0: + outcome['end_reason'] = game['parsed']['reason'] or 'elimination' + winner = end['winner_team'] + outcome['adjudication'] = 'decisive' if len(winners) == 1 else 'decisive-shared' + elif end['ticks'] >= cap: + outcome['end_reason'] = 'cap' + ranked = sorted(alive, key=strength, reverse=True) + if ranked and (len(ranked) == 1 or strength(ranked[0]) != strength(ranked[1])): + winner = ranked[0] + outcome['adjudication'] = 'cap-prestige' + else: + winner = None + outcome['adjudication'] = 'cap-tie' if ranked else 'cap-none-alive' + else: + outcome['end_reason'] = game['parsed']['reason'] or 'no-winner' + winner = None + outcome['adjudication'] = 'no-winner' + if winner is not None: + outcome['winner_team'] = winner + outcome['winner_slot'] = slot_of(winner) + eliminated = sorted((e['eliminated_tick'], t) for t, e in teams.items() if e['eliminated_tick'] >= 0) + outcome['elimination_order'] = [[slot_of(t), tick] for tick, t in eliminated] + # Placement 1 is the winner; other survivors by prestige, then eliminated colonies, the + # later-eliminated ahead. Ties break towards the lower start index, only to stay deterministic. + survivors = sorted((t for t in teams if t != winner and teams[t]['eliminated_tick'] < 0), + key=lambda t: (tuple(-v for v in strength(t)), slot_of(t))) + fallen = [t for _, t in sorted(((-e['eliminated_tick'], slot_of(t)), t) + for t, e in teams.items() if t != winner and e['eliminated_tick'] >= 0)] + order = ([winner] if winner is not None else []) + survivors + fallen + outcome['placement'] = {slot_of(t): place + 1 for place, t in enumerate(order)} + return outcome + + +def game_jobs(config, records): + jobs = [] + per_rotation = int(config['games_per_rotation']) + for k in range(per_rotation): + for rotation in range(config['rotation_count']): + for record in records: + if record.get('ok'): + seed = int(config['engine_seed_base']) + rotation * per_rotation + k + jobs.append({'map': record['key'], 'rotation': rotation, 'game': k, 'seed': seed}) + return jobs + + +def run_game(config, paths, record, job, directory=None, force=False): + directory = directory or paths['games'] / record['key'] / f'r{job["rotation"]}-k{job["game"]}' + result_file = directory / 'result.json' + if result_file.exists() and not force: + return json.loads(result_file.read_text()) + remove_tree(directory, paths['out']) + profile = directory / 'profile' + (profile / 'maps').mkdir(parents=True) + map_file = next(f for f in record['files'] if f['rotation'] == job['rotation']) + map_name = f'fairness-{record["key"]}-r{job["rotation"]}' + os.symlink(Path(map_file['path']).resolve(), profile / 'maps' / f'{map_name}.map') + env = dict(os.environ) + env.update(GLOB2_USER_DIR=str(profile), GLOB2_TEST_SEED=str(job['seed']), + GLOB2_TEAM_RESULTS='1', GLOB2_TEST_MAX_TICKS=str(int(config['tick_cap'])), + GLOB2_REPLAY_PATH=str(directory / 'game.replay')) + if config.get('timeline'): + env['GLOB2_TEAM_TIMELINE'] = '1' + command = [str(paths['glob2']), '-test-games-nox', '1', '--map', map_name, + '--matchup', ','.join([config['ai']] * record['colonies'])] + log = directory / 'game.log' + begun = time.monotonic() + with open(log, 'wb') as out: + try: + code = subprocess.run(command, cwd=ROOT, env=env, stdout=out, stderr=subprocess.STDOUT, + timeout=float(config['game_timeout_seconds'])).returncode + status = 'ok' if code == 0 else f'exit_{code}' + except subprocess.TimeoutExpired: + status = 'timeout' + wall = time.monotonic() - begun + text = log.read_text(errors='replace') + parsed = parse_game_log(text) + remove_tree(profile, paths['out']) + replay = directory / 'game.replay' + if not config.get('keep_replays') and replay.exists(): + replay.unlink() + game = {**job, 'status': status, 'wall_seconds': round(wall, 3), 'parsed': parsed, + 'command': command[1:], 'env': {k: env[k] for k in ( + 'GLOB2_TEST_SEED', 'GLOB2_TEAM_RESULTS', 'GLOB2_TEST_MAX_TICKS')}} + game['outcome'] = adjudicate(game, record, config) + # Keep only what the analysis reads; the full engine output can be large. + if game['outcome']['status'] == 'ok': + keep = [l for l in text.splitlines() if l.startswith(('GLOB2_', 'nox::', 'Random Seed'))] + log.write_text('\n'.join(keep) + '\n') + # Round-trip through JSON now so a fresh result and one read back from disk compare equal. + game = json.loads(json.dumps(game)) + result_file.write_text(json.dumps(game, indent=1)) + return game + + +def run_pool(tasks, jobs, work, describe): + results = [None] * len(tasks) + lock = threading.Lock() + done = 0 + begun = time.monotonic() + with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as pool: + futures = {pool.submit(work, task): i for i, task in enumerate(tasks)} + for future in concurrent.futures.as_completed(futures): + i = futures[future] + results[i] = future.result() + with lock: + done += 1 + elapsed = time.monotonic() - begun + eta = elapsed / done * (len(tasks) - done) + print(f'[{done}/{len(tasks)} {elapsed:.0f}s, eta {eta:.0f}s] {describe(tasks[i], results[i])}', + flush=True) + return results + + +# ---------------------------------------------------------------------------- statistics + +def gamma_q(a, x): + """Regularized upper incomplete gamma Q(a, x).""" + if x <= 0: + return 1.0 + log_prefix = -x + a * math.log(x) - math.lgamma(a) + if x < a + 1: + term = total = 1.0 / a + ap = a + for _ in range(100000): + ap += 1 + term *= x / ap + total += term + if abs(term) < abs(total) * 1e-15: + break + return max(0.0, 1.0 - total * math.exp(log_prefix)) + tiny = 1e-300 + b = x + 1 - a + c = 1 / tiny + d = 1 / b + h = d + for i in range(1, 100000): + an = -i * (i - a) + b += 2 + d = an * d + b + d = d if abs(d) > tiny else tiny + c = b + an / c + c = c if abs(c) > tiny else tiny + d = 1 / d + h *= d * c + if abs(d * c - 1) < 1e-15: + break + return min(1.0, math.exp(log_prefix) * h) + + +def chi_square_uniform(counts): + n, k = sum(counts), len(counts) + if n == 0 or k < 2: + return None, None + expected = n / k + statistic = sum((c - expected) ** 2 / expected for c in counts) + return statistic, gamma_q((k - 1) / 2, statistic / 2) + + +def compositions(n, k): + if k == 1: + yield (n,) + return + for first in range(n + 1): + for rest in compositions(n - first, k - 1): + yield (first,) + rest + + +def exact_uniform_p(counts, seed, draws=20000): + """Multinomial goodness of fit against uniform: P(an outcome no more likely than observed). + + Exact by enumeration while the outcome space is small, otherwise a seeded Monte Carlo.""" + n, k = sum(counts), len(counts) + if n == 0 or k < 2: + return None, 'none' + base = math.lgamma(n + 1) - n * math.log(k) + log_p = lambda xs: base - sum(math.lgamma(x + 1) for x in xs) + observed = log_p(counts) + 1e-9 + if math.comb(n + k - 1, k - 1) <= 250000: + return min(1.0, sum(math.exp(log_p(c)) for c in compositions(n, k) if log_p(c) <= observed)), 'exact' + rng = random.Random(seed) + hits = 0 + for _ in range(draws): + xs = [0] * k + for _ in range(n): + xs[rng.randrange(k)] += 1 + hits += log_p(xs) <= observed + return (hits + 1) / (draws + 1), 'monte-carlo' + + +def wilson(k, n): + if n == 0: + return None, None + p = k / n + denominator = 1 + Z95 ** 2 / n + centre = (p + Z95 ** 2 / (2 * n)) / denominator + half = Z95 * math.sqrt(p * (1 - p) / n + Z95 ** 2 / (4 * n * n)) / denominator + return max(0.0, centre - half), min(1.0, centre + half) + + +def benjamini_hochberg(p_values): + m = len(p_values) + order = sorted(range(m), key=lambda i: p_values[i]) + adjusted = [1.0] * m + running = 1.0 + for rank in range(m, 0, -1): + i = order[rank - 1] + running = min(running, p_values[i] * m / rank) + adjusted[i] = running + return adjusted + + +def holm(p_values): + m = len(p_values) + order = sorted(range(m), key=lambda i: p_values[i]) + adjusted = [1.0] * m + running = 0.0 + for rank, i in enumerate(order): + running = max(running, min(1.0, (m - rank) * p_values[i])) + adjusted[i] = running + return adjusted + + +def ranks(values): + order = sorted(range(len(values)), key=lambda i: values[i]) + result = [0.0] * len(values) + i = 0 + while i < len(order): + j = i + while j + 1 < len(order) and values[order[j + 1]] == values[order[i]]: + j += 1 + for position in range(i, j + 1): + result[order[position]] = (i + j) / 2 + 1 + i = j + 1 + return result + + +def pearson(x, y): + if len(x) < 3: + return None + mx, my = sum(x) / len(x), sum(y) / len(y) + sxx = sum((a - mx) ** 2 for a in x) + syy = sum((b - my) ** 2 for b in y) + if sxx <= 1e-18 or syy <= 1e-18: + return None + return sum((a - mx) * (b - my) for a, b in zip(x, y)) / math.sqrt(sxx * syy) + + +def spearman(x, y): + return pearson(ranks([round(v, 9) for v in x]), ranks([round(v, 9) for v in y])) + + +def bootstrap(items, statistic, draws, seed): + """Percentile 95% interval of statistic over items resampled with replacement.""" + if len(items) < 2: + return None + rng = random.Random(seed) + values = [] + for _ in range(draws): + value = statistic([items[rng.randrange(len(items))] for _ in items]) + if value is not None: + values.append(value) + if len(values) < draws / 2: + return None + values.sort() + return [values[int(0.025 * (len(values) - 1))], values[int(0.975 * (len(values) - 1))]] + + +def squared_bias(counts): + """Unbiased estimate of sum_s (p_s - 1/k)^2 from multinomial counts. + + E[chi2] = (k - 1) + (n - 1) k sum_s (p_s - 1/k)^2, so the plain squared deviation of the + observed shares, which is positive even for a perfectly fair map, is corrected for chance.""" + n, k = sum(counts), len(counts) + if n < 2 or k < 2: + return None + statistic, _ = chi_square_uniform(counts) + return (statistic - (k - 1)) / ((n - 1) * k) + + +def rms_points(mean_squared_bias, k): + """Root-mean-square deviation of per-start win probability from 1/k, in percentage points.""" + if mean_squared_bias is None: + return None + return 100 * math.sqrt(max(0.0, mean_squared_bias) / k) + + +def share_table(counts, seed): + n, k = sum(counts), len(counts) + statistic, p_chi2 = chi_square_uniform(counts) + p_exact, method = exact_uniform_p(counts, seed) + best = max(range(k), key=lambda i: (counts[i], -i)) if n else None + table = {'counts': counts, 'n': n, 'chi2': statistic, 'p_chi2': p_chi2, 'p': p_exact, + 'p_method': method, 'best': best, 'best_share': counts[best] / n if n else None, + 'best_share_wilson': list(wilson(counts[best], n)) if n else None, + 'dominance': counts[best] / n * k if n else None, + 'squared_bias': squared_bias(counts)} + table['rms_points'] = rms_points(table['squared_bias'], k) + table['shares_wilson'] = [list(wilson(c, n)) if n else None for c in counts] + return table + + +def hash_text(text): + value = 2166136261 + for ch in text.encode(): + value = ((value ^ ch) * 16777619) % (2 ** 32) + return value + + +def seed_for(*parts): + return hash_text('/'.join(str(p) for p in parts)) + + +# ---------------------------------------------------------------------------- analysis + +def load_results(out): + records = {} + for manifest in sorted((out / 'maps').glob('*/map.json')): + record = json.loads(manifest.read_text()) + records[record['key']] = record + games = [json.loads(result.read_text()) for result in sorted((out / 'games').glob('*/*/result.json'))] + return records, games + + +def analyse_map(record, games, config): + n = record['colonies'] + played = [g for g in games if g['outcome']['status'] == 'ok'] + wins = [0] * n + decisive = [0] * n + team_wins = [0] * n + placement_points = [0.0] * n + for game in played: + outcome = game['outcome'] + if outcome['winner_slot'] is not None: + wins[outcome['winner_slot']] += 1 + team_wins[outcome['winner_team']] += 1 + if outcome['adjudication'].startswith('decisive'): + decisive[outcome['winner_slot']] += 1 + for slot, place in outcome['placement'].items(): + placement_points[int(slot)] += (n - place) / (n - 1) if n > 1 else 1.0 + quality = [c['total'] for c in record['colonies']] if len(record['colonies']) == n else None + analysis = { + 'key': record['key'], 'method': record['method'], 'map_seed': record['map_seed'], + 'chosen_seed': record.get('chosen_seed'), 'games': len(games), 'played': len(played), + 'errors': len(games) - len(played), + 'cap_games': sum(g['outcome']['end_reason'] == 'cap' for g in played), + 'prestige_games': sum(g['outcome']['end_reason'] == 'prestige' for g in played), + 'unresolved': sum(g['outcome']['winner_slot'] is None for g in played), + 'start_mismatches': sum(g['outcome']['start_mismatch'] for g in played), + 'ticks_median': statistics.median([g['outcome']['ticks'] for g in played]) if played else None, + 'wall_seconds_mean': statistics.mean([g['wall_seconds'] for g in played]) if played else None, + 'slot': share_table(wins, seed_for(record['key'], 'slot')), + 'decisive_slot': share_table(decisive, seed_for(record['key'], 'decisive')), + 'team': share_table(team_wins, seed_for(record['key'], 'team')), + 'placement_score': [p / len(played) if played else None for p in placement_points], + 'quality': quality, 'quality_score': record.get('quality', {}).get('score'), + 'starts': record['starts'], + 'record_colonies': record['colonies'] if len(record['colonies']) == n else [], + } + analysis['quality_spearman'] = spearman(quality, wins) if quality and sum(wins) and n >= 3 else None + return analysis + + +def scorer_points(maps, factor): + """(map, within-map centred factor, centred win share, centred placement score) per colony.""" + points = [] + for m in maps: + wins = m['slot']['counts'] + n_wins = sum(wins) + if not m['record_colonies'] or n_wins == 0: + continue + values = [c[factor] for c in m['record_colonies']] + if max(values) - min(values) < 1e-9: + continue + k = len(values) + mean_value = sum(values) / k + mean_place = sum(m['placement_score']) / k + for slot in range(k): + points.append((m['key'], values[slot] - mean_value, wins[slot] / n_wins - 1 / k, + m['placement_score'][slot] - mean_place)) + return points + + +def scorer_check(maps, draws, seed): + check = {} + for factor in ['total', *FACTORS]: + points = scorer_points(maps, factor) + keys = sorted({p[0] for p in points}) + if len(points) < 3: + check[factor] = None + continue + + def rho(sample_keys, column): + chosen = [p for key in sample_keys for p in points if p[0] == key] + return spearman([p[1] for p in chosen], [p[column] for p in chosen]) + check[factor] = { + 'maps': len(keys), 'colonies': len(points), + 'wins_rho': rho(keys, 2), 'wins_rho_ci': bootstrap(keys, lambda s: rho(s, 2), draws, seed), + 'placement_rho': rho(keys, 3), + 'placement_rho_ci': bootstrap(keys, lambda s: rho(s, 3), draws, seed + 1), + } + # Does the start the scorer rates best actually win more than its fair share? + top_wins = top_n = 0 + for m in maps: + values = [c['total'] for c in m['record_colonies']] + if not values or max(values) - min(values) < 1e-9: + continue + top = max(range(len(values)), key=lambda i: values[i]) + top_wins += m['slot']['counts'][top] + top_n += m['slot']['n'] + check['top_rated_start'] = {'wins': top_wins, 'n': top_n, + 'share': top_wins / top_n if top_n else None, + 'wilson': list(wilson(top_wins, top_n)) if top_n else None} + return check + + +def analyse_generator(method, maps, config, catalog): + n = int(config['colonies']) + draws = int(config['bootstrap_draws']) + usable = [m for m in maps if m['slot']['n'] >= 2] + p_values = [m['slot']['p'] for m in usable] + bh = benjamini_hochberg(p_values) if p_values else [] + hl = holm(p_values) if p_values else [] + for m, q, h in zip(usable, bh, hl): + m['slot']['q_bh'] = q + m['slot']['p_holm'] = h + biases = [m['slot']['squared_bias'] for m in usable] + mean_bias = statistics.mean(biases) if biases else None + seed = seed_for('generator', method) + ci = bootstrap(biases, lambda s: rms_points(statistics.mean(s), n), draws, seed) if biases else None + pooled_slot = [sum(m['slot']['counts'][s] for m in maps) for s in range(n)] + pooled_team = [sum(m['team']['counts'][t] for m in maps) for t in range(n)] + # Is any of this generator's maps biased? Sum of per-map chi-square statistics, calibrated + # by simulating every map's winners under a fair map (seeded, so the report reproduces). + observed = sum(m['slot']['chi2'] for m in usable) if usable else None + pooled_p = None + if usable: + rng = random.Random(seed + 7) + simulations = 4000 + hits = 0 + for _ in range(simulations): + total = 0.0 + for m in usable: + xs = [0] * n + for _ in range(m['slot']['n']): + xs[rng.randrange(n)] += 1 + total += chi_square_uniform(xs)[0] + hits += total >= observed - 1e-9 + pooled_p = (hits + 1) / (simulations + 1) + info = catalog.get(method, {}) + played = sum(m['played'] for m in maps) + return { + 'method': method, 'name': info.get('nameKey', str(method)), 'id': info.get('id'), + 'revision': info.get('revision'), 'maps': len(maps), 'maps_with_winners': len(usable), + 'games': sum(m['games'] for m in maps), 'played': played, + 'errors': sum(m['errors'] for m in maps), + 'cap_share': sum(m['cap_games'] for m in maps) / played if played else None, + 'prestige_share': sum(m['prestige_games'] for m in maps) / played if played else None, + 'unresolved': sum(m['unresolved'] for m in maps), + 'start_mismatches': sum(m['start_mismatches'] for m in maps), + 'headline_rms_points': rms_points(mean_bias, n), 'headline_rms_points_ci': ci, + 'mean_squared_bias': mean_bias, + 'maps_p05': sum(p < 0.05 for p in p_values), 'maps_bh05': sum(q < 0.05 for q in bh), + 'maps_holm05': sum(h < 0.05 for h in hl), + 'expected_p05_by_chance': 0.05 * len(p_values), + 'dominance': {'min': min(m['slot']['dominance'] for m in usable), + 'median': statistics.median(m['slot']['dominance'] for m in usable), + 'max': max(m['slot']['dominance'] for m in usable)} if usable else None, + 'pooled_chi2': observed, 'pooled_p': pooled_p, + 'slot_index': share_table(pooled_slot, seed + 11), + 'team_index': share_table(pooled_team, seed + 13), + 'scorer': scorer_check(maps, draws, seed + 17), + 'ticks_median': statistics.median([m['ticks_median'] for m in maps if m['ticks_median']]) + if played else None, + 'wall_seconds_mean': statistics.mean([m['wall_seconds_mean'] for m in maps if m['wall_seconds_mean']]) + if played else None, + 'map_keys': [m['key'] for m in maps], + } + + +def load_catalog(study): + try: + entries = json.loads(subprocess.run([str(study), '--catalog'], cwd=ROOT, capture_output=True, + text=True, timeout=60).stdout) + return {e['method']: e for e in entries} + except Exception: + return {} + + +def summarize(out, config, paths): + records, games = load_results(out) + catalog = load_catalog(paths['study']) + by_map = {} + for game in games: + by_map.setdefault(game['map'], []).append(game) + maps = [analyse_map(record, by_map.get(key, []), config) + for key, record in records.items() if record.get('ok')] + generators = [] + for method in [g['method'] for g in config['generators']]: + chosen = [m for m in maps if m['method'] == method] + failed = [r for r in records.values() if r['method'] == method and not r.get('ok')] + summary = analyse_generator(method, chosen, config, catalog) + summary['failed_maps'] = [{'map_seed': r['map_seed'], 'failure': r.get('failure')} for r in failed] + generators.append(summary) + n = int(config['colonies']) + all_team = [sum(m['team']['counts'][t] for m in maps) for t in range(n)] + baseline_team = [sum(m['team']['counts'][t] for m in maps if m['method'] == BASELINE_METHOD) + for t in range(n)] + played = [g for g in games if g['outcome']['status'] == 'ok'] + walls = [g['wall_seconds'] for g in played] + ticks = [g['outcome']['ticks'] for g in played] + summary = { + 'config': config, 'git_revision': paths['git_revision'], + 'generated_at': datetime.datetime.now().isoformat(timespec='seconds'), + 'balanced': config['rotation_count'] == n, + 'games': len(games), 'played': len(played), + 'wall_seconds': {'mean': statistics.mean(walls) if walls else None, + 'median': statistics.median(walls) if walls else None, + 'max': max(walls) if walls else None, + 'total': sum(walls), + 'per_1000_ticks': sum(walls) / sum(ticks) * 1000 if ticks and sum(ticks) else None}, + 'ticks': {'median': statistics.median(ticks) if ticks else None, 'max': max(ticks) if ticks else None}, + 'end_reasons': {reason: sum(g['outcome']['end_reason'] == reason for g in played) + for reason in sorted({g['outcome']['end_reason'] for g in played}, key=str)}, + 'engine_team_index': {'all_generators': share_table(all_team, 101), + 'baseline': share_table(baseline_team, 103)}, + 'scorer_all_generators': scorer_check(maps, int(config['bootstrap_draws']), 107), + 'generators': generators, + 'maps': [{k: v for k, v in m.items() if k != 'record_colonies'} for m in maps], + 'verification': json.loads((out / 'verification.json').read_text()) + if (out / 'verification.json').exists() else None, + } + (out / 'summary.json').write_text(json.dumps(summary, indent=2)) + write_csvs(out, records, games) + (out / 'summary.md').write_text(markdown(summary, config)) + return summary + + +def write_csvs(out, records, games): + ordered = sorted(games, key=lambda g: (g['map'], g['rotation'], g['game'])) + with open(out / 'games.csv', 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(['generator', 'map_seed', 'map_roll_seed', 'rotation', 'engine_seed', 'status', + 'end_reason', 'adjudication', 'ticks', 'wall_seconds', 'winner_start', + 'winner_team', 'winner_x', 'winner_y', 'elimination_order', 'placement_by_start', + 'start_mismatch', 'orders', 'cap_checksum']) + for g in ordered: + record = records[g['map']] + o = g['outcome'] + end = g['parsed']['end'] or {} + start = record['starts'][o['winner_slot']] if o['winner_slot'] is not None else [None, None] + writer.writerow([record['method'], record['map_seed'], record.get('chosen_seed'), g['rotation'], + g['seed'], o['status'], o['end_reason'], o['adjudication'], o['ticks'], + g['wall_seconds'], o['winner_slot'], o['winner_team'], start[0], start[1], + ';'.join(f'{s}@{t}' for s, t in o['elimination_order']), + ';'.join(f'{s}:{p}' for s, p in sorted(o['placement'].items(), key=lambda i: int(i[0]))), + int(o['start_mismatch']), end.get('orders'), g['parsed']['cap_checksum']]) + team_fields = ['result', 'alive', 'eliminated_tick', 'prestige', 'units', 'workers', 'explorers', + 'warriors', 'buildings', 'sites'] + with open(out / 'colonies.csv', 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(['generator', 'map_seed', 'rotation', 'engine_seed', 'start', 'team', 'start_x', + 'start_y', 'placement', 'won', *team_fields, 'quality_total', + *[f'quality_{f}' for f in FACTORS]]) + for g in ordered: + record = records[g['map']] + n = record['colonies'] + for team, entry in sorted(g['parsed']['teams'].items(), key=lambda i: int(i[0])): + slot = (int(team) - g['rotation']) % n + quality = record['colonies'][slot] if len(record['colonies']) == n else {} + writer.writerow([record['method'], record['map_seed'], g['rotation'], g['seed'], slot, team, + *record['starts'][slot], g['outcome']['placement'].get(str(slot)), + int(g['outcome']['winner_slot'] == slot), + *[entry.get(k) for k in team_fields], quality.get('total'), + *[quality.get(f) for f in FACTORS]]) + with open(out / 'maps.csv', 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(['generator', 'map_seed', 'roll_seed', 'ok', 'failure', 'quality_score', + 'quality_fairness', 'starts', 'quality_by_start', 'map_fnv1a64_r0', + 'generation_seconds']) + for record in records.values(): + writer.writerow([record['method'], record['map_seed'], record.get('chosen_seed'), + int(bool(record.get('ok'))), record.get('failure'), + record.get('quality', {}).get('score'), record.get('quality', {}).get('fairness'), + ';'.join(f'{x}:{y}' for x, y in record.get('starts', [])), + ';'.join(f'{c["total"]:.3f}' for c in record.get('colonies', [])), + next((f['fnv1a64'] for f in record.get('files', []) if f['rotation'] == 0), None), + record.get('generation_seconds')]) + + +# ---------------------------------------------------------------------------- report + +def pct(value, digits=1): + return '-' if value is None else f'{100 * value:.{digits}f}%' + + +def num(value, digits=2): + return '-' if value is None else f'{value:.{digits}f}' + + +def pval(value): + if value is None: + return '-' + return '<0.001' if value < 0.001 else f'{value:.3f}' + + +def interval(ci, fmt=lambda v: f'{v:.1f}'): + return '' if not ci or ci[0] is None else f' [{fmt(ci[0])}, {fmt(ci[1])}]' + + +def markdown(summary, config): + n = int(config['colonies']) + lines = [f'# Map fairness tournament: {config["name"]}', ''] + if config.get('description'): + lines += [config['description'], ''] + wall = summary['wall_seconds'] + ends = ', '.join(f'{count} {reason}' for reason, count in summary['end_reasons'].items()) + lines += [ + f'- Revision `{summary["git_revision"]}`, generated {summary["generated_at"]}, preset `{config["preset_file"]}`.', + f'- {config["width"]}x{config["height"]} maps, {n} colonies, `{config["ai"]}` in every slot, free for all ' + f'(prestige victory on, as in the lobby), tick cap {config["tick_cap"]}.', + f'- Each map is the best of {config["candidates"]} lobby rolls of its seed; {config["rotation_count"]} ' + f'rotation(s) x {config["games_per_rotation"]} engine seed(s) = ' + f'{config["rotation_count"] * int(config["games_per_rotation"])} games per map.' + + ('' if summary['balanced'] else ' **Not rotation-balanced: start position and team index are confounded.**'), + f'- {summary["played"]} of {summary["games"]} games completed ({ends}); wall time per game mean ' + f'{num(wall["mean"], 1)} s, median {num(wall["median"], 1)} s, max {num(wall["max"], 1)} s ' + f'({num(wall["per_1000_ticks"], 2)} s per 1000 ticks); median game length {summary["ticks"]["median"]} ticks.', + '- A game that reaches the tick cap goes to the surviving colony with the most prestige ' + '(then buildings, then units); an exact tie stays unresolved and is left out of win counts.', + ] + verification = summary.get('verification') + if verification: + lines.append(f'- Reproducibility: {verification["identical"]} of {verification["checked"]} re-run games ' + f'matched their first run exactly (ticks, winner, order count, every team result).') + lines += ['', '## Headline by generator', '', + 'Position bias is the root-mean-square gap between each start\'s true win rate and the fair ' + f'1/{n}, corrected for the scatter raw win shares show even on a fair map, averaged over maps ' + '(95% bootstrap interval over maps). Biased maps counts maps whose wins by start reject a ' + 'uniform split: raw p < 0.05, and after Benjamini-Hochberg across that generator\'s maps. ' + 'Scorer rho is the within-map rank correlation between a colony\'s start-quality score and its ' + 'win share.', '', + '| Generator | Maps | Games | Cap | Position bias (pp) | Biased maps p<.05 / BH | Best start / fair (median) | Any bias p | Scorer rho (wins) |', + '| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | --- |'] + for g in summary['generators']: + scorer = (g['scorer'] or {}).get('total') or {} + lines.append( + f'| {g["name"]} ({g["method"]}) | {g["maps_with_winners"]}/{g["maps"]} | {g["played"]} | {pct(g["cap_share"], 0)} ' + f'| {num(g["headline_rms_points"], 1)}{interval(g["headline_rms_points_ci"])} ' + f'| {g["maps_p05"]} / {g["maps_bh05"]} (chance {g["expected_p05_by_chance"]:.1f}) ' + f'| {num((g["dominance"] or {}).get("median"))} | {pval(g["pooled_p"])} ' + f'| {num(scorer.get("wins_rho"))}{interval(scorer.get("wins_rho_ci"), lambda v: f"{v:.2f}")} |') + lines += ['', '## Engine team-index bias', '', + 'Wins by team index, pooled over rotations, so every index played every start equally often. ' + 'A skew here is the engine\'s processing order (team 0 also carries the passive local player of ' + '`-test-games-nox` and its AI polls last), not the map.', '', + '| Scope | Decided games | Wins by team ' + ' / '.join(str(t) for t in range(n)) + + ' | p (uniform) | Top team share [95%] | Bias (pp) |', + '| --- | ---: | --- | ---: | --- | ---: |'] + for label, table in (('Symmetric arena (baseline)', summary['engine_team_index']['baseline']), + ('All generators', summary['engine_team_index']['all_generators'])): + if table['n']: + lines.append(f'| {label} | {table["n"]} | {" / ".join(map(str, table["counts"]))} | {pval(table["p"])} ' + f'| team {table["best"]}: {pct(table["best_share"])}{interval(table["best_share_wilson"], pct)} ' + f'| {num(table["rms_points"], 1)} |') + for g in summary['generators']: + lines += ['', f'## {g["name"]} (generator {g["method"]}, id `{g["id"]}`, revision {g["revision"]})', ''] + if g['failed_maps']: + lines += ['Maps that failed to generate: ' + ', '.join( + f'{f["map_seed"]} ({f["failure"]})' for f in g['failed_maps']) + '.', ''] + lines += ['| Map seed (roll) | Games (cap) | Wins by start ' + ' / '.join(str(s) for s in range(n)) + + ' | p | BH q | Best start share [95%] | Best / fair | Bias (pp) | Quality by start | Quality rho |', + '| --- | --- | --- | ---: | ---: | --- | ---: | ---: | --- | ---: |'] + for m in summary['maps']: + if m['method'] != g['method']: + continue + s = m['slot'] + best = f'start {s["best"]}: {pct(s["best_share"])}{interval(s["best_share_wilson"], pct)}' \ + if s['n'] else '-' + lines.append( + f'| {m["map_seed"]} ({m["chosen_seed"]}) | {m["played"]} ({m["cap_games"]}) | {" / ".join(map(str, s["counts"]))} ' + f'| {pval(s["p"])} | {pval(s.get("q_bh"))} | {best} | {num(s["dominance"])} | {num(s["rms_points"], 1)} ' + f'| {" / ".join(f"{q:.2f}" for q in (m["quality"] or []))} | {num(m["quality_spearman"])} |') + slot, team = g['slot_index'], g['team_index'] + dominance = ' / '.join(num(g['dominance'][k]) for k in ('min', 'median', 'max')) if g['dominance'] else '-' + lines += ['', + f'- Wins by generator colony index, pooled over maps: {" / ".join(map(str, slot["counts"]))} ' + f'(p {pval(slot["p"])}); a skew means the generator favours a colony it places early or late.', + f'- Wins by team index, pooled: {" / ".join(map(str, team["counts"]))} (p {pval(team["p"])}).', + f'- Maps significant after Holm: {g["maps_holm05"]}; best-start dominance min / median / max: {dominance}.'] + scorer = g['scorer'] or {} + if scorer.get('total'): + top = scorer['top_rated_start'] + lines.append('- Scorer check: rho with win share ' + num(scorer['total']['wins_rho']) + + interval(scorer['total']['wins_rho_ci'], lambda v: f'{v:.2f}') + + ', with placement ' + num(scorer['total']['placement_rho']) + + interval(scorer['total']['placement_rho_ci'], lambda v: f'{v:.2f}') + + f'; the top-rated start won {top["wins"]} of {top["n"]} ({pct(top["share"])}' + + interval(top['wilson'], pct) + f', fair {pct(1 / n)}). By factor (wins rho): ' + + ', '.join(f'{f} {num((scorer.get(f) or {}).get("wins_rho"))}' for f in FACTORS) + '.') + elif g['method'] == BASELINE_METHOD: + lines.append('- Scorer check: not applicable, every colony scores the same by construction.') + overall = (summary['scorer_all_generators'] or {}).get('total') + if overall: + lines += ['', '## Scorer check across generators', '', + f'Within-map rank correlation between start-quality score and win share over {overall["colonies"]} ' + f'colonies on {overall["maps"]} maps: rho {num(overall["wins_rho"])}' + f'{interval(overall["wins_rho_ci"], lambda v: f"{v:.2f}")}; with placement ' + f'{num(overall["placement_rho"])}{interval(overall["placement_rho_ci"], lambda v: f"{v:.2f}")}.'] + lines.append('') + return '\n'.join(lines) + + +# ---------------------------------------------------------------------------- driver + +def resolve_paths(args, out): + return {'out': out, 'maps': out / 'maps', 'games': out / 'games', 'profiles': out / 'profiles', + 'glob2': Path(args.bin).resolve(), 'study': Path(args.study).resolve(), + 'git_revision': git_revision()} + + +def verify(config, paths, records, games): + """Re-run a few finished games from scratch and require identical outcomes.""" + count = int(config.get('verify_games') or 0) + finished = sorted((g for g in games if g['outcome']['status'] == 'ok'), + key=lambda g: (g['map'], g['rotation'], g['game'])) + checks = [] + for game in finished[:count]: + job = {k: game[k] for k in ('map', 'rotation', 'game', 'seed')} + directory = paths['out'] / 'verify' / game['map'] / f'r{game["rotation"]}-k{game["game"]}' + again = run_game(config, paths, records[game['map']], job, directory=directory, force=True) + same = (again['parsed']['end'] == game['parsed']['end'] and + again['parsed']['teams'] == game['parsed']['teams']) + checks.append({'map': game['map'], 'rotation': game['rotation'], 'seed': game['seed'], + 'identical': same, 'first': game['parsed']['end'], 'again': again['parsed']['end']}) + print(f'verify {game["map"]} r{game["rotation"]} seed {game["seed"]}: {"identical" if same else "DIFFERENT"}', + flush=True) + result = {'checked': len(checks), 'identical': sum(c['identical'] for c in checks), 'games': checks} + (paths['out'] / 'verification.json').write_text(json.dumps(result, indent=2)) + return result + + +def command_run(args): + overrides = {'generators': args.generators, 'map_seeds': args.map_seeds, 'jobs': args.jobs, + 'games_per_rotation': args.games_per_rotation, 'tick_cap': args.tick_cap, + 'size': args.size, 'colonies': args.colonies, 'verify_games': args.verify, + 'name': args.name} + config = load_config(args.preset, overrides) + out = Path(args.out).resolve() if args.out else ROOT / 'artifacts' / 'map-fairness' / config['name'] + out.mkdir(parents=True, exist_ok=True) + if args.fresh: + # Only this tool's own outputs, and never the run directory itself. + for name in RUN_DIRECTORIES: + remove_tree(out / name, out) + for name in RUN_FILES: + (out / name).unlink(missing_ok=True) + paths = resolve_paths(args, out) + for binary in (paths['glob2'], paths['study']): + if not binary.exists(): + raise SystemExit(f'error: {binary} not found; build with scons release=1 and ' + f'scons release=1 map-generator-study') + ignored = ('jobs', 'verify_games', 'description', 'git_revision') + comparable = {k: v for k, v in config.items() if k not in ignored} + previous = out / 'config.json' + if previous.exists(): + old = {k: v for k, v in json.loads(previous.read_text()).items() if k not in ignored} + if old != json.loads(json.dumps(comparable)): + raise SystemExit(f'error: {out} holds a run with a different configuration; use --fresh or --out') + previous.write_text(json.dumps(config | {'git_revision': paths['git_revision']}, indent=2)) + jobs = int(config['jobs']) + print(f'map fairness tournament "{config["name"]}" -> {out}', flush=True) + + tasks = [(g, s) for g in config['generators'] for s in config['map_seeds']] + records = run_pool(tasks, jobs, lambda t: produce_map(config, paths, t[0], t[1]), + lambda t, r: f'map {r["key"]}: ' + ('ok' if r.get('ok') else r.get('failure', '?'))) + by_key = {r['key']: r for r in records} + + def describe(job, game): + o = game['outcome'] + who = f'start {o["winner_slot"]} (team {o["winner_team"]})' if o['winner_slot'] is not None else 'no winner' + return (f'{job["map"]} r{job["rotation"]} seed {job["seed"]}: {o["status"]}, {o["end_reason"]}, {who}, ' + f'{o["ticks"]} ticks, {game["wall_seconds"]:.0f}s') + games = run_pool(game_jobs(config, records), jobs, + lambda j: run_game(config, paths, by_key[j['map']], j), describe) + if config.get('verify_games'): + verify(config, paths, by_key, games) + remove_tree(paths['profiles'], out) + summary = summarize(out, config, paths) + print(f'\nwrote {out / "summary.md"}, summary.json, games.csv, colonies.csv, maps.csv') + for g in summary['generators']: + print(f' {g["name"]} ({g["method"]}): position bias {num(g["headline_rms_points"], 1)} pp' + f'{interval(g["headline_rms_points_ci"])}, biased maps {g["maps_p05"]}/{g["maps_with_winners"]} ' + f'(BH {g["maps_bh05"]}), cap {pct(g["cap_share"], 0)}') + return 0 + + +def command_summarize(args): + out = Path(args.out).resolve() + config = json.loads((out / 'config.json').read_text()) + paths = resolve_paths(args, out) + summarize(out, config, paths) + print(f'wrote {out / "summary.md"}') + return 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = parser.add_subparsers(dest='command', required=True) + run = sub.add_parser('run', help='produce maps, play games and summarize') + run.add_argument('preset', help='preset name in tools/map-fairness/ or a path to a JSON preset') + run.add_argument('--out', help='output directory (default: artifacts/map-fairness/)') + run.add_argument('--name', help='run name, overriding the preset (also names the default output directory)') + run.add_argument('--fresh', action='store_true', help='discard earlier results in the output directory') + run.add_argument('--generators', help='comma-separated generator ids, overriding the preset') + run.add_argument('--map-seeds', help='e.g. 1001-1008 or 1001,1005') + run.add_argument('--games-per-rotation', type=int, help='engine seeds per map rotation') + run.add_argument('--size', type=int, help='map side in tiles (power of two)') + run.add_argument('--colonies', type=int) + run.add_argument('--tick-cap', type=int) + run.add_argument('--jobs', type=int, help='concurrent processes') + run.add_argument('--verify', type=int, help='re-run this many games to check reproducibility') + summarize_parser = sub.add_parser('summarize', help='recompute the summary of an existing run') + summarize_parser.add_argument('out', help='run directory') + for p in (run, summarize_parser): + p.add_argument('--bin', default=str(ROOT / 'build' / 'src' / 'glob2')) + p.add_argument('--study', default=str(ROOT / 'build' / 'src' / 'MapGeneratorStudy')) + args = parser.parse_args() + return command_run(args) if args.command == 'run' else command_summarize(args) + + +if __name__ == '__main__': + sys.exit(main()) From fab76ca8e1ee6bb23fd4e5a612949da04e37f9ef Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:31:23 -0400 Subject: [PATCH 061/136] Add on/off switch controls, shown as checkboxes, and save every generator option GeneratorControl gains a kind, Range or Toggle, with GeneratorControl::toggle() for switches and GeneratorControl::percentage() for the resource-amount controls to come (0 to 300 in steps of 25, default 100). The registry rejects a toggle that isn't exactly 0 to 1, and MapGeneratorStudy's catalog exports each control's kind. shared/Resources.h gains scaledCount() and scaledShare(), which return their input unchanged at 100. The custom game lobby draws a toggle as a checkbox row: click it, or press Space or Return while it has focus, and it takes part in Tab focus like any other control. The map editor's New Map screen shows toggles as check buttons, and closes up its rows for generators with many controls. Fjord's "Lake connects to fjords" becomes a toggle, keeping its id and meaning. Lobby preferences now save every control that has no legacy descriptor field (every newer generator's options, including Fjord's) in an optional options section; files written without it still load, with those controls at their defaults. This also fixes two existing bugs: - Fjord's lake size 50 reloaded as 30, because the legacy "use the default" sentinel was applied to every generator; it now applies only to the generators that predate the registry. - Fjord's lake size 0 or 90, or Ring world's lake density 0, made the whole settings file fail to load, because they lay outside the legacy fields' bounds; the bounds now cover every control. CustomGameSetupHarness tests both, round-trips every control's minimum and maximum, loads a file without the options section, ignores an unknown option and rejects out-of-range values. MapGeneratorDefaultsTest covers the registry contract for toggles and clicks every generator's checkbox in the editor; the harness toggles a lobby checkbox by click, Space and Return. Default maps are unchanged: all 225 default dumps (15 playable generators, 5 seeds, 128x128, 256x256 and 256x128) and their study and quality lines are byte-identical to 95e24a761's. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/custom-game-setup/README.md | 17 ++- docs/map-generators/ADDING_A_GENERATOR.md | 8 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 6 +- src/CustomGamePreferences.h | 55 ++++++++-- src/CustomGameScreen.cpp | 11 ++ src/LobbyControls.h | 26 +++++ src/NewMapScreen.cpp | 48 ++++++-- src/NewMapScreen.h | 10 +- .../LegacyGenerationDescriptor.cpp | 8 +- .../LegacyGenerationDescriptor.h | 3 + src/map/generator/core/GeneratorControls.cpp | 11 ++ src/map/generator/core/GeneratorControls.h | 13 +++ src/map/generator/core/GeneratorRegistry.cpp | 3 + .../generators/FjordContinentGenerator.cpp | 4 +- src/map/generator/shared/Resources.h | 9 ++ test/CustomGameSetupHarness.cpp | 103 +++++++++++++++++- test/MapGeneratorDefaultsTest.cpp | 95 +++++++++++++++- test/MapGeneratorStudy.cpp | 9 +- 18 files changed, 392 insertions(+), 47 deletions(-) diff --git a/docs/custom-game-setup/README.md b/docs/custom-game-setup/README.md index 3bc29bd26..56026f078 100644 --- a/docs/custom-game-setup/README.md +++ b/docs/custom-game-setup/README.md @@ -14,8 +14,10 @@ keeps the match summary and launch action available while dense content scrolls. drag or open choice menu. The displayed snapshot is the map that launches. Randomize, under the preview, rolls the same settings again with a new seed. - Expand Terrain, Resources and Layout to tune the applicable generator controls. - Reset to defaults, below them, returns width, height, colony count and the - landscape's own controls to their defaults, keeping the landscape itself. + Resource amounts are percentages of the landscape's own default (100). On/off + switches are checkbox rows: click one, or press Space or Return while it has + focus. Reset to defaults, below them, returns width, height, colony count and + the landscape's own controls to their defaults, keeping the landscape itself. Starting workers belong to Game Rules; premade maps retain authored units. - Each colony has a numbered color swatch, controller, AI/difficulty and team. You, AI, shared You + AI, and Closed are explicit choices. Shared control uses @@ -37,9 +39,14 @@ match. Returning to the lobby or restarting the app restores map mode and librar premade selection, all generator controls, controller/AI/team assignments (including hidden colonies), rules and expanded generator sections. Random mode creates a fresh preview using the saved parameters; temporary maps and seeds are -not stored as preferences. Missing premade maps retain the draft and show the -existing load error. Malformed or unsupported settings files fall back to the -normal four-player setup. Writes replace the old file atomically. +not stored as preferences. Generator controls that have a field in the legacy +map descriptor are saved there; every other control, including every switch and +resource amount, is saved in an `options` section after it. Files written before +that section existed still load, with those controls at their defaults, and an +option the game no longer has is ignored. Missing premade maps retain the draft +and show the existing load error. Malformed or unsupported settings files, or an +option value outside its control's range, fall back to the normal four-player +setup. Writes replace the old file atomically. Save/replay encodings are unchanged. Generated maps use owned temporary snapshots outside the map library; saves and replays remain self-contained after cleanup. diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index 3dd50b162..170a440de 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -18,7 +18,13 @@ A generator is a registered module, not a new branch in a central dispatch switc 3. Add that definition to the single list in `GeneratorRegistry::builtins()` and its source to `src/SConscript`. Add the display name and control labels to the translation tables using the existing translation workflow. 4. Write the callback as `bool generate(Game&, GenerationContext&)`. Read the immutable request, build into the supplied fresh game, set `context.stage` before operations that may fail, and return false with a useful `context.detail` if the layout cannot be placed. -Each `GeneratorControl` defines `id`, translation label, minimum, maximum, step, default, group, power-of-two display formatting and whether the value participates in terrain weighting. The all-zero-weight check reads this metadata, not generator-specific option names. IDs must be unique within a generator and cannot shadow shared controls. For irregular integer domains, supply a sorted, unique `allowedValues` list such as `{4, 8, 16}` with matching minimum and maximum. These are literal stored values; `powerOfTwo` instead formats stored exponents. Use `values()`, `indexOf()`, `valueAt()` and `displayValue()` in consumers, never selection-index arithmetic. Use `GeneratorControl::set()` for UI normalization. The service rejects malformed requests instead of silently changing their settings. +Each `GeneratorControl` defines `id`, translation label, minimum, maximum, step, default, group, power-of-two display formatting, whether the value participates in terrain weighting, and its `kind`. The all-zero-weight check reads this metadata, not generator-specific option names. IDs must be unique within a generator and cannot shadow shared controls. For irregular integer domains, supply a sorted, unique `allowedValues` list such as `{4, 8, 16}` with matching minimum and maximum. These are literal stored values; `powerOfTwo` instead formats stored exponents. Use `values()`, `indexOf()`, `valueAt()` and `displayValue()` in consumers, never selection-index arithmetic. Use `GeneratorControl::set()` for UI normalization. The service rejects malformed requests instead of silently changing their settings. + +A control's `kind` is `ControlKind::Range` unless it is an on/off switch. Declare a switch with `GeneratorControl::toggle(id, label, on, group)`: it stores 0 or 1, the lobby draws it as a checkbox row (click it, or press Space or Return while it has focus) and the editor as a check button, and the registry rejects a toggle with any other domain, step, allowed values, power-of-two formatting or terrain weight. Every 0/1 option is a toggle. A switch should change how a map feels without breaking its invariants: the map must still generate and pass the generator's own `validateWorld` in both states. + +Declare resource amounts with `GeneratorControl::percentage(id, label, maximum)`: 0 to `maximum` (300 unless given) in steps of 25, defaulting to 100, in the Resources group. The percentage scales the numbers that already decide that resource's amount — densities, clump sizes and counts, noise or fertility thresholds — and 100 must reproduce the unscaled map exactly: use `MapGeneration::scaledCount()` and `scaledShare()` from `shared/Resources.h`, which return their input unchanged at 100, and draw nothing from a random stream at 100 that the unscaled code didn't. Decide deliberately which placements are fairness guarantees (starter kits, reachability backstops, 1:1 guaranteed wheat and wood) and leave those unscaled, so every setting stays playable. Keep existing control ids and meanings; a generator that already exposes explicit amounts (Maze) keeps them. + +Lobby preferences save a control through its legacy descriptor field when it has one (`wheat`, `lake-size`, ...) and in the preferences' `options` section otherwise, so new controls persist with no further work. Shared settings are width and height exponents, colony count, starting workers and background terrain. The request's `options` map contains only controls for its selected generator. Generator-specific structs give these values meaningful names such as `lake_size`, `channel_width` and `bridge_width`; there is no reuse of unrelated descriptor slots. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index cbc8fac6a..06739a0a0 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -350,7 +350,9 @@ exactly two land neighbours. it is how a reviewer knows a seed's output isn't expected to match a prior build byte-for-byte. - `GeneratorControl`s are validated by `GeneratorRegistry`'s constructor: every control's default must land on a valid step from its minimum, and (`allowedValues` aside) `(maximum - minimum)` - must be evenly divisible by `step`. Every control label needs matching entries in + must be evenly divisible by `step`. A `ControlKind::Toggle` control must be exactly 0 to 1 in + steps of 1, with no allowed values, power-of-two formatting or terrain weight; the lobby and + editor show it as a checkbox. Every control label needs matching entries in `data/texts.en.txt` (`[Label]` / `Label`) and `data/texts.keys.txt` (`[Label]`), the same as any other UI string. @@ -361,7 +363,7 @@ exactly two land neighbours. [tuning] [headroom] [quality] [dump=path]`. `quality` reports `StartQualityReport`/ `ColonyQuality` per colony; `dump=` writes a plain-text terrain/resource grid for direct inspection or scripted flood-fill checks; `--catalog` dumps every registered generator's - controls as JSON. + controls as JSON, with each control's `kind` (`range` or `toggle`). - `test/MapGeneratorDefaultsTest.cpp` builds to `MapGeneratorDefaultsTest`, asserting the registry's and every control's contract: discrete domains, shape bounds, topology, home footprints, exact worker counts, seed repeatability and RNG stream isolation, and the diff --git a/src/CustomGamePreferences.h b/src/CustomGamePreferences.h index 871534463..4bf1f60b2 100644 --- a/src/CustomGamePreferences.h +++ b/src/CustomGamePreferences.h @@ -34,14 +34,13 @@ struct CustomGamePreferences { // Bounds are a safe envelope across every generator method's current // registry range for the reused legacy field (e.g. riverDiameter also - // stands in for lake size, channel width and bridge width; grassRatio - // also stands in for Isles' island size), widened from the original - // fixed 0-64 terrain-weight bound now that the registry allows 0-100 - // and Old Islands/Rugged Archipelago's island size reaches 70. This - // only needs to be at least as wide as what setMethodDefaults() and - // every registered control can produce; it is not a tight per-method - // bound, matching how this field list worked before the modular - // registry existed. + // stands in for river width, lake size - Fjord's reaches 0 and 90 - + // channel width and bridge width; craterDensity also stands in for + // Ring world's lake density, which reaches 0; grassRatio also stands + // in for Isles' island size). This only needs to be at least as wide + // as what setMethodDefaults() and every registered control can + // produce; it is not a tight per-method bound. Controls with no + // legacy field are saved in the options section instead. static const std::vector values = { #define FIELD(name, lo, hi) {#name, &MapGenerationDescriptor::name, lo, hi} FIELD(wDec, 6, 9), FIELD(hDec, 6, 9), @@ -49,8 +48,8 @@ struct CustomGamePreferences FIELD(grassRatio, 0, 100), FIELD(desertRatio, 0, 100), FIELD(wheatRatio, 0, 64), FIELD(woodRatio, 0, 64), FIELD(fruitRatio, 0, 64), FIELD(algaeRatio, 0, 64), - FIELD(stoneRatio, 0, 64), FIELD(riverDiameter, 1, 65), - FIELD(craterDensity, 1, 64), FIELD(extraIslands, 0, 8), + FIELD(stoneRatio, 0, 64), FIELD(riverDiameter, 0, 100), + FIELD(craterDensity, 0, 64), FIELD(extraIslands, 0, 8), FIELD(oldIslandSize, 1, 70), FIELD(oldBeach, 0, 4), FIELD(smooth, 1, 8), FIELD(nbTeams, 2, Team::MAX_COUNT), FIELD(nbWorkers, 1, 8) @@ -81,7 +80,16 @@ struct CustomGamePreferences out << f.name << ' ' << legacy.*(f.member) << '\n'; out << "resources"; for (auto value : legacy.resource) out << ' ' << value; - out << "\ncolonies\n"; + // Every control the legacy fields can't hold (newer generators' options, switches + // included). Files written before this section still load, with those at defaults. + std::vector options; + for (const auto &c : GenerationRequest::controls(setup.generator.method)) + if (!hasLegacyField(setup.generator.method, c.id)) + options.push_back(&c); + out << "\noptions " << options.size() << '\n'; + for (const auto *c : options) + out << c->id << ' ' << c->get(setup.generator) << '\n'; + out << "colonies\n"; for (const auto &c : setup.colonies) out << int(c.controller) << ' ' << int(c.ai) << ' ' << c.alliance << '\n'; out << "end\n"; @@ -135,7 +143,20 @@ struct CustomGamePreferences if (!number(parsed, 0, 64)) return false; value = parsed; } - if (!word("colonies")) return false; + std::vector> options; + std::string section; + if (!(in >> section)) return false; + if (section == "options") { + int count; + if (!number(count, 0, 256)) return false; + for (int i = 0; i < count; ++i) { + std::string id; + int value; + if (!(in >> id >> value)) return false; + options.emplace_back(id, value); + } + if (!word("colonies")) return false; + } else if (section != "colonies") return false; int humans = 0; for (auto &c : s.colonies) { int controller, ai; @@ -152,6 +173,16 @@ struct CustomGamePreferences draft.userMaps = user; legacy.logRepeatAreaTimes = repeat; s.generator = fromLegacyDescriptor(legacy, 0); + const auto &controls = GenerationRequest::controls(method); + for (const auto &[id, value] : options) { + const auto c = std::find_if(controls.begin(), controls.end(), + [&](const GeneratorControl &control) { return control.id == id; }); + // An option this build no longer has is dropped; a value outside its control's + // domain means the file is corrupt. + if (c == controls.end() || hasLegacyField(method, id)) continue; + if (c->normalize(value) != value) return false; + c->set(s.generator, value); + } if (s.random) s.capacity = s.generator.nbTeams; *this = draft; return true; diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 03ebf4d74..5e224a0c1 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -930,6 +930,17 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) continue; any = true; std::string id = "generator/" + c.id; + if (c.isToggle()) + { + ui.checkbox(id, {x, yy, leftW - 16, 28}, tr(c.label), c.get(g) != 0, + [this, c, changed](bool on) + { + c.set(setup.generator, on ? 1 : 0); + changed(); + }); + yy += 34; + continue; + } if (c.powerOfTwo || !c.allowedValues.empty()) { discrete(c, id); diff --git a/src/LobbyControls.h b/src/LobbyControls.h index 7042440d8..4da8116e9 100644 --- a/src/LobbyControls.h +++ b/src/LobbyControls.h @@ -133,6 +133,32 @@ class LobbyControls : public GAGGUI::RectangularWidget int fh = GAGCore::Toolkit::getFont(font)->getStringHeight("Ag"); text(r.x + 9, r.y + (r.h - fh) / 2, label, font, r.w - 18, !enabled || quiet); } + // A whole-row toggle: clicking anywhere on the row, or Space or Return while it has focus, + // flips it. + void checkbox(const std::string &id, SDL_Rect r, const std::string &label, bool checked, + std::function apply, bool enabled = true, + const char *font = "standard") + { + SDL_Rect clipping = clip(); + hits.push_back({id, r, clipping, [=] { apply(!checked); }, enabled, activeRegion}); + const int side = std::min(18, r.h - 6); + SDL_Rect mark{r.x + 5, r.y + (r.h - side) / 2, side, side}; + box(mark, checked ? gold : enabled ? panel : GAGCore::Color(222, 226, 212), 3); + surface()->drawRect(mark.x, mark.y, mark.w, mark.h, enabled ? ink : muted); + if (checked) + for (int t = 0; t < 2; ++t) + { + surface()->drawLine(mark.x + 4, mark.y + side / 2 + t, mark.x + side / 2 - 1, + mark.y + side - 5 + t, ink); + surface()->drawLine(mark.x + side / 2 - 1, mark.y + side - 5 + t, mark.x + side - 4, + mark.y + 4 + t, ink); + } + if (focus == id || + (!popup.open && inside(r, hoverX, hoverY) && inside(clipping, hoverX, hoverY))) + surface()->drawRect(r.x, r.y, r.w, r.h, focus == id ? ink : line); + int fh = GAGCore::Toolkit::getFont(font)->getStringHeight("Ag"); + text(mark.x + side + 9, r.y + (r.h - fh) / 2, label, font, r.w - side - 22, !enabled); + } void dropdown(const std::string &id, SDL_Rect r, const std::vector &options, int selected, std::function apply, const std::vector &enabled = {}, const std::string &help = "") diff --git a/src/NewMapScreen.cpp b/src/NewMapScreen.cpp index e3020a6b1..c158459ca 100644 --- a/src/NewMapScreen.cpp +++ b/src/NewMapScreen.cpp @@ -41,13 +41,23 @@ NewMapScreen::NewMapScreen(const GeneratorRegistry ®istry) : registry(registr auto addControl = [&](const GenerationRequest::Control &c, int method, int x, int y) { - auto *number = new Number(x, y, 114, 18, A, A, 18, "menu"); - for (int value : c.values()) - number->add(c.displayValue(value)); - addWidget(number); + Number *number = nullptr; + OnOffButton *toggle = nullptr; + if (c.isToggle()) + { + toggle = new OnOffButton(x, y, 18, 18, A, A, c.defaultValue != 0, TOGGLE); + addWidget(toggle); + } + else + { + number = new Number(x, y, 114, 18, A, A, 18, "menu"); + for (int value : c.values()) + number->add(c.displayValue(value)); + addWidget(number); + } auto *label = new Text(x + 120, y, A, A, "standard", tr(c.label)); addWidget(label); - controlWidgets.push_back({c, method, number, label}); + controlWidgets.push_back({c, method, number, toggle, label}); }; for (const auto &c : GenerationRequest::sharedControls()) { @@ -57,11 +67,14 @@ NewMapScreen::NewMapScreen(const GeneratorRegistry ®istry) : registry(registr } for (int m : registry.methods()) { + const auto &controls = registry.at(m).controls; + // Rows close up for generators with many controls, keeping clear of the OK and Cancel row. + const int pitch = std::min(20, 258 / std::max(1, int(controls.size()))); int y = 160; - for (const auto &c : registry.at(m).controls) + for (const auto &c : controls) { addControl(c, m, 310, y); - y += 20; + y += pitch; } } updateControls(); @@ -70,6 +83,11 @@ NewMapScreen::NewMapScreen(const GeneratorRegistry ®istry) : registry(registr addWidget(new Text(0, 18, ALIGN_FILL, A, "menu", tr("create map"))); } +Widget *NewMapScreen::ControlWidget::field() const +{ + return number ? static_cast(number) : static_cast(toggle); +} + void NewMapScreen::updateControls() { terrains->visible = descriptor.method == GenerationRequest::eUNIFORM; @@ -79,8 +97,12 @@ void NewMapScreen::updateControls() bool size = c.id == "width" || c.id == "height"; bool visible = widget.method == descriptor.method || (widget.method == -1 && (size || !terrains->visible)); - widget.number->visible = widget.label->visible = visible; - if (visible) + widget.field()->visible = widget.label->visible = visible; + if (!visible) + continue; + if (widget.toggle) + widget.toggle->setState(c.get(descriptor) != 0); + else widget.number->setNth(c.indexOf(c.get(descriptor))); } } @@ -100,10 +122,16 @@ void NewMapScreen::onAction(Widget *source, Action action, int par1, int par2) else if (action == NUMBER_ELEMENT_SELECTED) { for (const auto &widget : controlWidgets) - if (source == widget.number && widget.number->visible) + if (widget.number && source == widget.number && widget.number->visible) widget.definition.set(descriptor, widget.definition.valueAt(widget.number->getNth())); } + else if (action == BUTTON_STATE_CHANGED) + { + for (const auto &widget : controlWidgets) + if (widget.toggle && source == widget.toggle && widget.toggle->visible) + widget.definition.set(descriptor, widget.toggle->getState() ? 1 : 0); + } else if (action == LIST_ELEMENT_SELECTED) { if (source == terrains) diff --git a/src/NewMapScreen.h b/src/NewMapScreen.h index cc76b8436..dd9cf4ced 100644 --- a/src/NewMapScreen.h +++ b/src/NewMapScreen.h @@ -10,6 +10,7 @@ namespace GAGGUI { class Number; +class OnOffButton; class Text; class Ratio; class List; @@ -22,7 +23,8 @@ class NewMapScreen : public Glob2Screen enum { OK = 1, - CANCEL = 2 + CANCEL = 2, + TOGGLE = 3 }; public: @@ -33,9 +35,11 @@ class NewMapScreen : public Glob2Screen struct ControlWidget { GenerationRequest::Control definition; - int method; // -1 for shared controls - Number *number; + int method; // -1 for shared controls + Number *number; // range controls + OnOffButton *toggle; // toggle controls, shown as a check button Text *label; + Widget *field() const; }; std::vector controlWidgets; GenerationHistory history; diff --git a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp index f8dd61a19..c973ecff7 100644 --- a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp +++ b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp @@ -68,6 +68,10 @@ Sint32 D::*legacyField(int method, const std::string &id) return nullptr; } } // namespace +bool hasLegacyField(int method, const std::string &id) +{ + return id == "repeat" || legacyField(method, id) != nullptr; +} int GeneratorControl::get(const D &d) const { auto field = legacyField(d.method, id); @@ -138,7 +142,9 @@ GenerationRequest fromLegacyDescriptor(const D &d, std::uint32_t seed) for (const auto &c : D::controls(d.method)) { int value = c.get(d); - if (d.riverDiameter == 50) + // The historical "use the default" sentinel only exists in descriptors of the generators + // that predate the modular registry; for a newer one (Fjord's lake size) 50 is just 50. + if (d.riverDiameter == 50 && d.method <= D::eOLDISLANDS) { if (c.id == "lake-size") value = 30; diff --git a/src/map/generator/compatibility/LegacyGenerationDescriptor.h b/src/map/generator/compatibility/LegacyGenerationDescriptor.h index fe2cc565b..3f73db5a5 100644 --- a/src/map/generator/compatibility/LegacyGenerationDescriptor.h +++ b/src/map/generator/compatibility/LegacyGenerationDescriptor.h @@ -90,3 +90,6 @@ class MapGenerationDescriptor GenerationRequest fromLegacyDescriptor(const MapGenerationDescriptor &, std::uint32_t seed); MapGenerationDescriptor toLegacyDescriptor(const GenerationRequest &); +// Whether a generator's control is stored in the fixed legacy descriptor ("repeat" included); +// the options of generators added after it was frozen are not. +bool hasLegacyField(int method, const std::string &id); diff --git a/src/map/generator/core/GeneratorControls.cpp b/src/map/generator/core/GeneratorControls.cpp index b4196a5ad..3a3348933 100644 --- a/src/map/generator/core/GeneratorControls.cpp +++ b/src/map/generator/core/GeneratorControls.cpp @@ -5,6 +5,17 @@ #include "Team.h" #include #include +GeneratorControl GeneratorControl::toggle(std::string id, const char *label, bool on, + ControlGroup group) +{ + GeneratorControl c{std::move(id), label, 0, 1, 1, on ? 1 : 0, group}; + c.kind = ControlKind::Toggle; + return c; +} +GeneratorControl GeneratorControl::percentage(std::string id, const char *label, int maximum) +{ + return {std::move(id), label, 0, maximum, 25, 100, ControlGroup::Resources}; +} int GeneratorControl::normalize(int v) const { if (!allowedValues.empty()) diff --git a/src/map/generator/core/GeneratorControls.h b/src/map/generator/core/GeneratorControls.h index e7747615b..173fccaa8 100644 --- a/src/map/generator/core/GeneratorControls.h +++ b/src/map/generator/core/GeneratorControls.h @@ -11,6 +11,13 @@ enum class ControlGroup Layout, Shared }; +enum class ControlKind +{ + // A number from minimum to maximum in steps, or one of allowedValues. + Range, + // An on/off switch, stored as 0 or 1 and shown as a checkbox. + Toggle +}; struct GeneratorControl { std::string id; @@ -21,6 +28,12 @@ struct GeneratorControl bool terrainWeight = false; // Optional ordered domain, e.g. {4, 8, 16}; values are stored literally. std::vector allowedValues; + ControlKind kind = ControlKind::Range; + static GeneratorControl toggle(std::string id, const char *label, bool on, + ControlGroup group = ControlGroup::Layout); + // A resource amount as a percentage of the generator's own default, which is 100. + static GeneratorControl percentage(std::string id, const char *label, int maximum = 300); + bool isToggle() const { return kind == ControlKind::Toggle; } std::vector values() const; int indexOf(int value) const; int valueAt(int index) const; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 475b7f113..2321b3a50 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -42,6 +42,9 @@ GeneratorRegistry::GeneratorRegistry(std::vector values) c.allowedValues.end()) != c.allowedValues.end())) || (c.powerOfTwo && (c.minimum < 0 || c.maximum > 30)) || + (c.isToggle() && + (c.minimum != 0 || c.maximum != 1 || c.step != 1 || c.powerOfTwo || + c.terrainWeight || !c.allowedValues.empty())) || c.normalize(c.defaultValue) != c.defaultValue || !controls.insert(c.id).second) throw std::invalid_argument("Invalid generator control"); diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 47dc21d39..8f39eda1f 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -695,8 +695,8 @@ GeneratorDefinition fjordContinentDefinition() { ControlGroup::Terrain}, {"fjord-width", "Fjord width", 2, 10, 1, 4, ControlGroup::Terrain}, {"lake-size", "Lake size", 0, 90, 5, 45, ControlGroup::Terrain}, - {"lake-connected", "Lake connects to fjords", 0, 1, 1, 0, - ControlGroup::Terrain}, + GeneratorControl::toggle("lake-connected", "Lake connects to fjords", false, + ControlGroup::Terrain), {"resource-islands", "Resource islands", 0, 20, 1, 2, ControlGroup::Resources}}, generate}; diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h index efe5d9016..5a4553a8a 100644 --- a/src/map/generator/shared/Resources.h +++ b/src/map/generator/shared/Resources.h @@ -1,6 +1,15 @@ #pragma once #include "Regions.h" +#include namespace MapGeneration { +// A resource-amount percentage (GeneratorControl::percentage) applied to a count or a share, +// rounding to nearest. 100 returns the input unchanged, so a default map is exactly what it was. +inline std::int64_t scaledCount(std::int64_t count, int percent) { + return percent == 100 ? count : (count * percent + 50) / 100; +} +inline double scaledShare(double share, int percent) { + return percent == 100 ? share : share * percent / 100.0; +} struct ResourceDensities { int corn, wood, stone, algae, fruit; }; diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index f76733830..cc01e5851 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -105,6 +105,76 @@ struct CustomGameSetupHarness assert(!restored.setup.validation().empty()); std::cout << "PASS preferences round trip, hidden slots, bounds and corrupt-file recovery\n"; } + // Controls with no legacy descriptor field (every newer generator's, switches included) are + // saved in their own section, so a lobby restart keeps them. A file written before that + // section existed still loads, with those controls at their defaults. + static void preferencesOptions() + { + auto roundTrip = [](int method, const std::map &values) { + CustomGamePreferences draft; + draft.setup.random = true; + draft.setup.generatorHistory.select(draft.setup.generator, method); + for (const auto &[id, value] : values) + GenerationRequest::control(method, id).set(draft.setup.generator, value); + draft.setup.capacity = draft.setup.generator.nbTeams; + CustomGamePreferences reloaded; + const bool loaded = reloaded.decode(draft.encode()); + return loaded && reloaded.setup.generator.method == method && + reloaded.setup.generator.options == draft.setup.generator.options; + }; + const int fjord = GenerationRequest::eFJORDCONTINENT, ring = GenerationRequest::eRINGWORLD; + // Fjord's lake size shares the legacy field that held the old generators' "use the + // default" sentinel 50, and used to reload as 30. + assert(roundTrip(fjord, {{"lake-size", 50}})); + // Fjord's lake size 0 and 90, and Ring world's lake density 0, lie outside the legacy + // fields' old bounds and used to make the whole file fail to load. + assert(roundTrip(fjord, {{"lake-size", 0}}) && roundTrip(fjord, {{"lake-size", 90}})); + assert(roundTrip(ring, {{"lake-density", 0}})); + // Every control's whole range survives, whether it lives in a legacy field or not. + for (int method : GeneratorRegistry::builtins().methods(false)) + for (const auto &c : GenerationRequest::controls(method)) + assert(roundTrip(method, {{c.id, c.minimum}}) && roundTrip(method, {{c.id, c.maximum}})); + + CustomGamePreferences original; + original.setup.random = true; + original.setup.generatorHistory.select(original.setup.generator, fjord); + auto &g = original.setup.generator; + g.options["lake-connected"] = 1; + g.options["resource-islands"] = 7; + g.options["lake-size"] = 50; + original.setup.capacity = g.nbTeams; + const auto encoded = original.encode(); + CustomGamePreferences restored; + assert(restored.decode(encoded) && restored.encode() == encoded); + assert(restored.setup.generator.options == g.options); + + // The same file as an older build wrote it, without the options section. + const auto at = encoded.find("\noptions ") + 1, eol = encoded.find('\n', at); + const auto end = encoded.find("colonies\n"); + assert(at > 0 && eol < end); + assert(restored.decode(encoded.substr(0, at) + encoded.substr(end))); + GenerationRequest defaults; + defaults.setMethodDefaults(fjord); + for (const auto &c : GenerationRequest::controls(fjord)) + assert(c.get(restored.setup.generator) == (c.id == "lake-size" ? 50 : c.get(defaults))); + + // An option this build doesn't have is ignored; a value outside its domain is corrupt. + const int count = std::stoi(encoded.substr(at + 8, eol - at - 8)); + const auto withOption = [&](const std::string &line) { + return encoded.substr(0, at) + "options " + std::to_string(count + 1) + "\n" + line + + "\n" + encoded.substr(eol + 1); + }; + assert(restored.decode(withOption("retired-option 5"))); + assert(restored.setup.generator.options == g.options && restored.encode() == encoded); + for (const char *corrupt : {"lake-connected 2", "resource-islands 21", "coast-roughness -1"}) + { + assert(!restored.decode(withOption(corrupt))); + assert(restored.encode() == encoded); + } + assert(!restored.decode(encoded.substr(0, at) + "options 3\nlake-connected 1\n" + + encoded.substr(end))); + std::cout << "PASS preferences keep every generator option, and older files still load\n"; + } static void preferencesScreen(bool write) { auto *files = Toolkit::getFileManager(); @@ -370,10 +440,11 @@ struct CustomGameSetupHarness screen.listMaps(); auto paint = [&] { screen.dispatchPaint(false); }; - auto keyEvent = [&](SDL_Keycode key) { + auto keyEvent = [&](SDL_Keycode key, Uint16 modifiers = KMOD_NONE) { SDL_Event e = {}; e.type = SDL_KEYDOWN; e.key.keysym.sym = key; + e.key.keysym.mod = modifiers; screen.dispatchEvents(&e); paint(); }; @@ -654,6 +725,35 @@ struct CustomGameSetupHarness capture("rugged-archipelago-controls"); landscape(MapGenerationDescriptor::eOLDRANDOM); capture("shattered-coast-controls"); + // Switches are checkboxes: a click, or Space or Return on the focused row, flips one, and + // either edit invalidates the preview like any other generator control. + landscape(GenerationRequest::eFJORDCONTINENT); + screen.expanded[0] = screen.expanded[1] = screen.expanded[2] = true; + screen.onTimer(screen.previewDue); + assert(screen.validMap); + { + auto &options = screen.setup.generator.options; + const auto revision = screen.setup.mapRevision; + assert(options["lake-connected"] == 0); + clickControl("generator/lake-connected"); + assert(options["lake-connected"] == 1 && screen.setup.mapRevision != revision && + !screen.validMap && screen.previewPending); + assert(screen.controls->focus == "generator/lake-connected"); + keyEvent(SDLK_SPACE); + assert(options["lake-connected"] == 0); + keyEvent(SDLK_RETURN); + assert(options["lake-connected"] == 1); + // Tab walks off the checkbox and Shift+Tab back onto it, like any other control. + keyEvent(SDLK_TAB); + assert(screen.controls->focus != "generator/lake-connected"); + keyEvent(SDLK_TAB, KMOD_SHIFT); + assert(screen.controls->focus == "generator/lake-connected"); + screen.onTimer(screen.previewDue); + assert(screen.validMap); + capture("map-checkboxes"); + clickControl("generator/lake-connected"); + assert(options["lake-connected"] == 0); + } landscape(MapGenerationDescriptor::eRIVER); screen.activateGroup(screen.groups[0]); @@ -889,6 +989,7 @@ int main(int argc, char **argv) } Toolkit::getFileManager()->remove(CustomGamePreferences::filename); CustomGameSetupHarness::preferencesModel(); + CustomGameSetupHarness::preferencesOptions(); assert(SDLNet_Init() == 0); if (argc > 2 && std::string(argv[2]) == "ui") { diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 5b5a0bd91..fb01a4bec 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -14,8 +14,10 @@ #include "StartingPositions.h" #include "Unit.h" #include "Utilities.h" +#include #include #include +#include #include #include #include @@ -46,15 +48,39 @@ class MapGeneratorDefaultsTest static void edit(NewMapScreen &s, const char *label, int value) { for (auto &w : s.controlWidgets) - if (w.number->visible && std::strcmp(w.definition.label, label) == 0) + { + if (!w.field()->visible || std::strcmp(w.definition.label, label) != 0) + continue; + if (w.toggle) + { + assert(value == 0 || value == 1); + w.toggle->setState(value != 0); + s.onAction(w.toggle, GAGGUI::BUTTON_STATE_CHANGED, NewMapScreen::TOGGLE, value); + } + else { w.number->setNth(w.definition.indexOf(value)); s.onAction(w.number, GAGGUI::NUMBER_ELEMENT_SELECTED, 0, 0); - assert(w.definition.get(s.descriptor) == value); - return; } + assert(w.definition.get(s.descriptor) == value); + return; + } assert(false); } + // Presses and releases the mouse over a widget, the way a player clicks it. + static void click(NewMapScreen &s, GAGGUI::Widget *widget) + { + const auto box = static_cast(widget)->getScreenRect(); + for (Uint32 type : {Uint32(SDL_MOUSEBUTTONDOWN), Uint32(SDL_MOUSEBUTTONUP)}) + { + SDL_Event event = {}; + event.type = type; + event.button.button = SDL_BUTTON_LEFT; + event.button.x = box.x + box.w / 2; + event.button.y = box.y + box.h / 2; + s.dispatchEvents(&event); + } + } static std::uint64_t fingerprint(const Game &game) { std::uint64_t hash = 14695981039346656037ull; @@ -199,11 +225,14 @@ class MapGeneratorDefaultsTest false, false, {4, 8, 16}}, - {"test-gap", "Channel width", 1, 5, 2, 3, ControlGroup::Layout}}, + {"test-gap", "Channel width", 1, 5, 2, 3, ControlGroup::Layout}, + GeneratorControl::toggle("test-switch", "Lake connects to fjords", + false)}, [](Game &game, GenerationContext &context) { const int elevation = context.request.option("test-elevation"); assert(elevation == 8); + assert(context.request.option("test-switch") == 1); game.map.makeHomogenMap(GRASS); for (int team = 0; team < context.request.nbTeams; ++team) { @@ -212,16 +241,51 @@ class MapGeneratorDefaultsTest } return MapGeneration::placeStarts(game, context); }}); + // A toggle is exactly 0 or 1, shown as a checkbox: any other domain is a registration error. + { + const auto rejected = [](GeneratorControl control) + { + GeneratorDefinition definition{"test-toggle", 102, "uniform terrain", 1, false, + {std::move(control)}, + [](Game &, GenerationContext &) { return false; }}; + try + { + GeneratorRegistry({definition}); + } + catch (const std::invalid_argument &) + { + return true; + } + return false; + }; + const auto on = GeneratorControl::toggle("switch", "Lake connects to fjords", true); + assert(on.isToggle() && on.defaultValue == 1 && on.values() == std::vector({0, 1})); + assert(!rejected(on)); + const auto amount = GeneratorControl::percentage("amount", "Fruit"); + assert(!amount.isToggle() && amount.defaultValue == 100 && !rejected(amount)); + std::vector broken(6, on); + broken[0].maximum = 2; + broken[1].minimum = broken[1].defaultValue = 1; + broken[2].powerOfTwo = true; + broken[3].terrainWeight = true; + broken[4].allowedValues = {0, 1}; + broken[5].defaultValue = 2; + for (const auto &control : broken) + assert(rejected(control)); + } GeneratorRegistry registry(std::move(definitions)); NewMapScreen screen(registry); select(screen, 101); assert(screen.descriptor.option("test-elevation") == 6); + assert(screen.descriptor.option("test-switch") == 0); edit(screen, "Island size", 16); edit(screen, "Channel width", 5); + edit(screen, "Lake connects to fjords", 1); select(screen, D::eRIVER); select(screen, 101); assert(screen.descriptor.option("test-cell") == 16 && - screen.descriptor.option("test-gap") == 5); + screen.descriptor.option("test-gap") == 5 && + screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); assert(registry.selectionIndex(101) == 16); const auto playable = registry.methods(false); @@ -355,7 +419,7 @@ class MapGeneratorDefaultsTest assert(decoded.setData(encoded.getData(), encoded.getDataLength())); sameControls(fromLegacyDescriptor(decoded, 0), expected); } - if (output && m >= 4 && m <= 8) + if (output && ((m >= 4 && m <= 8) || m == D::eFJORDCONTINENT)) { s.gfx->drawFilledRect(0, 0, 640, 480, GAGCore::Color(34, 55, 42)); for (auto *w : s.widgets) @@ -365,6 +429,25 @@ class MapGeneratorDefaultsTest assert(IMG_SavePNG(s.gfx->getSDLSurface(), path.c_str()) == 0); } } + // Every switch is a check button in the editor, and clicking one flips the request's value. + for (int m : GeneratorRegistry::builtins().methods()) + { + select(s, m); + for (auto &w : s.controlWidgets) + if (w.method == m) + { + assert(w.definition.isToggle() == bool(w.toggle) && bool(w.number) != bool(w.toggle)); + assert(w.field()->visible && w.label->visible); + if (!w.toggle) + continue; + const int before = w.definition.get(s.descriptor); + assert(w.toggle->getState() == (before != 0)); + click(s, w.toggle); + assert(w.definition.get(s.descriptor) == 1 - before && w.toggle->getState() == !before); + click(s, w.toggle); + assert(w.definition.get(s.descriptor) == before); + } + } select(s, D::eRIVER); edit(s, "Water weight", 37); edit(s, "Width", 8); diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 577654b18..9b7c3d62e 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -45,11 +45,12 @@ int main(int argc, char **argv) for (size_t i = 0; i < controls.size(); ++i) { const auto &c = controls[i]; - std::printf("%s{\"id\":\"%s\",\"label\":\"%s\",\"min\":%d,\"max\":%d,\"step\":%d," - "\"default\":%d," + std::printf("%s{\"id\":\"%s\",\"label\":\"%s\",\"kind\":\"%s\",\"min\":%d,\"max\":%d," + "\"step\":%d,\"default\":%d," "\"group\":%d,\"powerOfTwo\":%s,\"values\":[", - i ? "," : "", c.id.c_str(), c.label, c.minimum, c.maximum, c.step, - c.defaultValue, int(c.group), c.powerOfTwo ? "true" : "false"); + i ? "," : "", c.id.c_str(), c.label, c.isToggle() ? "toggle" : "range", + c.minimum, c.maximum, c.step, c.defaultValue, int(c.group), + c.powerOfTwo ? "true" : "false"); const auto domain = c.values(); for (size_t j = 0; j < domain.size(); ++j) std::printf("%s%d", j ? "," : "", domain[j]); From 7da834a2ad6bcc4bff684e26f96e9b0ce77338f5 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:38:58 -0400 Subject: [PATCH 062/136] Fix tournament summaries and adjudicate stalled games by population - Keep each map's parsed start quality under colony_quality. The colony count was overwriting it, which crashed the summary. - At the tick cap, rank survivors by prestige, then units, then finished buildings, and record which criterion decided the game. On stalled maps every survivor had zero prestige, so a 4-to-3 building count had been deciding. Summaries recompute outcomes from each game's stored engine output, so rule changes apply without replaying games. - MapGeneratorStudy now checks that a reloaded map re-saves byte for byte, and GLOB2_STUDY_EXPLAIN dumps both saves. A freshly generated map and its reloaded copy differ only in the header's 20-byte content SHA1, which the fresh Game computes over a map offset of 0 before patching in the real offset. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 9 +++-- test/MapGeneratorStudy.cpp | 36 ++++++++++++++++-- tools/map_fairness_tournament.py | 44 +++++++++++++++------- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index 652b16366..87b612d8b 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -96,9 +96,12 @@ own team-index bias with no map effect at all. ## Adjudication A game ends when one colony has defeated every other, when total prestige reaches the lobby -threshold (the highest-prestige colony wins), or at the tick cap. At the cap, the surviving colony -with the most prestige wins, then the one with more finished buildings, then more units. That is -the engine's own prestige victory applied to an unfinished game. An exact tie stays unresolved and +threshold (the highest-prestige colony wins), or at the tick cap. At the cap, the surviving colonies +are ranked by prestige, then population (units), then finished buildings, and the top one wins. +Prestige comes first because it is the engine's own victory measure. Colonies that stall often +have no prestige at all, and population is then the clearest sign of which one is ahead. +`games.csv` records what decided each capped game (`cap-prestige`, `cap-units`, `cap-buildings` or +`cap-sole-survivor`), and the report also gives the bias counting decisive games only. An exact tie stays unresolved and is left out of win counts. The report gives the share of games decided at the cap. Placements rank the winner first, other survivors by prestige, then eliminated colonies, the last eliminated first. diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 61309f225..439137f3f 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -13,6 +13,7 @@ #include "Utilities.h" #include #include +#include #include #include #include @@ -55,6 +56,18 @@ std::string saveMapBytes(Game &game, const std::string &name) return std::string(backend->getBuffer(), backend->getPosition()); } +// The same save as named text fields, for diffing two saves of one map. A text stream cannot +// seek, so the header Game::save rewrites once the map offset is known is appended a second time. +std::string saveMapText(Game &game, const std::string &name) +{ + auto *backend = new GAGCore::MemoryStreamBackend(); + GAGCore::TextOutputStream stream(backend); // owns the backend + game.save(&stream, true, name); + stream.flush(); + backend->seekFromEnd(0); + return std::string(backend->getBuffer(), backend->getPosition()); +} + bool loadMapBytes(Game &game, const std::string &bytes) { // The backend's constructor copies the bytes in by writing them, which leaves it at the end. @@ -249,7 +262,24 @@ int saveRotatedMaps(Game &game, const GenerationResult &result, const Generation if (!loadMapBytes(canonical, generated)) return 5; const std::string first = saveMapBytes(canonical, label(0)); + // A freshly generated Game and the same map read back from its file save identically except + // for the header's content SHA1: a fresh Game still holds map offset 0 when Game::save hashes + // its first header write, before the real offset is patched in. Games only ever start from + // the file, so rotations work from the reloaded form, which must re-save byte for byte. const bool stable = first == generated; + bool idempotent = false; + { + Game again(nullptr); + idempotent = loadMapBytes(again, first) && saveMapBytes(again, label(0)) == first; + } + // GLOB2_STUDY_EXPLAIN= writes both saves as named fields, to see where they differ. + if (const char *explain = std::getenv("GLOB2_STUDY_EXPLAIN")) + { + std::ofstream(std::string(explain) + "-generated.map", std::ios::binary) << generated; + std::ofstream(std::string(explain) + "-reloaded.map", std::ios::binary) << first; + std::ofstream(std::string(explain) + "-generated.txt") << saveMapText(game, label(0)); + std::ofstream(std::string(explain) + "-reloaded.txt") << saveMapText(canonical, label(0)); + } const std::uint64_t world = worldHash(canonical); std::vector colonies; for (int t = 0; t < n; ++t) @@ -290,9 +320,9 @@ int saveRotatedMaps(Game &game, const GenerationResult &result, const Generation return 3; previous = bytes; } - std::printf("ROTATIONS,%d,%d,%d,%d,%d,%d\n", n, rotations, int(stable), int(consistent), - int(placed), int(roundTrip)); - return consistent && placed && roundTrip ? 0 : 6; + std::printf("ROTATIONS,%d,%d,%d,%d,%d,%d,%d\n", n, rotations, int(stable), int(consistent), + int(placed), int(roundTrip), int(idempotent)); + return consistent && placed && roundTrip && idempotent ? 0 : 6; } } // namespace int main(int argc, char **argv) diff --git a/tools/map_fairness_tournament.py b/tools/map_fairness_tournament.py index ea92bb418..061016360 100644 --- a/tools/map_fairness_tournament.py +++ b/tools/map_fairness_tournament.py @@ -156,7 +156,7 @@ def map_key(method, seed): def parse_study(stdout): - record = {'colonies': [], 'starts': [], 'files': [], 'options': {}} + record = {'colony_quality': [], 'starts': [], 'files': [], 'options': {}} for line in stdout.splitlines(): parts = line.strip().split(',') tag = parts[0] @@ -170,7 +170,7 @@ def parse_study(stdout): 'fairness': float(parts[3]), 'worst': float(parts[4]), 'best': float(parts[5])} elif tag == 'COLONY': - record['colonies'].append(dict(zip(COLONY_FIELDS, (float(v) for v in parts[2:])))) + record['colony_quality'].append(dict(zip(COLONY_FIELDS, (float(v) for v in parts[2:])))) elif tag == 'REQUEST': record['request'] = {'generator_id': parts[1], 'revision': int(parts[2]), 'seed': int(parts[3]), 'wDec': int(parts[4]), 'hDec': int(parts[5]), @@ -185,7 +185,7 @@ def parse_study(stdout): elif tag == 'ROTATIONS': record['rotation_checks'] = dict(zip( ['teams', 'rotations', 'save_load_stable', 'references_consistent', - 'colonies_placed', 'round_trip'], map(int, parts[1:7]))) + 'colonies_placed', 'round_trip', 'reload_idempotent'], map(int, parts[1:8]))) return record @@ -236,7 +236,8 @@ def produce_map(config, paths, generator, seed): if exit_code == 4 or (exit_code == 0 and not record.get('generated', False)): record.update(ok=False, failure='generation') elif exit_code != 0 or not checks or not all( - checks[k] for k in ('references_consistent', 'colonies_placed', 'round_trip')): + checks.get(k) for k in ('references_consistent', 'colonies_placed', 'round_trip', + 'reload_idempotent')): record.update(ok=False, failure=f'study exit {exit_code}') elif len(record['starts']) != colonies or len(record['files']) != config['rotation_count']: record.update(ok=False, failure='incomplete study output') @@ -296,7 +297,10 @@ def adjudicate(game, record, config): cap = int(config['tick_cap']) winners = [t for t, e in teams.items() if e['result'] == 'won'] alive = [t for t, e in teams.items() if e['alive']] - strength = lambda t: (teams[t]['prestige'], teams[t]['buildings'], teams[t]['units']) + # At the cap: prestige (the engine's own victory measure), then population, then finished + # buildings. Stalled colonies often have no prestige at all, so population usually decides. + criteria = ('prestige', 'units', 'buildings') + strength = lambda t: tuple(teams[t][c] for c in criteria) if end['winner_team'] >= 0: outcome['end_reason'] = game['parsed']['reason'] or 'elimination' winner = end['winner_team'] @@ -304,9 +308,13 @@ def adjudicate(game, record, config): elif end['ticks'] >= cap: outcome['end_reason'] = 'cap' ranked = sorted(alive, key=strength, reverse=True) - if ranked and (len(ranked) == 1 or strength(ranked[0]) != strength(ranked[1])): + if len(ranked) == 1: winner = ranked[0] - outcome['adjudication'] = 'cap-prestige' + outcome['adjudication'] = 'cap-sole-survivor' + elif ranked and strength(ranked[0]) != strength(ranked[1]): + winner = ranked[0] + decided_by = next(c for c, a, b in zip(criteria, strength(ranked[0]), strength(ranked[1])) if a != b) + outcome['adjudication'] = f'cap-{decided_by}' else: winner = None outcome['adjudication'] = 'cap-tie' if ranked else 'cap-none-alive' @@ -636,7 +644,8 @@ def analyse_map(record, games, config): decisive[outcome['winner_slot']] += 1 for slot, place in outcome['placement'].items(): placement_points[int(slot)] += (n - place) / (n - 1) if n > 1 else 1.0 - quality = [c['total'] for c in record['colonies']] if len(record['colonies']) == n else None + rows = record.get('colony_quality', []) + quality = [c['total'] for c in rows] if len(rows) == n else None analysis = { 'key': record['key'], 'method': record['method'], 'map_seed': record['map_seed'], 'chosen_seed': record.get('chosen_seed'), 'games': len(games), 'played': len(played), @@ -653,7 +662,7 @@ def analyse_map(record, games, config): 'placement_score': [p / len(played) if played else None for p in placement_points], 'quality': quality, 'quality_score': record.get('quality', {}).get('score'), 'starts': record['starts'], - 'record_colonies': record['colonies'] if len(record['colonies']) == n else [], + 'record_colonies': rows if len(rows) == n else [], } analysis['quality_spearman'] = spearman(quality, wins) if quality and sum(wins) and n >= 3 else None return analysis @@ -787,6 +796,11 @@ def load_catalog(study): def summarize(out, config, paths): records, games = load_results(out) + # Outcomes are recomputed from each game's stored engine output, so a change to the + # adjudication rule applies to earlier games without replaying them. + for game in games: + if game['map'] in records: + game['outcome'] = adjudicate(game, records[game['map']], config) catalog = load_catalog(paths['study']) by_map = {} for game in games: @@ -865,7 +879,8 @@ def write_csvs(out, records, games): n = record['colonies'] for team, entry in sorted(g['parsed']['teams'].items(), key=lambda i: int(i[0])): slot = (int(team) - g['rotation']) % n - quality = record['colonies'][slot] if len(record['colonies']) == n else {} + rows = record.get('colony_quality', []) + quality = rows[slot] if len(rows) == n else {} writer.writerow([record['method'], record['map_seed'], g['rotation'], g['seed'], slot, team, *record['starts'][slot], g['outcome']['placement'].get(str(slot)), int(g['outcome']['winner_slot'] == slot), @@ -881,7 +896,7 @@ def write_csvs(out, records, games): int(bool(record.get('ok'))), record.get('failure'), record.get('quality', {}).get('score'), record.get('quality', {}).get('fairness'), ';'.join(f'{x}:{y}' for x, y in record.get('starts', [])), - ';'.join(f'{c["total"]:.3f}' for c in record.get('colonies', [])), + ';'.join(f'{c["total"]:.3f}' for c in record.get('colony_quality', [])), next((f['fnv1a64'] for f in record.get('files', []) if f['rotation'] == 0), None), record.get('generation_seconds')]) @@ -923,9 +938,10 @@ def markdown(summary, config): + ('' if summary['balanced'] else ' **Not rotation-balanced: start position and team index are confounded.**'), f'- {summary["played"]} of {summary["games"]} games completed ({ends}); wall time per game mean ' f'{num(wall["mean"], 1)} s, median {num(wall["median"], 1)} s, max {num(wall["max"], 1)} s ' - f'({num(wall["per_1000_ticks"], 2)} s per 1000 ticks); median game length {summary["ticks"]["median"]} ticks.', - '- A game that reaches the tick cap goes to the surviving colony with the most prestige ' - '(then buildings, then units); an exact tie stays unresolved and is left out of win counts.', + f'({num(wall["per_1000_ticks"], 2)} s per 1000 ticks); median game length ' + f'{num(summary["ticks"]["median"], 0)} ticks.', + '- A game that reaches the tick cap goes to the surviving colony with the most prestige, then ' + 'units, then finished buildings; an exact tie stays unresolved and is left out of win counts.', ] verification = summary.get('verification') if verification: From e2616135cda4ab0ea8f149bcc09d91738db38860 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:40:47 -0400 Subject: [PATCH 063/136] Report tournament bias from decisive games and size the smoke preset - Each generator's headline now also gives the position bias counting only games won outright (elimination or prestige), with its bootstrap interval and the number of such games, so tick-cap adjudication cannot drive it. - The smoke preset plays three 128x128 maps per generator with every rotation twice: 96 games, about 15 minutes at 3 jobs, based on the measured ~27 s per 128x128 game with three games running. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- tools/map-fairness/smoke.json | 6 +++--- tools/map_fairness_tournament.py | 25 ++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/tools/map-fairness/smoke.json b/tools/map-fairness/smoke.json index bfd96f0e0..04c47b25e 100644 --- a/tools/map-fairness/smoke.json +++ b/tools/map-fairness/smoke.json @@ -1,9 +1,9 @@ { "name": "smoke", - "description": "Quick check that the tournament runs end to end: the Symmetric arena baseline and three other generators, two maps each, every rotation once.", + "description": "Quick fairness check: the Symmetric arena baseline and three other generators, three 128x128 maps each, every rotation played twice (96 games, about 15 minutes at 3 jobs).", "generators": [15, 9, 13, 4], - "map_seeds": "1001-1002", - "games_per_rotation": 1, + "map_seeds": "1001-1003", + "games_per_rotation": 2, "rotations": "all", "size": 128, "colonies": 4, diff --git a/tools/map_fairness_tournament.py b/tools/map_fairness_tournament.py index 061016360..67dddbd48 100644 --- a/tools/map_fairness_tournament.py +++ b/tools/map_fairness_tournament.py @@ -721,6 +721,21 @@ def rho(sample_keys, column): return check +def decisive_headline(maps, n, draws, seed): + """Position bias from games won outright (elimination or prestige), leaving out capped games.""" + usable = [m for m in maps if m['decisive_slot']['n'] >= 2] + biases = [m['decisive_slot']['squared_bias'] for m in usable] + mean_bias = statistics.mean(biases) if biases else None + return { + 'decisive_maps': len(usable), + 'decisive_games': sum(m['decisive_slot']['n'] for m in maps), + 'decisive_rms_points': rms_points(mean_bias, n), + 'decisive_rms_points_ci': bootstrap(biases, lambda s: rms_points(statistics.mean(s), n), draws, seed) + if biases else None, + 'decisive_maps_p05': sum(m['decisive_slot']['p'] < 0.05 for m in usable), + } + + def analyse_generator(method, maps, config, catalog): n = int(config['colonies']) draws = int(config['bootstrap_draws']) @@ -767,6 +782,8 @@ def analyse_generator(method, maps, config, catalog): 'start_mismatches': sum(m['start_mismatches'] for m in maps), 'headline_rms_points': rms_points(mean_bias, n), 'headline_rms_points_ci': ci, 'mean_squared_bias': mean_bias, + # The same headline counting only games won outright, so cap adjudication cannot drive it. + **decisive_headline(maps, n, draws, seed + 19), 'maps_p05': sum(p < 0.05 for p in p_values), 'maps_bh05': sum(q < 0.05 for q in bh), 'maps_holm05': sum(h < 0.05 for h in hl), 'expected_p05_by_chance': 0.05 * len(p_values), @@ -953,14 +970,16 @@ def markdown(summary, config): '(95% bootstrap interval over maps). Biased maps counts maps whose wins by start reject a ' 'uniform split: raw p < 0.05, and after Benjamini-Hochberg across that generator\'s maps. ' 'Scorer rho is the within-map rank correlation between a colony\'s start-quality score and its ' - 'win share.', '', - '| Generator | Maps | Games | Cap | Position bias (pp) | Biased maps p<.05 / BH | Best start / fair (median) | Any bias p | Scorer rho (wins) |', - '| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | --- |'] + 'win share. Decisive only repeats the position bias counting just games won outright, with the ' + 'number of such games, so tick-cap adjudication cannot drive it.', '', + '| Generator | Maps | Games | Cap | Position bias (pp) | Decisive only (pp, games) | Biased maps p<.05 / BH | Best start / fair (median) | Any bias p | Scorer rho (wins) |', + '| --- | ---: | ---: | ---: | --- | --- | --- | ---: | ---: | --- |'] for g in summary['generators']: scorer = (g['scorer'] or {}).get('total') or {} lines.append( f'| {g["name"]} ({g["method"]}) | {g["maps_with_winners"]}/{g["maps"]} | {g["played"]} | {pct(g["cap_share"], 0)} ' f'| {num(g["headline_rms_points"], 1)}{interval(g["headline_rms_points_ci"])} ' + f'| {num(g["decisive_rms_points"], 1)}{interval(g["decisive_rms_points_ci"])} ({g["decisive_games"]}) ' f'| {g["maps_p05"]} / {g["maps_bh05"]} (chance {g["expected_p05_by_chance"]:.1f}) ' f'| {num((g["dominance"] or {}).get("median"))} | {pval(g["pooled_p"])} ' f'| {num(scorer.get("wins_rho"))}{interval(scorer.get("wins_rho_ci"), lambda v: f"{v:.2f}")} |') From 741afdd9c25aa736e99c39ece8db256b4b91f3b1 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:52:56 -0400 Subject: [PATCH 064/136] Explain which map form the fairness tournament plays Rotation 0 is the generated map read back from its own file, which is what every game loads. It differs from a direct save of the freshly generated Game only in the header's 20-byte content SHA1. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index 87b612d8b..c190399b7 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -60,6 +60,9 @@ Everything is seeded, so the same preset reproduces the same maps, games and rep masks). The tool checks every rotation from its saved bytes: the terrain is unchanged, each colony sits under its new team number, every tile reference resolves, and relabelling N times reproduces rotation 0 byte for byte. In the games, each team's reported start must match too. + Rotation 0 is the generated map as read back from its own file, which is what every game + loads. It differs from a direct save of the freshly generated `Game` only in the header's + 20-byte content SHA1, which the fresh `Game` computes before the real map offset is patched in. 3. **Matches.** Each rotation is played `games_per_rotation` times with `glob2 -test-games-nox 1 --map --matchup nicowar,...`, with the engine seed pinned by `GLOB2_TEST_SEED` and nothing else varied. Every game runs in its own `GLOB2_USER_DIR`, which From 8b3e86e862b4be45eb23053bff7de9b707816308 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:54:34 -0400 Subject: [PATCH 065/136] Document measured fairness tournament cost Per-game wall time with four Nicowar colonies and three games at a time: 128x128 averages 26.9 s (4 of 8 games reached the 90,000-tick cap), 256x256 averages 288.7 s (all 8 reached it). The presets stay at 128x128, where games still end outright. The standard preset is about 5.5 hours at 3 jobs, and the doc gives the 256x256 alternatives and their cost. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 27 +++++++++++++++++++++- tools/map-fairness/standard.json | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index c190399b7..40a63640e 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -148,7 +148,32 @@ colony index (does the generator favour a colony it places early or late?) and w ## Cost - +Measured with a release build on an 8-core arm64 Mac shared with another build: four Nicowar +colonies, three games at a time and the default 90,000-tick cap (60 minutes of game time), on one +Symmetric arena and one Contested commons map, every rotation once. + +| Map | Games | Wall time per game: mean / median / range | Seconds per 1,000 ticks | Reached the cap | +| --- | ---: | --- | ---: | ---: | +| 128x128 | 8 | 26.9 / 24.1 / 9.3-59.3 s | 0.38 | 4 of 8 | +| 256x256 | 8 | 288.7 / 279.6 / 233.2-357.4 s | 3.21 | 8 of 8 | + +Generating a map with its five lobby rolls and every rotation check takes about a second, so games +are the whole cost. A run takes about `games x mean time per game / jobs`: + +| Run | Games | Projected wall time at 3 jobs | +| --- | ---: | --- | +| `smoke` (4 generators x 3 maps x 4 rotations x 2 seeds, 128x128) | 96 | about 15 minutes | +| `standard` (15 generators x 12 maps x 4 rotations x 3 seeds, 128x128) | 2,160 | about 5.5 hours | +| `standard --size 256` | 2,160 | about 58 hours | +| `standard --size 256 --map-seeds 2001-2004 --games-per-rotation 1` | 240 | about 6.5 hours | + +Game length varies by generator (a map where colonies stall reaches the cap sooner in wall time +than one they build out), and the machine was shared, so treat these as rough. + +The presets use 128x128 because at 256x256 no game finished within 90,000 ticks. Every 256x256 +result is then an adjudication: colonies were almost all still alive, several often shared the +same prestige, and population decided. A higher `--tick-cap` gets more games won outright, at a +proportional cost in wall time. ## Limits diff --git a/tools/map-fairness/standard.json b/tools/map-fairness/standard.json index b1a0249af..d8022671a 100644 --- a/tools/map-fairness/standard.json +++ b/tools/map-fairness/standard.json @@ -1,6 +1,6 @@ { "name": "standard", - "description": "Standard fairness tournament: every playable generator, twelve maps each, every rotation played by three engine seeds.", + "description": "Standard fairness tournament: every playable generator, twelve 128x128 maps each, every rotation played by three engine seeds (2,160 games, about 5.5 hours at 3 jobs).", "generators": [15, 9, 11, 12, 7, 6, 13, 14, 16, 8, 5, 4, 3, 1, 2], "map_seeds": "2001-2012", "games_per_rotation": 3, From aa2690f7c00411a9cf7e5b4b1bf00985165ff77e Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:55:49 -0400 Subject: [PATCH 066/136] Wrap boot tiles before guaranteeStartingResources floods from them The height-field generators fall back to their older largest-meadow site search when no set of sites can reach both wheat and wood, and that search can leave a boot tile past the map's edge. placeStarts wraps it onto the map afterwards, but guaranteeStartingResources runs first and indexed its flood buffers with the raw tile, writing outside them. On a map with no wood at all every seed takes that path: River seed 11 without wood crashed with SIGBUS on some runs, from heap corruption in floodReach. guaranteeStartingResources now wraps each boot tile itself. That is a no-op for any tile already on the map: all 225 default dumps (15 playable generators, 5 seeds, 128x128, 256x256 and 256x128) and their study and quality lines are byte-identical to 95e24a761's. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- src/map/generator/shared/Resources.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 5b5be29e0..545c9b303 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -475,7 +475,10 @@ void guaranteeStartingResources(Game &game, GenerationContext &context, int whea // exact wall tiles are cleared, so a genuinely small spot is correctly left untouched. const int minPocketTiles = 60; for (int team = 0; team < context.request.nbTeams; ++team) { - int bootX = context.bootX[team], bootY = context.bootY[team]; + // A caller can hand over a boot tile it has not wrapped onto the map yet: the height-field + // generators' fallback site search does, before placeStarts normalizes it. The floods below + // index by it directly. + int bootX = map.normalizeX(context.bootX[team]), bootY = map.normalizeY(context.bootY[team]); ReachResult reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); auto pocketSize = [&] { int n = 0; From 017770c34e2eadcccd5c3c725a1b1e1780e6638e Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:55:49 -0400 Subject: [PATCH 067/136] Add resource amounts and a hilltop stone switch to the height-field generators Swamp, River, Islands and Crater lakes gain Wheat, Wood, Stone and Algae amounts (percentages, 0 to 300 in steps of 25, default 100). Each scales its own band of the shared height field, which used to be fixed shares of it: algae the lowest sixth of the water, stone the third of the farmland share just above the beach, and wheat and wood the rest, now as two separate bands. No band reaches past the top of the grass. The existing Fruit control still counts fruit groves. Stone on hilltops (off) puts the stone band on the highest grass instead, so the shore goes straight to farmland. River gains Winding river (on); off, the river runs straight across the map. With no wheat or wood left, sites can't be ranked by their walk to both, so colonies take the older largest-meadow search, and the reachability backstop still gives every colony both within reach. Default maps are byte-identical to 95e24a761's (225 dumps: 15 generators, 5 seeds, 128x128, 256x256, 256x128). Every new control's minimum and maximum, and each switch in its other state, generated on all 6 sweep seeds at 256x256 for all four generators, with no failures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generators/CraterLakesGenerator.cpp | 31 ++++--- .../generator/generators/IslandsGenerator.cpp | 29 +++--- .../generator/generators/RiverGenerator.cpp | 36 ++++---- src/map/generator/generators/RiverGenerator.h | 4 +- .../generator/generators/SwampGenerator.cpp | 23 +++-- src/map/generator/shared/HeightMap.cpp | 25 +++--- src/map/generator/shared/HeightMap.h | 5 +- src/map/generator/shared/Terrain.cpp | 90 ++++++++++++++----- src/map/generator/shared/Terrain.h | 9 ++ 9 files changed, 156 insertions(+), 96 deletions(-) diff --git a/src/map/generator/generators/CraterLakesGenerator.cpp b/src/map/generator/generators/CraterLakesGenerator.cpp index dcc93c754..a711d21ef 100644 --- a/src/map/generator/generators/CraterLakesGenerator.cpp +++ b/src/map/generator/generators/CraterLakesGenerator.cpp @@ -9,9 +9,10 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const CraterLakesOptions options(context.request); - const HeightFieldOptions terrain{ + HeightFieldOptions terrain{ options.water, options.sand, options.grass, options.desert, options.smoothing, options.fruit, options.repeat, false}; + readResourceControls(terrain, context.request); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) { @@ -25,19 +26,17 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition craterLakesDefinition() { - return {"crater-lakes", - 4, - "Crater lakes", - 2, - false, - {{"water", "Water weight", 0, 100, 1, 25, ControlGroup::Terrain, false, true}, - {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, - {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, - {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, - {"smoothing", "Smoothing", 1, 8, 1, 6, ControlGroup::Terrain, false}, - {"lake-density", "Lake density", 10, 50, 5, 25, ControlGroup::Terrain, false}, - {"lake-size", "Lake size", 10, 40, 5, 25, ControlGroup::Terrain, false}, - {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, - {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, - generate}; + std::vector controls{ + {"water", "Water weight", 0, 100, 1, 25, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, + {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 6, ControlGroup::Terrain, false}, + {"lake-density", "Lake density", 10, 50, 5, 25, ControlGroup::Terrain, false}, + {"lake-size", "Lake size", 10, 40, 5, 25, ControlGroup::Terrain, false}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}}; + for (auto &c : heightFieldResourceControls()) + controls.push_back(std::move(c)); + controls.push_back({"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}); + return {"crater-lakes", 4, "Crater lakes", 2, false, std::move(controls), generate}; } diff --git a/src/map/generator/generators/IslandsGenerator.cpp b/src/map/generator/generators/IslandsGenerator.cpp index b128c6c2d..ab400f1b7 100644 --- a/src/map/generator/generators/IslandsGenerator.cpp +++ b/src/map/generator/generators/IslandsGenerator.cpp @@ -9,9 +9,10 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const IslandsOptions options(context.request); - const HeightFieldOptions terrain{ + HeightFieldOptions terrain{ options.water, options.sand, options.grass, options.desert, options.smoothing, options.fruit, options.repeat, false}; + readResourceControls(terrain, context.request); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) { @@ -27,18 +28,16 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition islandsDefinition() { - return {"islands", - 3, - "Islands", - 2, - false, - {{"water", "Water weight", 0, 100, 1, 55, ControlGroup::Terrain, false, true}, - {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, - {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, - {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, - {"smoothing", "Smoothing", 1, 8, 1, 4, ControlGroup::Terrain, false}, - {"extra-islands", "Extra islands", 0, 8, 1, 0, ControlGroup::Terrain, false}, - {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, - {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, - generate}; + std::vector controls{ + {"water", "Water weight", 0, 100, 1, 55, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, + {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 4, ControlGroup::Terrain, false}, + {"extra-islands", "Extra islands", 0, 8, 1, 0, ControlGroup::Terrain, false}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}}; + for (auto &c : heightFieldResourceControls()) + controls.push_back(std::move(c)); + controls.push_back({"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}); + return {"islands", 3, "Islands", 2, false, std::move(controls), generate}; } diff --git a/src/map/generator/generators/RiverGenerator.cpp b/src/map/generator/generators/RiverGenerator.cpp index ab03cb4e5..a4598b440 100644 --- a/src/map/generator/generators/RiverGenerator.cpp +++ b/src/map/generator/generators/RiverGenerator.cpp @@ -9,12 +9,16 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const RiverOptions options(context.request); - const HeightFieldOptions terrain{ + HeightFieldOptions terrain{ options.water, options.sand, options.grass, options.desert, options.smoothing, options.fruit, options.repeat, false}; + readResourceControls(terrain, context.request); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) - { hm.makeRiver(options.river_width * (w + h) / 2 / 100, smoothing); })) + { + hm.makeRiver(options.river_width * (w + h) / 2 / 100, smoothing, + options.winding); + })) return false; context.stage = "starts"; return placeStarts(game, context); @@ -22,18 +26,18 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition riverDefinition() { - return {"river", - 2, - "River", - 2, - false, - {{"water", "Water weight", 0, 100, 1, 45, ControlGroup::Terrain, false, true}, - {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, - {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, - {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, - {"smoothing", "Smoothing", 1, 8, 1, 4, ControlGroup::Terrain, false}, - {"river-width", "River width", 20, 65, 5, 35, ControlGroup::Terrain, false}, - {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, - {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, - generate}; + std::vector controls{ + {"water", "Water weight", 0, 100, 1, 45, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, + {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 4, ControlGroup::Terrain, false}, + {"river-width", "River width", 20, 65, 5, 35, ControlGroup::Terrain, false}, + // Off, the river runs straight across the map instead of meandering. + GeneratorControl::toggle("winding-river", "Winding river", true, ControlGroup::Terrain), + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}}; + for (auto &c : heightFieldResourceControls()) + controls.push_back(std::move(c)); + controls.push_back({"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}); + return {"river", 2, "River", 2, false, std::move(controls), generate}; } diff --git a/src/map/generator/generators/RiverGenerator.h b/src/map/generator/generators/RiverGenerator.h index ac8ad491b..9a1955b0e 100644 --- a/src/map/generator/generators/RiverGenerator.h +++ b/src/map/generator/generators/RiverGenerator.h @@ -5,10 +5,12 @@ struct RiverOptions { int water, sand, grass, desert, smoothing, river_width, fruit, repeat; + bool winding; explicit RiverOptions(const GenerationRequest &r) : water(r.option("water")), sand(r.option("sand")), grass(r.option("grass")), desert(r.option("desert")), smoothing(r.option("smoothing")), - river_width(r.option("river-width")), fruit(r.option("fruit")), repeat(r.option("repeat")) + river_width(r.option("river-width")), fruit(r.option("fruit")), repeat(r.option("repeat")), + winding(r.option("winding-river") != 0) { } }; diff --git a/src/map/generator/generators/SwampGenerator.cpp b/src/map/generator/generators/SwampGenerator.cpp index 14f82d68e..bee3b6616 100644 --- a/src/map/generator/generators/SwampGenerator.cpp +++ b/src/map/generator/generators/SwampGenerator.cpp @@ -9,8 +9,9 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const SwampOptions options(context.request); - const HeightFieldOptions terrain{ + HeightFieldOptions terrain{ options.water, 0, options.grass, 0, options.smoothing, options.fruit, options.repeat, true}; + readResourceControls(terrain, context.request); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) { hm.makeSwamp(smoothing); })) @@ -21,15 +22,13 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition swampDefinition() { - return {"swamp", - 1, - "Swamp", - 2, - false, - {{"water", "Water weight", 0, 100, 1, 35, ControlGroup::Terrain, false, true}, - {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, - {"smoothing", "Smoothing", 1, 8, 1, 6, ControlGroup::Terrain, false}, - {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, - {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, - generate}; + std::vector controls{ + {"water", "Water weight", 0, 100, 1, 35, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 6, ControlGroup::Terrain, false}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}}; + for (auto &c : heightFieldResourceControls()) + controls.push_back(std::move(c)); + controls.push_back({"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}); + return {"swamp", 1, "Swamp", 2, false, std::move(controls), generate}; } diff --git a/src/map/generator/shared/HeightMap.cpp b/src/map/generator/shared/HeightMap.cpp index cf8e77932..fc6ac1f10 100644 --- a/src/map/generator/shared/HeightMap.cpp +++ b/src/map/generator/shared/HeightMap.cpp @@ -199,7 +199,7 @@ void HeightMap::makeIslands(unsigned int count, float smoothingFactor) normalize(); } -void HeightMap::makeRiver(unsigned int maxDiameter, float smoothingFactor) +void HeightMap::makeRiver(unsigned int maxDiameter, float smoothingFactor, bool winding) { /// riverRadius refers to the distance between center of the river and the maximum distance that /// gets lowered. @@ -240,15 +240,20 @@ void HeightMap::makeRiver(unsigned int maxDiameter, float smoothingFactor) sqrt(pow(targetPointX - startingPointX, 2) + pow(targetPointY - startingPointY, 2)); for (float t = 0; t < straightRiverLength; t += straightRiverLength / 10.0 / (_w + _h)) { - float offset = (1.0 - cos(t / straightRiverLength * 2 * 3.14159265)) * - (_pn.Noise(t / 153.3) * 300.0 + _pn.Noise(t / 13.3) * 50.0 - 175.0); - if (t < straightRiverLength / 2.0) - offset += (1 + cos(t / straightRiverLength * 2 * 3.14159265)) * - (_pn.Noise(t / 153.3) * 300.0 + _pn.Noise(t / 13.3) * 50.0 - 175.0); - else - offset += (1 + cos(t / straightRiverLength * 2 * 3.14159265)) * - (_pn.Noise((straightRiverLength - t) / 153.3) * 300.0 + - _pn.Noise((straightRiverLength - t) / 13.3) * 50.0 - 175.0); + // The meander is a pure function of t, so leaving it out changes nothing else. + float offset = 0; + if (winding) + { + offset = (1.0 - cos(t / straightRiverLength * 2 * 3.14159265)) * + (_pn.Noise(t / 153.3) * 300.0 + _pn.Noise(t / 13.3) * 50.0 - 175.0); + if (t < straightRiverLength / 2.0) + offset += (1 + cos(t / straightRiverLength * 2 * 3.14159265)) * + (_pn.Noise(t / 153.3) * 300.0 + _pn.Noise(t / 13.3) * 50.0 - 175.0); + else + offset += (1 + cos(t / straightRiverLength * 2 * 3.14159265)) * + (_pn.Noise((straightRiverLength - t) / 153.3) * 300.0 + + _pn.Noise((straightRiverLength - t) / 13.3) * 50.0 - 175.0); + } float reachedPointX = targetDirectionX * t - targetDirectionY * offset / 4; float reachedPointY = targetDirectionY * t + targetDirectionX * offset / 4; lower((unsigned int)reachedPointX, (unsigned int)reachedPointY); diff --git a/src/map/generator/shared/HeightMap.h b/src/map/generator/shared/HeightMap.h index 37a4a06d7..803f44d25 100644 --- a/src/map/generator/shared/HeightMap.h +++ b/src/map/generator/shared/HeightMap.h @@ -51,9 +51,8 @@ class HeightMap /// class to generate heightmaps to decide where to put resource void makeSwamp(float smoothingFactor); /// a plain perlin height field void makeIslands(unsigned int count, float smoothingFactor); /// generates a 'swamp' with count hills - void - makeRiver(unsigned int maxDiameter, - float smoothingFactor); /// generates a 'swamp' with a river based on a random walk. + /// generates a 'swamp' with a river based on a random walk; a straight one unless winding. + void makeRiver(unsigned int maxDiameter, float smoothingFactor, bool winding = true); void makeCraters(unsigned int craterCount, unsigned int craterRadius, float smoothingFactor); /// generates a 'swamp' with craterCount craters private: diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index 4cac88786..a282842b1 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -16,6 +16,24 @@ #include #include using namespace MapGeneration; +std::vector MapGeneration::heightFieldResourceControls() +{ + return {GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::toggle("hilltop-stone", "Stone on hilltops", false, + ControlGroup::Resources)}; +} +void MapGeneration::readResourceControls(HeightFieldOptions &options, + const GenerationRequest &request) +{ + options.wheat = request.option("wheat-amount"); + options.wood = request.option("wood-amount"); + options.stone = request.option("stone-amount"); + options.algae = request.option("algae-amount"); + options.hilltopStone = request.option("hilltop-stone") != 0; +} bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, const HeightFieldOptions &options, const HeightFieldBuilder &build) @@ -25,7 +43,8 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, /// all under waterLevel is water, under sandLevel is beach, under grassLevel is grass and above /// grasslevel is desert - float waterLevel, sandLevel, grassLevel, wheatWoodLevel, algaeLevel, stoneLevel; + float waterLevel, sandLevel, grassLevel, wheatLevel, woodLevel, algaeLevel, stoneLevel, + stoneFloorLevel; /// to influence the roughness float smoothingFactor = (float)(options.smoothing + 4) * 3; /// the proportions requested through the gui can directly be translated into tile counts of the @@ -64,7 +83,23 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, } /// wheat/wood needs ground to stand on and water. So: wheatWoodTiles = waterTiles < grassTiles ? waterTiles / 2 : grassTiles / 2; - algaeTiles = waterTiles / 6; + algaeTiles = scaledCount(waterTiles / 6, options.algae); + // A third of that share is stone, right above the beach, and the rest farmland above the stone; + // each band is scaled on its own and none reaches past the top of the grass. Hilltop stone + // takes the highest grass instead, and farmland then starts at the beach. + const unsigned int landTop = waterTiles + sandTiles + grassTiles; + const unsigned int stoneTiles = + unsigned(std::min(grassTiles, scaledCount(wheatWoodTiles / 3, options.stone))); + const unsigned int farmTiles = wheatWoodTiles - wheatWoodTiles / 3; + const unsigned int stoneTop = options.hilltopStone + ? landTop + : std::min(landTop, waterTiles + sandTiles + stoneTiles); + const unsigned int stoneFloor = landTop - stoneTiles; + const unsigned int farmStart = options.hilltopStone ? waterTiles + sandTiles : stoneTop; + const unsigned int wheatTop = + unsigned(std::min(landTop, farmStart + scaledCount(farmTiles, options.wheat))); + const unsigned int woodTop = + unsigned(std::min(landTop, farmStart + scaledCount(farmTiles, options.wood))); /// histogram[i] collects the count of all terrain levels == i int histogram[2048]; @@ -79,8 +114,10 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, waterLevel = 0; sandLevel = 0; grassLevel = 0; - wheatWoodLevel = 0; + wheatLevel = 0; + woodLevel = 0; stoneLevel = 0; + stoneFloorLevel = 0; algaeLevel = 0; while ((waterLevel == 0) && (i < 2048)) { @@ -99,12 +136,15 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, while ((grassLevel == 0) && (i < 2048)) { accumulatedHistogram += histogram[i++]; - if (wheatWoodLevel == 0 && accumulatedHistogram >= waterTiles + sandTiles + wheatWoodTiles) - wheatWoodLevel = (float)(i - 1) / 2048.0; - if (stoneLevel == 0 && - accumulatedHistogram >= waterTiles + sandTiles + (wheatWoodTiles / 3)) + if (wheatLevel == 0 && accumulatedHistogram >= wheatTop) + wheatLevel = (float)(i - 1) / 2048.0; + if (woodLevel == 0 && accumulatedHistogram >= woodTop) + woodLevel = (float)(i - 1) / 2048.0; + if (stoneFloorLevel == 0 && accumulatedHistogram >= stoneFloor) + stoneFloorLevel = (float)(i - 1) / 2048.0; + if (stoneLevel == 0 && accumulatedHistogram >= stoneTop) stoneLevel = (float)(i - 1) / 2048.0; - if (accumulatedHistogram >= waterTiles + sandTiles + grassTiles) + if (accumulatedHistogram >= landTop) grassLevel = (float)(i - 1) / 2048.0; } for (unsigned y = 0; y < hHeightMap; y++) @@ -135,29 +175,33 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, for (unsigned x = 0; x < wHeightMap; x++) { int tmpResource = NO_RES; - if (hm(x + wHeightMap * y) < algaeLevel) + const float level = hm(x + wHeightMap * y); + const bool stoneBand = options.hilltopStone + ? level >= stoneFloorLevel && level < stoneLevel + : level < stoneLevel; + if (level < algaeLevel) { - tmpResource = ALGA; + if (options.algae > 0) + tmpResource = ALGA; // following places stone next to sand & water and keeps wheat & wood more inland // without clogging up the interior too badly } - else if (hm(x + wHeightMap * y) < stoneLevel) + else if (stoneBand && (options.stone > 0 || !options.hilltopStone)) { - tmpResource = STONE; + if (options.stone > 0) + tmpResource = STONE; } - else if (hm(x + wHeightMap * y) < wheatWoodLevel) + // patch to get smooth areas of wheat and wood: + // if the map is ascending at x+w/2,y set wheat. else set wood + else if (hm((x + wHeightMap / 2) % wHeightMap + wHeightMap * y) < + hm((x + wHeightMap / 2 + 1) % wHeightMap + wHeightMap * y)) { - // patch to get smooth areas of wheat and wood: - // if the map is ascending at x+w/2,y set wheat. else set wood - if (hm((x + wHeightMap / 2) % wHeightMap + wHeightMap * y) < - hm((x + wHeightMap / 2 + 1) % wHeightMap + wHeightMap * y)) - { + if (level < wheatLevel) tmpResource = CORN; - } - else - { - tmpResource = WOOD; - } + } + else if (level < woodLevel) + { + tmpResource = WOOD; } if (tmpResource != NO_RES) { diff --git a/src/map/generator/shared/Terrain.h b/src/map/generator/shared/Terrain.h index 4b39fc6c4..831ea208d 100644 --- a/src/map/generator/shared/Terrain.h +++ b/src/map/generator/shared/Terrain.h @@ -1,6 +1,8 @@ #pragma once +#include "GenerationRequest.h" #include "HeightMap.h" #include +#include class Game; struct GenerationContext; namespace MapGeneration @@ -9,7 +11,14 @@ struct HeightFieldOptions { int water, sand, grass, desert, smoothing, fruit, repeat; bool swamp = false; + // Each resource band as a percentage of its default size; 100 reproduces it exactly. + int wheat = 100, wood = 100, stone = 100, algae = 100; + // Stone on the highest grass rather than beside the shore. + bool hilltopStone = false; }; +// The resource controls every height-field generator shares, and reading them from a request. +std::vector heightFieldResourceControls(); +void readResourceControls(HeightFieldOptions &, const GenerationRequest &); using HeightFieldBuilder = std::function; bool generateHeightField(Game &, GenerationContext &, const HeightFieldOptions &, const HeightFieldBuilder &); From 68df0896b1adf99905b528fbb1e6df524172c21f Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:55:49 -0400 Subject: [PATCH 068/136] Add resource amounts and beach and bridge switches to Concrete islands and Isles Both gain Wheat, Wood and Stone amounts (percentages, 0 to 300 in steps of 25, default 100) for each colony's land, through divideUpPlayerLands: wheat and wood move how far in from the coast each field reaches (the height raise that decides it, 10 at 100), and stone scales the six stone deposits. The swarm is still placed beside where the default wheat field lies, so the base layout doesn't move. A colony's own fields are its whole starting kit on these maps, so at any other wheat or wood amount the reachability backstop runs as well. Concrete islands also gains Algae (how deep in each channel algae grows, 10 at 100) and Fruit (each neutral island's fruit count), and wheat grows the neutral islands' wheat half faster or slower than their fruit half. Isles also gains Algae (the size of each colony's algae patch, 5x5 at 100). setScaledResource scales a setResource square's tile count, placing the tiles nearest its centre in setResource's own order, so 100 is exactly the old call. Switches, both on by default: Sandy beaches (off, islands meet the water without their band of sand) on both, and Land bridges on Isles (off, the same bridge points are drawn but no bridge is built, so every colony's island stands alone). Default maps are byte-identical to 95e24a761's (225 dumps: 15 generators, 5 seeds, 128x128, 256x256, 256x128). Every new control's minimum and maximum, and each switch in its other state, generated on all 6 sweep seeds at 256x256, with no failures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generators/ConcreteIslandsGenerator.cpp | 42 +++++++++++++++---- .../generators/ConcreteIslandsGenerator.h | 7 +++- .../generator/generators/IslesGenerator.cpp | 38 ++++++++++------- src/map/generator/generators/IslesGenerator.h | 7 +++- src/map/generator/shared/Resources.cpp | 33 +++++++++++++++ src/map/generator/shared/Resources.h | 3 ++ .../generator/shared/StartingPositions.cpp | 26 +++++++----- src/map/generator/shared/StartingPositions.h | 8 +++- 8 files changed, 128 insertions(+), 36 deletions(-) diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.cpp b/src/map/generator/generators/ConcreteIslandsGenerator.cpp index eda63634f..ac26dd986 100644 --- a/src/map/generator/generators/ConcreteIslandsGenerator.cpp +++ b/src/map/generator/generators/ConcreteIslandsGenerator.cpp @@ -15,6 +15,7 @@ #include "Unit.h" #include #include +#include using namespace MapGeneration; #include "ConcreteIslandsGenerator.h" static bool generate(Game &game, GenerationContext &context) @@ -97,7 +98,7 @@ static bool generate(Game &game, GenerationContext &context) int total_height = heights[y * game.map.getW() + x]; if (total_height < 45) game.map.setUMatPos(x, y, WATER, 1); - else if (total_height >= 45 && total_height <= 55) + else if (options.sandy_beaches && total_height >= 45 && total_height <= 55) game.map.setUMatPos(x, y, SAND, 1); else game.map.setUMatPos(x, y, GRASS, 1); @@ -105,13 +106,15 @@ static bool generate(Game &game, GenerationContext &context) } game.map.controlSand(); - // Go through the map again and place alga - for (int x = 0; x < game.map.getW(); ++x) + // Go through the map again and place alga, down the deepest middle of every channel; the + // algae amount moves how deep that is. + const int algaeDepth = int(scaledCount(10, options.algae)); + for (int x = 0; x < game.map.getW() && options.algae > 0; ++x) { for (int y = 0; y < game.map.getH(); ++y) { int total_height = heights[y * game.map.getW() + x]; - if (total_height <= 10) + if (total_height <= algaeDepth) { game.map.setResource(x, y, ALGA, 1); } @@ -140,6 +143,14 @@ static bool generate(Game &game, GenerationContext &context) areaNumbers.push_back(areaNumber); areaNumber += 1; } + // The wheat half of the island grows faster or slower than the fruit half, in proportion + // to the wheat amount. + if (options.wheat != 100 && options.wheat > 0) + { + const int common = std::gcd(options.wheat, 100); + areaWeights[0] = options.wheat / common; + areaWeights[1] = 100 / common; + } // Divide the area. Its possible the area will be so small it can't be used if (divideUpArea(game.map, context, grid, islandAreaNumbers[i], areaWeights, areaNumbers)) @@ -147,11 +158,12 @@ static bool generate(Game &game, GenerationContext &context) // Fill in wheat std::vector points; getAllPoints(game.map, grid, areaNumbers[0], points); - fillInResource(game.map, context, points, CORN, 2); + if (options.wheat > 0) + fillInResource(game.map, context, points, CORN, 2); points.clear(); // Place some fruit - int fruit_n = context.stream("layout")() % 6 + 1; + int fruit_n = int(scaledCount(context.stream("layout")() % 6 + 1, options.fruit)); getAllPoints(game.map, grid, areaNumbers[1], points); chooseRandomPoints(game.map, context, points, fruit_n); for (unsigned int j = 0; j < points.size(); ++j) @@ -162,8 +174,13 @@ static bool generate(Game &game, GenerationContext &context) } } - if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber)) + if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber, + {options.wheat, options.wood, options.stone})) return false; + // A colony's own fields are its only starting wheat and wood; with other amounts than the + // default, make sure each still has both within reach. + if (options.wheat != 100 || options.wood != 100) + guaranteeStartingResources(game, context, 24, 32); // Initialize final team info for (int i = 0; i < context.request.nbTeams; ++i) @@ -181,6 +198,15 @@ GeneratorDefinition concreteIslandsDefinition() 1, false, {{"channel-width", "Channel width", 5, 8, 1, 5, ControlGroup::Terrain, false}, - {"extra-islands", "Extra islands", 0, 6, 1, 3, ControlGroup::Terrain, false}}, + {"extra-islands", "Extra islands", 0, 6, 1, 3, ControlGroup::Terrain, false}, + // Off, islands meet their channels without a band of sand. + GeneratorControl::toggle("sandy-beaches", "Sandy beaches", true, ControlGroup::Terrain), + // Wheat and wood scale each colony's fields and the neutral islands' wheat; stone + // each colony's deposits; algae the channels'; fruit the neutral islands'. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate}; } diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.h b/src/map/generator/generators/ConcreteIslandsGenerator.h index 337ee47ae..5bc407e44 100644 --- a/src/map/generator/generators/ConcreteIslandsGenerator.h +++ b/src/map/generator/generators/ConcreteIslandsGenerator.h @@ -5,8 +5,13 @@ struct ConcreteIslandsOptions { int channel_width, extra_islands; + bool sandy_beaches; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit ConcreteIslandsOptions(const GenerationRequest &r) - : channel_width(r.option("channel-width")), extra_islands(r.option("extra-islands")) + : channel_width(r.option("channel-width")), extra_islands(r.option("extra-islands")), + sandy_beaches(r.option("sandy-beaches") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) { } }; diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index 005756f92..b048941b0 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -110,8 +110,9 @@ static bool generate(Game &game, GenerationContext &context) } } } - // Make the connection - if (!failed) + // Make the connection. Without land bridges the same points are still drawn, so the + // rest of the map stays as it was, but the islands are left apart. + if (!failed && options.land_bridges) { for (unsigned int p = 0; p < linePoints.size(); ++p) { @@ -157,7 +158,7 @@ static bool generate(Game &game, GenerationContext &context) int total_height = heightmap[y * game.map.getW() + x]; if (total_height < 90) game.map.setUMatPos(x, y, WATER, 1); - else if (total_height > 95 && total_height < 105) + else if (options.sandy_beaches && total_height > 95 && total_height < 105) game.map.setUMatPos(x, y, SAND, 1); else game.map.setUMatPos(x, y, GRASS, 1); @@ -191,7 +192,7 @@ static bool generate(Game &game, GenerationContext &context) { int d = distances[y * game.map.getW() + x]; int d2 = connectorDistances[y * game.map.getW() + x]; - if (d == 8 && d2 > bridgeWidth) + if (d == 8 && (!options.land_bridges || d2 > bridgeWidth)) { possible.push_back(MapGeneratorPoint(x, y)); } @@ -202,21 +203,19 @@ static bool generate(Game &game, GenerationContext &context) return false; } int r = context.stream("layout")() % possible.size(); - for (int x = -2; x <= 2; ++x) - { - int nx = game.map.normalizeX(possible[r].x + x); - for (int y = -2; y <= 2; ++y) - { - int ny = game.map.normalizeY(possible[r].y + y); - game.map.setResource(nx, ny, ALGA, 1); - } - } + // A five by five patch of algae at the default amount. + setScaledResource(game.map, possible[r].x, possible[r].y, ALGA, 5, options.algae); } - if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber)) + if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber, + {options.wheat, options.wood, options.stone})) { return false; } + // A colony's own fields are its only wheat and wood; with other amounts than the default, + // make sure each still has both within reach. + if (options.wheat != 100 || options.wood != 100) + guaranteeStartingResources(game, context, 24, 32); // Initialize final team info for (int i = 0; i < context.request.nbTeams; ++i) @@ -234,6 +233,15 @@ GeneratorDefinition islesDefinition() 2, false, {{"island-size", "Island size", 45, 65, 5, 60, ControlGroup::Terrain, false}, - {"bridge-width", "Land bridge width", 3, 6, 1, 4, ControlGroup::Terrain, false}}, + {"bridge-width", "Land bridge width", 3, 6, 1, 4, ControlGroup::Terrain, false}, + // Off, every colony's island stands alone in the sea. + GeneratorControl::toggle("land-bridges", "Land bridges", true, ControlGroup::Terrain), + // Off, islands meet the sea without a band of sand. + GeneratorControl::toggle("sandy-beaches", "Sandy beaches", true, ControlGroup::Terrain), + // Wheat and wood scale each colony's fields, stone its deposits, algae its patch. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount")}, generate}; } diff --git a/src/map/generator/generators/IslesGenerator.h b/src/map/generator/generators/IslesGenerator.h index e7b2e5da2..658e9863a 100644 --- a/src/map/generator/generators/IslesGenerator.h +++ b/src/map/generator/generators/IslesGenerator.h @@ -5,8 +5,13 @@ struct IslesOptions { int island_size, bridge_width; + bool land_bridges, sandy_beaches; + int wheat, wood, stone, algae; // percentages of the default amounts explicit IslesOptions(const GenerationRequest &r) - : island_size(r.option("island-size")), bridge_width(r.option("bridge-width")) + : island_size(r.option("island-size")), bridge_width(r.option("bridge-width")), + land_bridges(r.option("land-bridges") != 0), sandy_beaches(r.option("sandy-beaches") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")) { } }; diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 545c9b303..aa49d6dce 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include using namespace MapGeneration; @@ -40,6 +41,38 @@ int placeResourceClump(Map &map, GenerationContext &context, return placed; } +void setScaledResource(Map &map, int x, int y, int resourceType, int size, int percent) { + if (percent == 100) { + map.setResource(x, y, resourceType, size); + return; + } + const int half = size >> 1; + const int target = int(scaledCount(std::int64_t(2 * half + 1) * (2 * half + 1), percent)); + if (target <= 0) + return; + int reach = 0; + while ((2 * reach + 1) * (2 * reach + 1) < target) + ++reach; + const int side = 2 * reach + 1; + // Offset i is column i / side, row i % side, the order setResource visits its square in. Keep + // the `target` offsets nearest the centre (by ring, then by distance round it). + const auto rank = [side, reach](int i) { + const int dx = std::abs(i / side - reach), dy = std::abs(i % side - reach); + return std::make_pair(std::max(dx, dy), dx + dy); + }; + std::vector order(size_t(side) * side); + for (size_t i = 0; i < order.size(); ++i) + order[i] = int(i); + std::stable_sort(order.begin(), order.end(), [&](int a, int b) { return rank(a) < rank(b); }); + std::vector chosen(order.size(), 0); + for (int k = 0; k < target; ++k) + chosen[size_t(order[size_t(k)])] = 1; + for (int i = 0; i < int(chosen.size()); ++i) + if (chosen[size_t(i)]) + map.setResource(map.normalizeX(x + i / side - reach), map.normalizeY(y + i % side - reach), + resourceType, 1); +} + int placeResourceClumpInArea(Map &map, GenerationContext &context, const std::vector &points, int resourceType, int radius) { diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h index 5a4553a8a..3ae094db9 100644 --- a/src/map/generator/shared/Resources.h +++ b/src/map/generator/shared/Resources.h @@ -21,6 +21,9 @@ int placeResourceClump(Map &, GenerationContext &, MapGeneratorPoint center, int placeResourceClumpInArea(Map &, GenerationContext &, const std::vector &, int resourceType, int radius); +// Map::setResource(x, y, type, size) scaled to `percent` of that square's tiles: the tiles +// nearest its centre, placed in setResource's own order, so 100 is exactly that call. +void setScaledResource(Map &, int x, int y, int resourceType, int size, int percent); void scatterResources(Game &, GenerationContext &, const ResourceDensities &); // Ensures every team has wheat within wheatRange and wood within woodRange of its boot tile, // walking through the same reachable-space flood used to measure that distance (so anything diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index d559a7ad5..250b1a8bb 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -22,7 +22,8 @@ using namespace MapGeneration; namespace MapGeneration { bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector &grid, - std::vector &teamAreaNumbers, int &areaNumber) + std::vector &teamAreaNumbers, int &areaNumber, + const PlayerLandResources &resources) { context.stage = "resources and starts"; int typeNum = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); @@ -80,36 +81,39 @@ bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector wheatWoodPoints; std::vector wheatPoints; getAllPoints(game.map, grid, areaNumbers[3], wheatWoodPoints); getAllPoints(game.map, grid, areaNumbers[4], wheatWoodPoints); getAllPoints(game.map, grid, areaNumbers[5], wheatWoodPoints); - adjustHeightmapFromPoints(game.map, wheatWoodPoints, heightmap, 10); + const int woodRise = int(scaledCount(10, resources.wood)); + adjustHeightmapFromPoints(game.map, wheatWoodPoints, heightmap, woodRise); for (unsigned int j = 0; j < wheatWoodPoints.size(); ++j) { int h = heightmap[wheatWoodPoints[j].y * game.map.getW() + wheatWoodPoints[j].x]; - if (h > 50) + if (h > 50 && resources.wood > 0) { game.map.setResource(wheatWoodPoints[j].x, wheatWoodPoints[j].y, WOOD, 1); } } wheatWoodPoints.clear(); - // Place wheat + // Place wheat. The swarm goes beside the default wheat field, whatever amount is + // actually placed, so the base layout doesn't move with the wheat amount. getAllPoints(game.map, grid, areaNumbers[0], wheatWoodPoints); getAllPoints(game.map, grid, areaNumbers[1], wheatWoodPoints); getAllPoints(game.map, grid, areaNumbers[2], wheatWoodPoints); - adjustHeightmapFromPoints(game.map, wheatWoodPoints, heightmap, 10); + const int wheatRise = int(scaledCount(10, resources.wheat)); + adjustHeightmapFromPoints(game.map, wheatWoodPoints, heightmap, wheatRise); for (unsigned int j = 0; j < wheatWoodPoints.size(); ++j) { int h = heightmap[wheatWoodPoints[j].y * game.map.getW() + wheatWoodPoints[j].x]; - if (h > 50) - { + if (h > 50 && resources.wheat > 0) game.map.setResource(wheatWoodPoints[j].x, wheatWoodPoints[j].y, CORN, 1); + if (h - wheatRise + 10 > 50) wheatPoints.push_back(wheatWoodPoints[j]); - } } // These are all points in the base @@ -122,7 +126,7 @@ bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector stoneLocations = baseLocations; chooseRandomPoints(game.map, context, stoneLocations, numberOfStone); for (unsigned int j = 0; j < stoneLocations.size(); ++j) @@ -182,6 +186,8 @@ bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vectorstartPosX = b->posX; game.teams[i]->startPosY = b->posY; game.teams[i]->startPosSet = Team::START_POS_FROM_SWARM; + context.bootX[i] = b->posX; + context.bootY[i] = b->posY; // Place units around the swarm std::vector unitLocations = baseLocations; diff --git a/src/map/generator/shared/StartingPositions.h b/src/map/generator/shared/StartingPositions.h index ac7c8370a..618f241e6 100644 --- a/src/map/generator/shared/StartingPositions.h +++ b/src/map/generator/shared/StartingPositions.h @@ -2,8 +2,14 @@ #include "Regions.h" namespace MapGeneration { +// Each colony's wheat and wood fields and its stone deposits, as percentages of the default kit. +struct PlayerLandResources +{ + int wheat = 100, wood = 100, stone = 100; +}; bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector &grid, - std::vector &teamAreaNumbers, int &areaNumber); + std::vector &teamAreaNumbers, int &areaNumber, + const PlayerLandResources &resources = {}); void chooseFreeForBuildingSquares(Game &game, std::vector &points, BuildingType *type, int team); void chooseFreeForGroundUnits(Map &map, std::vector &points, int team); From 244da6d970c192238960bac56afadc2e1a065d99 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 17:55:50 -0400 Subject: [PATCH 069/136] Add resource amounts and a switch each to Old random and Old islands Shattered coast (Old random) and Rugged archipelago (Old islands) gain Wheat, Wood, Stone and Algae amounts (percentages, 0 to 300 in steps of 25, default 100). Both place each colony's deposits as setResource squares; each amount now scales its squares' area around the same centre through setScaledResource. The reachability backstop still runs after them. Old random gains Colony meadows (on): off, colonies start in the shattered terrain itself, without the cleared grass square around each one. Old islands gains Extra starting deposit (on): off, an island gets no fourth deposit of whichever of wheat or wood came out smaller. Default maps are byte-identical to 95e24a761's (225 dumps: 15 generators, 5 seeds, 128x128, 256x256, 256x128). Every new control's minimum and maximum, and each switch in its other state, generated on all 6 sweep seeds at 256x256, with no failures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generators/RuggedArchipelagoGenerator.cpp | 28 +++++++++++++------ .../generators/RuggedArchipelagoGenerator.h | 7 ++++- .../generators/ShatteredCoastGenerator.cpp | 23 +++++++++++---- .../generators/ShatteredCoastGenerator.h | 10 ++++++- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index da48e329a..490d1d32d 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -306,7 +306,8 @@ static void resources(Game &game, GenerationContext &context, (int)(((w + h) * options.island_size) / (400.0 * sqrt((double)context.request.nbTeams))); if (islandsSize < 8) islandsSize = 8; - // let's add resources... + // let's add resources... Each deposit is a square sized to the island; the amount controls + // scale its area around the same centre. int smoothResources = islandsSize / 4; for (int s = 0; s < context.request.nbTeams; s++) { @@ -324,7 +325,7 @@ static void resources(Game &game, GenerationContext &context, amount = 1; p = d - 1 - amount / 2; if (amount > 0) - map.setResource(bootX[s], bootY[s] - p, WOOD, amount); + setScaledResource(map, bootX[s], bootY[s] - p, WOOD, amount, options.wood); smallestAmount = amount; smallestResource = WOOD; @@ -338,7 +339,7 @@ static void resources(Game &game, GenerationContext &context, amount = 1; p = d - 1 - amount / 2; if (amount > 0) - map.setResource(bootX[s] - p, bootY[s], CORN, amount); + setScaledResource(map, bootX[s] - p, bootY[s], CORN, amount, options.wheat); if (amount < smallestAmount) { smallestAmount = amount; @@ -349,9 +350,9 @@ static void resources(Game &game, GenerationContext &context, for (d = 0; d < islandsSize; d++) if (!map.isGrass(bootX[s], bootY[s] + d)) break; - map.setResource(bootX[s], bootY[s] + p, STONE, 1); + setScaledResource(map, bootX[s], bootY[s] + p, STONE, 1, options.stone); - // We add the resource with the smallest amount: + // We add the resource with the smallest amount, unless that extra deposit is switched off: for (d = 0; d < islandsSize; d++) if (!map.isGrass(bootX[s] + d, bootY[s] + d)) break; @@ -360,8 +361,9 @@ static void resources(Game &game, GenerationContext &context, if (amount < 1) amount = 1; p = d - 1 - amount / 2; - if (amount > 0) - map.setResource(bootX[s] + p, bootY[s] + p, smallestResource, amount); + if (amount > 0 && options.extra_deposit) + setScaledResource(map, bootX[s] + p, bootY[s] + p, smallestResource, amount, + smallestResource == CORN ? options.wheat : options.wood); // ALGAE for (d = 0; d < 2 * islandsSize; d++) @@ -371,7 +373,7 @@ static void resources(Game &game, GenerationContext &context, amount = smoothResources; p = d + smoothResources - 1 + amount / 2; if (amount > 0) - map.setResource(bootX[s] + p, bootY[s], ALGA, amount); + setScaledResource(map, bootX[s] + p, bootY[s], ALGA, amount, options.algae); } // Let's smooth resources... @@ -404,6 +406,14 @@ GeneratorDefinition ruggedArchipelagoDefinition() 1, false, {{"island-size", "Island size", 50, 70, 1, 65, ControlGroup::Terrain, false}, - {"beach-size", "Beach size", 0, 4, 1, 1, ControlGroup::Terrain, false}}, + {"beach-size", "Beach size", 0, 4, 1, 1, ControlGroup::Terrain, false}, + // Off, an island gets no fourth deposit of whichever of wheat or wood came out smaller. + GeneratorControl::toggle("extra-deposit", "Extra starting deposit", true, + ControlGroup::Resources), + // The area of each island's own wheat, wood, stone and algae deposits. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount")}, generate}; } diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.h b/src/map/generator/generators/RuggedArchipelagoGenerator.h index d411aa6db..7aab3c1f4 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.h +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.h @@ -5,8 +5,13 @@ struct RuggedArchipelagoOptions { int island_size, beach_size; + bool extra_deposit; + int wheat, wood, stone, algae; // percentages of the default amounts explicit RuggedArchipelagoOptions(const GenerationRequest &r) - : island_size(r.option("island-size")), beach_size(r.option("beach-size")) + : island_size(r.option("island-size")), beach_size(r.option("beach-size")), + extra_deposit(r.option("extra-deposit") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")) { } }; diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index e489616b9..6a7fa4b89 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -619,9 +619,10 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast map.setUMTerrain(maxX + dx, maxY + dy, GRASS); } - // Let's add some green space for teams: + // Let's add some green space for teams: a meadow around every colony, unless the colonies are + // to start in the shattered terrain itself. int squareSize = 5 + (int)(sqrt((double)minDistSquare) / 4.5); - for (int team = 0; team < nbTeams; team++) + for (int team = 0; team < nbTeams && options.colony_meadows; team++) { map.setUMatPos(context.bootX[team] + 2, context.bootY[team] + 0, GRASS, squareSize); map.setUMatPos(context.bootX[team] + 2, context.bootY[team] + 2, GRASS, squareSize); @@ -727,9 +728,11 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa dx *= d; dy *= d; + // Every deposit is a square of `amount` tiles a side; the amount controls scale its area. int amount = context.request.resourceAmounts[res]; if (amount > 0) - map.setResource(bootX[team] + dx, bootY[team] + dy, res, amount); + setScaledResource(map, bootX[team] + dx, bootY[team] + dy, res, amount, + options.percent(res)); } if (smallestWidth < limitDist) @@ -768,7 +771,8 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa int amount = context.request.resourceAmounts[smallestResource]; if (amount > 0) - map.setResource(bootX[team] + dx, bootY[team] + dy, smallestResource, amount); + setScaledResource(map, bootX[team] + dx, bootY[team] + dy, smallestResource, amount, + options.percent(smallestResource)); } int maxDir = 0; @@ -803,7 +807,7 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa int amount = context.request.resourceAmounts[ALGA]; if (amount > 0) - map.setResource(bootX[team] + dx, bootY[team] + dy, ALGA, amount); + setScaledResource(map, bootX[team] + dx, bootY[team] + dy, ALGA, amount, options.algae); } // Let's smooth resources... @@ -843,6 +847,13 @@ GeneratorDefinition shatteredCoastDefinition() {{"water", "Water weight", 0, 100, 1, 40, ControlGroup::Terrain, false, true}, {"sand", "Sand weight", 0, 100, 1, 4, ControlGroup::Terrain, false, true}, {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, - {"smoothing", "Smoothing", 1, 8, 1, 3, ControlGroup::Terrain, false}}, + {"smoothing", "Smoothing", 1, 8, 1, 3, ControlGroup::Terrain, false}, + // Off, colonies start in the shattered terrain rather than in a cleared meadow. + GeneratorControl::toggle("colony-meadows", "Colony meadows", true, ControlGroup::Terrain), + // The area of each colony's own wheat, wood, stone and algae deposits. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount")}, generate}; } diff --git a/src/map/generator/generators/ShatteredCoastGenerator.h b/src/map/generator/generators/ShatteredCoastGenerator.h index f13138660..c629fe7a1 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.h +++ b/src/map/generator/generators/ShatteredCoastGenerator.h @@ -5,10 +5,18 @@ struct ShatteredCoastOptions { int water, sand, grass, smoothing; + bool colony_meadows; + int wheat, wood, stone, algae; // percentages of the default amounts explicit ShatteredCoastOptions(const GenerationRequest &r) : water(r.option("water")), sand(r.option("sand")), grass(r.option("grass")), - smoothing(r.option("smoothing")) + smoothing(r.option("smoothing")), colony_meadows(r.option("colony-meadows") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")) { } + int percent(int resourceType) const + { + return resourceType == CORN ? wheat : resourceType == WOOD ? wood : resourceType == STONE ? stone : algae; + } }; GeneratorDefinition shatteredCoastDefinition(); From 8139286e8756eb0bcb5e9aa763620a7afc401abe Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:18:53 -0400 Subject: [PATCH 070/136] Judge tournament bias against a fair-map floor, and test the scorer properly - The headline position bias now carries a 95% t interval over maps rather than a percentile bootstrap, which under-covers badly at three maps, plus a fair-map floor: the 95th percentile the headline reaches when every map is fair with the same decided games per map. On the smoke run the Symmetric arena baseline reads 13.4 pp against a 14.4 pp floor, while the other three generators read 25.6 to 34.1 pp. - Scorer correlations gain a permutation p-value that shuffles quality among each map's own colonies, intervals only from five maps up, and a permutation null for the top-rated start. Symmetric arena is left out of the scorer check: its colonies are identical by construction, and the small score differences it shows come from the build-site count, which is not symmetric under rotation. - test/test_map_fairness_tournament.py covers adjudication, log parsing and the statistics offline, with no build and no games needed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 28 ++- test/test_map_fairness_tournament.py | 175 ++++++++++++++++++ tools/map_fairness_tournament.py | 201 ++++++++++++++++----- 3 files changed, 347 insertions(+), 57 deletions(-) create mode 100644 test/test_map_fairness_tournament.py diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index 40a63640e..914f7b80d 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -118,8 +118,15 @@ the winner first, other survivors by prestige, then eliminated colonies, the las estimate is corrected for chance: with `n` decided games and chi-square statistic `X` against uniform, `(X - (N - 1)) / ((n - 1) N)` is an unbiased estimate of `sum_s (p_s - 1/N)^2`. Averaged over maps, clipped at zero and converted to an RMS, it reads 0 for fair maps. With four colonies, - one start winning 40% and the others 20% is 8.7 pp. The bracket is a 95% bootstrap interval over - maps, so it covers both map-to-map variation and the game-to-game noise within each map. + one start winning 40% and the others 20% is 8.7 pp. The bracket is a 95% t interval over maps. + Each map's estimate carries its own game-to-game noise, so the interval covers that noise as well + as real differences between maps. With only a few maps it is honestly wide. +- **Fair-map floor** is the 95th percentile the position bias reaches when every map is perfectly + fair, simulated with the same number of decided games per map. Even fair maps show some bias on a + small sample, so a headline below this floor cannot be told apart from a fair generator. With + three maps of eight games each the floor is about 14 pp; more games per map lower it. +- **Decisive only** repeats the position bias counting only games won outright, with the number of + such games, so games decided by tick-cap adjudication cannot drive it. - **Biased maps** counts maps whose wins by start reject a uniform split at p < 0.05, raw and after Benjamini-Hochberg across that generator's maps (false discovery rate 5%). "Chance" is how many raw rejections a generator with only fair maps would expect. Holm-adjusted counts are in the @@ -132,12 +139,17 @@ the winner first, other survivors by prestige, then eliminated colonies, the las start's share with a Wilson 95% interval. - **Any bias p** tests whether any of the generator's maps is biased: the sum of the per-map chi-square statistics, calibrated by simulating every map's winners under a fair split. -- **Scorer rho** is the rank correlation between a colony's start-quality total and its win share - on the same map, pooled over maps after centring both within each map, with a bootstrap interval - over maps. Near +1, the scorer ranks starts the way games do. Near 0, it does not predict who - wins. The per-generator section adds the correlation with placement (which uses every colony's - finish, not just the winner's), per scoring factor, and how often the start the scorer rated best - actually won. Symmetric arena scores every colony alike, so it has no scorer check. +- **Scorer rho** is the rank correlation between a colony's start-quality total and its win share on + the same map, pooled over maps after centring both within each map. Near +1 the scorer ranks + starts the way games do; near 0 it does not predict who wins. Its p-value comes from a + permutation test that shuffles the quality scores among each map's own colonies, which keeps whole + maps intact and stays valid with few maps; a bootstrap interval over maps appears only from five + maps up, because fewer cannot support one. The per-generator section adds the correlation with + placement (every colony's finish, not just the winner's), the correlation for each scoring factor, + and how often the start the scorer rated best won, against a permutation null that gives each + map's win counts to a start picked at random. Symmetric arena is left out of the scorer check: its + colonies are identical by construction, and the small score differences it does show come from the + build-site count, which is not symmetric under rotation. The engine team-index table reports wins by team index for the baseline and for all generators pooled, with an exact test and Wilson intervals. diff --git a/test/test_map_fairness_tournament.py b/test/test_map_fairness_tournament.py new file mode 100644 index 000000000..f1f7460cf --- /dev/null +++ b/test/test_map_fairness_tournament.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-or-later +"""Offline checks for tools/map_fairness_tournament.py: adjudication, parsing and statistics. + +Needs no build and no games: every case is synthetic engine output or a known statistical value. + + python3 test/test_map_fairness_tournament.py +""" +import json +import random +import statistics +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) +import map_fairness_tournament as t + +RECORD = {'colonies': 4, 'starts': [[0, 0], [1, 1], [2, 2], [3, 3]]} +CONFIG = {'tick_cap': 90000} + + +def team(result, alive, elim, slot, prestige=0, units=0, buildings=0): + return {'result': result, 'alive': alive, 'eliminated_tick': elim, 'start': RECORD['starts'][slot], + 'prestige': prestige, 'units': units, 'workers': units, 'explorers': 0, 'warriors': 0, + 'buildings': buildings, 'sites': 0} + + +def game(rotation, end, teams, reason=None): + # Through JSON, as a result read back from disk: integer keys become strings. + return json.loads(json.dumps({'rotation': rotation, 'status': 'ok', + 'parsed': {'end': end, 'teams': teams, 'reason': reason, + 'cap_checksum': None}})) + + +def ended(ticks, winner): + return {'ticks': ticks, 'winner_team': winner, 'seed': 1, 'map': 'x', 'orders': 1} + + +def check_adjudication(): + slot = lambda team_index, r: (team_index - r) % 4 + # Decisive win on rotation 1: team 2 holds start 1. Eliminations at 100, 300 and 200 ticks. + a = game(1, ended(4000, 2), { + 0: team('lost', 0, 100, slot(0, 1)), 1: team('lost', 0, 300, slot(1, 1)), + 2: team('won', 1, -1, slot(2, 1), prestige=50), 3: team('lost', 0, 200, slot(3, 1))}, 'elimination') + o = t.adjudicate(a, RECORD, CONFIG) + assert (o['winner_slot'], o['winner_team'], o['adjudication']) == (1, 2, 'decisive'), o + assert o['elimination_order'] == [[3, 100], [2, 200], [0, 300]], o['elimination_order'] + assert o['placement'] == {1: 1, 0: 2, 2: 3, 3: 4}, o['placement'] + assert not o['start_mismatch'] and o['end_reason'] == 'elimination' + # Tick cap, population decides between two colonies level on prestige. + b = game(1, ended(90000, -1), { + 0: team('undecided', 1, -1, slot(0, 1), prestige=10, units=5, buildings=9), + 1: team('undecided', 1, -1, slot(1, 1), prestige=12, units=20, buildings=3), + 2: team('lost', 0, 500, slot(2, 1)), + 3: team('undecided', 1, -1, slot(3, 1), prestige=12, units=25, buildings=5)}) + o = t.adjudicate(b, RECORD, CONFIG) + assert (o['end_reason'], o['adjudication']) == ('cap', 'cap-units'), o + assert (o['winner_team'], o['winner_slot']) == (3, 2), o + # Team 1 (start 0) outranks team 0 (start 3) on prestige, so it places second. + assert o['placement'] == {2: 1, 0: 2, 3: 3, 1: 4}, o['placement'] + # Tick cap, buildings decide when prestige and population are level. + c = game(0, ended(90000, -1), { + 0: team('undecided', 1, -1, 0, prestige=5, units=10, buildings=7), + 1: team('undecided', 1, -1, 1, prestige=5, units=10, buildings=4), + 2: team('undecided', 1, -1, 2, prestige=5, units=9, buildings=9), + 3: team('undecided', 1, -1, 3, prestige=4, units=30, buildings=30)}) + o = t.adjudicate(c, RECORD, CONFIG) + assert (o['adjudication'], o['winner_slot']) == ('cap-buildings', 0), o + # An exact tie at the top stays unresolved, and placements still cover every start. + d = game(0, ended(90000, -1), { + 0: team('undecided', 1, -1, 0, prestige=7, units=4, buildings=2), + 1: team('undecided', 1, -1, 1, prestige=7, units=4, buildings=2), + 2: team('lost', 0, 900, 2), 3: team('lost', 0, 800, 3)}) + o = t.adjudicate(d, RECORD, CONFIG) + assert o['winner_slot'] is None and o['adjudication'] == 'cap-tie', o + assert sorted(o['placement']) == [0, 1, 2, 3] and o['placement'][2] == 3, o['placement'] + # A team standing somewhere other than its rotated start is flagged. + e = json.loads(json.dumps(a)) + e['parsed']['teams']['0']['start'] = [9, 9] + assert t.adjudicate(e, RECORD, CONFIG)['start_mismatch'] + print('adjudication checks passed') + + +def check_parsing(): + text = '\n'.join([ + 'nox::game started', 'nox::gui.game.totalPrestigeReached', + 'GLOB2_GAME_END ticks=31234 winner_team=1 seed=5 map="fairness-g9-s1001-r2" orders=812 ' + 'players=team0:local,team1:Nicowar', + 'GLOB2_TEAM_RESULT team=0 result=lost alive=1 eliminated_tick=-1 start=84,103 prestige=40 units=20 ' + 'workers=15 explorers=2 warriors=3 buildings=12 sites=1', + 'GLOB2_TEAM_RESULT team=1 result=won alive=1 eliminated_tick=-1 start=21,84 prestige=90 units=40 ' + 'workers=30 explorers=4 warriors=6 buildings=25 sites=2']) + p = t.parse_game_log(text) + assert p['end'] == {'ticks': 31234, 'winner_team': 1, 'seed': 5, + 'map': 'fairness-g9-s1001-r2', 'orders': 812}, p['end'] + assert p['reason'] == 'prestige' and p['teams'][1]['start'] == [21, 84] + assert p['teams'][0]['buildings'] == 12 and p['teams'][1]['result'] == 'won' + study = t.parse_study('\n'.join([ + 'SAMPLED,1001,42,5', 'ROTATIONS,4,4,0,1,1,1,1', 'START,0,7,9', + 'MAPFILE,0,/tmp/map-r0.map,1024,123', 'OPTION,width,7', + 'COLONY,0,' + ','.join(['1'] * 15)])) + assert study['rotation_checks'] == {'teams': 4, 'rotations': 4, 'save_load_stable': 0, + 'references_consistent': 1, 'colonies_placed': 1, + 'round_trip': 1, 'reload_idempotent': 1}, study['rotation_checks'] + assert study['chosen_seed'] == 42 and study['starts'] == [[7, 9]] + assert len(study['colony_quality']) == 1 and study['colony_quality'][0]['total'] == 1.0 + assert t.parse_int_list('1001-1003,1007') == [1001, 1002, 1003, 1007] + print('parsing checks passed') + + +def check_statistics(): + # Chi-square tail against textbook critical values, and the Wilson interval. + assert abs(t.gamma_q(1.5, 7.8147 / 2) - 0.05) < 1e-4 and abs(t.gamma_q(1.5, 11.345 / 2) - 0.01) < 1e-4 + assert abs(t.gamma_q(0.5, 3.8415 / 2) - 0.05) < 1e-4 and abs(t.gamma_q(3.5, 14.067 / 2) - 0.05) < 1e-4 + assert [round(v, 4) for v in t.wilson(5, 10)] == [0.2366, 0.7634] + # Exact multinomial test: every game won by one start out of four is (1/4)^3. + assert abs(t.exact_uniform_p([4, 0, 0, 0], 1)[0] - 0.015625) < 1e-9 + assert t.exact_uniform_p([1, 1, 1, 1], 1)[0] > 0.999 + assert t.exact_uniform_p([20, 0, 0, 0, 0, 0, 0, 12], 3)[1] == 'monte-carlo' + assert [round(v, 3) for v in t.benjamini_hochberg([0.01, 0.04, 0.03, 0.2])] == [0.04, 0.053, 0.053, 0.2] + assert [round(v, 3) for v in t.holm([0.01, 0.04, 0.03, 0.2])] == [0.04, 0.09, 0.09, 0.2] + assert t.spearman([1, 2, 3, 4], [10, 20, 30, 40]) == 1.0 and t.spearman([1, 2, 3, 4], [4, 3, 2, 1]) == -1.0 + # Student t quantiles: tabulated to 30 degrees of freedom, approximated past it (t(120)=1.9799). + assert t.t975(2) == 4.303 and t.t975(30) == 2.042 and abs(t.t975(120) - 1.9799) < 0.005 + # The bias estimate is unbiased: zero for fair maps, the true value for a skewed one. + rng = random.Random(5) + for probabilities, expected in (([0.25] * 4, 0.0), ([0.4, 0.2, 0.2, 0.2], 0.03)): + estimates = [] + for _ in range(4000): + counts = [0] * 4 + for _ in range(16): + value, total = rng.random(), 0.0 + for i, p in enumerate(probabilities): + total += p + if value < total: + counts[i] += 1 + break + else: + counts[-1] += 1 + estimates.append(t.squared_bias(counts)) + assert abs(statistics.mean(estimates) - expected) < 0.01, (probabilities, statistics.mean(estimates)) + # A t interval over maps covers zero when the per-map estimates are noisy, and the fair-map + # floor is deterministic and sits where a small sample's noise does. + low, high = t.bias_interval([0.1429, 0.0353, 0.0353], 4) + assert low == 0.0 and 20 < high < 27, (low, high) + floor = t.fair_map_floor([8, 8, 8], 4) + assert 11 < floor < 18 and t.fair_map_floor([8, 8, 8], 4) == floor, floor + assert t.fair_map_floor([48] * 12, 4) < floor, 'more games per map must lower the floor' + # A map where one start wins nearly everything is flagged, with dominance 4x its fair share. + table = t.share_table([12, 2, 1, 1], 1) + assert table['best'] == 0 and table['p'] < 0.001 and table['rms_points'] > 20, table + assert t.share_table([4, 0, 0, 0], 1)['dominance'] == 4.0 + print('statistics checks passed') + + +def check_scorer(): + # Quality matching wins exactly on three maps: the permutation test should notice. + points = [(f'm{m}', q, q / 10, q / 10) for m in range(3) for q in (-1.5, -0.5, 0.5, 1.5)] + rho = t.spearman([p[1] for p in points], [p[2] for p in points]) + assert rho == 1.0 + assert t.scorer_permutation_p(points, rho, 2, 500, 1) < 0.02 + # Quality unrelated to wins: no signal. + shuffled = [(f'm{m}', q, w, w) for m in range(3) + for q, w in zip((-1.5, -0.5, 0.5, 1.5), (0.1, -0.1, 0.15, -0.15))] + rho = t.spearman([p[1] for p in shuffled], [p[2] for p in shuffled]) + assert t.scorer_permutation_p(shuffled, rho, 2, 500, 1) > 0.1 + print('scorer checks passed') + + +if __name__ == '__main__': + check_adjudication() + check_parsing() + check_statistics() + check_scorer() + print('all harness checks passed') diff --git a/tools/map_fairness_tournament.py b/tools/map_fairness_tournament.py index 67dddbd48..a45c24d65 100644 --- a/tools/map_fairness_tournament.py +++ b/tools/map_fairness_tournament.py @@ -41,6 +41,11 @@ PRESET_DIR = ROOT / 'tools' / 'map-fairness' BASELINE_METHOD = 15 # Symmetric arena: exactly symmetric starts, the fairness baseline. Z95 = 1.959963984540054 +# Two-sided 95% Student t quantiles for 1..30 degrees of freedom. +T975 = [12.706, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228, 2.201, 2.179, 2.160, 2.145, + 2.131, 2.120, 2.110, 2.101, 2.093, 2.086, 2.080, 2.074, 2.069, 2.064, 2.060, 2.056, 2.052, 2.048, + 2.045, 2.042] +MIN_MAPS_FOR_INTERVAL = 5 # below this a bootstrap over maps cannot say much FACTORS = ['wheat', 'wood', 'fertility', 'depth', 'room', 'isolation'] COLONY_FIELDS = ['wheat_distance', 'wood_distance', 'catchment_tiles', 'build_sites', 'resource_amount', 'rival_distance', 'rivals_within_threat', 'mean_fertility', @@ -327,7 +332,7 @@ def adjudicate(game, record, config): outcome['winner_slot'] = slot_of(winner) eliminated = sorted((e['eliminated_tick'], t) for t, e in teams.items() if e['eliminated_tick'] >= 0) outcome['elimination_order'] = [[slot_of(t), tick] for tick, t in eliminated] - # Placement 1 is the winner; other survivors by prestige, then eliminated colonies, the + # Placement 1 is the winner; other survivors by strength, then eliminated colonies, the # later-eliminated ahead. Ties break towards the lower start index, only to stay deterministic. survivors = sorted((t for t in teams if t != winner and teams[t]['eliminated_tick'] < 0), key=lambda t: (tuple(-v for v in strength(t)), slot_of(t))) @@ -572,6 +577,11 @@ def bootstrap(items, statistic, draws, seed): return [values[int(0.025 * (len(values) - 1))], values[int(0.975 * (len(values) - 1))]] +def t975(df): + """Two-sided 95% Student t quantile; past 30 degrees of freedom a close approximation.""" + return T975[df - 1] if df <= len(T975) else Z95 + 2.4 / df + + def squared_bias(counts): """Unbiased estimate of sum_s (p_s - 1/k)^2 from multinomial counts. @@ -591,6 +601,43 @@ def rms_points(mean_squared_bias, k): return 100 * math.sqrt(max(0.0, mean_squared_bias) / k) +def bias_interval(biases, k): + """95% t interval over maps for the mean squared bias, expressed as position bias in points. + + Each map's estimate carries its own game-to-game noise, so the spread across maps covers that + noise as well as real differences between maps. With few maps it is honestly wide.""" + if len(biases) < 2: + return None + mean = statistics.mean(biases) + half = t975(len(biases) - 1) * statistics.stdev(biases) / math.sqrt(len(biases)) + return [rms_points(mean - half, k), rms_points(mean + half, k)] + + +_FLOOR_CACHE = {} + + +def fair_map_floor(game_counts, k, draws=2000): + """95th percentile of the position-bias headline if every map were perfectly fair, given the + same number of decided games per map: the level a headline has to clear to mean anything.""" + if not game_counts: + return None + key = (tuple(sorted(game_counts)), k, draws) + if key not in _FLOOR_CACHE: + rng = random.Random(hash_text(repr(key))) + values = [] + for _ in range(draws): + biases = [] + for n in key[0]: + counts = [0] * k + for _ in range(n): + counts[rng.randrange(k)] += 1 + biases.append(squared_bias(counts)) + values.append(rms_points(statistics.mean(biases), k)) + values.sort() + _FLOOR_CACHE[key] = values[int(0.95 * (len(values) - 1))] + return _FLOOR_CACHE[key] + + def share_table(counts, seed): n, k = sum(counts), len(counts) statistic, p_chi2 = chi_square_uniform(counts) @@ -688,8 +735,30 @@ def scorer_points(maps, factor): return points +def scorer_permutation_p(points, observed, column, draws, seed): + """Two-sided p for a pooled rank correlation: shuffle quality among each map's own colonies.""" + if observed is None: + return None + by_map = {} + for point in points: + by_map.setdefault(point[0], []).append(point) + rng = random.Random(seed) + hits = 0 + for _ in range(draws): + xs, ys = [], [] + for rows in by_map.values(): + qualities = [p[1] for p in rows] + rng.shuffle(qualities) + xs += qualities + ys += [p[column] for p in rows] + value = spearman(xs, ys) + hits += value is not None and abs(value) >= abs(observed) - 1e-12 + return (hits + 1) / (draws + 1) + + def scorer_check(maps, draws, seed): check = {} + permutation_draws = min(draws, 2000) for factor in ['total', *FACTORS]: points = scorer_points(maps, factor) keys = sorted({p[0] for p in points}) @@ -700,38 +769,48 @@ def scorer_check(maps, draws, seed): def rho(sample_keys, column): chosen = [p for key in sample_keys for p in points if p[0] == key] return spearman([p[1] for p in chosen], [p[column] for p in chosen]) - check[factor] = { - 'maps': len(keys), 'colonies': len(points), - 'wins_rho': rho(keys, 2), 'wins_rho_ci': bootstrap(keys, lambda s: rho(s, 2), draws, seed), - 'placement_rho': rho(keys, 3), - 'placement_rho_ci': bootstrap(keys, lambda s: rho(s, 3), draws, seed + 1), - } - # Does the start the scorer rates best actually win more than its fair share? + enough = len(keys) >= MIN_MAPS_FOR_INTERVAL + entry = {'maps': len(keys), 'colonies': len(points), 'wins_rho': rho(keys, 2), + 'placement_rho': rho(keys, 3), + 'wins_rho_ci': bootstrap(keys, lambda s: rho(s, 2), draws, seed) if enough else None, + 'placement_rho_ci': bootstrap(keys, lambda s: rho(s, 3), draws, seed + 1) if enough else None} + if factor == 'total': + entry['wins_rho_p'] = scorer_permutation_p(points, entry['wins_rho'], 2, permutation_draws, seed + 2) + entry['placement_rho_p'] = scorer_permutation_p(points, entry['placement_rho'], 3, + permutation_draws, seed + 3) + check[factor] = entry + # Does the start the scorer rates best win more than a start picked at random on the same map? top_wins = top_n = 0 + alternatives = [] for m in maps: values = [c['total'] for c in m['record_colonies']] - if not values or max(values) - min(values) < 1e-9: + if not values or max(values) - min(values) < 1e-9 or not m['slot']['n']: continue top = max(range(len(values)), key=lambda i: values[i]) top_wins += m['slot']['counts'][top] top_n += m['slot']['n'] - check['top_rated_start'] = {'wins': top_wins, 'n': top_n, - 'share': top_wins / top_n if top_n else None, - 'wilson': list(wilson(top_wins, top_n)) if top_n else None} + alternatives.append(m['slot']['counts']) + p_value = None + if alternatives: + rng = random.Random(seed + 4) + hits = sum(sum(rng.choice(counts) for counts in alternatives) >= top_wins + for _ in range(permutation_draws)) + p_value = (hits + 1) / (permutation_draws + 1) + check['top_rated_start'] = {'wins': top_wins, 'n': top_n, 'maps': len(alternatives), + 'share': top_wins / top_n if top_n else None, 'p_one_sided': p_value} return check -def decisive_headline(maps, n, draws, seed): +def decisive_headline(maps, n): """Position bias from games won outright (elimination or prestige), leaving out capped games.""" usable = [m for m in maps if m['decisive_slot']['n'] >= 2] biases = [m['decisive_slot']['squared_bias'] for m in usable] - mean_bias = statistics.mean(biases) if biases else None return { 'decisive_maps': len(usable), 'decisive_games': sum(m['decisive_slot']['n'] for m in maps), - 'decisive_rms_points': rms_points(mean_bias, n), - 'decisive_rms_points_ci': bootstrap(biases, lambda s: rms_points(statistics.mean(s), n), draws, seed) - if biases else None, + 'decisive_rms_points': rms_points(statistics.mean(biases), n) if biases else None, + 'decisive_rms_points_ci': bias_interval(biases, n), + 'decisive_fair_floor': fair_map_floor([m['decisive_slot']['n'] for m in usable], n), 'decisive_maps_p05': sum(m['decisive_slot']['p'] < 0.05 for m in usable), } @@ -749,7 +828,6 @@ def analyse_generator(method, maps, config, catalog): biases = [m['slot']['squared_bias'] for m in usable] mean_bias = statistics.mean(biases) if biases else None seed = seed_for('generator', method) - ci = bootstrap(biases, lambda s: rms_points(statistics.mean(s), n), draws, seed) if biases else None pooled_slot = [sum(m['slot']['counts'][s] for m in maps) for s in range(n)] pooled_team = [sum(m['team']['counts'][t] for m in maps) for t in range(n)] # Is any of this generator's maps biased? Sum of per-map chi-square statistics, calibrated @@ -780,10 +858,11 @@ def analyse_generator(method, maps, config, catalog): 'prestige_share': sum(m['prestige_games'] for m in maps) / played if played else None, 'unresolved': sum(m['unresolved'] for m in maps), 'start_mismatches': sum(m['start_mismatches'] for m in maps), - 'headline_rms_points': rms_points(mean_bias, n), 'headline_rms_points_ci': ci, + 'headline_rms_points': rms_points(mean_bias, n), 'headline_rms_points_ci': bias_interval(biases, n), + 'headline_fair_floor': fair_map_floor([m['slot']['n'] for m in usable], n), 'mean_squared_bias': mean_bias, # The same headline counting only games won outright, so cap adjudication cannot drive it. - **decisive_headline(maps, n, draws, seed + 19), + **decisive_headline(maps, n), 'maps_p05': sum(p < 0.05 for p in p_values), 'maps_bh05': sum(q < 0.05 for q in bh), 'maps_holm05': sum(h < 0.05 for h in hl), 'expected_p05_by_chance': 0.05 * len(p_values), @@ -793,7 +872,8 @@ def analyse_generator(method, maps, config, catalog): 'pooled_chi2': observed, 'pooled_p': pooled_p, 'slot_index': share_table(pooled_slot, seed + 11), 'team_index': share_table(pooled_team, seed + 13), - 'scorer': scorer_check(maps, draws, seed + 17), + # Symmetric arena's colonies are identical by construction, so there is nothing to predict. + 'scorer': scorer_check(maps, draws, seed + 17) if method != BASELINE_METHOD else None, 'ticks_median': statistics.median([m['ticks_median'] for m in maps if m['ticks_median']]) if played else None, 'wall_seconds_mean': statistics.mean([m['wall_seconds_mean'] for m in maps if m['wall_seconds_mean']]) @@ -853,7 +933,8 @@ def summarize(out, config, paths): for reason in sorted({g['outcome']['end_reason'] for g in played}, key=str)}, 'engine_team_index': {'all_generators': share_table(all_team, 101), 'baseline': share_table(baseline_team, 103)}, - 'scorer_all_generators': scorer_check(maps, int(config['bootstrap_draws']), 107), + 'scorer_all_generators': scorer_check([m for m in maps if m['method'] != BASELINE_METHOD], + int(config['bootstrap_draws']), 107), 'generators': generators, 'maps': [{k: v for k, v in m.items() if k != 'record_colonies'} for m in maps], 'verification': json.loads((out / 'verification.json').read_text()) @@ -894,12 +975,13 @@ def write_csvs(out, records, games): for g in ordered: record = records[g['map']] n = record['colonies'] + placement = {int(k): v for k, v in g['outcome']['placement'].items()} for team, entry in sorted(g['parsed']['teams'].items(), key=lambda i: int(i[0])): slot = (int(team) - g['rotation']) % n rows = record.get('colony_quality', []) quality = rows[slot] if len(rows) == n else {} writer.writerow([record['method'], record['map_seed'], g['rotation'], g['seed'], slot, team, - *record['starts'][slot], g['outcome']['placement'].get(str(slot)), + *record['starts'][slot], placement.get(slot), int(g['outcome']['winner_slot'] == slot), *[entry.get(k) for k in team_fields], quality.get('total'), *[quality.get(f) for f in FACTORS]]) @@ -938,6 +1020,22 @@ def interval(ci, fmt=lambda v: f'{v:.1f}'): return '' if not ci or ci[0] is None else f' [{fmt(ci[0])}, {fmt(ci[1])}]' +def rho_text(entry, which): + """'0.21 (p 0.398) [-0.15, 0.57]' for a scorer correlation, with whatever is available.""" + value = entry.get(f'{which}_rho') + text = num(value) + if entry.get(f'{which}_rho_p') is not None: + text += f' (p {pval(entry[f"{which}_rho_p"])})' + return text + interval(entry.get(f'{which}_rho_ci'), lambda v: f'{v:.2f}') + + +def top_rated_text(top, n): + if not top['n']: + return 'no map had a top-rated start' + return (f'the start the scorer rated best won {top["wins"]} of {top["n"]} games on {top["maps"]} maps ' + f'({pct(top["share"])}, fair {pct(1 / n)}; one-sided permutation p {pval(top["p_one_sided"])})') + + def markdown(summary, config): n = int(config['colonies']) lines = [f'# Map fairness tournament: {config["name"]}', ''] @@ -966,23 +1064,28 @@ def markdown(summary, config): f'matched their first run exactly (ticks, winner, order count, every team result).') lines += ['', '## Headline by generator', '', 'Position bias is the root-mean-square gap between each start\'s true win rate and the fair ' - f'1/{n}, corrected for the scatter raw win shares show even on a fair map, averaged over maps ' - '(95% bootstrap interval over maps). Biased maps counts maps whose wins by start reject a ' - 'uniform split: raw p < 0.05, and after Benjamini-Hochberg across that generator\'s maps. ' - 'Scorer rho is the within-map rank correlation between a colony\'s start-quality score and its ' - 'win share. Decisive only repeats the position bias counting just games won outright, with the ' - 'number of such games, so tick-cap adjudication cannot drive it.', '', - '| Generator | Maps | Games | Cap | Position bias (pp) | Decisive only (pp, games) | Biased maps p<.05 / BH | Best start / fair (median) | Any bias p | Scorer rho (wins) |', - '| --- | ---: | ---: | ---: | --- | --- | --- | ---: | ---: | --- |'] + f'1/{n}, corrected for the scatter raw win shares show even on a fair map and averaged over maps; ' + 'the bracket is a 95% t interval over maps. Fair-map floor is the 95th percentile that headline ' + 'reaches when every map is perfectly fair, with the same decided games per map: a headline below ' + 'it cannot be told apart from a fair generator. Decisive only repeats the position bias counting ' + 'just games won outright, so tick-cap adjudication cannot drive it. Biased maps counts maps whose ' + 'wins by start reject a uniform split, raw p < 0.05 and after Benjamini-Hochberg across that ' + 'generator\'s maps. Any bias p tests whether any of its maps is biased. Scorer rho is the ' + 'within-map rank correlation between a colony\'s start-quality score and its win share, with a ' + 'permutation p-value.', '', + '| Generator | Maps | Games | Cap | Position bias (pp) [95%] | Fair-map floor (pp) | Decisive only (pp, games) | Biased maps p<.05 / BH | Best start / fair (median) | Any bias p | Scorer rho (wins) |', + '| --- | ---: | ---: | ---: | --- | ---: | --- | --- | ---: | ---: | --- |'] for g in summary['generators']: - scorer = (g['scorer'] or {}).get('total') or {} + scorer = (g['scorer'] or {}).get('total') + scorer_cell = rho_text(scorer, 'wins') if scorer else ('n/a' if g['method'] == BASELINE_METHOD else '-') lines.append( f'| {g["name"]} ({g["method"]}) | {g["maps_with_winners"]}/{g["maps"]} | {g["played"]} | {pct(g["cap_share"], 0)} ' f'| {num(g["headline_rms_points"], 1)}{interval(g["headline_rms_points_ci"])} ' + f'| {num(g["headline_fair_floor"], 1)} ' f'| {num(g["decisive_rms_points"], 1)}{interval(g["decisive_rms_points_ci"])} ({g["decisive_games"]}) ' f'| {g["maps_p05"]} / {g["maps_bh05"]} (chance {g["expected_p05_by_chance"]:.1f}) ' f'| {num((g["dominance"] or {}).get("median"))} | {pval(g["pooled_p"])} ' - f'| {num(scorer.get("wins_rho"))}{interval(scorer.get("wins_rho_ci"), lambda v: f"{v:.2f}")} |') + f'| {scorer_cell} |') lines += ['', '## Engine team-index bias', '', 'Wins by team index, pooled over rotations, so every index played every start equally often. ' 'A skew here is the engine\'s processing order (team 0 also carries the passive local player of ' @@ -1023,23 +1126,23 @@ def markdown(summary, config): f'- Maps significant after Holm: {g["maps_holm05"]}; best-start dominance min / median / max: {dominance}.'] scorer = g['scorer'] or {} if scorer.get('total'): - top = scorer['top_rated_start'] - lines.append('- Scorer check: rho with win share ' + num(scorer['total']['wins_rho']) - + interval(scorer['total']['wins_rho_ci'], lambda v: f'{v:.2f}') - + ', with placement ' + num(scorer['total']['placement_rho']) - + interval(scorer['total']['placement_rho_ci'], lambda v: f'{v:.2f}') - + f'; the top-rated start won {top["wins"]} of {top["n"]} ({pct(top["share"])}' - + interval(top['wilson'], pct) + f', fair {pct(1 / n)}). By factor (wins rho): ' + total = scorer['total'] + lines.append(f'- Scorer check: rho with win share {rho_text(total, "wins")}, with placement ' + f'{rho_text(total, "placement")}; {top_rated_text(scorer["top_rated_start"], n)}. ' + 'By factor (wins rho): ' + ', '.join(f'{f} {num((scorer.get(f) or {}).get("wins_rho"))}' for f in FACTORS) + '.') elif g['method'] == BASELINE_METHOD: - lines.append('- Scorer check: not applicable, every colony scores the same by construction.') - overall = (summary['scorer_all_generators'] or {}).get('total') - if overall: + lines.append('- Scorer check: left out. Symmetric arena colonies are identical by construction; the small ' + 'score differences between them come from the build-site count, which is not symmetric ' + 'under rotation.') + overall = summary['scorer_all_generators'] or {} + if overall.get('total'): + total = overall['total'] lines += ['', '## Scorer check across generators', '', - f'Within-map rank correlation between start-quality score and win share over {overall["colonies"]} ' - f'colonies on {overall["maps"]} maps: rho {num(overall["wins_rho"])}' - f'{interval(overall["wins_rho_ci"], lambda v: f"{v:.2f}")}; with placement ' - f'{num(overall["placement_rho"])}{interval(overall["placement_rho_ci"], lambda v: f"{v:.2f}")}.'] + f'Within-map rank correlation between start-quality score and win share over {total["colonies"]} ' + f'colonies on {total["maps"]} maps, Symmetric arena left out: rho {rho_text(total, "wins")}; with ' + f'placement {rho_text(total, "placement")}. Across the same maps, ' + f'{top_rated_text(overall["top_rated_start"], n)}.'] lines.append('') return '\n'.join(lines) @@ -1122,8 +1225,8 @@ def describe(job, game): print(f'\nwrote {out / "summary.md"}, summary.json, games.csv, colonies.csv, maps.csv') for g in summary['generators']: print(f' {g["name"]} ({g["method"]}): position bias {num(g["headline_rms_points"], 1)} pp' - f'{interval(g["headline_rms_points_ci"])}, biased maps {g["maps_p05"]}/{g["maps_with_winners"]} ' - f'(BH {g["maps_bh05"]}), cap {pct(g["cap_share"], 0)}') + f'{interval(g["headline_rms_points_ci"])} (fair-map floor {num(g["headline_fair_floor"], 1)}), ' + f'biased maps {g["maps_p05"]}/{g["maps_with_winners"]} (BH {g["maps_bh05"]}), cap {pct(g["cap_share"], 0)}') return 0 From eb596f641281ed78686b91026b6e782704aea255 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:20:31 -0400 Subject: [PATCH 071/136] Point the tournament doc at its offline test Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index 914f7b80d..e29a7382b 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -46,6 +46,9 @@ Presets live in `tools/map-fairness/*.json`. Most keys can also be set on the co Everything is seeded, so the same preset reproduces the same maps, games and report. +`python3 test/test_map_fairness_tournament.py` checks the adjudication, the log parsing and the +statistics against synthetic engine output and known values. It needs no build and plays no games. + ## What a run does 1. **Map production.** `MapGeneratorStudy w= h= teams= quality candidates=5 From 99e5b7666d8e6dd16163bcc089c9f73789227f92 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:33:25 -0400 Subject: [PATCH 072/136] Keep every colony buildable when a resource amount is turned up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resources block ground units and buildings alike, so a resource amount well above the default can wall a colony into a pocket with nowhere to put a building, or cover the very sites a generator's start search was looking at. guaranteeStartingResources does not catch the first case — a colony buried in wheat has wheat at its feet, so it counts as served — and the second failed generation outright. Three passes close that gap, none of them running at the default amounts: - openCrampedStarts (shared/Resources.h) clears the resource tiles nearest a cramped colony, one ring at a time outwards, until it can walk to 16 tiles where a 4x4 building fits within 24 steps: the study tool's own viability measure, counted from the team's own workers. A colony that already has the room is untouched, and a colony on a genuinely small spot simply runs out of rings. Protected walls (a generator's stone ridgelines) are never cleared. Callers re-run the guarantee afterwards, since the clearing can take the nearest crop along with the wall. - The height-field generators call it through openStartsBuriedByAmounts after placeStarts, and Concrete islands, Isles, Old random and Old islands after their own backstop. - divideUpPlayerLands only looked 1 to 2 tiles out from where the default-sized wheat field would lie for a swarm site. At a non-default amount it now widens that window out from the same wheat rather than failing, so a colony still starts beside its own farmland. - placeSettlement now measures the workers' room as well as the swarm's: a worker tile is a free tile inside the home mask touching the swarm, and a kit filling the ground against it could leave too few and fail the map. The nearest-tie pick is still the same random one, so a colony that had the room does not move; only when it is short does the search take the nearest site that has it. Over 40 seeds at 256x256, Fjord continent left some colony under 16 build sites on 33 maps at wheat 300% and 22 at wood 300%; it is now 0 of 20 at both, as are Ring world and all four height-field generators. Fjord's defaults are cramped on 13 of 60 seeds, which predates this branch and is untouched here, since default output cannot change. Concrete islands at wheat 300% and Contested commons at fruit 0% each failed one seed in twenty before these passes and neither does now. Default maps are byte-identical to 95e24a761's (225 dumps: 15 generators, 5 seeds, 128x128, 256x256, 256x128, plus the 225 study and quality lines). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generators/ConcreteIslandsGenerator.cpp | 12 ++- .../generators/CraterLakesGenerator.cpp | 5 +- .../generator/generators/IslandsGenerator.cpp | 5 +- .../generator/generators/IslesGenerator.cpp | 12 ++- .../generator/generators/RiverGenerator.cpp | 5 +- .../generators/RuggedArchipelagoGenerator.cpp | 10 +++ .../generators/ShatteredCoastGenerator.cpp | 10 +++ .../generator/generators/SwampGenerator.cpp | 5 +- src/map/generator/shared/Resources.cpp | 78 +++++++++++++++++++ src/map/generator/shared/Resources.h | 13 ++++ src/map/generator/shared/Settlements.cpp | 40 +++++++++- .../generator/shared/StartingPositions.cpp | 44 +++++++---- src/map/generator/shared/Terrain.cpp | 15 ++++ src/map/generator/shared/Terrain.h | 4 + 14 files changed, 234 insertions(+), 24 deletions(-) diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.cpp b/src/map/generator/generators/ConcreteIslandsGenerator.cpp index ac26dd986..95d23dcd2 100644 --- a/src/map/generator/generators/ConcreteIslandsGenerator.cpp +++ b/src/map/generator/generators/ConcreteIslandsGenerator.cpp @@ -177,10 +177,16 @@ static bool generate(Game &game, GenerationContext &context) if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber, {options.wheat, options.wood, options.stone})) return false; - // A colony's own fields are its only starting wheat and wood; with other amounts than the - // default, make sure each still has both within reach. - if (options.wheat != 100 || options.wood != 100) + // A colony's own fields are its only starting wheat and wood, and a field or deposit grown well + // past its default size can also wall the colony in with nowhere left to build. At any amount + // other than the default, open up such a colony and then make sure each still has both crops + // within reach, in case the clearing took the nearest one along with the wall. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100 || options.fruit != 100) + { + openCrampedStarts(game, context); guaranteeStartingResources(game, context, 24, 32); + } // Initialize final team info for (int i = 0; i < context.request.nbTeams; ++i) diff --git a/src/map/generator/generators/CraterLakesGenerator.cpp b/src/map/generator/generators/CraterLakesGenerator.cpp index a711d21ef..9aa9b15b0 100644 --- a/src/map/generator/generators/CraterLakesGenerator.cpp +++ b/src/map/generator/generators/CraterLakesGenerator.cpp @@ -21,7 +21,10 @@ static bool generate(Game &game, GenerationContext &context) })) return false; context.stage = "starts"; - return placeStarts(game, context); + if (!placeStarts(game, context)) + return false; + openStartsBuriedByAmounts(game, context, terrain); + return true; } GeneratorDefinition craterLakesDefinition() diff --git a/src/map/generator/generators/IslandsGenerator.cpp b/src/map/generator/generators/IslandsGenerator.cpp index ab400f1b7..116cd714e 100644 --- a/src/map/generator/generators/IslandsGenerator.cpp +++ b/src/map/generator/generators/IslandsGenerator.cpp @@ -23,7 +23,10 @@ static bool generate(Game &game, GenerationContext &context) })) return false; context.stage = "starts"; - return placeStarts(game, context); + if (!placeStarts(game, context)) + return false; + openStartsBuriedByAmounts(game, context, terrain); + return true; } GeneratorDefinition islandsDefinition() diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index b048941b0..8846331f1 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -212,10 +212,16 @@ static bool generate(Game &game, GenerationContext &context) { return false; } - // A colony's own fields are its only wheat and wood; with other amounts than the default, - // make sure each still has both within reach. - if (options.wheat != 100 || options.wood != 100) + // A colony's own fields are its only wheat and wood, and a field or deposit grown well past its + // default size can also wall the colony in with nowhere left to build. At any amount other than + // the default, open up such a colony and then make sure each still has both crops within reach, + // in case the clearing took the nearest one along with the wall. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100) + { + openCrampedStarts(game, context); guaranteeStartingResources(game, context, 24, 32); + } // Initialize final team info for (int i = 0; i < context.request.nbTeams; ++i) diff --git a/src/map/generator/generators/RiverGenerator.cpp b/src/map/generator/generators/RiverGenerator.cpp index a4598b440..6a1d70f95 100644 --- a/src/map/generator/generators/RiverGenerator.cpp +++ b/src/map/generator/generators/RiverGenerator.cpp @@ -21,7 +21,10 @@ static bool generate(Game &game, GenerationContext &context) })) return false; context.stage = "starts"; - return placeStarts(game, context); + if (!placeStarts(game, context)) + return false; + openStartsBuriedByAmounts(game, context, terrain); + return true; } GeneratorDefinition riverDefinition() diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index 490d1d32d..4bb530763 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -395,6 +395,16 @@ static bool generate(Game &game, GenerationContext &context) // wood even though the search ran in every direction. Top up anyone still missing either // within comfortable range; islands that were already generous are left untouched. guaranteeStartingResources(game, context, 24, 32); + // A deposit grown well past its default size can wall a colony into its own clearing with + // nowhere left to build on an island this small, which the guarantee above doesn't address: a + // colony buried in wood has wood at its feet. At any amount other than the default, open such a + // colony back up and guarantee the crops once more. At the defaults none of this runs. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100) + { + openCrampedStarts(game, context); + guaranteeStartingResources(game, context, 24, 32); + } return true; } diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 6a7fa4b89..119c20b93 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -834,6 +834,16 @@ static bool generate(Game &game, GenerationContext &context) // placement method. Top up anyone still missing either within comfortable range now that // placeStarts() has already carved its own clearing, so there's nothing left to step on. guaranteeStartingResources(game, context, 24, 32); + // A deposit grown well past its default size can wall a colony into its own clearing with + // nowhere left to build, which the guarantee above doesn't address: a colony buried in wheat + // has wheat at its feet. At any amount other than the default, open such a colony back up and + // guarantee the crops once more. At the defaults none of this runs. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100) + { + openCrampedStarts(game, context); + guaranteeStartingResources(game, context, 24, 32); + } return true; } diff --git a/src/map/generator/generators/SwampGenerator.cpp b/src/map/generator/generators/SwampGenerator.cpp index bee3b6616..368cc77a3 100644 --- a/src/map/generator/generators/SwampGenerator.cpp +++ b/src/map/generator/generators/SwampGenerator.cpp @@ -17,7 +17,10 @@ static bool generate(Game &game, GenerationContext &context) { hm.makeSwamp(smoothing); })) return false; context.stage = "starts"; - return placeStarts(game, context); + if (!placeStarts(game, context)) + return false; + openStartsBuriedByAmounts(game, context, terrain); + return true; } GeneratorDefinition swampDefinition() diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index aa49d6dce..171fa53c8 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -12,6 +12,7 @@ #include "Regions.h" #include "StartingPositions.h" #include "Terrain.h" +#include "Unit.h" #include #include #include @@ -543,4 +544,81 @@ void guaranteeStartingResources(Game &game, GenerationContext &context, int whea placeReachable(WOOD); } } + +namespace { +// Where a team's own workers can walk within `range` — resources, water and buildings all block a +// unit — and how many of the tiles reached start a 4x4 building. Seeded from the workers rather +// than the boot tile, which is the swarm's own tile, so this counts the room a colony actually has. +struct WorkerReach { + std::vector dist; + int sites = 0; +}; +WorkerReach reachFromWorkers(Map &map, int team, int range) { + const int w = map.getW(), h = map.getH(); + WorkerReach r; + r.dist.assign(size_t(w) * h, -1); + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) { + r.dist[size_t(y) * w + x] = 0; + q[qTail++] = y * w + x; + } + } + while (qHead < qTail) { + const int p = q[qHead++]; + const int x = p % w, y = p / w; + if (map.isFreeForBuilding(x, y, 4, 4)) + ++r.sites; + if (r.dist[p] >= range) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy), np = ny * w + nx; + if (r.dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { + r.dist[np] = r.dist[p] + 1; + q[qTail++] = np; + } + } + } + return r; +} +} // namespace + +void openCrampedStarts(Game &game, GenerationContext &context, int sites, int range, + const std::vector *protectedWalls) { + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + if (protectedWalls && protectedWalls->size() != size_t(w) * h) + throw GenerationFailure("Protected wall mask does not match the map size"); + for (int team = 0; team < context.request.nbTeams; ++team) { + WorkerReach reach = reachFromWorkers(map, team, range); + if (reach.sites >= sites) + continue; + // Rings measured with resources ignored, so they walk out through the wall itself instead of + // stopping at its inner face the way the colony's own flood does. + const int bootX = map.normalizeX(context.bootX[team]), + bootY = map.normalizeY(context.bootY[team]); + const std::vector open = terrainOnlyReach(map, bootX, bootY, range, protectedWalls); + std::vector> rings(size_t(range) + 1); + for (int p = 0; p < w * h; ++p) + if (open[p] >= 1 && open[p] <= range && map.isResource(p % w, p / w) && + !(protectedWalls && (*protectedWalls)[p])) + rings[open[p]].push_back(p); + // One ring at a time, nearest first, re-measuring after each: the ring that finally gives the + // colony its room is the last one cleared, so nothing further out is touched. A colony on a + // genuinely small spot — a real islet, with nothing to open up — just runs out of rings. + for (int ring = 1; ring <= range && reach.sites < sites; ++ring) { + if (rings[ring].empty()) + continue; + for (const int p : rings[ring]) + map.setNoResource(p % w, p / w, 1); + reach = reachFromWorkers(map, team, range); + } + } +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h index 3ae094db9..98e6198b7 100644 --- a/src/map/generator/shared/Resources.h +++ b/src/map/generator/shared/Resources.h @@ -37,4 +37,17 @@ void scatterResources(Game &, GenerationContext &, const ResourceDensities &); void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, int woodRange, int clearRadius = 0, const std::vector *protectedWalls = nullptr); +// A colony with nowhere to build is unplayable however rich the ground around it is: resources +// block ground units and buildings alike, so a resource amount well above the default can wall a +// swarm into a pocket with no room for a 4x4 building. guaranteeStartingResources above only opens +// a pocket for a team that cannot reach a crop at all, which is the opposite case — a team buried +// in wheat has wheat right at its feet. This clears the resource tiles nearest such a colony, one +// ring at a time outwards, until it can walk to `sites` tiles where a 4x4 building fits within +// `range` steps (the study tool's own viability measure, counted from the team's own workers), or +// until the rings pass `range`. A colony that already has the room is left untouched, so this only +// ever takes away a wall that a non-default amount built. Protected walls are never cleared. +// Call guaranteeStartingResources again afterwards: the clearing can take away the nearest wheat +// or wood along with the wall. +void openCrampedStarts(Game &game, GenerationContext &context, int sites = 16, int range = 24, + const std::vector *protectedWalls = nullptr); } // namespace MapGeneration diff --git a/src/map/generator/shared/Settlements.cpp b/src/map/generator/shared/Settlements.cpp index 539c7d919..4da54cc92 100644 --- a/src/map/generator/shared/Settlements.cpp +++ b/src/map/generator/shared/Settlements.cpp @@ -29,6 +29,7 @@ bool placeSettlement(Game &game, GenerationContext &context, int team, auto inside = [&](int x, int y) { return home[game.map.normalizeY(y) * w + game.map.normalizeX(x)] != 0; }; std::vector candidates; + std::vector> eligible; int nearest = std::numeric_limits::max(); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) @@ -47,6 +48,7 @@ bool placeSettlement(Game &game, GenerationContext &context, int team, continue; int distance = game.map.warpDistSquare(x, y, game.map.normalizeX(anchor.x), game.map.normalizeY(anchor.y)); + eligible.emplace_back(distance, MapGeneratorPoint(x, y)); if (distance < nearest) { nearest = distance; @@ -57,7 +59,43 @@ bool placeSettlement(Game &game, GenerationContext &context, int team, } if (candidates.empty()) return fail("no swarm footprint fits inside the home region"); - const auto p = candidates[context.bounded(stream, candidates.size())]; + // The workers need room as well as the swarm: a worker tile is a free tile inside the home + // region touching the swarm, and Map::doesPosTouchBuilding counts diagonals, so those are the + // ring around its footprint. A colony whose own kit fills the ground right against the swarm + // can leave too few of them, which used to fail the whole map. Measure that room before the + // building goes down, so a site further from the anchor can be taken instead. The first pick is + // the same random one as ever, drawn the same way from the same nearest-tie list, so any colony + // that already had the room stays exactly where it was. + auto workerRoom = [&](const MapGeneratorPoint &at) + { + int room = 0; + for (int y = at.y - 1; y <= at.y + buildingType->height; ++y) + for (int x = at.x - 1; x <= at.x + buildingType->width; ++x) + { + if (x >= at.x && x < at.x + buildingType->width && y >= at.y && + y < at.y + buildingType->height) + continue; + if (inside(x, y) && + game.map.isFreeForGroundUnit(game.map.normalizeX(x), game.map.normalizeY(y), + false, 1u << team)) + ++room; + } + return room; + }; + MapGeneratorPoint p = candidates[context.bounded(stream, candidates.size())]; + if (workerRoom(p) < context.request.nbWorkers) + { + const MapGeneratorPoint *roomier = nullptr; + int roomiestDistance = std::numeric_limits::max(); + for (const auto &[distance, at] : eligible) + if (distance < roomiestDistance && workerRoom(at) >= context.request.nbWorkers) + { + roomiestDistance = distance; + roomier = &at; + } + if (roomier) + p = *roomier; + } Building *building = game.addBuilding(p.x, p.y, type, team, 1, 0); if (!building) return fail("swarm placement failed"); diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index 250b1a8bb..27bb938f0 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -148,27 +148,45 @@ bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector startingLocations; - for (unsigned int j = 0; j < baseLocations.size(); ++j) + auto startsWithinOfWheat = [&](int window) { - int minValue = 100000; - for (int x = 0; x < 4; ++x) + std::vector found; + for (unsigned int j = 0; j < baseLocations.size(); ++j) { - for (int y = 0; y < 4; ++y) + int minValue = 100000; + for (int x = 0; x < 4; ++x) { - int nx = game.map.normalizeX(baseLocations[j].x + x); - int ny = game.map.normalizeY(baseLocations[j].y + y); - minValue = std::min(wheatDistance[ny * game.map.getW() + nx], minValue); + for (int y = 0; y < 4; ++y) + { + int nx = game.map.normalizeX(baseLocations[j].x + x); + int ny = game.map.normalizeY(baseLocations[j].y + y); + minValue = std::min(wheatDistance[ny * game.map.getW() + nx], minValue); + } + } + if (minValue >= 1 && minValue <= window) + { + found.push_back(baseLocations[j]); } } - if (minValue >= 1 && minValue <= 2) - { - startingLocations.push_back(baseLocations[j]); - } - } + return found; + }; + std::vector startingLocations = startsWithinOfWheat(2); // Place swarms chooseFreeForBuildingSquares(game, startingLocations, swarm, i); + // A field grown well past its default size covers the building sites beside it, and the + // window above only looks 1 to 2 tiles out from where the default-sized wheat field + // would lie. Rather than fail, look further out from that same wheat for a site the + // fields have left clear: the colony still starts beside its own farmland, just not + // right up against it. Only a non-default amount can reach this. + for (int window = 6; + startingLocations.empty() && window <= 24 && + (resources.wheat != 100 || resources.wood != 100 || resources.stone != 100); + window += 6) + { + startingLocations = startsWithinOfWheat(window); + chooseFreeForBuildingSquares(game, startingLocations, swarm, i); + } if (startingLocations.size() == 0) { return false; diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index a282842b1..aa4cddd10 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -34,6 +34,21 @@ void MapGeneration::readResourceControls(HeightFieldOptions &options, options.algae = request.option("algae-amount"); options.hilltopStone = request.option("hilltop-stone") != 0; } +void MapGeneration::openStartsBuriedByAmounts(Game &game, GenerationContext &context, + const HeightFieldOptions &options) +{ + if (options.wheat == 100 && options.wood == 100 && options.stone == 100 && + options.algae == 100) + return; + // The resource bands are painted from map-wide noise levels with no awareness of where any team + // starts, so an amount well above the default widens them until they can wall a colony in with + // nowhere left to build — which the wheat/wood guarantee inside generateHeightField doesn't + // address, since a colony buried in wheat has wheat at its feet. placeStarts has already carved + // out the swarm's own rectangle by now, so nothing needs to be kept clear for it, and the + // guarantee runs once more in case the clearing took the colony's nearest crop with the wall. + openCrampedStarts(game, context); + guaranteeStartingResources(game, context, 24, 32); +} bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, const HeightFieldOptions &options, const HeightFieldBuilder &build) diff --git a/src/map/generator/shared/Terrain.h b/src/map/generator/shared/Terrain.h index 831ea208d..38913e44e 100644 --- a/src/map/generator/shared/Terrain.h +++ b/src/map/generator/shared/Terrain.h @@ -19,6 +19,10 @@ struct HeightFieldOptions // The resource controls every height-field generator shares, and reading them from a request. std::vector heightFieldResourceControls(); void readResourceControls(HeightFieldOptions &, const GenerationRequest &); +// Call after placeStarts: at any non-default resource amount, open up a colony that the widened +// bands walled in (openCrampedStarts) and re-run the wheat/wood guarantee in case the clearing +// took its nearest crop too. Nothing happens at the default amounts. +void openStartsBuriedByAmounts(Game &, GenerationContext &, const HeightFieldOptions &); using HeightFieldBuilder = std::function; bool generateHeightField(Game &, GenerationContext &, const HeightFieldOptions &, const HeightFieldBuilder &); From 3a82503df4f7f09644df28363b5e44b8b8ce9f1b Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:33:25 -0400 Subject: [PATCH 073/136] Add resource amounts and switches to Contested commons, Maze and Fjord continent Contested commons gains Wheat, Wood, Stone, Algae and Fruit amounts (percentages, 0 to 300 in steps of 25, default 100): the amounts set how many of the commons' zones become wheat, wood or fruit, the size of its single quarry and how much algae grows in the moat, with any extra chance drawn from a stream of its own. Each home island's kit stays exactly as it was, so the amounts change what the contested middle is worth, not what a colony starts with. Switches, both on: Moat bridges (off, no bridge crosses the moat and the commons must be swum to) and Jagged coastlines (off, the islands and the commons are smooth rounds). Fjord continent gains the same five amounts: they scale the ambient scatter, the core's stone and fruit clumps, the lake's and the open sea's algae, and the extra clumps along the fjord banks; the per-team starter kits and the one guaranteed corn and wood clump on every bank side stay unscaled. Switches: Sandy lake shore (on; off, the central lake gets an ordinary beach instead of its wide no-man's-land) and Fjord bank deposits (on; off, only the guaranteed clumps remain). lake-connected is now declared as a switch, with the same id, meaning and default as before. Maze keeps its existing explicit Wheat, Wood, Stone, Algae and Fruit counts, which already are its amounts, and gains two switches: Sand roads (on; off, the passages are grass from shore to shore, open to farmland and to building) and Treasure in dead ends (on; off, the same fruit is scattered along the passages' shores instead of hidden at the ends). Default maps are byte-identical to 95e24a761's (225 dumps: 15 generators, 5 seeds, 128x128, 256x256, 256x128). Every new control's minimum and maximum and every switch's other state generated on all 20 sweep seeds at 256x256 with no failures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generators/ContestedCommonsGenerator.cpp | 43 +++++++-- .../generators/ContestedCommonsGenerator.h | 2 + .../generators/FjordContinentGenerator.cpp | 91 +++++++++++++++---- .../generators/FjordContinentGenerator.h | 2 + .../generator/generators/MazeGenerator.cpp | 26 ++++-- src/map/generator/generators/MazeGenerator.h | 1 + 6 files changed, 129 insertions(+), 36 deletions(-) diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp index 8df8696ce..f67fecadb 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.cpp +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -109,8 +109,8 @@ static bool generate(Game &game, GenerationContext &context) // the commons radius is large. Size everything off the worst case each jaggedness value // can actually produce, not off the ideal circle, so the sizing math and the moat's // physical enforcement (below) agree on where the real boundary is. - const double teamJaggedness = 0.35; - const double commonsJaggedness = 0.32; + const double teamJaggedness = options.jaggedCoasts ? 0.35 : 0.0; + const double commonsJaggedness = options.jaggedCoasts ? 0.32 : 0.0; const int homeRadius = std::max(10, minDist * options.homeSize / 100); for (unsigned int i = 0; i < teamPoints.size(); ++i) @@ -157,7 +157,7 @@ static bool generate(Game &game, GenerationContext &context) // split further down only ever touches the round island, never a narrow connecting strip. int bridgeAreaNumber = areaNumber; areaNumber += 1; - for (unsigned int b = 0; b < bridgeAngles.size(); ++b) + for (unsigned int b = 0; b < bridgeAngles.size() && options.moatBridges; ++b) { double theta = bridgeAngles[b] * pi / 180.0; for (int r = commonsRadius - 2; r <= commonsRadius + moatWidth + 2; ++r) @@ -219,7 +219,14 @@ static bool generate(Game &game, GenerationContext &context) if (r >= commonsRadius - 1 && r < commonsRadius + moatWidth) { game.map.setUMatPos(x, y, WATER, 1); - if (context.bounded("layout", 4) == 0) + // A quarter of the ring gets algae. The algae amount thins or thickens that with + // draws from a stream of its own, so the layout's draws stay the same. + bool seeded = context.bounded("layout", 4) == 0; + if (options.algae < 100) + seeded = seeded && int(context.bounded("commons-algae", 100)) < options.algae; + else if (options.algae > 100) + seeded = seeded || int(context.bounded("commons-algae", 300)) < options.algae - 100; + if (seeded) game.map.setResource(x, y, ALGA, 1); } } @@ -280,9 +287,10 @@ static bool generate(Game &game, GenerationContext &context) ROLE_FRUIT = 3, ROLE_QUARRY = 4 }; - int woodTarget = std::max(1, zoneCount * 2 / 7); - int cornTarget = std::max(1, zoneCount * 2 / 7); - int fruitTarget = std::max(1, zoneCount / 7); + // The amount controls scale how many zones take each role. + int woodTarget = int(scaledCount(std::max(1, zoneCount * 2 / 7), options.wood)); + int cornTarget = int(scaledCount(std::max(1, zoneCount * 2 / 7), options.wheat)); + int fruitTarget = int(scaledCount(std::max(1, zoneCount / 7), options.fruit)); std::vector order(zoneCount); for (int z = 0; z < zoneCount; ++z) @@ -338,7 +346,7 @@ static bool generate(Game &game, GenerationContext &context) if (!pts.empty()) { MapGeneratorPoint quarry = pts[context.bounded("layout", pts.size())]; - game.map.setResource(quarry.x, quarry.y, STONE, 5); + setScaledResource(game.map, quarry.x, quarry.y, STONE, 5, options.stone); } } else if (role[z] == ROLE_WOOD) @@ -434,7 +442,11 @@ static bool generate(Game &game, GenerationContext &context) ContestedCommonsOptions::ContestedCommonsOptions(const GenerationRequest &r) : homeSize(r.option("home-island-size")), commonsSize(r.option("commons-size")), - moatWidth(r.option("moat-width")), bridgeCount(r.option("bridge-count")) + moatWidth(r.option("moat-width")), bridgeCount(r.option("bridge-count")), + moatBridges(r.option("moat-bridges") != 0), jaggedCoasts(r.option("jagged-coasts") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) { } @@ -448,6 +460,17 @@ GeneratorDefinition contestedCommonsDefinition() {{"home-island-size", "Home island size", 20, 35, 5, 25, ControlGroup::Terrain}, {"commons-size", "Commons size", 250, 500, 50, 400, ControlGroup::Terrain}, {"moat-width", "Moat width", 20, 50, 5, 35, ControlGroup::Terrain}, - {"bridge-count", "Bridge count", 1, 5, 1, 3, ControlGroup::Layout}}, + {"bridge-count", "Bridge count", 1, 5, 1, 3, ControlGroup::Layout}, + // Off, no bridge crosses the commons' moat. + GeneratorControl::toggle("moat-bridges", "Moat bridges", true, ControlGroup::Layout), + // Off, the home islands and the commons are smooth rounds. + GeneratorControl::toggle("jagged-coasts", "Jagged coastlines", true, ControlGroup::Terrain), + // How many of the commons' zones are wheat, wood and fruit, the size of its quarry and + // the moat's algae. Every home island's own starter fields stay as they are. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate}; } diff --git a/src/map/generator/generators/ContestedCommonsGenerator.h b/src/map/generator/generators/ContestedCommonsGenerator.h index 50476fab9..ee1c74891 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.h +++ b/src/map/generator/generators/ContestedCommonsGenerator.h @@ -5,6 +5,8 @@ struct ContestedCommonsOptions { int homeSize, commonsSize, moatWidth, bridgeCount; + bool moatBridges, jaggedCoasts; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit ContestedCommonsOptions(const GenerationRequest &r); }; GeneratorDefinition contestedCommonsDefinition(); diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 8f39eda1f..30a00702d 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -226,7 +226,7 @@ carveFjords(Game &game, GenerationContext &context, const FjordLayout &layout, // well inside lakeR (fjordInnerR, computed in computeLayout), so this carve is what actually // opens each fjord into the lake - the fjords' own tapered tips would otherwise be too narrow // to reliably merge with it. -void carveLake(Game &game, const FjordLayout &layout) { +void carveLake(Game &game, const FjordLayout &layout, bool sandyShore) { if (!layout.hasLake) return; for (int y = 0; y < layout.H; ++y) { @@ -241,7 +241,10 @@ void carveLake(Game &game, const FjordLayout &layout) { // A sandy no-man's-land ring just outside the lake - wider than controlSand's own thin // coastal fringe would give it, so the open ground around the lake reads as a deliberate // contested space rather than an ordinary beach. Only touches tiles the lake/fjord carving - // above left as land, so it never overwrites water. + // above left as land, so it never overwrites water. Without it, the lake has an ordinary + // beach and grass to build and farm on right up to it. + if (!sandyShore) + return; const double sandOuterR = layout.lakeR + layout.lakeR * 0.5; for (int y = 0; y < layout.H; ++y) { for (int x = 0; x < layout.W; ++x) { @@ -429,7 +432,8 @@ bool verifyConnectivity(Game &game, const FjordLayout &layout, // fruit type instead of a single random pick, so finding this area feels like a genuinely rich // destination and not a single repeated deposit. void placeCoreResources(Game &game, GenerationContext &context, const FjordLayout &layout, - std::vector &grid, int &areaNumber) { + const FjordContinentOptions &options, std::vector &grid, + int &areaNumber) { int coreArea = areaNumber++; std::vector corePts; for (int y = 0; y < layout.H; ++y) { @@ -448,13 +452,13 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou } } if (!corePts.empty()) { - for (int stoneClump = 0; stoneClump < 3; ++stoneClump) + for (int stoneClump = 0; stoneClump < scaledCount(3, options.stone); ++stoneClump) placeResourceClump(game.map, context, corePts[context.bounded("resources", corePts.size())], STONE, 3); - constexpr int kFruitClumpsPerType = 3; + const int fruitClumpsPerType = int(scaledCount(3, options.fruit)); for (int fruitType = 0; fruitType < 3; ++fruitType) - for (int clump = 0; clump < kFruitClumpsPerType; ++clump) + for (int clump = 0; clump < fruitClumpsPerType; ++clump) placeResourceClump(game.map, context, corePts[context.bounded("resources", corePts.size())], CHERRY + fruitType, 2); @@ -467,7 +471,7 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou // placed dead center, at the shape transform's own origin - the lake's exact geometric middle // - so there's always at least one unambiguously centered deposit regardless of how the // interior sampling below happens to land. - if (layout.hasLake) { + if (layout.hasLake && options.algae > 0) { const int centerX = game.map.normalizeX((int)std::lround(layout.W / 2.0)); const int centerY = game.map.normalizeY((int)std::lround(layout.H / 2.0)); placeResourceClump(game.map, context, MapGeneratorPoint(centerX, centerY), ALGA, 2); @@ -486,7 +490,7 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou // The center clump above already guarantees at least one, so this is purely bonus // coverage for a lake big enough to have real interior room left over - no forced minimum. if (!lakeWater.empty()) - for (int i = 0; i < int(lakeWater.size()) / 40; ++i) + for (int i = 0; i < scaledCount(int(lakeWater.size()) / 40, options.algae); ++i) placeResourceClump( game.map, context, lakeWater[context.bounded("resources", lakeWater.size())], ALGA, 2); @@ -496,7 +500,8 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou // 8) Algae out in the open sea: any water tile clearly beyond the coastline // (not a fjord, not the moat-ish water right against the shore) gets an // occasional patch. -void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout &layout) { +void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout &layout, + int algaePercent) { std::vector algaeWater; for (int y = 0; y < layout.H; ++y) { for (int x = 0; x < layout.W; ++x) { @@ -512,7 +517,7 @@ void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout } } if (!algaeWater.empty()) - for (int i = 0; i < std::max(1, int(algaeWater.size()) / 180); ++i) + for (int i = 0; i < scaledCount(std::max(1, int(algaeWater.size()) / 180), algaePercent); ++i) placeResourceClump( game.map, context, algaeWater[context.bounded("resources", algaeWater.size())], ALGA, 2); @@ -568,7 +573,8 @@ bool placeStarterKits(Game &game, GenerationContext &context, const FjordLayout // overwrite the guarantee. Every side of every fjord receives both resources // at distinct points along its length. bool placeBankResources(Game &game, GenerationContext &context, const FjordLayout &layout, - const std::vector> &fjordCenterlines) { + const std::vector> &fjordCenterlines, + const FjordContinentOptions &options) { if (layout.nbTeams < 2) return true; for (int k = 0; k < layout.nbTeams; ++k) { @@ -578,6 +584,8 @@ bool placeBankResources(Game &game, GenerationContext &context, const FjordLayou !placeBankClump(game.map, context, fjordCenterlines[k], 0.68, side, WOOD)) return false; + if (!options.bankDeposits) + continue; // A handful more, lighter clumps at other points along the same banks besides // the two guaranteed spots above, so walking a fjord's edge feels like following @@ -594,8 +602,17 @@ bool placeBankResources(Game &game, GenerationContext &context, const FjordLayou // unchanged - the same rebalance as the ambient scatter above, for consistency. const int roll = context.bounded("resources", 8); const int resourceType = roll < 4 ? CORN : roll < 6 ? WOOD : STONE; - placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, - resourceType); + // The amount controls place each rolled clump that many hundredths of a time: whole + // copies, and one more by chance from a stream of its own, so the rolls stay the same. + const int percent = resourceType == CORN ? options.wheat + : resourceType == WOOD ? options.wood + : options.stone; + int copies = percent / 100; + if (percent % 100 != 0 && int(context.bounded("fjord-bank-amounts", 100)) < percent % 100) + ++copies; + for (int copy = 0; copy < copies; ++copy) + placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, + resourceType); } } } @@ -624,7 +641,7 @@ static bool generate(Game &game, GenerationContext &context) { stampContinent(game, layout); std::vector> fjordCenterlines = carveFjords(game, context, layout, options); - carveLake(game, layout); + carveLake(game, layout, options.sandyLakeShore); placeOutlierIslands(game, context, layout, options, grid, areaNumber); game.map.controlSand(); @@ -633,8 +650,8 @@ static bool generate(Game &game, GenerationContext &context) { if (!verifyConnectivity(game, layout, teamPts)) return false; - placeCoreResources(game, context, layout, grid, areaNumber); - placeOpenSeaAlgae(game, context, layout); + placeCoreResources(game, context, layout, options, grid, areaNumber); + placeOpenSeaAlgae(game, context, layout, options.algae); if (!placeStarterKits(game, context, layout, teamPts)) return false; @@ -655,10 +672,14 @@ static bool generate(Game &game, GenerationContext &context) { // fight that. // corn:wood at 2:1 rather than even - wood was reading as overrepresented in practice, and // total density is held constant (was 18+18=36) rather than just adding more corn on top. - scatterResources(game, context, {/*corn=*/24, /*wood=*/12, /*stone=*/10, /*algae=*/0, /*fruit=*/3}); + scatterResources(game, context, + {/*corn=*/int(scaledCount(24, options.wheat)), + /*wood=*/int(scaledCount(12, options.wood)), + /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, + /*fruit=*/int(scaledCount(3, options.fruit))}); context.stage = "fjord bank resources"; - if (!placeBankResources(game, context, layout, fjordCenterlines)) + if (!placeBankResources(game, context, layout, fjordCenterlines, options)) return false; // The starter kit guarantees wheat and wood exist somewhere near each boot tile, but the @@ -670,6 +691,18 @@ static bool generate(Game &game, GenerationContext &context) { // RuggedArchipelago and ShatteredCoast already rely on for the same class of problem. guaranteeStartingResources(game, context, 24, 32); + // The scatter and bank clumps above are sized by the resource amounts, and the ambient scatter + // covers up to two thirds of the continent's grass at the top of their range. That can leave a + // colony walled into its own clearing with nowhere to build, which the guarantee above doesn't + // address: a colony buried in wheat has wheat at its feet, so it counts as served. At any + // non-default amount, open such a colony back up and re-run the guarantee in case the clearing + // took its nearest crop with the wall. At the defaults none of this runs. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100 || options.fruit != 100) { + openCrampedStarts(game, context); + guaranteeStartingResources(game, context, 24, 32); + } + return true; } @@ -681,7 +714,11 @@ FjordContinentOptions::FjordContinentOptions(const GenerationRequest &r) fjordWidth(r.option("fjord-width")), resourceIslands(r.option("resource-islands")), lakeSize(r.option("lake-size")), - lakeConnected(r.option("lake-connected")) {} + lakeConnected(r.option("lake-connected")), + sandyLakeShore(r.option("sandy-lake-shore") != 0), + bankDeposits(r.option("bank-deposits") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} GeneratorDefinition fjordContinentDefinition() { return {"fjord-continent", @@ -698,6 +735,20 @@ GeneratorDefinition fjordContinentDefinition() { GeneratorControl::toggle("lake-connected", "Lake connects to fjords", false, ControlGroup::Terrain), {"resource-islands", "Resource islands", 0, 20, 1, 2, - ControlGroup::Resources}}, + ControlGroup::Resources}, + // Off, the lake has an ordinary beach instead of a wide ring of sand. + GeneratorControl::toggle("sandy-lake-shore", "Sandy lake shore", true, + ControlGroup::Terrain), + // Off, each fjord bank keeps only its guaranteed wheat and wood. + GeneratorControl::toggle("bank-deposits", "Fjord bank deposits", true, + ControlGroup::Resources), + // The ambient scatter, the core's stone and fruit, the lake's and open sea's algae and + // the fjord banks' extra deposits. Starter kits and each bank's guaranteed wheat and + // wood stay as they are. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate}; } diff --git a/src/map/generator/generators/FjordContinentGenerator.h b/src/map/generator/generators/FjordContinentGenerator.h index 1d594a696..0e2e2b093 100644 --- a/src/map/generator/generators/FjordContinentGenerator.h +++ b/src/map/generator/generators/FjordContinentGenerator.h @@ -5,6 +5,8 @@ struct FjordContinentOptions { int continentSize, roughness, fjordWidth, resourceIslands, lakeSize, lakeConnected; + bool sandyLakeShore, bankDeposits; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit FjordContinentOptions(const GenerationRequest &r); }; GeneratorDefinition fjordContinentDefinition(); diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index c39eac81c..01cd4d6ea 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -413,11 +413,12 @@ void placeTreasure(Map &map, const MazeGrid &g, int cell, int exit, int half, in // Deposits scattered along the shores of every passage outside the homes, as compact clumps // grown over free shore tiles. Only tiles within kShoreBand of a shore are eligible, which is -// what keeps each passage's middle clear. Densities are per 256 shore tiles. +// what keeps each passage's middle clear. Densities are per 256 shore tiles; fruitTiles of fruit, +// when the dead ends hold no treasure, follow in small clumps of each kind in turn. void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, const std::vector &isHome, const std::vector &onRoad, int half, - const MazeOptions &o) { + const MazeOptions &o, int fruitTiles) { const int w = map.getW(), h = map.getH(); const std::vector depth = shoreDepth(map, onRoad); const int band = std::min(kShoreBand, half - 1); @@ -473,6 +474,8 @@ void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, remaining -= clump(layer.first, std::min(remaining, 4 + int(context.bounded("resources", 9)))); } + for (int attempt = 0, kind = 0; fruitTiles > 0 && attempt < shore; ++attempt, kind = (kind + 1) % 3) + fruitTiles -= clump(CHERRY + kind, std::min(fruitTiles, 3 + int(context.bounded("resources", 3)))); } // Algae is seeded along the channels. They are only a tile or two wide, so any water tile can @@ -591,8 +594,11 @@ bool generate(Game &game, GenerationContext &context) { // buildings need pure grass, so nothing can grow over a road tile or be built on one. A road's // tile rectangle takes undermap sand on (x0, x1] x (y0, y1], giving each of its tiles at least // one sand corner and leaving every tile around it untouched. + // Without roads, passages are grass from shore to shore. std::vector onRoad(size_t(map.getW()) * map.getH(), 0); for (const TileRect &r : roads) { + if (!o.sandRoads) + break; fillUndermap(map, r.x0 + 1, r.y0 + 1, r.x1 - r.x0, r.y1 - r.y0, SAND); for (int y = r.y0; y <= r.y1; ++y) for (int x = r.x0; x <= r.x1; ++x) @@ -629,7 +635,8 @@ bool generate(Game &game, GenerationContext &context) { // Fruit is treasure: one patch at the far end of every dead end that isn't a home, with fruit // types dealt round-robin so every kind is somewhere in the maze and a colony has to go and // fight for the ones it lacks. Placed before the shore scatter, which works around them. - if (o.fruit > 0) { + // Without treasure the same fruit is scattered along the passages' shores instead. + if (o.fruit > 0 && o.treasure) { for (size_t i = deadEnds.size(); i > 1; --i) std::swap(deadEnds[i - 1], deadEnds[context.bounded("resources", i)]); const int firstType = context.bounded("resources", 3); @@ -637,7 +644,8 @@ bool generate(Game &game, GenerationContext &context) { placeTreasure(map, g, deadEnds[i], exitOf[deadEnds[i]], half, o.fruit, CHERRY + int((firstType + i) % 3)); } - scatterThroughMaze(map, context, g, isHome, onRoad, half, o); + scatterThroughMaze(map, context, g, isHome, onRoad, half, o, + o.treasure ? 0 : int(deadEnds.size()) * o.fruit); seedAlgae(map, context, o.algae); return true; } @@ -689,7 +697,8 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { MazeOptions::MazeOptions(const GenerationRequest &r) : cellSize(r.option("cell-size")), channelWidth(r.option("channel-width")), loopiness(r.option("loopiness")), corn(r.option("wheat")), wood(r.option("wood")), - stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")) {} + stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")), + sandRoads(r.option("sand-roads") != 0), treasure(r.option("dead-end-treasure") != 0) {} GeneratorDefinition mazeDefinition() { return {"maze", @@ -710,7 +719,12 @@ GeneratorDefinition mazeDefinition() { {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}}, + {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}, + // Off, passages are grass from shore to shore, with no sand road down the middle. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // Off, the treasure's fruit is scattered along the passages' shores instead. + GeneratorControl::toggle("dead-end-treasure", "Treasure in dead ends", true, + ControlGroup::Resources)}, generate, true, validate, diff --git a/src/map/generator/generators/MazeGenerator.h b/src/map/generator/generators/MazeGenerator.h index c518434cd..60fc19767 100644 --- a/src/map/generator/generators/MazeGenerator.h +++ b/src/map/generator/generators/MazeGenerator.h @@ -5,6 +5,7 @@ struct MazeOptions { int cellSize, channelWidth, loopiness; int corn, wood, stone, algae, fruit; + bool sandRoads, treasure; explicit MazeOptions(const GenerationRequest &r); }; GeneratorDefinition mazeDefinition(); From aa6d0c68ccfe3270113a1d83d50311812f4704d8 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:33:25 -0400 Subject: [PATCH 074/136] Add resource amounts and switches to Watershed, Stone highlands, Symmetric arena and Ring world MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four gain amounts as percentages of their defaults (0 to 300 in steps of 25, default 100, except where noted), and one or two switches each. Starter kits stay unscaled everywhere, so every setting leaves each colony a start. Watershed: separate wheat and wood farmland budgets (they shared one before, which is what the default still does), stone outcrops, the fruit groves at the confluences, and the algae off the mouths and in the shallows. Switches, both on: River delta (off, the trunk keeps one mouth and grows no delta lobe, with the other distributaries still planned so the rest of the network is unchanged) and Meandering rivers (off, every river runs without its meanders). Stone highlands: the ponds' farmland and algae, and — for stone, 0 to 200 — how much of the ridgeline comes out two tiles thick rather than one, which is what stone means on this map. Switch: Fruit in home valleys (off by default, as before; on, groves may also grow in the valleys colonies start in instead of only the empty ones). Symmetric arena: the shore farmland's wheat and wood, the stone outcrops and the algae, on top of the existing Resource richness. Fruit is 0 to 100 and keeps that share of the orchard's fruit orbits nearest the centre, never fewer than three, so all three fruits always remain. Switches, both on: Moat (off, the centre island joins the land around it and has no causeways, though the moat's width still spaces the homes) and Stone in the orchard (off, every orbit stays fruit). Ring world: the ambient scatter's wheat, wood, stone and fruit, and the shallows' algae; the starter patches and the resource islands' prizes are unscaled. Switches, both on: Winding belt (off, the belt's centre line runs straight around the map) and Colonies on both coasts (off, every colony takes the same coast). Default maps are byte-identical to 95e24a761's (225 dumps: 15 generators, 5 seeds, 128x128, 256x256, 256x128). Every new control's minimum and maximum and every switch's other state generated on all 20 sweep seeds at 256x256 with no failures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generators/RingWorldGenerator.cpp | 58 ++++++++++--- .../generator/generators/RingWorldGenerator.h | 2 + .../generators/StoneHighlandsGenerator.cpp | 63 +++++++++----- .../generators/StoneHighlandsGenerator.h | 2 + .../generators/SymmetricArenaGenerator.cpp | 59 +++++++++++-- .../generators/SymmetricArenaGenerator.h | 2 + .../generators/WatershedGenerator.cpp | 84 ++++++++++++++----- .../generator/generators/WatershedGenerator.h | 2 + 8 files changed, 208 insertions(+), 64 deletions(-) diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index e27c93e15..06335fca3 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -206,7 +206,8 @@ Belt shapeBelt(const Axes &axes, GenerationContext &context, const RingWorldOpti double bendSlope = 0, swingSlope = 0; const std::vector bend = closedCurve(length, 1.6, rng, bendSlope); const std::vector widthCurve = closedCurve(length, 1.2, rng, swingSlope); - double bendAmplitude = 0.11 * breadth; + // A belt that doesn't wind still draws its curve, so the rest of the belt is unchanged. + double bendAmplitude = options.windingBelt ? 0.11 * breadth : 0.0; if (bendSlope > 0) bendAmplitude = std::min(bendAmplitude, kMaxBendSlope / bendSlope); if (swingSlope > 0) @@ -365,9 +366,10 @@ bool siteFits(const Map &map, int x, int y) { } // Colonies are dealt evenly spaced slots around the ring from a random starting point, with a -// little jitter, and alternate between the belt's two coasts. Within its slot a colony takes the -// site whose nearest water is closest to kHomeShore tiles away, so every start has the same shore. -bool placeColonies(Game &game, GenerationContext &context, const Belt &belt) { +// little jitter, and alternate between the belt's two coasts (or all take one). Within its slot a +// colony takes the site whose nearest water is closest to kHomeShore tiles away, so every start +// has the same shore. +bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, bool bothCoasts) { Map &map = game.map; const Axes &axes = belt.axes; const int width = axes.width, height = axes.height, length = axes.length(); @@ -396,7 +398,7 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt) { for (int team = 0; team < teams; ++team) { const double jitter = (int(context.bounded("colonies", 2001)) - 1000) / 1000.0 * 0.08 * slot; const double target = first + team * slot + jitter; - const double side = (firstSide + team) % 2 ? 1.0 : -1.0; + const double side = (firstSide + (bothCoasts ? team : 0)) % 2 ? 1.0 : -1.0; const double spacing = 0.5 * slot; int bestX = -1, bestY = -1; double bestScore = std::numeric_limits::max(); @@ -565,7 +567,7 @@ void stockIslands(Map &map, GenerationContext &context, const std::vector water(area), dry(area); @@ -581,7 +583,7 @@ void seedAlgae(Map &map, GenerationContext &context) { shallows.emplace_back(i % width, i / width); if (shallows.empty()) return; - for (int clump = 0; clump < int(shallows.size()) / 90; ++clump) + for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) placeResourceClump(map, context, shallows[context.bounded("resources", shallows.size())], ALGA, 1); } @@ -727,7 +729,7 @@ bool generate(Game &game, GenerationContext &context) { map.rebuildTerrain(); context.stage = "ring colonies"; - if (!placeColonies(game, context, belt)) + if (!placeColonies(game, context, belt, options.bothCoasts)) return false; context.stage = "ring resources"; @@ -735,13 +737,27 @@ bool generate(Game &game, GenerationContext &context) { stockIslands(map, context, islands); // The same ambient layer as Fjord continent: corn:wood 2:1, some stone, rare fruit. Algae is // seeded along the shallows below instead, since the shared band only scatters over land. - scatterResources(game, context, {/*corn=*/24, /*wood=*/12, /*stone=*/10, /*algae=*/0, - /*fruit=*/3}); - seedAlgae(map, context); + scatterResources(game, context, + {/*corn=*/int(scaledCount(24, options.wheat)), + /*wood=*/int(scaledCount(12, options.wood)), + /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, + /*fruit=*/int(scaledCount(3, options.fruit))}); + seedAlgae(map, context, options.algae); clearAroundSwarms(map, context); guaranteeStartingResources(game, context, 24, 32, 6); clearAroundSwarms(map, context); + // The scatter above is sized by the resource amounts, and at the top of their range it can wall + // a colony into its own clearing with nowhere to build — which the guarantee doesn't address, + // since a colony buried in wheat has wheat at its feet. At any non-default amount, open such a + // colony back up, then guarantee and clear again as above. At the defaults none of this runs. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100 || options.fruit != 100) { + openCrampedStarts(game, context); + guaranteeStartingResources(game, context, 24, 32, 6); + clearAroundSwarms(map, context); + } + context.stage = "ring road"; return openBeltRoad(game, context, axes); } @@ -846,7 +862,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { RingWorldOptions::RingWorldOptions(const GenerationRequest &r) : beltWidth(r.option("belt-width")), coastRoughness(r.option("coast-roughness")), - lakeDensity(r.option("lake-density")), resourceIslands(r.option("resource-islands")) {} + lakeDensity(r.option("lake-density")), resourceIslands(r.option("resource-islands")), + windingBelt(r.option("winding-belt") != 0), bothCoasts(r.option("both-coasts") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) {} GeneratorDefinition ringWorldDefinition() { return {"ring-world", @@ -859,7 +879,19 @@ GeneratorDefinition ringWorldDefinition() { {{"belt-width", "Belt width", 30, 70, 5, 45, ControlGroup::Terrain}, {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, {"lake-density", "Lake density", 0, 8, 1, 2, ControlGroup::Terrain}, - {"resource-islands", "Resource islands", 0, 10, 1, 2, ControlGroup::Resources}}, + {"resource-islands", "Resource islands", 0, 10, 1, 2, ControlGroup::Resources}, + // Off, the belt's centre line runs straight round the map. + GeneratorControl::toggle("winding-belt", "Winding belt", true, ControlGroup::Terrain), + // Off, every colony starts on the same coast of the belt. + GeneratorControl::toggle("both-coasts", "Colonies on both coasts", true, + ControlGroup::Layout), + // The ambient scatter's wheat, wood, stone and fruit and the shallows' algae. Every + // home's starter patches and each island's prize stay as they are. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, validate, diff --git a/src/map/generator/generators/RingWorldGenerator.h b/src/map/generator/generators/RingWorldGenerator.h index 2758033ec..d06cd8662 100644 --- a/src/map/generator/generators/RingWorldGenerator.h +++ b/src/map/generator/generators/RingWorldGenerator.h @@ -5,6 +5,8 @@ struct RingWorldOptions { int beltWidth, coastRoughness, lakeDensity, resourceIslands; + bool windingBelt, bothCoasts; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit RingWorldOptions(const GenerationRequest &r); }; GeneratorDefinition ringWorldDefinition(); diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 673a672b1..ea0bf0970 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -715,15 +715,16 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { break; } } - // Stretches of ridgeline two tiles thick, where a noise field says so. More stone never opens - // a gap. + // Stretches of ridgeline two tiles thick, where a noise field says so: 45% of the ridge at the + // default stone amount, which scales that share. More stone never opens a gap. const std::vector thickness = periodicNoise(t.w, t.h, std::max(4, o.valleySize / 2), context.stream("highlands-ridge")); - const int thickLevel = percentile(thickness, 55); + const int thickShare = int(std::min(100, scaledCount(45, o.stone))); + const int thickLevel = percentile(thickness, 100 - thickShare); std::vector thick = L.ridge; static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; for (int i = 0; i < n; ++i) { - if (!L.ridge[i] || thickness[i] < thickLevel) + if (!L.ridge[i] || thickShare <= 0 || thickness[i] < thickLevel) continue; for (const auto &s : steps) { const int j = t.at(i % t.w + s[0], i / t.w + s[1]); @@ -951,7 +952,8 @@ void stampBox(const Torus &t, std::vector &mask, int cx, int cy, // Ambient farmland: the most fertile ground around each pond, in patches, split 2:1 between // wheat and wood by an unrelated noise field so the two crops alternate around the shore. void scatterFarmland(Map &map, GenerationContext &context, const Layout &L, - const std::vector &pondDistance, const std::vector &keepClear) { + const std::vector &pondDistance, const std::vector &keepClear, + int wheatPercent, int woodPercent) { const Torus &t = L.t; const int n = t.w * t.h; const Fertility::Field fertility = Fertility::forMap(map, false); @@ -981,21 +983,28 @@ void scatterFarmland(Map &map, GenerationContext &context, const Layout &L, std::vector &tiles = pool[v]; const int wanted = std::min(int(std::int64_t(pondTiles[v]) * kFarmlandPercent / 100), int(tiles.size()) * 2 / 3); - if (wanted <= 0) + // Two thirds wheat and the rest wood, each scaled by its amount; more than the default may + // take every eligible tile, keeping the two crops' proportions. + int wheat = int(scaledCount(wanted * 2 / 3, wheatPercent)); + int total = wheat + int(scaledCount(wanted - wanted * 2 / 3, woodPercent)); + if (total > int(tiles.size())) { + wheat = int(std::int64_t(wheat) * tiles.size() / total); + total = int(tiles.size()); + } + if (total <= 0) continue; std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { return fertility.at(a % t.w, a / t.w) > fertility.at(b % t.w, b / t.w); }); - tiles.resize(wanted); + tiles.resize(total); std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { return split[a] < split[b]; }); - const int wheat = wanted * 2 / 3; - for (int k = 0; k < wanted; ++k) + for (int k = 0; k < total; ++k) map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheat ? CORN : WOOD, 1); } } // Algae in the open water of every pond. -void seedAlgae(Map &map, GenerationContext &context, const Layout &L) { +void seedAlgae(Map &map, GenerationContext &context, const Layout &L, int algaePercent) { const Torus &t = L.t; const std::vector noise = periodicNoise(t.w, t.h, 6, context.stream("highlands-algae")); std::vector water, levels; @@ -1004,19 +1013,20 @@ void seedAlgae(Map &map, GenerationContext &context, const Layout &L) { water.push_back(i); levels.push_back(noise[i]); } - if (water.empty()) + if (water.empty() || algaePercent <= 0) return; - const int level = percentile(levels, kAlgaePercent); + const int level = percentile( + levels, int(std::min(100, scaledCount(kAlgaePercent, algaePercent)))); for (int i : water) if (noise[i] <= level) map.setResource(i % t.w, i / t.w, ALGA, 1); } // Fruit is rare: small groves beside the ponds of valleys no colony starts in, a prize for -// whoever holds the passes to them. +// whoever holds the passes to them - or in any valley, colonies' own included, if asked. void plantFruit(Map &map, GenerationContext &context, const Layout &L, const std::vector &pondDistance, const std::vector &keepClear, - const std::vector &homeValley, int fruit) { + const std::vector &homeValley, int fruit, bool inHomeValleys) { if (fruit <= 0) return; const Torus &t = L.t; @@ -1035,7 +1045,7 @@ void plantFruit(Map &map, GenerationContext &context, const Layout &L, std::vector valleys; for (int pass = 0; pass < 2 && valleys.empty(); ++pass) for (int v = 0; v < L.valleys; ++v) - if (!pool[v].empty() && (pass == 1 || !homeValley[v])) + if (!pool[v].empty() && (pass == 1 || inHomeValleys || !homeValley[v])) valleys.push_back(v); if (valleys.empty()) return; @@ -1103,9 +1113,9 @@ bool generate(Game &game, GenerationContext &context) { homeValley[L.homes[team].valley] = 1; furnishHome(map, L, context.bootX[team], context.bootY[team], L.homes[team].valley); } - scatterFarmland(map, context, L, pondDistance, keepClear); - seedAlgae(map, context, L); - plantFruit(map, context, L, pondDistance, keepClear, homeValley, o.fruit); + scatterFarmland(map, context, L, pondDistance, keepClear, o.wheat, o.wood); + seedAlgae(map, context, L, o.algae); + plantFruit(map, context, L, pondDistance, keepClear, homeValley, o.fruit, o.homeValleyFruit); // The kits above already put wheat and wood a few steps from every swarm; this is only the // backstop, and the ridges are designed walls it must never clear. guaranteeStartingResources(game, context, 24, 32, 0, &L.ridge); @@ -1231,7 +1241,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { StoneHighlandsOptions::StoneHighlandsOptions(const GenerationRequest &r) : valleySize(r.option("valley-size")), passWidth(r.option("pass-width")), - loopiness(r.option("loopiness")), pondSize(r.option("pond-size")), fruit(r.option("fruit")) {} + loopiness(r.option("loopiness")), pondSize(r.option("pond-size")), fruit(r.option("fruit")), + homeValleyFruit(r.option("home-valley-fruit") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")) {} GeneratorDefinition stoneHighlandsDefinition() { return {"stone-highlands", @@ -1247,7 +1260,17 @@ GeneratorDefinition stoneHighlandsDefinition() { // Pond area as a percentage of each valley's area. {"pond-size", "Pond size", 4, 16, 2, 8, ControlGroup::Terrain}, // Fruit groves per 128x128 tiles of map. - {"fruit", "Fruit", 0, 12, 1, 4, ControlGroup::Resources}}, + {"fruit", "Fruit", 0, 12, 1, 4, ControlGroup::Resources}, + // On, fruit groves may grow in the valleys colonies start in too. + GeneratorControl::toggle("home-valley-fruit", "Fruit in home valleys", false, + ControlGroup::Resources), + // Wheat and wood scale the farmland around the ponds and algae the ponds' own; stone + // scales how much of the ridgeline is two tiles thick (45% at 100). Every colony's + // starting kit stays as it is. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount", 200), + GeneratorControl::percentage("algae-amount", "Algae amount")}, generate, true, validateRequest, diff --git a/src/map/generator/generators/StoneHighlandsGenerator.h b/src/map/generator/generators/StoneHighlandsGenerator.h index 652e22a3d..6ff251b9e 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.h +++ b/src/map/generator/generators/StoneHighlandsGenerator.h @@ -4,6 +4,8 @@ #include "GeneratorDefinition.h" struct StoneHighlandsOptions { int valleySize, passWidth, loopiness, pondSize, fruit; + bool homeValleyFruit; + int wheat, wood, stone, algae; // percentages of the default amounts explicit StoneHighlandsOptions(const GenerationRequest &r); }; GeneratorDefinition stoneHighlandsDefinition(); diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index 2984632ab..ae9750dbc 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -6,6 +6,7 @@ #include "GenerationContext.h" #include "GlobalContainer.h" #include "HeightMap.h" +#include "Resources.h" #include "Unit.h" #include #include @@ -18,6 +19,8 @@ #include #include #include +using MapGeneration::scaledCount; +using MapGeneration::scaledShare; // A symmetric arena: an orchard of all three fruits and some stone on an island behind a moat, // crossed by causeways, with the colonies spaced round it so that every colony's ground, route @@ -479,7 +482,9 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La homeDisc = stamp(s, homeDisc, true); homeZone = stamp(s, homeZone, true); t.ponds = stamp(s, pond, true); - t.causeways = stamp(s, gate, true); + // Without a moat the orchard island joins the land round it, and no causeway is needed; the + // moat's width still spaces the homes. + t.causeways = o.moat ? stamp(s, gate, true) : std::vector(n, 0); t.moat.assign(n, 0); std::vector lakeEligible(n, 0); @@ -487,7 +492,7 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La for (int u = 0; u < w; ++u) { const size_t i = size_t(v) * w + u; const std::int64_t r2 = cornerRadius2(w, h, u, v); - t.moat[i] = !within(r2, a.centre) && within(r2, a.centre + a.moat); + t.moat[i] = o.moat && !within(r2, a.centre) && within(r2, a.centre + a.moat); lakeEligible[i] = !within(r2, a.centre + a.moat + kApron) && !homeZone[i]; } // Capped so big maps get more lakes rather than bigger ones: more shore, more farmland. @@ -679,14 +684,24 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou rock[i] = open && waterSteps[i] >= 3; alga[i] = water[i] && landSteps[i] >= 2; } + // The amount controls scale each layer on top of richness: wheat and wood their shares of the + // farmland (2:1 at the defaults, which use the shares exactly as they were). + const bool farmDefault = o.wheat == 100 && o.wood == 100; + const double wheatShare = scaledShare(0.3 * richness * 2 / 3, o.wheat); + const double woodShare = scaledShare(0.3 * richness / 3, o.wood); + const double farmShare = + farmDefault ? std::min(0.9, 0.3 * richness) : std::min(0.9, wheatShare + woodShare); + const double woodFraction = farmDefault ? 1.0 / 3 + : wheatShare + woodShare > 0.0 ? woodShare / (wheatShare + woodShare) + : 0.0; const auto farmland = topShare(orbitNoise(context, s, "arena-farmland", coarse, false), farm, - std::min(0.9, 0.3 * richness), true); + farmShare, true); const auto wood = topShare(orbitNoise(context, s, "arena-split", 6.0f, false), farmland, - 1.0 / 3, true); + woodFraction, true); const auto outcrops = topShare(orbitNoise(context, s, "arena-stone", coarse, false), rock, - 0.012 * richness, true); + scaledShare(0.012 * richness, o.stone), true); const auto algae = topShare(orbitNoise(context, s, "arena-algae", coarse, false), alga, - 0.1 * richness, true); + scaledShare(0.1 * richness, o.algae), true); for (size_t i = 0; i < n; ++i) { if (farmland[i]) plan[i] = wood[i] ? WOOD : CORN; @@ -755,14 +770,24 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou return p->value != q->value ? p->value > q->value : p->key < q->key; }); const size_t stones = - std::min(ranked.size() - 3, size_t(std::lround(0.2 * double(ranked.size())))); + o.orchardStone ? std::min(ranked.size() - 3, size_t(std::lround(0.2 * double(ranked.size())))) + : 0; for (size_t k = 0; k < stones; ++k) ranked[k]->stone = true; std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) { return p->radius2 != q->radius2 ? p->radius2 < q->radius2 : p->key < q->key; }); + // The fruit amount keeps that share of the fruit orbits nearest the centre, never fewer than + // three so every fruit stays; the rest are left as open grass. + const size_t fruitOrbits = ranked.size() - stones; + const size_t keptFruit = + std::clamp(size_t(scaledCount(std::int64_t(fruitOrbits), o.fruit)), + std::min(3, fruitOrbits), fruitOrbits); int fruit = l.orchardPattern; + size_t fruitPlanted = 0; for (const Orbit *orbit : ranked) { + if (!orbit->stone && fruitPlanted++ >= keptFruit) + continue; const int type = orbit->stone ? STONE : CHERRY + fruit++ % 3; for (int i : orbit->tiles) plan[size_t(i)] = type; @@ -1066,7 +1091,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { SymmetricArenaOptions::SymmetricArenaOptions(const GenerationRequest &r) : centreSize(r.option("centre-size")), moatWidth(r.option("moat-width")), causewayWidth(r.option("causeway-width")), causeways(r.option("causeways")), - lakes(r.option("lakes")), richness(r.option("richness")) {} + lakes(r.option("lakes")), richness(r.option("richness")), moat(r.option("moat") != 0), + orchardStone(r.option("orchard-stone") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} GeneratorDefinition symmetricArenaDefinition() { return {"symmetric-arena", @@ -1083,7 +1111,20 @@ GeneratorDefinition symmetricArenaDefinition() { {"causeway-width", "Causeway width", 2, 8, 1, 4, ControlGroup::Layout}, {"causeways", "Causeways", 1, 2, 1, 1, ControlGroup::Layout}, {"lakes", "Lakes", 0, 40, 5, 20, ControlGroup::Terrain}, - {"richness", "Resource richness", 0, 200, 25, 100, ControlGroup::Resources}}, + {"richness", "Resource richness", 0, 200, 25, 100, ControlGroup::Resources}, + // Off, no water rings the orchard island; it joins the land around it. + GeneratorControl::toggle("moat", "Moat", true, ControlGroup::Terrain), + // Off, every grove in the orchard is fruit. + GeneratorControl::toggle("orchard-stone", "Stone in the orchard", true, + ControlGroup::Resources), + // Each ambient layer on top of richness: the farmland's wheat and wood, stone outcrops + // and algae. Fruit keeps that share of the orchard's groves nearest the centre, never + // fewer than three. Every home's kit stays as it is. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount", 100)}, generate, true, validate, diff --git a/src/map/generator/generators/SymmetricArenaGenerator.h b/src/map/generator/generators/SymmetricArenaGenerator.h index 069b8d6b3..a03a83837 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.h +++ b/src/map/generator/generators/SymmetricArenaGenerator.h @@ -4,6 +4,8 @@ #include "GeneratorDefinition.h" struct SymmetricArenaOptions { int centreSize, moatWidth, causewayWidth, causeways, lakes, richness; + bool moat, orchardStone; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit SymmetricArenaOptions(const GenerationRequest &r); }; GeneratorDefinition symmetricArenaDefinition(); diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index 97b7646b0..0382ea936 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -311,6 +311,8 @@ class RiverPlanner { // Per reach: where each tributary joins it, and from which side. std::vector>> joins; int trunk = -1; + // 0 plans every river without its meander. + double meanderScale = 1.0; int sideOf(int r, int i, Vec source) const { const Vec t = tangent(r, i), p = reaches[r].points[i]; @@ -407,7 +409,7 @@ class RiverPlanner { end * (t * t * t)); } const std::vector base = resample(raw); - const double amplitude = std::min(6.5, 0.1 * span) * meander; + const double amplitude = std::min(6.5, 0.1 * span) * meander * meanderScale; const double waves = std::max(1.0, span / 34.0); const double phase1 = between(context, kRiverStream, 0, 2 * kPi); const double phase2 = between(context, kRiverStream, 0, 2 * kPi); @@ -713,15 +715,18 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo const double clearance = 2 * estimate + 5; const double northMargin = estimate + 8, southMargin = estimate + 6; RiverPlanner planner(frame, layout.north, layout.south, clearance, northMargin, southMargin); + planner.meanderScale = o.meanders ? 1.0 : 0.0; - // The delta: one to three distributaries fanning out from an apex to the coast. + // The delta: one to three distributaries fanning out from an apex to the coast. Without a + // delta there is no lobe and only the middle mouth is kept; the others are still planned, so + // every later draw is the one it would have been. const double deltaLength = std::clamp(0.12 * across + 6, 12.0, 44.0); const double spread = std::clamp(0.16 * along, 18.0, 44.0); const int mouthLimit = std::max(1, int(0.7 * along / spread) + 1); const int mouthCount = std::min(mouthLimit, 1 + int(context.bounded(kLayoutStream, 3))); const double u0 = between(context, kLayoutStream, 0, along); layout.south.lobeCentre = u0; - layout.south.lobeHeight = 0.3 * sea; + layout.south.lobeHeight = o.delta ? 0.3 * sea : 0.0; layout.south.lobeWidth = 1.1 * spread + 6; const Vec apex{u0, layout.south.at(u0) - deltaLength}; static const double shares[3][3] = {{1, 0, 0}, {0.5, 0.5, 0}, {0.32, 0.36, 0.32}}; @@ -729,11 +734,13 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo for (int m = 0; m < mouthCount; ++m) { const double u = u0 + (m - (mouthCount - 1) / 2.0) * spread * between(context, kLayoutStream, 0.85, 1.15); - mouths.push_back({u, layout.south.at(u)}); Reach reach; reach.points = planner.path(apex, {u, layout.south.at(u) + std::min(8.0, 0.25 * sea)}, {0, 1}, 0.5, context); reach.distributary = true; + if (!o.delta && m != mouthCount / 2) + continue; + mouths.push_back({u, layout.south.at(u)}); planner.add(std::move(reach)); } @@ -805,9 +812,10 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo reach.radius[i] = radiusFor(flow); } } - for (int m = 0; m < mouthCount; ++m) { + const int distributaries = int(mouths.size()); + for (int m = 0; m < distributaries; ++m) { Reach &reach = planner.reaches[m]; - const double radius = radiusFor(std::max(1.0, outflow[trunk] * shares[mouthCount - 1][m])); + const double radius = radiusFor(std::max(1.0, outflow[trunk] * shares[distributaries - 1][m])); const double last = double(reach.points.size() - 1); reach.radius.resize(reach.points.size()); for (size_t i = 0; i < reach.points.size(); ++i) @@ -1427,7 +1435,7 @@ void placeStarterKits(Game &game, GenerationContext &context, int teams, const T // to wood, with the crop chosen by a coarse field so stretches of bank favour one or the other. void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, const Fertility::Field &fertility, const std::vector &keepClear, - Reservations &reservations) { + Reservations &reservations, const WatershedOptions &o) { Map &map = game.map; const int w = t.w, h = t.h; std::mt19937 &random = context.stream("watershed-farmland"); @@ -1451,22 +1459,38 @@ void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, return a.score != b.score ? a.score > b.score : a.index < b.index; }); int budget = int(order.size()) / 5; + // Other wheat or wood amounts than the default give each crop a budget of its own: its share + // of the candidates the crop field gives it, scaled. + const bool shared = o.wheat == 100 && o.wood == 100; + int cornBudget = 0, woodBudget = 0; + if (!shared && !order.empty()) { + std::int64_t cornCandidates = 0; + for (const Candidate &c : order) + cornCandidates += crop.at(c.index % w + 0.5, c.index / w + 0.5) < 0.62; + const int cornShare = int(budget * cornCandidates / std::int64_t(order.size())); + cornBudget = int(scaledCount(cornShare, o.wheat)); + woodBudget = int(scaledCount(budget - cornShare, o.wood)); + } for (const Candidate &c : order) { - if (budget <= 0) + if (shared ? budget <= 0 : cornBudget <= 0 && woodBudget <= 0) break; const int x = c.index % w, y = c.index / w; const int radius = context.bounded("resources", 5) < 2 && t.grassDepth[c.index] > 3 ? 3 : 2; if (!reservations.free(x, y, radius)) continue; const int type = crop.at(x + 0.5, y + 0.5) < 0.62 ? CORN : WOOD; - budget -= placeResourceClump(map, context, MapGeneratorPoint(x, y), type, radius); + int &remaining = shared ? budget : type == CORN ? cornBudget : woodBudget; + if (remaining <= 0) + continue; + remaining -= placeResourceClump(map, context, MapGeneratorPoint(x, y), type, radius); reservations.reserve(x, y, radius); } } // Stone outcrops at the edge of the dry uplands, well away from the rivers. void placeStone(Game &game, GenerationContext &context, const Tiles &t, - const std::vector &keepClear, Reservations &reservations) { + const std::vector &keepClear, Reservations &reservations, + int stonePercent) { Map &map = game.map; const int w = t.w, h = t.h; std::vector candidates; @@ -1482,7 +1506,7 @@ void placeStone(Game &game, GenerationContext &context, const Tiles &t, if (edge || t.waterSteps[i] >= 14) candidates.push_back(int(i)); } - const int outcrops = std::max(2, w * h / 3000); + const int outcrops = int(scaledCount(std::max(2, w * h / 3000), stonePercent)); std::vector placed; for (int attempt = 0; attempt < outcrops * 20 && int(placed.size()) < outcrops && !candidates.empty(); ++attempt) { @@ -1502,13 +1526,13 @@ void placeStone(Game &game, GenerationContext &context, const Tiles &t, // Fruit is rare: a small grove in the fertile wedge where two rivers meet, one kind at a time. void placeFruit(Game &game, GenerationContext &context, const Layout &layout, const Tiles &t, const Fertility::Field &fertility, const std::vector &keepClear, - Reservations &reservations) { + Reservations &reservations, int fruitPercent) { Map &map = game.map; const int w = t.w, h = t.h; std::vector junctions = layout.junctions; for (size_t i = junctions.size(); i > 1; --i) std::swap(junctions[i - 1], junctions[context.bounded("resources", i)]); - const int groves = std::max(3, w * h / 6000); + const int groves = int(scaledCount(std::max(3, w * h / 6000), fruitPercent)); int type = int(context.bounded("resources", 3)), placed = 0; for (size_t j = 0; j < junctions.size() && placed < groves; ++j) { const int jx = int(std::floor(junctions[j].x)), jy = int(std::floor(junctions[j].y)); @@ -1537,7 +1561,8 @@ void placeFruit(Game &game, GenerationContext &context, const Layout &layout, co // Algae off every mouth and in the sea's shallows, where there is sand near enough for it to // grow back. -void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, const Tiles &t) { +void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, const Tiles &t, + int algaePercent) { Map &map = game.map; const int w = t.w, h = t.h; for (Vec mouth : layout.mouths) { @@ -1550,7 +1575,7 @@ void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, co if (dx * dx + dy * dy <= 100 && t.water[i] && t.waterSteps[i] == 0) water.push_back(int(i)); } - for (int clump = 0; clump < 3 && !water.empty(); ++clump) { + for (int clump = 0; clump < scaledCount(3, algaePercent) && !water.empty(); ++clump) { const int i = water[context.bounded("resources", water.size())]; placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 2); } @@ -1571,7 +1596,7 @@ void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, co if (c.y < layout.north.at(c.x) - 1 || c.y > layout.south.at(c.x) + 1) shallows.push_back(int(i)); } - for (int clump = 0; clump < int(shallows.size()) / 60; ++clump) { + for (int clump = 0; clump < scaledCount(int(shallows.size()) / 60, algaePercent); ++clump) { const int i = shallows[context.bounded("resources", shallows.size())]; placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 1); } @@ -1701,10 +1726,10 @@ bool generate(Game &game, GenerationContext &context) { wrapIndex(context.bootX[team] + dx, w)] = 1; Reservations reservations(w, h); placeStarterKits(game, context, teams, tiles, fertility, reservations); - placeFruit(game, context, layout, tiles, fertility, keepClear, reservations); - placeStone(game, context, tiles, keepClear, reservations); - placeFarmland(game, context, tiles, fertility, keepClear, reservations); - placeAlgae(game, context, layout, tiles); + placeFruit(game, context, layout, tiles, fertility, keepClear, reservations, o.fruit); + placeStone(game, context, tiles, keepClear, reservations, o.stone); + placeFarmland(game, context, tiles, fertility, keepClear, reservations, o); + placeAlgae(game, context, layout, tiles, o.algae); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) if (keepClear[size_t(y) * w + x] && map.isResource(x, y) && !map.isWater(x, y)) @@ -1793,7 +1818,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { WatershedOptions::WatershedOptions(const GenerationRequest &r) : riverDensity(r.option("river-density")), riverWidth(r.option("river-width")), - dryness(r.option("dryness")), fords(r.option("fords")) {} + dryness(r.option("dryness")), fords(r.option("fords")), delta(r.option("river-delta") != 0), + meanders(r.option("meanders") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} GeneratorDefinition watershedDefinition() { return {"watershed", @@ -1810,7 +1838,19 @@ GeneratorDefinition watershedDefinition() { {"dryness", "Dryness", 0, 10, 1, 5, ControlGroup::Terrain}, // How often a ford crosses a river: about one per 80 tiles of river at 1, one per 26 // at 5, and at least one on every reach long enough to hold one. - {"fords", "Fords", 1, 5, 1, 3, ControlGroup::Layout}}, + {"fords", "Fords", 1, 5, 1, 3, ControlGroup::Layout}, + // Off, the trunk reaches the sea through a single mouth, with no delta lobe. + GeneratorControl::toggle("river-delta", "River delta", true, ControlGroup::Terrain), + // Off, rivers run without their meanders. + GeneratorControl::toggle("meanders", "Meandering rivers", true, ControlGroup::Terrain), + // The farmland along the rivers (wheat and wood each), stone outcrops, fruit groves at + // the confluences and algae off the mouths and shallows. Every colony's starter kit + // stays as it is. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, validate, diff --git a/src/map/generator/generators/WatershedGenerator.h b/src/map/generator/generators/WatershedGenerator.h index a9fb30516..75377205c 100644 --- a/src/map/generator/generators/WatershedGenerator.h +++ b/src/map/generator/generators/WatershedGenerator.h @@ -4,6 +4,8 @@ #include "GeneratorDefinition.h" struct WatershedOptions { int riverDensity, riverWidth, dryness, fords; + bool delta, meanders; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit WatershedOptions(const GenerationRequest &r); }; GeneratorDefinition watershedDefinition(); From f975ede478f7f021fe390ce23dcadca204689484 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:33:25 -0400 Subject: [PATCH 075/136] Document the resource amounts, the switches and keeping colonies buildable MAP_GENERATOR_FRAMEWORK.md gains a "Resource amounts and switches" section: what an amount is (a percentage of the generator's default, identity at 100), a per-generator table of exactly what each one scales and what each switch does in both states, which placements stay unscaled because they are fairness guarantees, and how openCrampedStarts, openStartsBuriedByAmounts, the widened start search in Concrete islands and Isles, and placeSettlement's worker-room fallback keep a colony buildable at the top of a range. Each generator's own section notes its new settings as well. ADDING_A_GENERATOR.md tells an author to check the top of a range as carefully as the bottom: a map that still passes validateWorld can still be unplayable if the widened bands bury a colony, which passes to openCrampedStarts at non-default amounts, and a start search that picks sites relative to a scaled field has to widen rather than fail. It also records what placeSettlement now measures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/ADDING_A_GENERATOR.md | 4 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 97 ++++++++++++++++--- 2 files changed, 84 insertions(+), 17 deletions(-) diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index 170a440de..e0e48cbc1 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -24,6 +24,8 @@ A control's `kind` is `ControlKind::Range` unless it is an on/off switch. Declar Declare resource amounts with `GeneratorControl::percentage(id, label, maximum)`: 0 to `maximum` (300 unless given) in steps of 25, defaulting to 100, in the Resources group. The percentage scales the numbers that already decide that resource's amount — densities, clump sizes and counts, noise or fertility thresholds — and 100 must reproduce the unscaled map exactly: use `MapGeneration::scaledCount()` and `scaledShare()` from `shared/Resources.h`, which return their input unchanged at 100, and draw nothing from a random stream at 100 that the unscaled code didn't. Decide deliberately which placements are fairness guarantees (starter kits, reachability backstops, 1:1 guaranteed wheat and wood) and leave those unscaled, so every setting stays playable. Keep existing control ids and meanings; a generator that already exposes explicit amounts (Maze) keeps them. +Check the top of each range as carefully as the bottom. Resources block ground units and buildings alike, so an amount well above the default can wall a colony into a pocket with nowhere to build, or cover the building sites a start search was looking at — a map that still passes `validateWorld` and is still unplayable. `MapGeneration::openCrampedStarts()` clears the resource tiles nearest such a colony, ring by ring, until it can reach 16 tiles where a 4x4 building fits within 24 steps; call it at non-default amounts only (the height-field generators go through `openStartsBuriedByAmounts()`) and re-run `guaranteeStartingResources()` afterwards, since the clearing can take the nearest crop with the wall. If a generator picks its start sites relative to a scaled field, make that search widen rather than fail when the field covers everything it wanted. `MapGeneratorStudy`'s `tuning` line reports the worst colony's build sites and crop distances, which is how to see both problems. + Lobby preferences save a control through its legacy descriptor field when it has one (`wheat`, `lake-size`, ...) and in the preferences' `options` section otherwise, so new controls persist with no further work. Shared settings are width and height exponents, colony count, starting workers and background terrain. The request's `options` map contains only controls for its selected generator. Generator-specific structs give these values meaningful names such as `lake_size`, `channel_width` and `bridge_width`; there is no reuse of unrelated descriptor slots. @@ -52,7 +54,7 @@ Use `shared/Topology` for graph distances, sparse-label adjacency and passable-g `Regions::splitUpPoints` keeps its existing local search by default. Whole-region weighted search is optional, bounded, and does not promise a global optimum. Weights travel with points during the final shuffle. For a single site, its spacing budget is half the shorter map dimension. Weights are positive integers up to 10,000. -`placeSettlement` takes an explicit home mask and preferred building anchor. It checks the entire swarm footprint, places every requested worker, records the start and returns a diagnostic on failure. Create teams and bake the terrain first. Keep resource policy in the generator, before or after settlement as appropriate. Discard failed candidates even if a swarm has already been placed. +`placeSettlement` takes an explicit home mask and preferred building anchor. It checks the entire swarm footprint, places every requested worker, records the start and returns a diagnostic on failure. Create teams and bake the terrain first. Keep resource policy in the generator, before or after settlement as appropriate. Discard failed candidates even if a swarm has already been placed. It also weighs the workers' room, not just the swarm's: a worker tile is a free tile inside the home mask touching the swarm, so a kit filling the ground against it can leave too few. The nearest-tie pick is still the random one, and only when that site is short of room does the search fall back to the nearest site that has it, rather than failing the map. Add optional pure `validateRequest` and `validateWorld` callbacks to the definition for relationships between settings and generator-specific invariants. Return an empty string on success or useful diagnostic detail on failure. Request validation is shared by the editor, lobby and service. Final validation runs after the common structural checks. Validate the finished terrain when later carving, resources or buildings can alter connectivity; never silently change the request to satisfy an invariant. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 06739a0a0..1e0b658cc 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -59,6 +59,55 @@ Archipelago predate the shared resource/placement machinery and still place reso to each boot tile with their own compass-direction search, rather than through `scatterResources`/`chooseBalancedStarts`. +## Resource amounts and switches + +Every playable generator has amount controls for the resources it places (wheat, wood and stone +everywhere; algae and fruit where it places them) and one to three on/off switches for its own +sub-behaviours. An amount is a percentage of the generator's default, 100, from 0 to 300 in steps +of 25 unless noted. It scales the numbers that already decided that amount, and at 100 every map is +exactly what it was. Fairness placements (starter kits, 1:1 guaranteed wheat and wood, the +reachability backstop) stay unscaled wherever a generator has them, so an amount of 0 empties the +ambient layer but still leaves every colony a start. Maze keeps its explicit densities. + +| Generator | What the amounts scale | Switches (default) | +|---|---|---| +| Swamp, River, Islands, Crater lakes | Each resource's band of the height field: algae the lowest sixth of the water, stone a third of the farmland share just above the beach, wheat and wood the rest as two separate bands, none past the top of the grass. Fruit still counts groves | Stone on hilltops (off): stone on the highest grass instead of by the shore. River also: Winding river (on) | +| Concrete islands | Each colony's wheat and wood fields (how far in from the coast they reach) and its six stone deposits; the channels' algae (how deep it grows); the neutral islands' wheat half and fruit count | Sandy beaches (on) | +| Isles | Each colony's fields and stone deposits, and its algae patch (5×5 at 100) | Land bridges (on); Sandy beaches (on) | +| Old random | The area of each colony's wheat, wood, stone and algae squares | Colony meadows (on): the cleared grass square round each colony | +| Old islands | The area of each island's deposits | Extra starting deposit (on): the fourth deposit, of whichever of wheat or wood came out smaller | +| Contested commons | How many of the commons' zones are wheat, wood or fruit, its quarry's size and the moat's algae; home islands' fields are unscaled | Moat bridges (on); Jagged coastlines (on) | +| Maze | No new amounts: its existing Wheat, Wood, Stone, Algae and Fruit controls already set them | Sand roads (on); Treasure in dead ends (on): off, the same fruit is scattered along the passages' shores | +| Fjord continent | The ambient scatter, the core's stone and fruit clumps, the lake's and open sea's algae, and the banks' extra clumps; starter kits and bank guarantees are unscaled | Lake connects to fjords (off); Sandy lake shore (on); Fjord bank deposits (on) | +| Watershed | Separate wheat and wood farmland budgets, stone outcrops, confluence fruit groves, and algae at the mouths and in the shallows; starter kits are unscaled | River delta (on); Meandering rivers (on) | +| Stone highlands | The ponds' farmland (wheat and wood) and algae, and how much of the ridgeline is two tiles thick (stone, 0 to 200); kits are unscaled and Fruit still counts groves | Fruit in home valleys (off) | +| Symmetric arena | The farmland's wheat and wood, stone outcrops and algae, on top of Resource richness; fruit (0 to 100) keeps that share of the orchard's groves nearest the centre, never fewer than three | Moat (on); Stone in the orchard (on) | +| Ring world | The ambient scatter's wheat, wood, stone and fruit, and the shallows' algae; starter patches and island prizes are unscaled | Winding belt (on); Colonies on both coasts (on) | + +`scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and +return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of +its tiles, nearest the centre first, placed in `setResource`'s own order. Where an amount has to +add random draws (Contested commons' moat algae, Fjord's bank clumps), they come from streams of +their own, so the rest of the layout doesn't reshuffle. + +A high amount can also bury a colony: resources block ground units and buildings alike, so a +widened farmland band or ambient scatter can wall a swarm into a pocket with nowhere to put a +building. `guaranteeStartingResources` does not cover that case — a colony buried in wheat has +wheat at its feet, so it counts as served — so `openCrampedStarts` (`shared/Resources.h`) clears +the resource tiles nearest such a colony, one ring at a time outwards, until it can walk to 16 +tiles where a 4x4 building fits within 24 steps, and the caller then re-runs the guarantee in case +the clearing took the nearest crop too. A colony that already has the room is untouched. The +height-field generators (through `openStartsBuriedByAmounts`), Fjord and Ring world run it at any +non-default amount, and never at the defaults. Concrete islands and Isles need it differently: +their colonies' fields can cover every building site the start search looks at, so at a non-default +amount that search widens its window out from the default field rather than failing. + +`placeSettlement` guards the same class of problem for the colonies it places: it measures the +workers' room (free tiles inside the home mask touching the swarm) before the swarm goes down, and +if the nearest-tie site the random pick landed on is short of room it takes the nearest site that +has it instead of failing the map. The pick itself is unchanged wherever the room was already +there, so this only rescues maps that used to fail outright. + ## Resource placement `shared/Resources.cpp` gives generators a small set of composable primitives rather than one @@ -102,8 +151,11 @@ fixed resource pass: (nothing reachable even through terrain alone) is left untouched, since there's nothing on the other side of a wall that isn't there. `protectedWalls` is an optional width×height mask of resource tiles that belong to the map's design, such as Stone highlands' ridgelines; they are - treated like terrain, never cleared and never looked past. RuggedArchipelago, ShatteredCoast, - Fjord, Watershed, Stone highlands and Ring world call this. + treated like terrain, never cleared and never looked past. It wraps each boot tile onto the map + first, since the height-field generators' fallback site search can hand over one past the edge. + RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands and Ring world call this, as + do the height-field generators, and Concrete islands and Isles at any wheat or wood amount + other than 100. Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane down every passage, and its `validateWorld` confirms every colony can still walk to every other. @@ -164,8 +216,9 @@ The richest generator, and the one most of this framework's resource work was pr angularly-neighboring teams as a smooth S-curve centerline from just outside the coast in to `coreR` (or, in lake-connected mode, well inside the lake — see below). - **Central lake.** `lake-size` (0–90%, of `coreR`; 0 disables it) carves a lake at the exact - center, ringed by a sandy no-man's-land wider than `Map::controlSand()`'s own coastal fringe. - `lake-connected` (default off) decides whether the fjords actually cut through into the lake — + center, ringed by a sandy no-man's-land wider than `Map::controlSand()`'s own coastal fringe + (`sandy-lake-shore`, on by default; off, the lake has an ordinary beach). + `lake-connected` (a switch, default off) decides whether the fjords actually cut through into the lake — every peninsula then water-isolated from its neighbors, boats required — or stop short behind a solid land ring, keeping mutual land connectivity; the latter is verified with an explicit flood-fill after construction rather than assumed, and only runs in that mode, since a @@ -183,7 +236,8 @@ The richest generator, and the one most of this framework's resource work was pr far away in some other direction. - **Fjord banks.** Every fjord guarantees one corn and one wood clump per side, placed last so nothing else can overwrite the guarantee, plus six lighter best-effort clumps per side mixing - corn/wood/stone along the same bank. + corn/wood/stone along the same bank (`bank-deposits`, on by default). Each amount places its + rolled clumps that many hundredths of a time, drawing any chance from a stream of its own. - **Ambient layer and backstop.** `scatterResources` fills the continent interior at the end (corn:wood 2:1, fruit, stone; algae left to the dedicated shoreline/lake passes above), and `guaranteeStartingResources` runs last as the reachability backstop described above. @@ -213,7 +267,8 @@ cul-de-sac. so every cell is linked to the rest of the maze by ground that can never be closed: `Map::incResource` only seeds a resource on its own terrain and buildings need pure grass, so nothing grows over a road or is built on one. At a home the road stops against the swarm's - footprint. Shore distances for the resource scatter ignore the road. + footprint. Shore distances for the resource scatter ignore the road. With `sand-roads` off + (on by default), passages are grass from shore to shore, open to farmland and buildings. - **Walls.** Every closed boundary has a stone spine covering the whole boundary line from corner to corner, with `channel-width` all-water tiles on either side (default 2). Perpendicular walls share their corner tile, so a boundary can only be crossed — on foot or swimming — where it's @@ -232,7 +287,8 @@ cul-de-sac. every passage's shores, never more than three tiles in, so each passage keeps a clear lane down its middle however the maze turns. Fruit is treasure: every dead end that isn't a home gets one compact patch of `fruit` tiles near its far end, with fruit types dealt round-robin so every - kind is somewhere in the maze. Algae is seeded along the channels. + kind is somewhere in the maze. With `dead-end-treasure` off (on by default) the same fruit is + scattered along the passages' shores instead. Algae is seeded along the channels. - **Checked, not assumed.** `validateWorld` floods walkable tiles (water, buildings and every resource, including wall spines, block it) from colony 0's workers, and fails the candidate if any colony isn't reached. @@ -248,7 +304,9 @@ dry sandy uplands, and sand fords across the channels. - **Rivers.** Springs are sampled by land area (`river-density`) and join the network downstream of them nearest-first; a path that runs into another river becomes its tributary. Width grows with the square root of the springs upstream (`river-width`), and the trunk splits into one to - three distributaries on a delta lobe. + three distributaries on a delta lobe (`river-delta`, on by default; off, one mouth and no lobe, + with the other distributaries still planned so the rest of the network is unchanged). Every + path meanders (`meanders`, on by default; off, rivers run without their meanders). - **Channels that keep their water.** Channels are stamped wide enough to keep a four-connected water core under an all-at-once version of `controlSand`'s grass-to-sand rule; `Map::controlSand()` is then required to change nothing. @@ -275,7 +333,8 @@ the whole game. - **Ridges.** Basin sites spaced about `valley-size` apart are scattered on the torus, and every tile takes its nearest site after a periodic warp. A tile becomes ridge when any of its eight neighbours has a lower label, so no unit can slip through diagonally; noise thickens some - stretches to two tiles, and enclosed pockets under 40 tiles are filled. + stretches to two tiles (45% of the ridge at the default stone amount, which scales that + share), and enclosed pockets under 40 tiles are filled. - **Passes.** A random spanning tree of `pass-width` openings, cut mid-ridge, joins every valley; `loopiness` opens that share of the remaining shared ridges. - **Homes and ponds.** Colonies take the roomiest valleys by farthest-point spreading, one per @@ -283,7 +342,8 @@ the whole game. interiors away from ridges, passes and homes, with two-tile beaches so algae can regrow; valleys under 120 tiles get none. - **Resources.** Identical 1:1 wheat and wood kits beside every home, 2:1 farmland ringing the - ponds, algae in the water, and `fruit` groves (per 128×128) only in valleys no colony starts in. + ponds, algae in the water, and `fruit` groves (per 128×128) only in valleys no colony starts in + (unless `home-valley-fruit`, off by default, lets them grow in home valleys too). `guaranteeStartingResources` runs with the ridges as protected walls, and any resource left in or beside a pass is removed. `validateRequest` rejects maps smaller than four valleys or with fewer than 1,024 tiles per colony. @@ -307,11 +367,14 @@ colonies identical starting ground. order dependent, and resource amounts drawn from the engine RNG are equalised across each orbit. - **Centre.** `centre-size` sets the island's radius and `moat-width` the water around it; `causeways` and `causeway-width` choose one gate straight towards each colony or two flanking it. - The orchard deals two-by-two fruit groves out an orbit at a time, turns some orbits to stone and - always keeps all three fruits. It is the map's only fruit. + With `moat` off (on by default) the island joins the land around it, with no causeways; the + moat's width still spaces the homes. The orchard deals two-by-two fruit groves out an orbit at + a time, turns some orbits to stone (`orchard-stone`, on by default) and always keeps all three + fruits; the fruit amount keeps that share of the fruit orbits nearest the centre, never fewer + than three. It is the map's only fruit. - **Outside the ring.** Symmetric lakes (`lakes`), shore farmland, stone outcrops and algae scaled - by `richness`. Each home gets a pond and a fixed kit, and each colony's shortest route to its - causeways is kept as clear land. `scatterResources` is not used. + by `richness` and each one's own amount. Each home gets a pond and a fixed kit, and each + colony's shortest route to its causeways is kept as clear land. `scatterResources` is not used. - **Checked, not assumed.** `validateWorld` requires corner, terrain, deposit, building and unit invariance under every symmetry, with a consistent colony permutation that reaches every colony, and equal walking distances from every colony to wheat, wood, each fruit and the orchard. Only @@ -327,11 +390,13 @@ exactly two land neighbours. - **Belt.** The centre line and width are whole-number harmonics of the map length and the coast is lattice noise whose cells tile the torus, so the seam can't be seen. A dry spine beside the centre line and one shared coast budget keep the belt a single landmass and the ocean a band - whatever `belt-width` (a percentage of the map's breadth) and `coast-roughness` ask for. Terrain + whatever `belt-width` (a percentage of the map's breadth) and `coast-roughness` ask for. With + `winding-belt` off (on by default) the centre line runs straight round the map. Terrain is stamped with an order-independent beach pass rather than `Map::controlSand()`. - **Lakes and islands.** `lake-density` adds inland lakes that keep land between them and the ocean and stay off the spine; `resource-islands` (per 128×128) adds themed islands out at sea. -- **Colonies and resources.** Evenly spaced, jittered slots alternate coasts and sit on the +- **Colonies and resources.** Evenly spaced, jittered slots alternate coasts (`both-coasts`, on + by default; off, every colony takes the same coast) and sit on the spine-connected belt at one fixed distance from water, with identical starter patches; then `scatterResources`, a shallows algae pass and `guaranteeStartingResources`. A final road pass clears only the deposits on the cheapest walk from each colony to the next, closing the loop. From 5dbc8e3babce5fbee2a677919dfa8fcc2a312cf4 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:33:25 -0400 Subject: [PATCH 076/136] Translate the new resource amount and switch labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 24 labels the generator knobs added — the Wheat, Wood, Stone, Algae and Fruit amounts and every new switch — get entries in data/texts.keys.txt and data/texts.en.txt and real translations in all 32 other tables, each following that table's own existing terminology for the resources, the moat, the delta and the rest rather than falling back to English. python3 data/check_translations.py --strict reports 0 untranslated, 0 missing keys and 0 structural errors for every table. test/test_translations.py (5 tests), test/test_font_coverage.py (1 test) and test/test_text_area_layout.py (13 cases) pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- data/texts.ar.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.br.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ca.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.cz.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.de.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.dk.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.en.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.eo.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.es.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.eu.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.fa.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.fi.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.fr.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.gr.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.hu.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.id.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.it.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ja.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.keys.txt | 24 ++++++++++++++++++++++ data/texts.ko.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.nl.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.pl.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.pt.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ro.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.ru.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.si.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.sk.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.sr.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.sv.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.tr.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.uk.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.vi.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.zh-cn.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ data/texts.zh-tw.txt | 48 ++++++++++++++++++++++++++++++++++++++++++++ 34 files changed, 1608 insertions(+) diff --git a/data/texts.ar.txt b/data/texts.ar.txt index a0e700e4d..4946e65f2 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1868,3 +1868,51 @@ OpenGL غير متوفر في هذا الإصدار. عرض الحزام [The belt is too short for this many colonies; use a longer map or fewer colonies.] الحزام أقصر من أن يتسع لهذا العدد من المستعمرات؛ استخدم خريطة أطول أو عددًا أقل من المستعمرات. +[Wheat amount] +كمية القمح +[Wood amount] +كمية الخشب +[Stone amount] +كمية الحجر +[Algae amount] +كمية الطحالب +[Fruit amount] +كمية الفاكهة +[Stone on hilltops] +الحجارة على قمم التلال +[Winding river] +نهر متعرج +[Sandy beaches] +شواطئ رملية +[Land bridges] +الجسور البرية +[Colony meadows] +مروج حول المستعمرات +[Extra starting deposit] +رواسب إضافية عند البداية +[Moat bridges] +جسور فوق الخندق المائي +[Jagged coastlines] +سواحل متعرجة +[Sand roads] +طرق رملية +[Treasure in dead ends] +كنوز في الطرق المسدودة +[Fjord bank deposits] +رواسب على ضفاف المضائق البحرية +[Sandy lake shore] +شاطئ رملي حول البحيرة +[River delta] +دلتا النهر +[Meandering rivers] +أنهار كثيرة التعرج +[Fruit in home valleys] +الفاكهة في وديان البداية +[Moat] +الخندق المائي +[Stone in the orchard] +حجارة في البستان +[Colonies on both coasts] +مستعمرات على الساحلين +[Winding belt] +حزام متعرج diff --git a/data/texts.br.txt b/data/texts.br.txt index 4f292c8c9..cd488d94a 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1866,3 +1866,51 @@ Mundo anel Largura da faixa [The belt is too short for this many colonies; use a longer map or fewer colonies.] A faixa é curta demais para tantas colônias; use um mapa mais longo ou menos colônias. +[Wheat amount] +Quantidade de trigo +[Wood amount] +Quantidade de madeira +[Stone amount] +Quantidade de pedra +[Algae amount] +Quantidade de algas +[Fruit amount] +Quantidade de frutas +[Stone on hilltops] +Pedra no topo das colinas +[Winding river] +Rio sinuoso +[Sandy beaches] +Praias de areia +[Land bridges] +Pontes de terra +[Colony meadows] +Campos ao redor das colônias +[Extra starting deposit] +Depósito inicial extra +[Moat bridges] +Pontes sobre o fosso +[Jagged coastlines] +Costas recortadas +[Sand roads] +Estradas de areia +[Treasure in dead ends] +Tesouros em becos sem saída +[Fjord bank deposits] +Depósitos nas margens dos fiordes +[Sandy lake shore] +Margem arenosa do lago +[River delta] +Delta do rio +[Meandering rivers] +Rios com meandros +[Fruit in home valleys] +Frutas nos vales iniciais +[Moat] +Fosso +[Stone in the orchard] +Pedra no pomar +[Colonies on both coasts] +Colônias em ambas as costas +[Winding belt] +Faixa sinuosa diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 4496c48fe..7c7ecdb13 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1878,3 +1878,51 @@ Món anell Amplada del cinturó [The belt is too short for this many colonies; use a longer map or fewer colonies.] El cinturó és massa curt per a tantes colònies; feu servir un mapa més llarg o menys colònies. +[Wheat amount] +Quantitat de blat +[Wood amount] +Quantitat de fusta +[Stone amount] +Quantitat de pedra +[Algae amount] +Quantitat d'algues +[Fruit amount] +Quantitat de fruita +[Stone on hilltops] +Pedra als cims dels turons +[Winding river] +Riu sinuós +[Sandy beaches] +Platges de sorra +[Land bridges] +Ponts de terra +[Colony meadows] +Prats al voltant de les colònies +[Extra starting deposit] +Dipòsit inicial addicional +[Moat bridges] +Ponts sobre el fossat +[Jagged coastlines] +Costes retallades +[Sand roads] +Camins de sorra +[Treasure in dead ends] +Tresors als atzucacs +[Fjord bank deposits] +Dipòsits a les ribes dels fiords +[Sandy lake shore] +Riba sorrenca del llac +[River delta] +Delta del riu +[Meandering rivers] +Rius amb meandres +[Fruit in home valleys] +Fruita a les valls inicials +[Moat] +Fossat +[Stone in the orchard] +Pedra a l'hort +[Colonies on both coasts] +Colònies a les dues costes +[Winding belt] +Cinturó sinuós diff --git a/data/texts.cz.txt b/data/texts.cz.txt index b7c1801d5..f5b6a5f46 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1870,3 +1870,51 @@ Prstencový svět Šířka pásu [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pás je pro tolik kolonií příliš krátký; použijte delší mapu nebo méně kolonií. +[Wheat amount] +Množství pšenice +[Wood amount] +Množství dřeva +[Stone amount] +Množství kamení +[Algae amount] +Množství řas +[Fruit amount] +Množství ovoce +[Stone on hilltops] +Kamení na vrcholcích kopců +[Winding river] +Klikatá řeka +[Sandy beaches] +Písečné pláže +[Land bridges] +Pevninské mosty +[Colony meadows] +Louky kolem kolonií +[Extra starting deposit] +Ložisko navíc na začátku +[Moat bridges] +Mosty přes příkop +[Jagged coastlines] +Členité pobřeží +[Sand roads] +Písečné cesty +[Treasure in dead ends] +Poklady ve slepých uličkách +[Fjord bank deposits] +Ložiska na březích fjordů +[Sandy lake shore] +Písečný břeh jezera +[River delta] +Říční delta +[Meandering rivers] +Meandrující řeky +[Fruit in home valleys] +Ovoce v domovských údolích +[Moat] +Vodní příkop +[Stone in the orchard] +Kamení v sadu +[Colonies on both coasts] +Kolonie na obou pobřežích +[Winding belt] +Zvlněný pás diff --git a/data/texts.de.txt b/data/texts.de.txt index c0c2550d5..27f00160d 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1870,3 +1870,51 @@ Ringwelt Gürtelbreite [The belt is too short for this many colonies; use a longer map or fewer colonies.] Der Gürtel ist zu kurz für so viele Kolonien; verwenden Sie eine längere Karte oder weniger Kolonien. +[Wheat amount] +Weizenmenge +[Wood amount] +Holzmenge +[Stone amount] +Steinmenge +[Algae amount] +Algenmenge +[Fruit amount] +Obstmenge +[Stone on hilltops] +Stein auf den Hügelkuppen +[Winding river] +Gewundener Fluss +[Sandy beaches] +Sandstrände +[Land bridges] +Landbrücken +[Colony meadows] +Wiesen um die Kolonien +[Extra starting deposit] +Zusätzliches Startvorkommen +[Moat bridges] +Brücken über den Graben +[Jagged coastlines] +Zerklüftete Küsten +[Sand roads] +Sandwege +[Treasure in dead ends] +Schätze in Sackgassen +[Fjord bank deposits] +Vorkommen an den Fjordufern +[Sandy lake shore] +Sandiges Seeufer +[River delta] +Flussdelta +[Meandering rivers] +Mäandernde Flüsse +[Fruit in home valleys] +Obst in den Starttälern +[Moat] +Wassergraben +[Stone in the orchard] +Stein im Obstgarten +[Colonies on both coasts] +Kolonien an beiden Küsten +[Winding belt] +Gewundener Gürtel diff --git a/data/texts.dk.txt b/data/texts.dk.txt index ccd5dce47..6ff6e345b 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1946,3 +1946,51 @@ Ringverden Bæltebredde [The belt is too short for this many colonies; use a longer map or fewer colonies.] Bæltet er for kort til så mange kolonier; brug et længere kort eller færre kolonier. +[Wheat amount] +Mængde hvede +[Wood amount] +Mængde træ +[Stone amount] +Mængde sten +[Algae amount] +Mængde alger +[Fruit amount] +Mængde frugt +[Stone on hilltops] +Sten på bakketoppene +[Winding river] +Slyngende flod +[Sandy beaches] +Sandstrande +[Land bridges] +Landbroer +[Colony meadows] +Enge omkring kolonierne +[Extra starting deposit] +Ekstra startforekomst +[Moat bridges] +Broer over voldgraven +[Jagged coastlines] +Takkede kyster +[Sand roads] +Sandveje +[Treasure in dead ends] +Skatte i blindgyder +[Fjord bank deposits] +Forekomster langs fjordbredderne +[Sandy lake shore] +Sandet søbred +[River delta] +Floddelta +[Meandering rivers] +Bugtede floder +[Fruit in home valleys] +Frugt i startdalene +[Moat] +Voldgrav +[Stone in the orchard] +Sten i frugtplantagen +[Colonies on both coasts] +Kolonier på begge kyster +[Winding belt] +Bugtet bælte diff --git a/data/texts.en.txt b/data/texts.en.txt index 4f95a9118..9b69f1df4 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1858,3 +1858,51 @@ Ring world Belt width [The belt is too short for this many colonies; use a longer map or fewer colonies.] The belt is too short for this many colonies; use a longer map or fewer colonies. +[Wheat amount] +Wheat amount +[Wood amount] +Wood amount +[Stone amount] +Stone amount +[Algae amount] +Algae amount +[Fruit amount] +Fruit amount +[Stone on hilltops] +Stone on hilltops +[Winding river] +Winding river +[Sandy beaches] +Sandy beaches +[Land bridges] +Land bridges +[Colony meadows] +Colony meadows +[Extra starting deposit] +Extra starting deposit +[Moat bridges] +Moat bridges +[Jagged coastlines] +Jagged coastlines +[Sand roads] +Sand roads +[Treasure in dead ends] +Treasure in dead ends +[Fjord bank deposits] +Fjord bank deposits +[Sandy lake shore] +Sandy lake shore +[River delta] +River delta +[Meandering rivers] +Meandering rivers +[Fruit in home valleys] +Fruit in home valleys +[Moat] +Moat +[Stone in the orchard] +Stone in the orchard +[Colonies on both coasts] +Colonies on both coasts +[Winding belt] +Winding belt diff --git a/data/texts.eo.txt b/data/texts.eo.txt index fd6957291..cd1683632 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1868,3 +1868,51 @@ Ringa mondo Larĝo de zono [The belt is too short for this many colonies; use a longer map or fewer colonies.] La zono estas tro mallonga por tiom da kolonioj; uzu pli longan mapon aŭ malpli da kolonioj. +[Wheat amount] +Kvanto de tritiko +[Wood amount] +Kvanto de ligno +[Stone amount] +Kvanto de ŝtono +[Algae amount] +Kvanto de algoj +[Fruit amount] +Kvanto de fruktoj +[Stone on hilltops] +Ŝtono sur montetopintoj +[Winding river] +Serpentuma rivero +[Sandy beaches] +Sablaj plaĝoj +[Land bridges] +Terpontoj +[Colony meadows] +Herbejoj ĉirkaŭ kolonioj +[Extra starting deposit] +Kroma komenca kuŝejo +[Moat bridges] +Pontoj super la fosaĵo +[Jagged coastlines] +Dentitaj marbordoj +[Sand roads] +Sablaj vojoj +[Treasure in dead ends] +Trezoroj en senelirejoj +[Fjord bank deposits] +Kuŝejoj ĉe fjordaj bordoj +[Sandy lake shore] +Sabla lagbordo +[River delta] +Riverdelto +[Meandering rivers] +Meandrantaj riveroj +[Fruit in home valleys] +Fruktoj en hejmvaloj +[Moat] +Fosaĵo +[Stone in the orchard] +Ŝtono en la fruktoĝardeno +[Colonies on both coasts] +Kolonioj ĉe ambaŭ marbordoj +[Winding belt] +Serpentuma zono diff --git a/data/texts.es.txt b/data/texts.es.txt index 46e3c476d..d0d6ecbeb 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1870,3 +1870,51 @@ Mundo anillo Anchura del cinturón [The belt is too short for this many colonies; use a longer map or fewer colonies.] El cinturón es demasiado corto para tantas colonias; use un mapa más largo o menos colonias. +[Wheat amount] +Cantidad de trigo +[Wood amount] +Cantidad de madera +[Stone amount] +Cantidad de piedra +[Algae amount] +Cantidad de algas +[Fruit amount] +Cantidad de fruta +[Stone on hilltops] +Piedra en las cimas de las colinas +[Winding river] +Río sinuoso +[Sandy beaches] +Playas de arena +[Land bridges] +Puentes terrestres +[Colony meadows] +Praderas alrededor de las colonias +[Extra starting deposit] +Depósito inicial adicional +[Moat bridges] +Puentes sobre el foso +[Jagged coastlines] +Costas escarpadas +[Sand roads] +Caminos de arena +[Treasure in dead ends] +Tesoros en los callejones sin salida +[Fjord bank deposits] +Depósitos en las orillas de los fiordos +[Sandy lake shore] +Orilla arenosa del lago +[River delta] +Delta del río +[Meandering rivers] +Ríos serpenteantes +[Fruit in home valleys] +Fruta en los valles iniciales +[Moat] +Foso +[Stone in the orchard] +Piedra en el huerto +[Colonies on both coasts] +Colonias en ambas costas +[Winding belt] +Cinturón sinuoso diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 348ef74b6..7d227e324 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1880,3 +1880,51 @@ Eraztun-mundua Gerrikoaren zabalera [The belt is too short for this many colonies; use a longer map or fewer colonies.] Gerrikoa laburregia da hainbeste koloniarentzat; erabili mapa luzeagoa edo kolonia gutxiago. +[Wheat amount] +Gari kopurua +[Wood amount] +Egur kopurua +[Stone amount] +Harri kopurua +[Algae amount] +Alga kopurua +[Fruit amount] +Fruta kopurua +[Stone on hilltops] +Harria muinoen gailurretan +[Winding river] +Ibai bihurgunetsua +[Sandy beaches] +Harea-hondartzak +[Land bridges] +Lur-zubiak +[Colony meadows] +Belardiak kolonien inguruan +[Extra starting deposit] +Hasierako baliabide gehigarria +[Moat bridges] +Lubanarroko zubiak +[Jagged coastlines] +Kostalde malkarrak +[Sand roads] +Harea-bideak +[Treasure in dead ends] +Altxorrak kale itsuetan +[Fjord bank deposits] +Fiordoen ertzetako baliabideak +[Sandy lake shore] +Aintziraren ertz hareatsua +[River delta] +Ibai-delta +[Meandering rivers] +Ibai meandrodunak +[Fruit in home valleys] +Fruta hasierako haranetan +[Moat] +Lubanarroa +[Stone in the orchard] +Harria fruta-baratzean +[Colonies on both coasts] +Koloniak bi kostaldeetan +[Winding belt] +Gerriko bihurgunetsua diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 90b69016c..85bc7b4d5 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1868,3 +1868,51 @@ OpenGL در این بیلد موجود نیست. پهنای کمربند [The belt is too short for this many colonies; use a longer map or fewer colonies.] کمربند برای این تعداد مستعمره بیش از حد کوتاه است؛ از نقشهٔ بلندتر یا مستعمره‌های کمتر استفاده کنید. +[Wheat amount] +مقدار گندم +[Wood amount] +مقدار چوب +[Stone amount] +مقدار سنگ +[Algae amount] +میزان جلبک +[Fruit amount] +مقدار میوه +[Stone on hilltops] +سنگ روی قلهٔ تپه‌ها +[Winding river] +رودخانهٔ پرپیچ‌وخم +[Sandy beaches] +ساحل‌های شنی +[Land bridges] +پل‌های خشکی +[Colony meadows] +چمنزار پیرامون مستعمره‌ها +[Extra starting deposit] +ذخیرهٔ آغازین اضافی +[Moat bridges] +پل‌های روی خندق آبی +[Jagged coastlines] +ساحل‌های دندانه‌دار +[Sand roads] +جاده‌های شنی +[Treasure in dead ends] +گنج در بن‌بست‌ها +[Fjord bank deposits] +ذخایر کرانهٔ آبدره‌ها +[Sandy lake shore] +کرانهٔ شنی دریاچه +[River delta] +دلتای رودخانه +[Meandering rivers] +رودخانه‌های مارپیچ +[Fruit in home valleys] +میوه در دره‌های آغازین +[Moat] +خندق آبی +[Stone in the orchard] +سنگ در باغ میوه +[Colonies on both coasts] +مستعمره‌ها در هر دو ساحل +[Winding belt] +کمربند پرپیچ‌وخم diff --git a/data/texts.fi.txt b/data/texts.fi.txt index ab8caf845..6a048d4be 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1870,3 +1870,51 @@ Rengasmaailma Vyöhykkeen leveys [The belt is too short for this many colonies; use a longer map or fewer colonies.] Vyöhyke on liian lyhyt näin monelle siirtokunnalle; käytä pidempää karttaa tai vähemmän siirtokuntia. +[Wheat amount] +Viljan määrä +[Wood amount] +Puun määrä +[Stone amount] +Kiven määrä +[Algae amount] +Levän määrä +[Fruit amount] +Hedelmien määrä +[Stone on hilltops] +Kiveä kukkuloiden laella +[Winding river] +Mutkitteleva joki +[Sandy beaches] +Hiekkarannat +[Land bridges] +Maasillat +[Colony meadows] +Niityt siirtokuntien ympärillä +[Extra starting deposit] +Ylimääräinen aloitusesiintymä +[Moat bridges] +Sillat vallihaudan yli +[Jagged coastlines] +Rikkonaiset rannikot +[Sand roads] +Hiekkatiet +[Treasure in dead ends] +Aarteita umpikujissa +[Fjord bank deposits] +Esiintymiä vuonojen rannoilla +[Sandy lake shore] +Hiekkainen järvenranta +[River delta] +Jokisuisto +[Meandering rivers] +Meanderoivat joet +[Fruit in home valleys] +Hedelmiä kotilaaksoissa +[Moat] +Vallihauta +[Stone in the orchard] +Kiveä hedelmätarhassa +[Colonies on both coasts] +Siirtokuntia molemmilla rannikoilla +[Winding belt] +Mutkitteleva vyöhyke diff --git a/data/texts.fr.txt b/data/texts.fr.txt index df98ddc51..817218c64 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1880,3 +1880,51 @@ Monde-anneau Largeur de la ceinture [The belt is too short for this many colonies; use a longer map or fewer colonies.] La ceinture est trop courte pour autant de colonies ; utilisez une carte plus longue ou moins de colonies. +[Wheat amount] +Quantité de blé +[Wood amount] +Quantité de bois +[Stone amount] +Quantité de pierre +[Algae amount] +Quantité d'algues +[Fruit amount] +Quantité de fruits +[Stone on hilltops] +Pierre au sommet des collines +[Winding river] +Rivière sinueuse +[Sandy beaches] +Plages de sable +[Land bridges] +Ponts terrestres +[Colony meadows] +Prairies autour des colonies +[Extra starting deposit] +Gisement de départ supplémentaire +[Moat bridges] +Ponts sur les douves +[Jagged coastlines] +Côtes découpées +[Sand roads] +Chemins de sable +[Treasure in dead ends] +Trésors dans les culs-de-sac +[Fjord bank deposits] +Gisements sur les rives des fjords +[Sandy lake shore] +Rive sableuse du lac +[River delta] +Delta de la rivière +[Meandering rivers] +Rivières à méandres +[Fruit in home valleys] +Fruits dans les vallées de départ +[Moat] +Douves +[Stone in the orchard] +Pierre dans le verger +[Colonies on both coasts] +Colonies sur les deux côtes +[Winding belt] +Ceinture sinueuse diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 0db8d5d30..f453aa416 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1940,3 +1940,51 @@ Cortex Πλάτος ζώνης [The belt is too short for this many colonies; use a longer map or fewer colonies.] Η ζώνη είναι πολύ κοντή για τόσες αποικίες. Χρησιμοποιήστε μακρύτερο χάρτη ή λιγότερες αποικίες. +[Wheat amount] +Ποσότητα σιταριού +[Wood amount] +Ποσότητα ξύλου +[Stone amount] +Ποσότητα πέτρας +[Algae amount] +Ποσότητα φυκιών +[Fruit amount] +Ποσότητα φρούτων +[Stone on hilltops] +Πέτρα στις κορυφές των λόφων +[Winding river] +Ελικοειδής ποταμός +[Sandy beaches] +Αμμώδεις παραλίες +[Land bridges] +Χερσαίες γέφυρες +[Colony meadows] +Λιβάδια γύρω από τις αποικίες +[Extra starting deposit] +Επιπλέον αρχικό κοίτασμα +[Moat bridges] +Γέφυρες πάνω από την τάφρο +[Jagged coastlines] +Οδοντωτές ακτές +[Sand roads] +Αμμώδεις δρόμοι +[Treasure in dead ends] +Θησαυροί στα αδιέξοδα +[Fjord bank deposits] +Κοιτάσματα στις όχθες των φιόρδ +[Sandy lake shore] +Αμμώδης όχθη λίμνης +[River delta] +Δέλτα ποταμού +[Meandering rivers] +Μαιανδρικοί ποταμοί +[Fruit in home valleys] +Φρούτα στις αρχικές κοιλάδες +[Moat] +Τάφρος +[Stone in the orchard] +Πέτρα στον οπωρώνα +[Colonies on both coasts] +Αποικίες και στις δύο ακτές +[Winding belt] +Ελικοειδής ζώνη diff --git a/data/texts.hu.txt b/data/texts.hu.txt index f8c6a331e..92f71c33d 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1870,3 +1870,51 @@ Gyűrűvilág Öv szélessége [The belt is too short for this many colonies; use a longer map or fewer colonies.] Az öv túl rövid ennyi kolóniához; használjon hosszabb térképet vagy kevesebb kolóniát. +[Wheat amount] +Búza mennyisége +[Wood amount] +Fa mennyisége +[Stone amount] +Kő mennyisége +[Algae amount] +Alga mennyisége +[Fruit amount] +Gyümölcs mennyisége +[Stone on hilltops] +Kő a dombtetőkön +[Winding river] +Kanyargó folyó +[Sandy beaches] +Homokos strandok +[Land bridges] +Földhidak +[Colony meadows] +Rétek a kolóniák körül +[Extra starting deposit] +Extra kezdő lelőhely +[Moat bridges] +Hidak a várárkon +[Jagged coastlines] +Tagolt partok +[Sand roads] +Homokutak +[Treasure in dead ends] +Kincsek a zsákutcákban +[Fjord bank deposits] +Lelőhelyek a fjordok partján +[Sandy lake shore] +Homokos tópart +[River delta] +Folyódelta +[Meandering rivers] +Kanyargó folyók +[Fruit in home valleys] +Gyümölcs a kezdővölgyekben +[Moat] +Várárok +[Stone in the orchard] +Kő a gyümölcsösben +[Colonies on both coasts] +Kolóniák mindkét parton +[Winding belt] +Kanyargó öv diff --git a/data/texts.id.txt b/data/texts.id.txt index 68a9fb52a..e50e8dc0b 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1866,3 +1866,51 @@ Dunia cincin Lebar sabuk [The belt is too short for this many colonies; use a longer map or fewer colonies.] Sabuk terlalu pendek untuk koloni sebanyak ini; gunakan peta yang lebih panjang atau lebih sedikit koloni. +[Wheat amount] +Jumlah gandum +[Wood amount] +Jumlah kayu +[Stone amount] +Jumlah batu +[Algae amount] +Jumlah ganggang +[Fruit amount] +Jumlah buah +[Stone on hilltops] +Batu di puncak bukit +[Winding river] +Sungai berkelok +[Sandy beaches] +Pantai berpasir +[Land bridges] +Jembatan darat +[Colony meadows] +Padang rumput di sekitar koloni +[Extra starting deposit] +Sumber daya awal tambahan +[Moat bridges] +Jembatan di atas parit air +[Jagged coastlines] +Pantai bergerigi +[Sand roads] +Jalan pasir +[Treasure in dead ends] +Harta di jalan buntu +[Fjord bank deposits] +Sumber daya di tepi fjord +[Sandy lake shore] +Tepi danau berpasir +[River delta] +Delta sungai +[Meandering rivers] +Sungai berliku-liku +[Fruit in home valleys] +Buah di lembah awal +[Moat] +Parit air +[Stone in the orchard] +Batu di kebun buah +[Colonies on both coasts] +Koloni di kedua pantai +[Winding belt] +Sabuk berkelok diff --git a/data/texts.it.txt b/data/texts.it.txt index c0af5e0f0..1e6d2a61b 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1932,3 +1932,51 @@ Mondo ad anello Larghezza della fascia [The belt is too short for this many colonies; use a longer map or fewer colonies.] La fascia è troppo corta per così tante colonie; usa una mappa più lunga o meno colonie. +[Wheat amount] +Quantità di frumento +[Wood amount] +Quantità di legno +[Stone amount] +Quantità di roccia +[Algae amount] +Quantità di alghe +[Fruit amount] +Quantità di frutta +[Stone on hilltops] +Roccia sulle cime delle colline +[Winding river] +Fiume tortuoso +[Sandy beaches] +Spiagge sabbiose +[Land bridges] +Ponti terrestri +[Colony meadows] +Prati intorno alle colonie +[Extra starting deposit] +Giacimento iniziale aggiuntivo +[Moat bridges] +Ponti sul fossato +[Jagged coastlines] +Coste frastagliate +[Sand roads] +Strade di sabbia +[Treasure in dead ends] +Tesori nei vicoli ciechi +[Fjord bank deposits] +Giacimenti sulle rive dei fiordi +[Sandy lake shore] +Riva sabbiosa del lago +[River delta] +Delta del fiume +[Meandering rivers] +Fiumi con meandri +[Fruit in home valleys] +Frutta nelle valli iniziali +[Moat] +Fossato +[Stone in the orchard] +Roccia nel frutteto +[Colonies on both coasts] +Colonie su entrambe le coste +[Winding belt] +Fascia sinuosa diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 131bcddeb..cdc33ae8a 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1866,3 +1866,51 @@ Globulation 2 のカーソルを表示します。 帯の幅 [The belt is too short for this many colonies; use a longer map or fewer colonies.] このコロニー数に対して帯が短すぎます。より長いマップを使うか、コロニーを減らしてください。 +[Wheat amount] +小麦の量 +[Wood amount] +木材の量 +[Stone amount] +石の量 +[Algae amount] +藻の量 +[Fruit amount] +フルーツの量 +[Stone on hilltops] +丘の上の石 +[Winding river] +蛇行する川 +[Sandy beaches] +砂浜 +[Land bridges] +陸橋 +[Colony meadows] +コロニー周辺の草地 +[Extra starting deposit] +追加の初期資源 +[Moat bridges] +堀に架かる橋 +[Jagged coastlines] +入り組んだ海岸線 +[Sand roads] +砂の道 +[Treasure in dead ends] +行き止まりの宝 +[Fjord bank deposits] +フィヨルド岸の資源 +[Sandy lake shore] +湖の砂浜 +[River delta] +河口の三角州 +[Meandering rivers] +蛇行する川筋 +[Fruit in home valleys] +開始地点の谷のフルーツ +[Moat] +堀 +[Stone in the orchard] +果樹園の石 +[Colonies on both coasts] +両岸のコロニー +[Winding belt] +曲がりくねった帯 diff --git a/data/texts.keys.txt b/data/texts.keys.txt index a8d0b9222..8b1dcacfb 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -927,3 +927,27 @@ [Ring world] [Belt width] [The belt is too short for this many colonies; use a longer map or fewer colonies.] +[Wheat amount] +[Wood amount] +[Stone amount] +[Algae amount] +[Fruit amount] +[Stone on hilltops] +[Winding river] +[Sandy beaches] +[Land bridges] +[Colony meadows] +[Extra starting deposit] +[Moat bridges] +[Jagged coastlines] +[Sand roads] +[Treasure in dead ends] +[Fjord bank deposits] +[Sandy lake shore] +[River delta] +[Meandering rivers] +[Fruit in home valleys] +[Moat] +[Stone in the orchard] +[Colonies on both coasts] +[Winding belt] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 636368a6e..98ec4f811 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1866,3 +1866,51 @@ Globulation 2 커서를 표시합니다. 띠 너비 [The belt is too short for this many colonies; use a longer map or fewer colonies.] 이 식민지 수에 비해 띠가 너무 짧습니다. 더 긴 지도를 사용하거나 식민지 수를 줄이세요. +[Wheat amount] +밀 양 +[Wood amount] +나무 양 +[Stone amount] +돌 양 +[Algae amount] +해조류 양 +[Fruit amount] +과일 양 +[Stone on hilltops] +언덕 꼭대기의 돌 +[Winding river] +구불구불한 강 +[Sandy beaches] +모래 해변 +[Land bridges] +육교 +[Colony meadows] +식민지 주변 초원 +[Extra starting deposit] +추가 시작 자원 +[Moat bridges] +해자 위의 다리 +[Jagged coastlines] +들쭉날쭉한 해안선 +[Sand roads] +모래길 +[Treasure in dead ends] +막다른 길의 보물 +[Fjord bank deposits] +피오르 기슭의 자원 +[Sandy lake shore] +모래 호숫가 +[River delta] +강 삼각주 +[Meandering rivers] +굽이치는 강 +[Fruit in home valleys] +시작 골짜기의 과일 +[Moat] +해자 +[Stone in the orchard] +과수원의 돌 +[Colonies on both coasts] +양쪽 해안의 식민지 +[Winding belt] +구불구불한 띠 diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 78d77aae7..9127d3367 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1894,3 +1894,51 @@ Ringwereld Gordelbreedte [The belt is too short for this many colonies; use a longer map or fewer colonies.] De gordel is te kort voor zoveel kolonies; gebruik een langere kaart of minder kolonies. +[Wheat amount] +Hoeveelheid tarwe +[Wood amount] +Hoeveelheid hout +[Stone amount] +Hoeveelheid steen +[Algae amount] +Hoeveelheid zeewier +[Fruit amount] +Hoeveelheid vruchten +[Stone on hilltops] +Steen op heuveltoppen +[Winding river] +Kronkelende rivier +[Sandy beaches] +Zandstranden +[Land bridges] +Landbruggen +[Colony meadows] +Weiden rond de kolonies +[Extra starting deposit] +Extra startvoorraad +[Moat bridges] +Bruggen over de gracht +[Jagged coastlines] +Grillige kusten +[Sand roads] +Zandwegen +[Treasure in dead ends] +Schatten in doodlopende gangen +[Fjord bank deposits] +Voorraden langs de fjordoevers +[Sandy lake shore] +Zandige meeroever +[River delta] +Rivierdelta +[Meandering rivers] +Meanderende rivieren +[Fruit in home valleys] +Vruchten in de startdalen +[Moat] +Slotgracht +[Stone in the orchard] +Steen in de boomgaard +[Colonies on both coasts] +Kolonies aan beide kusten +[Winding belt] +Kronkelende gordel diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 7c91c9e5a..4cacb0e99 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1870,3 +1870,51 @@ Groble są zbyt blisko siebie; użyj mniej licznych lub węższych grobli albo w Szerokość pasa [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pas jest za krótki dla tylu kolonii; użyj dłuższej mapy lub mniejszej liczby kolonii. +[Wheat amount] +Ilość pszenicy +[Wood amount] +Ilość drewna +[Stone amount] +Ilość kamienia +[Algae amount] +Ilość alg +[Fruit amount] +Ilość owoców +[Stone on hilltops] +Kamień na szczytach wzgórz +[Winding river] +Kręta rzeka +[Sandy beaches] +Piaszczyste plaże +[Land bridges] +Mosty lądowe +[Colony meadows] +Łąki wokół kolonii +[Extra starting deposit] +Dodatkowe złoże startowe +[Moat bridges] +Mosty nad fosą +[Jagged coastlines] +Postrzępione wybrzeża +[Sand roads] +Piaszczyste drogi +[Treasure in dead ends] +Skarby w ślepych zaułkach +[Fjord bank deposits] +Złoża na brzegach fiordów +[Sandy lake shore] +Piaszczysty brzeg jeziora +[River delta] +Delta rzeki +[Meandering rivers] +Meandrujące rzeki +[Fruit in home valleys] +Owoce w dolinach startowych +[Moat] +Fosa +[Stone in the orchard] +Kamień w sadzie +[Colonies on both coasts] +Kolonie na obu wybrzeżach +[Winding belt] +Kręty pas diff --git a/data/texts.pt.txt b/data/texts.pt.txt index acede099f..745861ee1 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1876,3 +1876,51 @@ Mundo anel Largura da faixa [The belt is too short for this many colonies; use a longer map or fewer colonies.] A faixa é demasiado curta para tantas colónias; use um mapa mais longo ou menos colónias. +[Wheat amount] +Quantidade de trigo +[Wood amount] +Quantidade de madeira +[Stone amount] +Quantidade de pedra +[Algae amount] +Quantidade de algas +[Fruit amount] +Quantidade de fruta +[Stone on hilltops] +Pedra no topo das colinas +[Winding river] +Rio sinuoso +[Sandy beaches] +Praias de areia +[Land bridges] +Pontes terrestres +[Colony meadows] +Prados à volta das colónias +[Extra starting deposit] +Depósito inicial extra +[Moat bridges] +Pontes sobre o fosso +[Jagged coastlines] +Costas recortadas +[Sand roads] +Estradas de areia +[Treasure in dead ends] +Tesouros em becos sem saída +[Fjord bank deposits] +Depósitos nas margens dos fiordes +[Sandy lake shore] +Margem arenosa do lago +[River delta] +Delta do rio +[Meandering rivers] +Rios com meandros +[Fruit in home valleys] +Fruta nos vales iniciais +[Moat] +Fosso +[Stone in the orchard] +Pedra no pomar +[Colonies on both coasts] +Colónias em ambas as costas +[Winding belt] +Faixa sinuosa diff --git a/data/texts.ro.txt b/data/texts.ro.txt index f4ff1e847..3d9363e9f 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1870,3 +1870,51 @@ Lume inelară Lățimea centurii [The belt is too short for this many colonies; use a longer map or fewer colonies.] Centura este prea scurtă pentru atâtea colonii; folosiți o hartă mai lungă sau mai puține colonii. +[Wheat amount] +Cantitatea de grâu +[Wood amount] +Cantitatea de lemn +[Stone amount] +Cantitatea de piatră +[Algae amount] +Cantitatea de alge +[Fruit amount] +Cantitatea de fructe +[Stone on hilltops] +Piatră pe vârfurile dealurilor +[Winding river] +Râu șerpuit +[Sandy beaches] +Plaje nisipoase +[Land bridges] +Punți de uscat +[Colony meadows] +Pajiști în jurul coloniilor +[Extra starting deposit] +Zăcământ inițial suplimentar +[Moat bridges] +Poduri peste șanțul cu apă +[Jagged coastlines] +Coaste crestate +[Sand roads] +Drumuri de nisip +[Treasure in dead ends] +Comori în fundături +[Fjord bank deposits] +Zăcăminte pe malurile fiordurilor +[Sandy lake shore] +Mal nisipos al lacului +[River delta] +Delta râului +[Meandering rivers] +Râuri cu meandre +[Fruit in home valleys] +Fructe în văile de pornire +[Moat] +Șanț cu apă +[Stone in the orchard] +Piatră în livadă +[Colonies on both coasts] +Colonii pe ambele coaste +[Winding belt] +Centură șerpuită diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 6571f8ee9..8c65fcb38 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1868,3 +1868,51 @@ OpenGL недоступен в этой сборке. Ширина пояса [The belt is too short for this many colonies; use a longer map or fewer colonies.] Пояс слишком короткий для такого числа колоний; используйте карту подлиннее или меньше колоний. +[Wheat amount] +Количество пшеницы +[Wood amount] +Количество дерева +[Stone amount] +Количество камня +[Algae amount] +Количество водорослей +[Fruit amount] +Количество фруктов +[Stone on hilltops] +Камень на вершинах холмов +[Winding river] +Извилистая река +[Sandy beaches] +Песчаные пляжи +[Land bridges] +Перешейки +[Colony meadows] +Луга вокруг колоний +[Extra starting deposit] +Дополнительная стартовая залежь +[Moat bridges] +Мосты через ров +[Jagged coastlines] +Изрезанные берега +[Sand roads] +Песчаные дороги +[Treasure in dead ends] +Сокровища в тупиках +[Fjord bank deposits] +Залежи на берегах фьордов +[Sandy lake shore] +Песчаный берег озера +[River delta] +Дельта реки +[Meandering rivers] +Петляющие реки +[Fruit in home valleys] +Фрукты в стартовых долинах +[Moat] +Ров +[Stone in the orchard] +Камень в саду +[Colonies on both coasts] +Колонии на обоих берегах +[Winding belt] +Извилистый пояс diff --git a/data/texts.si.txt b/data/texts.si.txt index 23359e903..c9bfcffa3 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1870,3 +1870,51 @@ Obročasti svet Širina pasu [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pas je prekratek za toliko kolonij; uporabite daljši zemljevid ali manj kolonij. +[Wheat amount] +Količina pšenice +[Wood amount] +Količina lesa +[Stone amount] +Količina kamenja +[Algae amount] +Količina alg +[Fruit amount] +Količina sadja +[Stone on hilltops] +Kamenje na vrhovih gričev +[Winding river] +Vijugasta reka +[Sandy beaches] +Peščene plaže +[Land bridges] +Kopenski mostovi +[Colony meadows] +Travniki okoli kolonij +[Extra starting deposit] +Dodatno začetno nahajališče +[Moat bridges] +Mostovi čez vodni jarek +[Jagged coastlines] +Razčlenjene obale +[Sand roads] +Peščene poti +[Treasure in dead ends] +Zakladi v slepih ulicah +[Fjord bank deposits] +Nahajališča na bregovih fjordov +[Sandy lake shore] +Peščena obala jezera +[River delta] +Rečna delta +[Meandering rivers] +Vijugaste reke +[Fruit in home valleys] +Sadje v začetnih dolinah +[Moat] +Vodni jarek +[Stone in the orchard] +Kamenje v sadovnjaku +[Colonies on both coasts] +Kolonije na obeh obalah +[Winding belt] +Vijugast pas diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 0f1ddd36c..11da37ea0 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1878,3 +1878,51 @@ Prstencový svet Šírka pásu [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pás je pre toľko kolónií príliš krátky; použite dlhšiu mapu alebo menej kolónií. +[Wheat amount] +Množstvo pšenice +[Wood amount] +Množstvo dreva +[Stone amount] +Množstvo kameňa +[Algae amount] +Množstvo rias +[Fruit amount] +Množstvo ovocia +[Stone on hilltops] +Kameň na vrcholoch kopcov +[Winding river] +Kľukatá rieka +[Sandy beaches] +Piesočnaté pláže +[Land bridges] +Pevninské mosty +[Colony meadows] +Lúky okolo kolónií +[Extra starting deposit] +Ložisko navyše na začiatku +[Moat bridges] +Mosty cez vodnú priekopu +[Jagged coastlines] +Členité pobrežie +[Sand roads] +Piesočnaté cesty +[Treasure in dead ends] +Poklady v slepých uličkách +[Fjord bank deposits] +Ložiská na brehoch fjordov +[Sandy lake shore] +Piesočnatý breh jazera +[River delta] +Riečna delta +[Meandering rivers] +Meandrujúce rieky +[Fruit in home valleys] +Ovocie v domovských údoliach +[Moat] +Vodná priekopa +[Stone in the orchard] +Kameň v sade +[Colonies on both coasts] +Kolónie na oboch pobrežiach +[Winding belt] +Zvlnený pás diff --git a/data/texts.sr.txt b/data/texts.sr.txt index d083d6450..ae3ce6eea 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1870,3 +1870,51 @@ OpenGL није доступан у овој верзији. Ширина појаса [The belt is too short for this many colonies; use a longer map or fewer colonies.] Појас је прекратак за толико колонија; користите дужу мапу или мање колонија. +[Wheat amount] +Количина жита +[Wood amount] +Количина дрвета +[Stone amount] +Количина камена +[Algae amount] +Количина алги +[Fruit amount] +Количина воћа +[Stone on hilltops] +Камен на врховима брда +[Winding river] +Кривудава река +[Sandy beaches] +Пешчане плаже +[Land bridges] +Копнени мостови +[Colony meadows] +Ливаде око колонија +[Extra starting deposit] +Додатно почетно налазиште +[Moat bridges] +Мостови преко воденог рова +[Jagged coastlines] +Разуђене обале +[Sand roads] +Пешчани путеви +[Treasure in dead ends] +Благо у ћорсокацима +[Fjord bank deposits] +Налазишта на обалама фјордова +[Sandy lake shore] +Пешчана обала језера +[River delta] +Делта реке +[Meandering rivers] +Меандрирајуће реке +[Fruit in home valleys] +Воће у почетним долинама +[Moat] +Водени ров +[Stone in the orchard] +Камен у воћњаку +[Colonies on both coasts] +Колоније на обе обале +[Winding belt] +Кривудав појас diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 1b6dcd64a..61975764a 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1880,3 +1880,51 @@ Ringvärld Bältets bredd [The belt is too short for this many colonies; use a longer map or fewer colonies.] Bältet är för kort för så många kolonier; använd en längre karta eller färre kolonier. +[Wheat amount] +Mängd vete +[Wood amount] +Mängd trä +[Stone amount] +Mängd sten +[Algae amount] +Mängd alger +[Fruit amount] +Mängd frukt +[Stone on hilltops] +Sten på kullarnas toppar +[Winding river] +Slingrande flod +[Sandy beaches] +Sandstränder +[Land bridges] +Landbroar +[Colony meadows] +Ängar runt kolonierna +[Extra starting deposit] +Extra startfyndighet +[Moat bridges] +Broar över vallgraven +[Jagged coastlines] +Taggiga kuster +[Sand roads] +Sandvägar +[Treasure in dead ends] +Skatter i återvändsgränder +[Fjord bank deposits] +Fyndigheter längs fjordstränderna +[Sandy lake shore] +Sandig sjöstrand +[River delta] +Flodens delta +[Meandering rivers] +Meandrande floder +[Fruit in home valleys] +Frukt i startdalarna +[Moat] +Vallgrav +[Stone in the orchard] +Sten i fruktträdgården +[Colonies on both coasts] +Kolonier på båda kusterna +[Winding belt] +Slingrande bälte diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 6f9d1c512..ba5187f09 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1878,3 +1878,51 @@ Halka dünya Kuşak genişliği [The belt is too short for this many colonies; use a longer map or fewer colonies.] Kuşak bu kadar koloni için çok kısa; daha uzun bir harita veya daha az koloni kullanın. +[Wheat amount] +Buğday miktarı +[Wood amount] +Odun miktarı +[Stone amount] +Taş miktarı +[Algae amount] +Yosun miktarı +[Fruit amount] +Meyve miktarı +[Stone on hilltops] +Tepelerin üstünde taş +[Winding river] +Kıvrımlı nehir +[Sandy beaches] +Kumsallar +[Land bridges] +Kara köprüleri +[Colony meadows] +Koloni çevresinde çayırlar +[Extra starting deposit] +Ek başlangıç kaynağı +[Moat bridges] +Su hendeği köprüleri +[Jagged coastlines] +Girintili kıyılar +[Sand roads] +Kum yollar +[Treasure in dead ends] +Çıkmaz yollarda hazine +[Fjord bank deposits] +Fiyort kıyılarında kaynaklar +[Sandy lake shore] +Kumlu göl kıyısı +[River delta] +Nehir deltası +[Meandering rivers] +Menderesli nehirler +[Fruit in home valleys] +Başlangıç vadilerinde meyve +[Moat] +Su hendeği +[Stone in the orchard] +Meyve bahçesinde taş +[Colonies on both coasts] +Her iki kıyıda koloniler +[Winding belt] +Kıvrımlı kuşak diff --git a/data/texts.uk.txt b/data/texts.uk.txt index e70eeb5b6..c66fdc54d 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1866,3 +1866,51 @@ OpenGL недоступний у цій збірці. Ширина поясу [The belt is too short for this many colonies; use a longer map or fewer colonies.] Пояс закороткий для такої кількості колоній; використайте довшу карту або менше колоній. +[Wheat amount] +Кількість пшениці +[Wood amount] +Кількість деревини +[Stone amount] +Кількість каменю +[Algae amount] +Кількість водоростей +[Fruit amount] +Кількість фруктів +[Stone on hilltops] +Камінь на вершинах пагорбів +[Winding river] +Звивиста річка +[Sandy beaches] +Піщані пляжі +[Land bridges] +Перешийки +[Colony meadows] +Луки навколо колоній +[Extra starting deposit] +Додатковий стартовий поклад +[Moat bridges] +Мости через рів +[Jagged coastlines] +Порізані береги +[Sand roads] +Піщані дороги +[Treasure in dead ends] +Скарби в глухих кутах +[Fjord bank deposits] +Поклади на берегах фіордів +[Sandy lake shore] +Піщаний берег озера +[River delta] +Дельта річки +[Meandering rivers] +Звивисті річки +[Fruit in home valleys] +Фрукти в стартових долинах +[Moat] +Рів +[Stone in the orchard] +Камінь у саду +[Colonies on both coasts] +Колонії на обох узбережжях +[Winding belt] +Звивистий пояс diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 1218b31af..eadfbbdf0 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1866,3 +1866,51 @@ Thế giới hình vòng Độ rộng vành đai [The belt is too short for this many colonies; use a longer map or fewer colonies.] Vành đai quá ngắn cho số thuộc địa này; hãy dùng bản đồ dài hơn hoặc ít thuộc địa hơn. +[Wheat amount] +Lượng lúa mì +[Wood amount] +Lượng gỗ +[Stone amount] +Lượng đá +[Algae amount] +Lượng tảo +[Fruit amount] +Lượng trái cây +[Stone on hilltops] +Đá trên đỉnh đồi +[Winding river] +Sông uốn khúc +[Sandy beaches] +Bãi cát +[Land bridges] +Cầu đất +[Colony meadows] +Đồng cỏ quanh thuộc địa +[Extra starting deposit] +Mỏ tài nguyên khởi đầu bổ sung +[Moat bridges] +Cầu qua hào nước +[Jagged coastlines] +Bờ biển lởm chởm +[Sand roads] +Đường cát +[Treasure in dead ends] +Kho báu ở ngõ cụt +[Fjord bank deposits] +Mỏ tài nguyên ven vịnh hẹp +[Sandy lake shore] +Bờ hồ đầy cát +[River delta] +Châu thổ sông +[Meandering rivers] +Sông quanh co +[Fruit in home valleys] +Trái cây ở thung lũng khởi đầu +[Moat] +Hào nước +[Stone in the orchard] +Đá trong vườn cây ăn quả +[Colonies on both coasts] +Thuộc địa ở cả hai bờ +[Winding belt] +Vành đai uốn lượn diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 09a33cc20..bedbe644a 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1866,3 +1866,51 @@ OpenGL 在此版本中不可用。 环带宽度 [The belt is too short for this many colonies; use a longer map or fewer colonies.] 环带太短,容纳不下这么多殖民地;请使用更长的地图或减少殖民地数量。 +[Wheat amount] +小麦数量 +[Wood amount] +木材数量 +[Stone amount] +石头数量 +[Algae amount] +海藻数量 +[Fruit amount] +水果数量 +[Stone on hilltops] +山顶的石头 +[Winding river] +蜿蜒的河流 +[Sandy beaches] +沙滩 +[Land bridges] +陆桥 +[Colony meadows] +殖民地周围的草地 +[Extra starting deposit] +额外的起始资源 +[Moat bridges] +护城河上的桥梁 +[Jagged coastlines] +曲折的海岸线 +[Sand roads] +沙土路 +[Treasure in dead ends] +死路尽头的宝藏 +[Fjord bank deposits] +峡湾岸边的资源 +[Sandy lake shore] +沙质湖岸 +[River delta] +河口三角洲 +[Meandering rivers] +曲流河道 +[Fruit in home valleys] +起始山谷中的水果 +[Moat] +护城河 +[Stone in the orchard] +果园中的石头 +[Colonies on both coasts] +两岸都有殖民地 +[Winding belt] +蜿蜒的环带 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index f2231b979..f399dfcf6 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1942,3 +1942,51 @@ OpenGL 在此版本中不可用。 環帶寬度 [The belt is too short for this many colonies; use a longer map or fewer colonies.] 環帶太短,容納不下這麼多殖民地;請使用更長的地圖或減少殖民地數量。 +[Wheat amount] +小麥數量 +[Wood amount] +木材數量 +[Stone amount] +石頭數量 +[Algae amount] +海藻數量 +[Fruit amount] +水果數量 +[Stone on hilltops] +山頂的石頭 +[Winding river] +蜿蜒的河流 +[Sandy beaches] +沙灘 +[Land bridges] +陸橋 +[Colony meadows] +殖民地周圍的草地 +[Extra starting deposit] +額外的起始資源 +[Moat bridges] +護城河上的橋樑 +[Jagged coastlines] +曲折的海岸線 +[Sand roads] +沙土路 +[Treasure in dead ends] +死路盡頭的寶藏 +[Fjord bank deposits] +峽灣岸邊的資源 +[Sandy lake shore] +沙質湖岸 +[River delta] +河口三角洲 +[Meandering rivers] +曲流河道 +[Fruit in home valleys] +起始山谷中的水果 +[Moat] +護城河 +[Stone in the orchard] +果園中的石頭 +[Colonies on both coasts] +兩岸都有殖民地 +[Winding belt] +蜿蜒的環帶 From adb0021a76d13e6c923bd14698bffd6d524c93ad Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:38:47 -0400 Subject: [PATCH 077/136] Add the engine baseline preset The reported engine team-index bias comes from a Symmetric arena run whose preset was untracked, so it could not be reproduced from the repository. Ship it as tools/map-fairness/baseline.json: six 128x128 maps with seeds disjoint from the smoke preset's, every rotation played by four engine seeds. Its colonies are identical by construction, so wins by start should be uniform and wins by team index isolate the engine's processing order. A run directory no longer refuses a rerun just because the preset moved: preset_file is provenance, not a setting that changes the games played. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- tools/map-fairness/baseline.json | 15 +++++++++++++++ tools/map_fairness_tournament.py | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tools/map-fairness/baseline.json diff --git a/tools/map-fairness/baseline.json b/tools/map-fairness/baseline.json new file mode 100644 index 000000000..ddde4e5bf --- /dev/null +++ b/tools/map-fairness/baseline.json @@ -0,0 +1,15 @@ +{ + "name": "baseline", + "description": "Engine baseline: Symmetric arena only, six 128x128 maps with map seeds disjoint from the smoke preset's, every rotation played by four engine seeds (96 games, about 25 minutes at 3 jobs). Its colonies are identical by construction, so wins by start should be uniform and wins by team index show the engine's own processing-order bias with no map effect.", + "generators": [15], + "map_seeds": "1101-1106", + "games_per_rotation": 4, + "rotations": "all", + "size": 128, + "colonies": 4, + "candidates": 5, + "ai": "nicowar", + "tick_cap": 90000, + "jobs": 3, + "verify_games": 2 +} diff --git a/tools/map_fairness_tournament.py b/tools/map_fairness_tournament.py index a45c24d65..286bb7168 100644 --- a/tools/map_fairness_tournament.py +++ b/tools/map_fairness_tournament.py @@ -1195,7 +1195,8 @@ def command_run(args): if not binary.exists(): raise SystemExit(f'error: {binary} not found; build with scons release=1 and ' f'scons release=1 map-generator-study') - ignored = ('jobs', 'verify_games', 'description', 'git_revision') + # Provenance and pure throughput settings, not things that change the games played. + ignored = ('jobs', 'verify_games', 'description', 'git_revision', 'preset_file') comparable = {k: v for k, v in config.items() if k not in ignored} previous = out / 'config.json' if previous.exists(): From d007bcf1acbf104c5a4487f1acf33ce32f53bea2 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:39:15 -0400 Subject: [PATCH 078/136] Mention the baseline preset where presets are run Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index e29a7382b..1d7161a20 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -19,8 +19,13 @@ Then run a preset: ```bash python3 tools/map_fairness_tournament.py run smoke # quick end-to-end check python3 tools/map_fairness_tournament.py run standard # the standard measurement +python3 tools/map_fairness_tournament.py run baseline # engine team-index bias, symmetric maps only ``` +The `baseline` preset plays only Symmetric arena, whose colonies are identical by construction. Its +wins by start should come out uniform, and its wins by team index measure the engine's own +processing-order bias with no map effect mixed in. + Results go to `artifacts/map-fairness//` (ignored by Git). A run resumes where it stopped: finished maps and games are kept, so re-running the same command after an interruption only plays what is missing. `--fresh` discards the run's earlier outputs. A changed configuration refuses to From cecfef98b100262fc23355a9f6f773d22ef13c13 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 18:40:41 -0400 Subject: [PATCH 079/136] Skip a colony marker another marker is drawn over MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LobbyMapPreview clamps each marker's top-left corner so its whole 16x16 square stays inside the preview, which on a narrow preview squeezes a whole edge of the map onto one row or column: a 128x512 map is 34 pixels wide in the compact window, so every start past about x=60 of 128 lands on the same column and every start past about y=445 of 512 on the same row. Two of the four colonies can then share a marker pixel, where the later marker covers the earlier one and the check read the covering colony's colour: marker mismatch rectangular-128x512: start (79,485) map 128x512 rect 455,179 34x137 expected 137,229,45 got 45,229,229 generateMap draws a fresh root seed on every run, so which map this check sees varies from run to run and whether two markers coincide varies with it — the check has been able to fail like this all along, and the compact window is where the preview is narrow enough for it to happen. No generator is involved: Contested commons at 128x512 with 4 colonies is byte-identical between 95e24a761 and this branch on all of seeds 1 to 40. The check now works out every marker's position first and verifies only those no later marker is drawn over, so it still checks each visible marker at both window sizes, and prints what it saw when one does not match. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- test/CustomGameSetupHarness.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index cc01e5851..530a5f456 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -786,10 +786,29 @@ struct CustomGameSetupHarness }; const auto corner = pixel(rect.x + 1, rect.y + 1); assert(corner[0] || corner[1] || corner[2]); // no thumbnail letterbox inside the map + // LobbyMapPreview clamps a marker's top-left corner so the whole 16x16 square stays inside + // the preview, which on a narrow one squeezes a whole edge of the map onto one row or column: + // a 128x512 map is 34 pixels wide in the compact window, so every start past about x=60 + // lands on the same column. Two starts can then share a pixel, where the later marker covers + // the earlier one legitimately, so only a start nothing is drawn over can be checked. + std::vector markerX, markerY; for (const auto &start : expectedStarts) { - const int x = rect.x + std::clamp(start.x * rect.w / mapW, 2, std::max(2, rect.w - 18)); - const int y = rect.y + std::clamp(start.y * rect.h / mapH, 2, std::max(2, rect.h - 18)); - const auto swatch = pixel(x + 1, y + 1); + markerX.push_back(rect.x + std::clamp(start.x * rect.w / mapW, 2, std::max(2, rect.w - 18))); + markerY.push_back(rect.y + std::clamp(start.y * rect.h / mapH, 2, std::max(2, rect.h - 18))); + } + for (size_t i = 0; i < expectedStarts.size(); ++i) { + bool covered = false; + for (size_t j = i + 1; j < expectedStarts.size() && !covered; ++j) + covered = markerX[j] == markerX[i] && markerY[j] == markerY[i]; + if (covered) + continue; + const auto &start = expectedStarts[i]; + const auto swatch = pixel(markerX[i] + 1, markerY[i] + 1); + if (!(swatch[0] == start.color.r && swatch[1] == start.color.g && swatch[2] == start.color.b)) + std::printf("marker mismatch %s: start (%d,%d) map %dx%d rect %d,%d %dx%d expected" + " %d,%d,%d got %d,%d,%d\n", + name.c_str(), start.x, start.y, mapW, mapH, rect.x, rect.y, rect.w, rect.h, + start.color.r, start.color.g, start.color.b, swatch[0], swatch[1], swatch[2]); assert(swatch[0] == start.color.r && swatch[1] == start.color.g && swatch[2] == start.color.b); } SDL_FreeSurface(rgba); From 283ee824a58e9142384dc0f9b8d1f0703c52023d Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 20:53:37 -0400 Subject: [PATCH 080/136] Fix the Windows build: near and far are macros there Stone highlands declared `int near[3], far[3]` and Watershed an `int near`. Both names are legacy macros in Windows' windef.h that expand to nothing, so mingw-w64 read `int near[3]` as a structured binding declaration and the file failed to compile. Every CI run on this branch has been red on windows (mingw-w64) since Stone highlands was added; Watershed would have failed next, once the build got past it. Renamed to nearIds/farIds and nearestRiver, with a note at each site so they do not come back. A pure rename: all 45 map dumps (15 generators x 3 seeds) are byte-identical, and MapGeneratorDefaultsTest, CustomGameSetupHarness and TestsRunner (198 tests) all pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- .../generator/generators/StoneHighlandsGenerator.cpp | 12 +++++++----- src/map/generator/generators/WatershedGenerator.cpp | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index ea0bf0970..3c4d98923 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -774,17 +774,19 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { const int i = y * t.w + x; if (!L.ridge[i]) continue; - int near[3], far[3]; - const int count = nearbyValleys(L, x, y, 2, near, 3); + // Not near/far: both are legacy macros in Windows' windef.h that expand to nothing, which + // turns `int near[3]` into a structured binding declaration and fails to compile on mingw. + int nearIds[3], farIds[3]; + const int count = nearbyValleys(L, x, y, 2, nearIds, 3); if (count >= 3) { alongRidge[i] = 0; queue.push_back(i); } for (int p = 0; p < count; ++p) for (int q = p + 1; q < count; ++q) - anyPair[{near[p], near[q]}].push_back(i); - if (count == 2 && nearbyValleys(L, x, y, reach, far, 3) == 2) - exclusive[{near[0], near[1]}].push_back(i); + anyPair[{nearIds[p], nearIds[q]}].push_back(i); + if (count == 2 && nearbyValleys(L, x, y, reach, farIds, 3) == 2) + exclusive[{nearIds[0], nearIds[1]}].push_back(i); } for (size_t head = 0; head < queue.size(); ++head) { const int p = queue[head], px = p % t.w, py = p / t.w; diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index 0382ea936..0f7124b0d 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -486,9 +486,11 @@ class RiverPlanner { continue; // Clear of every other river; and while it swings in to join its parent, arriving at an // angle rather than running alongside it. - int near = -1; - if (nearest2(p, parent, -1, &near) < sq(toEnd > joinZone ? clearance : 0.75 * clearance)) { - verdict.blocking = near; + // Not near: it is a legacy macro in Windows' windef.h and expands to nothing there. + int nearestRiver = -1; + if (nearest2(p, parent, -1, &nearestRiver) < + sq(toEnd > joinZone ? clearance : 0.75 * clearance)) { + verdict.blocking = nearestRiver; verdict.where = p; return verdict; } From 82e01557f03092dc81b20cc08fcee27a29b8de27 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Fri, 11 Sep 2026 20:54:51 -0400 Subject: [PATCH 081/136] Note the Windows near/far macro trap in the development reference The mingw build broke on `int near[3]` and the compiler error pointed at structured bindings rather than at the macro, which cost a CI cycle to diagnose. Record it where the other platform traps live. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014TBmePvnWuhjRHwGdT5UPq --- docs/development-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/development-notes.md b/docs/development-notes.md index 901377b1d..5baab5b79 100644 --- a/docs/development-notes.md +++ b/docs/development-notes.md @@ -118,6 +118,9 @@ on macOS need an intermediate filename, e.g. `git mv Foo.cpp temp.cpp` then `git mv temp.cpp foo.cpp`. Keep comments terse and about the current code; put change history and rationale in commit messages; omit tombstone or “moved to” comments. Diagnostics use `std::cerr`; there is no logging facility to target. +Windows headers define `near`, `far` and `small` as macros, so never name an identifier +after one: mingw expands `int near[3]` to `int [3]`, which then fails as a structured +binding declaration, and the error points at the syntax rather than at the macro. For unused-include cleanup, generate `compile_commands.json` and use `tools/remove-unused-includes.py`; do not apply blind bulk fixes. Rebuild client, From 43426ae493f4b84d778cbe3df69d357a77176efb Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 08:17:08 -0400 Subject: [PATCH 082/136] Add the City states map generator A large shared commons in the middle of the map, ringed by a strait, and round it one big home for every colony: a wedge of the outer land with its own lake, fields and quarry, cut off from its neighbours by water channels, walled with stone one tile in from every beach, and joined to the commons by a single stone-lined causeway. The commons is richer towards its centre, where an orchard of all three fruits stands, and swimming pools let armies cross the water later without ever getting past a wall. Every roll differs: the coasts carry bays and headlands and the channels bow; each map draws one home layout (a lake, a creek with a ford, stone ridges with passes, a marsh, sand flanks) and one heart for the commons (a lake, a stone crag, an island reached by fords, a delta of rivers with fords, a forest belt); sand lies in patches over the ground. Every home feature is designed in the wedge's own frame and stamped into every home alike, so the layout is fair for any colony count. Grass may never touch water in this engine, so a sand lane always lies between the water and the wall; the causeway as a barrier includes it. validateWorld rebuilds the design and requires every causeway and ford open, every designed stone tile present, the heart reachable, no colony reachable from any beach with the roads shut, no home able to reach the commons or a neighbour with the causeways shut, and the colonies' walks to their landings within a bound of each other. Legacy id 17. Controls: commons size, strait width, causeway width, coast roughness, valleys, resource islands, sand patches, frontier richness, a stone-walls switch and the five resource amounts. Strings in all 33 text tables; the framework reference documents the design; the registry contract test's injected-generator index moves up by one. Verified: 999/1000 sweep rolls over ten configurations (2 to 12 colonies, 128 to 512 maps) pass generation and validation, MapGeneratorDefaultsTest and CustomGameSetupHarness pass, check_translations.py --strict passes, the client builds, and the map was played and approved by the maintainer over four iterations. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 16 + data/texts.br.txt | 16 + data/texts.ca.txt | 16 + data/texts.cz.txt | 16 + data/texts.de.txt | 16 + data/texts.dk.txt | 16 + data/texts.en.txt | 16 + data/texts.eo.txt | 16 + data/texts.es.txt | 16 + data/texts.eu.txt | 16 + data/texts.fa.txt | 16 + data/texts.fi.txt | 16 + data/texts.fr.txt | 16 + data/texts.gr.txt | 16 + data/texts.hu.txt | 16 + data/texts.id.txt | 16 + data/texts.it.txt | 16 + data/texts.ja.txt | 16 + data/texts.keys.txt | 8 + data/texts.ko.txt | 16 + data/texts.nl.txt | 16 + data/texts.pl.txt | 16 + data/texts.pt.txt | 16 + data/texts.ro.txt | 16 + data/texts.ru.txt | 16 + data/texts.si.txt | 16 + data/texts.sk.txt | 16 + data/texts.sr.txt | 16 + data/texts.sv.txt | 16 + data/texts.tr.txt | 16 + data/texts.uk.txt | 16 + data/texts.vi.txt | 16 + data/texts.zh-cn.txt | 16 + data/texts.zh-tw.txt | 16 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 65 +- src/SConscript | 1 + src/map/generator/core/GenerationRequest.h | 3 +- src/map/generator/core/GeneratorRegistry.cpp | 3 +- .../generators/CityStatesGenerator.cpp | 1417 +++++++++++++++++ .../generators/CityStatesGenerator.h | 12 + test/MapGeneratorDefaultsTest.cpp | 2 +- 41 files changed, 2033 insertions(+), 6 deletions(-) create mode 100644 src/map/generator/generators/CityStatesGenerator.cpp create mode 100644 src/map/generator/generators/CityStatesGenerator.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 4946e65f2..5ed8c258a 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1868,6 +1868,22 @@ OpenGL غير متوفر في هذا الإصدار. عرض الحزام [The belt is too short for this many colonies; use a longer map or fewer colonies.] الحزام أقصر من أن يتسع لهذا العدد من المستعمرات؛ استخدم خريطة أطول أو عددًا أقل من المستعمرات. +[City states] +دول المدن +[Strait width] +عرض المضيق +[Valleys] +الوديان +[Frontier richness] +ثراء الأراضي الجديدة +[City states need at least one colony.] +تحتاج دول المدن إلى مستعمرة واحدة على الأقل. +[Stone walls] +أسوار حجرية +[Sand patches] +بقع رملية +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +الأراضي المشاع والمضيق لا يتركان مساحة كافية للقواعد؛ استخدم خريطة أكبر أو مستعمرات أقل أو أراضي مشاع أصغر أو مضيقًا أضيق. [Wheat amount] كمية القمح [Wood amount] diff --git a/data/texts.br.txt b/data/texts.br.txt index cd488d94a..10b259a86 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1866,6 +1866,22 @@ Mundo anel Largura da faixa [The belt is too short for this many colonies; use a longer map or fewer colonies.] A faixa é curta demais para tantas colônias; use um mapa mais longo ou menos colônias. +[City states] +Cidades-estado +[Strait width] +Largura do estreito +[Valleys] +Vales +[Frontier richness] +Riqueza da fronteira +[City states need at least one colony.] +Cidades-estado precisam de pelo menos uma colônia. +[Stone walls] +Muralhas de pedra +[Sand patches] +Manchas de areia +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +A área comum e o estreito deixam pouco espaço para as bases; use um mapa maior, menos colônias, uma área comum menor ou um estreito mais estreito. [Wheat amount] Quantidade de trigo [Wood amount] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 7c7ecdb13..0f6f00220 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1878,6 +1878,22 @@ Món anell Amplada del cinturó [The belt is too short for this many colonies; use a longer map or fewer colonies.] El cinturó és massa curt per a tantes colònies; feu servir un mapa més llarg o menys colònies. +[City states] +Ciutats estat +[Strait width] +Amplada de l'estret +[Valleys] +Valls +[Frontier richness] +Riquesa de la frontera +[City states need at least one colony.] +Les ciutats estat necessiten almenys una colònia. +[Stone walls] +Murs de pedra +[Sand patches] +Clapes de sorra +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +L'àrea comuna i l'estret deixen massa poc espai per a les bases; fes servir un mapa més gran, menys colònies, una àrea comuna més petita o un estret més estret. [Wheat amount] Quantitat de blat [Wood amount] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index f5b6a5f46..2e2c1a767 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1870,6 +1870,22 @@ Prstencový svět Šířka pásu [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pás je pro tolik kolonií příliš krátký; použijte delší mapu nebo méně kolonií. +[City states] +Městské státy +[Strait width] +Šířka průlivu +[Valleys] +Údolí +[Frontier richness] +Bohatství pohraničí +[City states need at least one colony.] +Městské státy potřebují alespoň jednu kolonii. +[Stone walls] +Kamenné hradby +[Sand patches] +Písčiny +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Společné území a průliv ponechávají příliš málo místa pro domovské základny; použijte větší mapu, méně kolonií, menší společné území nebo užší průliv. [Wheat amount] Množství pšenice [Wood amount] diff --git a/data/texts.de.txt b/data/texts.de.txt index 27f00160d..4f5a11714 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1870,6 +1870,22 @@ Ringwelt Gürtelbreite [The belt is too short for this many colonies; use a longer map or fewer colonies.] Der Gürtel ist zu kurz für so viele Kolonien; verwenden Sie eine längere Karte oder weniger Kolonien. +[City states] +Stadtstaaten +[Strait width] +Breite der Meerenge +[Valleys] +Täler +[Frontier richness] +Reichtum der Neuen Welt +[City states need at least one colony.] +Stadtstaaten brauchen mindestens eine Kolonie. +[Stone walls] +Steinmauern +[Sand patches] +Sandflächen +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Gemeinschaftsland und Meerenge lassen zu wenig Platz für die Heimatbasen; verwende eine größere Karte, weniger Kolonien, ein kleineres Gemeinschaftsland oder eine schmalere Meerenge. [Wheat amount] Weizenmenge [Wood amount] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index 6ff6e345b..bf2d2285c 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1946,6 +1946,22 @@ Ringverden Bæltebredde [The belt is too short for this many colonies; use a longer map or fewer colonies.] Bæltet er for kort til så mange kolonier; brug et længere kort eller færre kolonier. +[City states] +Bystater +[Strait width] +Strædets bredde +[Valleys] +Dale +[Frontier richness] +Grænselandets rigdom +[City states need at least one colony.] +Bystater kræver mindst én koloni. +[Stone walls] +Stenmure +[Sand patches] +Sandpletter +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Fællesområdet og strædet efterlader for lidt plads til hjembaserne; brug et større kort, færre kolonier, et mindre fællesområde eller et smallere stræde. [Wheat amount] Mængde hvede [Wood amount] diff --git a/data/texts.en.txt b/data/texts.en.txt index 9b69f1df4..e16a26384 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1858,6 +1858,22 @@ Ring world Belt width [The belt is too short for this many colonies; use a longer map or fewer colonies.] The belt is too short for this many colonies; use a longer map or fewer colonies. +[City states] +City states +[Strait width] +Strait width +[Valleys] +Valleys +[Frontier richness] +Frontier richness +[City states need at least one colony.] +City states need at least one colony. +[Stone walls] +Stone walls +[Sand patches] +Sand patches +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait. [Wheat amount] Wheat amount [Wood amount] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index cd1683632..34effa2f1 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1868,6 +1868,22 @@ Ringa mondo Larĝo de zono [The belt is too short for this many colonies; use a longer map or fewer colonies.] La zono estas tro mallonga por tiom da kolonioj; uzu pli longan mapon aŭ malpli da kolonioj. +[City states] +Urboŝtatoj +[Strait width] +Larĝo de la markolo +[Valleys] +Valoj +[Frontier richness] +Riĉeco de la limregiono +[City states need at least one colony.] +Urboŝtatoj bezonas almenaŭ unu kolonion. +[Stone walls] +Ŝtonaj muroj +[Sand patches] +Sablaj makuloj +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +La komunareo kaj la markolo lasas tro malmulte da spaco por la hejmaj bazoj; uzu pli grandan mapon, malpli da kolonioj, pli malgrandan komunareon aŭ pli mallarĝan markolon. [Wheat amount] Kvanto de tritiko [Wood amount] diff --git a/data/texts.es.txt b/data/texts.es.txt index d0d6ecbeb..12e22a1a3 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1870,6 +1870,22 @@ Mundo anillo Anchura del cinturón [The belt is too short for this many colonies; use a longer map or fewer colonies.] El cinturón es demasiado corto para tantas colonias; use un mapa más largo o menos colonias. +[City states] +Ciudades estado +[Strait width] +Anchura del estrecho +[Valleys] +Valles +[Frontier richness] +Riqueza de la frontera +[City states need at least one colony.] +Las ciudades estado necesitan al menos una colonia. +[Stone walls] +Muros de piedra +[Sand patches] +Parches de arena +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +El área común y el estrecho dejan demasiado poco espacio para las bases; usa un mapa más grande, menos colonias, un área común más pequeña o un estrecho más angosto. [Wheat amount] Cantidad de trigo [Wood amount] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 7d227e324..6477c0af4 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1880,6 +1880,22 @@ Eraztun-mundua Gerrikoaren zabalera [The belt is too short for this many colonies; use a longer map or fewer colonies.] Gerrikoa laburregia da hainbeste koloniarentzat; erabili mapa luzeagoa edo kolonia gutxiago. +[City states] +Hiri-estatuak +[Strait width] +Itsasartearen zabalera +[Valleys] +Haranak +[Frontier richness] +Mugaldeko aberastasuna +[City states need at least one colony.] +Hiri-estatuek gutxienez kolonia bat behar dute. +[Stone walls] +Harrizko hormak +[Sand patches] +Harea-guneak +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Eremu komunak eta itsasarteak leku gutxiegi uzten diete oinarriei; erabili mapa handiagoa, kolonia gutxiago, eremu komun txikiagoa edo itsasarte estuagoa. [Wheat amount] Gari kopurua [Wood amount] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 85bc7b4d5..6aeabaec0 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1868,6 +1868,22 @@ OpenGL در این بیلد موجود نیست. پهنای کمربند [The belt is too short for this many colonies; use a longer map or fewer colonies.] کمربند برای این تعداد مستعمره بیش از حد کوتاه است؛ از نقشهٔ بلندتر یا مستعمره‌های کمتر استفاده کنید. +[City states] +دولت‌شهرها +[Strait width] +پهنای تنگه +[Valleys] +دره‌ها +[Frontier richness] +ثروت سرزمین‌های مرزی +[City states need at least one colony.] +دولت‌شهرها دست‌کم به یک مستعمره نیاز دارند. +[Stone walls] +دیوارهای سنگی +[Sand patches] +لکه‌های شنی +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +سرزمین مشترک و تنگه فضای کافی برای پایگاه‌ها باقی نمی‌گذارند؛ از نقشهٔ بزرگ‌تر، مستعمرات کمتر، سرزمین مشترک کوچک‌تر یا تنگهٔ باریک‌تر استفاده کنید. [Wheat amount] مقدار گندم [Wood amount] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 6a048d4be..4cd818073 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1870,6 +1870,22 @@ Rengasmaailma Vyöhykkeen leveys [The belt is too short for this many colonies; use a longer map or fewer colonies.] Vyöhyke on liian lyhyt näin monelle siirtokunnalle; käytä pidempää karttaa tai vähemmän siirtokuntia. +[City states] +Kaupunkivaltiot +[Strait width] +Salmen leveys +[Valleys] +Laaksot +[Frontier richness] +Rajamaan rikkaus +[City states need at least one colony.] +Kaupunkivaltiot tarvitsevat vähintään yhden siirtokunnan. +[Stone walls] +Kivimuurit +[Sand patches] +Hiekkaläikät +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Yhteisalue ja salmi jättävät liian vähän tilaa kotitukikohdille; käytä suurempaa karttaa, vähemmän siirtokuntia, pienempää yhteisaluetta tai kapeampaa salmea. [Wheat amount] Viljan määrä [Wood amount] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 817218c64..7dcd0e0b6 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1880,6 +1880,22 @@ Monde-anneau Largeur de la ceinture [The belt is too short for this many colonies; use a longer map or fewer colonies.] La ceinture est trop courte pour autant de colonies ; utilisez une carte plus longue ou moins de colonies. +[City states] +Cités-États +[Strait width] +Largeur du détroit +[Valleys] +Vallées +[Frontier richness] +Richesse de la frontière +[City states need at least one colony.] +Les cités-États ont besoin d'au moins une colonie. +[Stone walls] +Murs de pierre +[Sand patches] +Zones de sable +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +La zone commune et le détroit laissent trop peu de place aux bases ; utilisez une carte plus grande, moins de colonies, une zone commune plus petite ou un détroit plus étroit. [Wheat amount] Quantité de blé [Wood amount] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index f453aa416..fd3348e13 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1940,6 +1940,22 @@ Cortex Πλάτος ζώνης [The belt is too short for this many colonies; use a longer map or fewer colonies.] Η ζώνη είναι πολύ κοντή για τόσες αποικίες. Χρησιμοποιήστε μακρύτερο χάρτη ή λιγότερες αποικίες. +[City states] +Πόλεις-κράτη +[Strait width] +Πλάτος πορθμού +[Valleys] +Κοιλάδες +[Frontier richness] +Πλούτος των νέων εδαφών +[City states need at least one colony.] +Οι πόλεις-κράτη χρειάζονται τουλάχιστον μία αποικία. +[Stone walls] +Πέτρινα τείχη +[Sand patches] +Αμμώδεις εκτάσεις +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Η κοινή περιοχή και ο πορθμός αφήνουν πολύ λίγο χώρο για τις βάσεις· χρησιμοποιήστε μεγαλύτερο χάρτη, λιγότερες αποικίες, μικρότερη κοινή περιοχή ή στενότερο πορθμό. [Wheat amount] Ποσότητα σιταριού [Wood amount] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 92f71c33d..56efdf65f 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1870,6 +1870,22 @@ Gyűrűvilág Öv szélessége [The belt is too short for this many colonies; use a longer map or fewer colonies.] Az öv túl rövid ennyi kolóniához; használjon hosszabb térképet vagy kevesebb kolóniát. +[City states] +Városállamok +[Strait width] +Szoros szélessége +[Valleys] +Völgyek +[Frontier richness] +A határvidék gazdagsága +[City states need at least one colony.] +A városállamokhoz legalább egy kolónia kell. +[Stone walls] +Kőfalak +[Sand patches] +Homokfoltok +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +A közös terület és a szoros túl kevés helyet hagy a bázisoknak; használj nagyobb térképet, kevesebb kolóniát, kisebb közös területet vagy keskenyebb szorost. [Wheat amount] Búza mennyisége [Wood amount] diff --git a/data/texts.id.txt b/data/texts.id.txt index e50e8dc0b..47fd6bbb3 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1866,6 +1866,22 @@ Dunia cincin Lebar sabuk [The belt is too short for this many colonies; use a longer map or fewer colonies.] Sabuk terlalu pendek untuk koloni sebanyak ini; gunakan peta yang lebih panjang atau lebih sedikit koloni. +[City states] +Negara kota +[Strait width] +Lebar selat +[Valleys] +Lembah +[Frontier richness] +Kekayaan perbatasan +[City states need at least one colony.] +Negara kota membutuhkan setidaknya satu koloni. +[Stone walls] +Tembok batu +[Sand patches] +Petak pasir +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Tanah bersama dan selat menyisakan terlalu sedikit ruang untuk pangkalan; gunakan peta yang lebih besar, koloni lebih sedikit, tanah bersama lebih kecil, atau selat lebih sempit. [Wheat amount] Jumlah gandum [Wood amount] diff --git a/data/texts.it.txt b/data/texts.it.txt index 1e6d2a61b..d190590e9 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1932,6 +1932,22 @@ Mondo ad anello Larghezza della fascia [The belt is too short for this many colonies; use a longer map or fewer colonies.] La fascia è troppo corta per così tante colonie; usa una mappa più lunga o meno colonie. +[City states] +Città-stato +[Strait width] +Larghezza dello stretto +[Valleys] +Valli +[Frontier richness] +Ricchezza della frontiera +[City states need at least one colony.] +Le città-stato hanno bisogno di almeno una colonia. +[Stone walls] +Mura di pietra +[Sand patches] +Chiazze di sabbia +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +L'area comune e lo stretto lasciano troppo poco spazio alle basi; usa una mappa più grande, meno colonie, un'area comune più piccola o uno stretto più stretto. [Wheat amount] Quantità di frumento [Wood amount] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index cdc33ae8a..51f3feb4a 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1866,6 +1866,22 @@ Globulation 2 のカーソルを表示します。 帯の幅 [The belt is too short for this many colonies; use a longer map or fewer colonies.] このコロニー数に対して帯が短すぎます。より長いマップを使うか、コロニーを減らしてください。 +[City states] +都市国家 +[Strait width] +海峡の幅 +[Valleys] +谷 +[Frontier richness] +辺境の豊かさ +[City states need at least one colony.] +都市国家には少なくとも一つのコロニーが必要です。 +[Stone walls] +石の城壁 +[Sand patches] +砂地 +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +共有地と海峡が本拠地に残す余地が少なすぎます。より大きなマップ、より少ないコロニー、より小さな共有地、またはより狭い海峡を使ってください。 [Wheat amount] 小麦の量 [Wood amount] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 8b1dcacfb..4350b1c0e 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -927,6 +927,14 @@ [Ring world] [Belt width] [The belt is too short for this many colonies; use a longer map or fewer colonies.] +[City states] +[Strait width] +[Valleys] +[Frontier richness] +[City states need at least one colony.] +[Stone walls] +[Sand patches] +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] [Wheat amount] [Wood amount] [Stone amount] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 98ec4f811..e2cc64df9 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1866,6 +1866,22 @@ Globulation 2 커서를 표시합니다. 띠 너비 [The belt is too short for this many colonies; use a longer map or fewer colonies.] 이 식민지 수에 비해 띠가 너무 짧습니다. 더 긴 지도를 사용하거나 식민지 수를 줄이세요. +[City states] +도시 국가 +[Strait width] +해협 너비 +[Valleys] +계곡 +[Frontier richness] +변경의 풍요로움 +[City states need at least one colony.] +도시 국가에는 최소 한 개의 식민지가 필요합니다. +[Stone walls] +돌 성벽 +[Sand patches] +모래밭 +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +공유지와 해협이 본거지에 남기는 공간이 너무 적습니다. 더 큰 지도, 더 적은 식민지, 더 작은 공유지 또는 더 좁은 해협을 사용하세요. [Wheat amount] 밀 양 [Wood amount] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 9127d3367..91a7b6f72 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1894,6 +1894,22 @@ Ringwereld Gordelbreedte [The belt is too short for this many colonies; use a longer map or fewer colonies.] De gordel is te kort voor zoveel kolonies; gebruik een langere kaart of minder kolonies. +[City states] +Stadstaten +[Strait width] +Breedte van de zeestraat +[Valleys] +Valleien +[Frontier richness] +Rijkdom van de Nieuwe Wereld +[City states need at least one colony.] +Stadstaten hebben minstens één kolonie nodig. +[Stone walls] +Stenen muren +[Sand patches] +Zandvlakken +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Het gemeenschappelijke gebied en de zeestraat laten te weinig ruimte voor de thuisbases; gebruik een grotere kaart, minder kolonies, een kleiner gemeenschappelijk gebied of een smallere zeestraat. [Wheat amount] Hoeveelheid tarwe [Wood amount] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 4cacb0e99..ebebe2e11 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1870,6 +1870,22 @@ Groble są zbyt blisko siebie; użyj mniej licznych lub węższych grobli albo w Szerokość pasa [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pas jest za krótki dla tylu kolonii; użyj dłuższej mapy lub mniejszej liczby kolonii. +[City states] +Miasta-państwa +[Strait width] +Szerokość cieśniny +[Valleys] +Doliny +[Frontier richness] +Bogactwo pogranicza +[City states need at least one colony.] +Miasta-państwa potrzebują co najmniej jednej kolonii. +[Stone walls] +Kamienne mury +[Sand patches] +Płaty piasku +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Teren wspólny i cieśnina zostawiają zbyt mało miejsca na bazy; użyj większej mapy, mniejszej liczby kolonii, mniejszego terenu wspólnego lub węższej cieśniny. [Wheat amount] Ilość pszenicy [Wood amount] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 745861ee1..757893f9d 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1876,6 +1876,22 @@ Mundo anel Largura da faixa [The belt is too short for this many colonies; use a longer map or fewer colonies.] A faixa é demasiado curta para tantas colónias; use um mapa mais longo ou menos colónias. +[City states] +Cidades-estado +[Strait width] +Largura do estreito +[Valleys] +Vales +[Frontier richness] +Riqueza da fronteira +[City states need at least one colony.] +As cidades-estado precisam de pelo menos uma colónia. +[Stone walls] +Muralhas de pedra +[Sand patches] +Manchas de areia +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +A área comum e o estreito deixam demasiado pouco espaço para as bases; use um mapa maior, menos colónias, uma área comum mais pequena ou um estreito mais estreito. [Wheat amount] Quantidade de trigo [Wood amount] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 3d9363e9f..43ffa4c45 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1870,6 +1870,22 @@ Lume inelară Lățimea centurii [The belt is too short for this many colonies; use a longer map or fewer colonies.] Centura este prea scurtă pentru atâtea colonii; folosiți o hartă mai lungă sau mai puține colonii. +[City states] +Orașe-stat +[Strait width] +Lățimea strâmtorii +[Valleys] +Văi +[Frontier richness] +Bogăția frontierei +[City states need at least one colony.] +Orașele-stat au nevoie de cel puțin o colonie. +[Stone walls] +Ziduri de piatră +[Sand patches] +Petice de nisip +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Teritoriul comun și strâmtoarea lasă prea puțin loc pentru baze; folosește o hartă mai mare, mai puține colonii, un teritoriu comun mai mic sau o strâmtoare mai îngustă. [Wheat amount] Cantitatea de grâu [Wood amount] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 8c65fcb38..e2065b4d0 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1868,6 +1868,22 @@ OpenGL недоступен в этой сборке. Ширина пояса [The belt is too short for this many colonies; use a longer map or fewer colonies.] Пояс слишком короткий для такого числа колоний; используйте карту подлиннее или меньше колоний. +[City states] +Города-государства +[Strait width] +Ширина пролива +[Valleys] +Долины +[Frontier richness] +Богатство новых земель +[City states need at least one colony.] +Городам-государствам нужна хотя бы одна колония. +[Stone walls] +Каменные стены +[Sand patches] +Песчаные участки +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Общая земля и пролив оставляют слишком мало места для баз; используйте карту побольше, меньше колоний, общую землю поменьше или пролив поуже. [Wheat amount] Количество пшеницы [Wood amount] diff --git a/data/texts.si.txt b/data/texts.si.txt index c9bfcffa3..78d4f369e 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1870,6 +1870,22 @@ Obročasti svet Širina pasu [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pas je prekratek za toliko kolonij; uporabite daljši zemljevid ali manj kolonij. +[City states] +Mestne države +[Strait width] +Širina ožine +[Valleys] +Doline +[Frontier richness] +Bogastvo obmejnih dežel +[City states need at least one colony.] +Mestne države potrebujejo vsaj eno kolonijo. +[Stone walls] +Kamniti zidovi +[Sand patches] +Peščene zaplate +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Skupno ozemlje in ožina puščata premalo prostora za domače baze; uporabite večji zemljevid, manj kolonij, manjše skupno ozemlje ali ožjo ožino. [Wheat amount] Količina pšenice [Wood amount] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 11da37ea0..ee334a154 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1878,6 +1878,22 @@ Prstencový svet Šírka pásu [The belt is too short for this many colonies; use a longer map or fewer colonies.] Pás je pre toľko kolónií príliš krátky; použite dlhšiu mapu alebo menej kolónií. +[City states] +Mestské štáty +[Strait width] +Šírka prielivu +[Valleys] +Údolia +[Frontier richness] +Bohatstvo pohraničia +[City states need at least one colony.] +Mestské štáty potrebujú aspoň jednu kolóniu. +[Stone walls] +Kamenné hradby +[Sand patches] +Piesčiny +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Spoločné územie a prieliv ponechávajú príliš málo miesta pre domovské základne; použite väčšiu mapu, menej kolónií, menšie spoločné územie alebo užší prieliv. [Wheat amount] Množstvo pšenice [Wood amount] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index ae3ce6eea..a5325797c 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1870,6 +1870,22 @@ OpenGL није доступан у овој верзији. Ширина појаса [The belt is too short for this many colonies; use a longer map or fewer colonies.] Појас је прекратак за толико колонија; користите дужу мапу или мање колонија. +[City states] +Градови-државе +[Strait width] +Ширина мореуза +[Valleys] +Долине +[Frontier richness] +Богатство нових земаља +[City states need at least one colony.] +Градовима-државама треба бар једна колонија. +[Stone walls] +Камени зидови +[Sand patches] +Пешчане површине +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Заједничко подручје и мореуз остављају премало места за базе; користите већу мапу, мање колонија, мање заједничко подручје или ужи мореуз. [Wheat amount] Количина жита [Wood amount] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 61975764a..241350812 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1880,6 +1880,22 @@ Ringvärld Bältets bredd [The belt is too short for this many colonies; use a longer map or fewer colonies.] Bältet är för kort för så många kolonier; använd en längre karta eller färre kolonier. +[City states] +Stadsstater +[Strait width] +Sundets bredd +[Valleys] +Dalar +[Frontier richness] +Gränslandets rikedom +[City states need at least one colony.] +Stadsstater kräver minst en koloni. +[Stone walls] +Stenmurar +[Sand patches] +Sandfläckar +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Allmänningen och sundet lämnar för lite plats åt hembaserna; använd en större karta, färre kolonier, en mindre allmänning eller ett smalare sund. [Wheat amount] Mängd vete [Wood amount] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index ba5187f09..133e54a4c 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1878,6 +1878,22 @@ Halka dünya Kuşak genişliği [The belt is too short for this many colonies; use a longer map or fewer colonies.] Kuşak bu kadar koloni için çok kısa; daha uzun bir harita veya daha az koloni kullanın. +[City states] +Şehir devletleri +[Strait width] +Boğaz genişliği +[Valleys] +Vadiler +[Frontier richness] +Sınır bölgesinin zenginliği +[City states need at least one colony.] +Şehir devletleri için en az bir koloni gerekir. +[Stone walls] +Taş duvarlar +[Sand patches] +Kum lekeleri +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Ortak alan ve boğaz üslere çok az yer bırakıyor; daha büyük bir harita, daha az koloni, daha küçük bir ortak alan ya da daha dar bir boğaz kullanın. [Wheat amount] Buğday miktarı [Wood amount] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index c66fdc54d..4408ff9cc 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1866,6 +1866,22 @@ OpenGL недоступний у цій збірці. Ширина поясу [The belt is too short for this many colonies; use a longer map or fewer colonies.] Пояс закороткий для такої кількості колоній; використайте довшу карту або менше колоній. +[City states] +Міста-держави +[Strait width] +Ширина протоки +[Valleys] +Долини +[Frontier richness] +Багатство нових земель +[City states need at least one colony.] +Містам-державам потрібна хоча б одна колонія. +[Stone walls] +Кам'яні стіни +[Sand patches] +Піщані ділянки +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Спільна земля і протока залишають замало місця для баз; використайте більшу карту, менше колоній, меншу спільну землю або вужчу протоку. [Wheat amount] Кількість пшениці [Wood amount] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index eadfbbdf0..383b0715c 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1866,6 +1866,22 @@ Thế giới hình vòng Độ rộng vành đai [The belt is too short for this many colonies; use a longer map or fewer colonies.] Vành đai quá ngắn cho số thuộc địa này; hãy dùng bản đồ dài hơn hoặc ít thuộc địa hơn. +[City states] +Thành bang +[Strait width] +Chiều rộng eo biển +[Valleys] +Thung lũng +[Frontier richness] +Độ trù phú của vùng biên +[City states need at least one colony.] +Thành bang cần ít nhất một thuộc địa. +[Stone walls] +Tường đá +[Sand patches] +Bãi cát +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +Vùng đất chung và eo biển để lại quá ít chỗ cho các căn cứ; hãy dùng bản đồ lớn hơn, ít thuộc địa hơn, vùng đất chung nhỏ hơn hoặc eo biển hẹp hơn. [Wheat amount] Lượng lúa mì [Wood amount] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index bedbe644a..929b32c57 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1866,6 +1866,22 @@ OpenGL 在此版本中不可用。 环带宽度 [The belt is too short for this many colonies; use a longer map or fewer colonies.] 环带太短,容纳不下这么多殖民地;请使用更长的地图或减少殖民地数量。 +[City states] +城邦 +[Strait width] +海峡宽度 +[Valleys] +山谷 +[Frontier richness] +边疆富饶度 +[City states need at least one colony.] +城邦至少需要一个殖民地。 +[Stone walls] +石墙 +[Sand patches] +沙地 +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +公地区域与海峡留给基地的空间太少;请使用更大的地图、更少的殖民地、更小的公地区域或更窄的海峡。 [Wheat amount] 小麦数量 [Wood amount] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index f399dfcf6..63e5d3e0b 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1942,6 +1942,22 @@ OpenGL 在此版本中不可用。 環帶寬度 [The belt is too short for this many colonies; use a longer map or fewer colonies.] 環帶太短,容納不下這麼多殖民地;請使用更長的地圖或減少殖民地數量。 +[City states] +城邦 +[Strait width] +海峽寬度 +[Valleys] +山谷 +[Frontier richness] +邊疆富饒度 +[City states need at least one colony.] +城邦至少需要一個殖民地。 +[Stone walls] +石牆 +[Sand patches] +沙地 +[The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] +公地區域與海峽留給基地的空間太少;請使用更大的地圖、更少的殖民地、更小的公地區域或更窄的海峽。 [Wheat amount] 小麥數量 [Wood amount] diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 91d26ae74..552d6480e 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -41,6 +41,7 @@ reused after a generator is retired. | `stone-highlands` | 14 | Stone highlands | Its own — see below | | `symmetric-arena` | 15 | Symmetric arena | Its own — see below | | `ring-world` | 16 | Ring world | Its own — see below | +| `city-states` | 17 | City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard) and the sea's algae and island prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -83,6 +84,7 @@ ambient layer but still leaves every colony a start. Maze keeps its explicit den | Stone highlands | The ponds' farmland (wheat and wood) and algae, and how much of the ridgeline is two tiles thick (stone, 0 to 200); kits are unscaled and Fruit still counts groves | Fruit in home valleys (off) | | Symmetric arena | The farmland's wheat and wood, stone outcrops and algae, on top of Resource richness; fruit (0 to 100) keeps that share of the orchard's groves nearest the centre, never fewer than three | Moat (on); Stone in the orchard (on) | | Ring world | The ambient scatter's wheat, wood, stone and fruit, and the shallows' algae; starter patches and island prizes are unscaled | Winding belt (on); Colonies on both coasts (on) | +| City states | The commons' fields and grove, and everything in the New World: farmland, stone outcrops, fruit groves, the heart's orchard and the shallows' algae; every home's kit and its wall are unscaled | Bridges shared by pairs (on): off, every gap between neighbouring compounds gets a bridge | `scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of @@ -150,11 +152,12 @@ fixed resource pass: up whichever resource is still out of range. A colony genuinely isolated on its own small spot (nothing reachable even through terrain alone) is left untouched, since there's nothing on the other side of a wall that isn't there. `protectedWalls` is an optional width×height mask of - resource tiles that belong to the map's design, such as Stone highlands' ridgelines; they are + resource tiles that belong to the map's design, such as Stone highlands' ridgelines or City + states' walls; they are treated like terrain, never cleared and never looked past. It wraps each boot tile onto the map first, since the height-field generators' fallback site search can hand over one past the edge. - RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands and Ring world call this, as - do the height-field generators, and Concrete islands and Isles at any wheat or wood amount + RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands, Ring world and City states + call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount other than 100. Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane down every passage, and its `validateWorld` confirms every colony can still walk to every @@ -405,6 +408,62 @@ exactly two land neighbours. with water, buildings and every resource blocking, and requires every colony reached, a walkable loop around the map, and a body of water that wraps beside the belt. +## City states + +A large shared commons in the middle of the map, ringed by a strait, and round it one big home for +every colony: a wedge of the outer land with its own lake, fields and quarry, cut off from its +neighbours by water channels and from the commons by the strait. The only way off a home on foot is +its causeway, a road across the strait lined with stone, landing on the commons at the home's own +angle; a wall of stone round every home's coast keeps anything landing from the sea on the beach. +The commons is where the game is fought, richer towards its centre, where an orchard of all +three fruits stands round the central lake; once swimming pools let armies cross water anywhere, the +causeways stop being the only way in. + +- **Geometry.** A pure function of the request (`geometryFor`). The commons' radius is + `commons-size` percent of half the shorter side, the strait `strait-width` percent of the shorter + side, and the homes reach out to the map's half side less a rim of sea, so opposite homes never + meet across the wrap. Both coasts are radial shapes (`coast-roughness`); the strait follows the + commons' coast. Every home is the same wedge turned round the centre, so the layout is fair for + any colony count. `validateRequest` needs at least 20 tiles of home between the strait and the + sea, and at each home's inner coast at least the causeway plus ten tiles of arc beyond its + channel. +- **Causeways and walls.** One causeway per home at the wedge's middle: a `causeway-width` road + across the strait with shoulders either side. With `stone-walls` (on), stone stands on every + solid-grass shoulder tile and, round every home, on every solid-grass tile that touches the + sea's margin (the land whose corners the beach reaches, and any beach joined to it), so every + step off a beach lands on stone and the road is the only way in. Grass may never touch water, so the beach pass always + leaves a sand lane outside the stone that a unit can land on and walk along but never leave; the + causeway as a barrier, and as the ground kept clear of deposits with both its approaches, + includes its lanes. Home lakes keep seven tiles from the sea so the two beaches never meet. +- **Every roll differs.** Both coasts are random harmonic profiles periodic in the wedge + (`coast-roughness`): bays and headlands with a finer ripple on the commons' coast, which the + strait follows, and bays into the flanks of every home's inner and outer coasts, all held flat + around each causeway; every channel bows sideways by the same random amount. One home layout + and one heart layout are drawn per map. Homes: Lakeland (one lake), Riverside (a creek from the + lake towards one flank with a sand ford), Highland (two stone ridges out to the sea with a pass + each), Marsh (four ponds) and Barrens (a band of sand with a corridor through it). Hearts: a + lake with the orchard on its shore, a stone crag with a gap towards every landing, an island in + the lake reached by a ford from every landing, a delta of rivers from the lake to the strait + between the landings with a ford each, and a belt of forest round the orchard. `sand` adds + patches of sand (per 64×128 of land) over homes and commons, clear of lakes, landings and + coasts. Every home feature is designed in the wedge's frame, arc across it and radius out, and + stamped into every home alike, so any roll stays fair by rotation. +- **Homes.** Each has one lake at six tenths of its depth (held clear of both coasts), the swarm + between its causeway and the lake, an identical unscaled kit of 40 wheat and 30 wood beside the lake and a stone deposit beyond + it, then its own scaled ambient farmland on fertile ground, outcrops and a grove. +- **Commons.** A central lake of `kHeartShare` of its radius, `valleys` extra lakes (per 128×128 of + commons) likelier towards the centre, farmland on fertile ground weighted by depth inward per + `frontier-richness`, outcrops and groves by the same weight, and the orchard of the three fruits + on the central lake's shore. `resource-islands` (per 128×128 of sea) raises islets with a prize + each out in the sea. Algae seeds every shallows. `guaranteeStartingResources` runs with the + walls' stone protected, causeways and approaches are cleared, and a cheapest-walk pass keeps a + way open from every swarm to its causeway and from every landing to the heart. +- **Checked, not assumed.** `validateWorld` rebuilds the design and requires every causeway road + and ford walkable and every designed stone tile present (shoulders, walls, ridges and crag), every colony and the heart reachable on foot from + colony 0, no colony reachable from any beach with the roads shut, no home able to reach the + commons or another home with the causeways shut, and the colonies' walks to their landings within + twelve steps of each other. `validateRequest` needs at least 20 tiles of home depth. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort diff --git a/src/SConscript b/src/SConscript index cee337bb8..42b232f6a 100644 --- a/src/SConscript +++ b/src/SConscript @@ -176,6 +176,7 @@ map/generator/core/GenerationService.cpp map/generator/core/GenerationValidation.cpp map/generator/core/GeneratorControls.cpp map/generator/core/GeneratorRegistry.cpp +map/generator/generators/CityStatesGenerator.cpp map/generator/generators/ConcreteIslandsGenerator.cpp map/generator/generators/ContestedCommonsGenerator.cpp map/generator/generators/MazeGenerator.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index eefec4fba..ab3fbc917 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -32,7 +32,8 @@ struct GenerationRequest { eWATERSHED = 13, eSTONEHIGHLANDS = 14, eSYMMETRICARENA = 15, - eRINGWORLD = 16 + eRINGWORLD = 16, + eCITYSTATES = 17 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 2321b3a50..10ab74817 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "GeneratorRegistry.h" +#include "CityStatesGenerator.h" #include "ConcreteIslandsGenerator.h" #include "ContestedCommonsGenerator.h" #include "CraterLakesGenerator.h" @@ -81,7 +82,7 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), stoneHighlandsDefinition(), symmetricArenaDefinition(), - ringWorldDefinition(), ruggedArchipelagoDefinition(), + ringWorldDefinition(), cityStatesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), riverDefinition(), uniformDefinition()}); return registry; diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp new file mode 100644 index 000000000..f19de9324 --- /dev/null +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -0,0 +1,1417 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CityStatesGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "HeightMap.h" +#include "Resources.h" +#include "Settlements.h" +#include "Unit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// City states: a large shared commons in the middle of the map, ringed by a strait, and round it +// one big home for every colony - a wedge of the outer land with its own lake, fields and quarry, +// cut off from its neighbours by water channels and from the commons by the strait. The only way +// off a home on foot is its causeway: a road across the strait, lined with stone, that lands on +// the commons at the home's own angle - and a wall of stone runs round every home's coast, one +// tile in from the beach, so a swimmer can land on the beach but never leave it. The commons is +// where the game is fought - richer towards its centre, where an orchard of all three fruits +// stands - and once swimming pools let armies cross water anywhere, the causeways stop being the +// only way in. +// +// Every roll differs: the coasts have bays and headlands, the channels bend, the homes take one of +// several layouts (a lake, a creek with a ford, stone ridges with passes, a marsh, a band of sand) +// and the heart of the commons one of several (a lake, a stone crag, an island, a river delta, a +// forest), and sand lies in patches over the ground. Everything a home is made of is designed in +// the wedge's own frame - arc across it and radius out from the centre - and stamped into every +// home alike, so however a roll comes out, every colony gets the same home turned round the +// centre, and the layout is fair for any colony count. +// +// The whole design is a pure function of the request, so validateWorld rebuilds it and checks the +// finished world. Terrain is written straight to the undermap with an order-independent beach +// pass, as Ring world does. +namespace { + +constexpr double pi = 3.14159265358979323846; + +// Sea kept between the homes' outer coast and the map's wrap, as a share of the half side. +constexpr double kRimShare = 0.05; +constexpr int kRimMinimum = 4; +// Coast roughness at 100: bays and headlands on the commons' coast as a share of its radius, a +// finer ripple on top, and bays into the homes' inner and outer coasts as shares of their depth. +constexpr double kCoastAmplitude = 0.28; +constexpr double kRippleAmplitude = 0.04; +constexpr double kInnerBayShare = 0.36; +constexpr double kOuterBayShare = 0.5; +// The channels between homes bow sideways by up to this many tiles, all the same way. +constexpr double kChannelBend = 9.0; +// Land outside a causeway's road, on each side: stone stands on every solid-grass tile of it, and +// the beach pass turns the outermost vertex to sand, which leaves a sand lane a unit can walk along +// beside the stone (grass may never touch water, so no wall can stand right at the water's edge). +constexpr double kShoulder = 3.5; +// The causeway as a barrier includes those sand lanes, for keeping deposits off and for checking. +constexpr double kLane = 1.5; +// How far each causeway's approaches are kept clear of deposits, on both shores. +constexpr int kLanding = 10; +// A home needs this much land between the strait and the outer sea, and this much arc at its +// inner coast beyond the channel and the causeway. +constexpr int kMinimumDepth = 20; +constexpr int kMinimumArc = 10; +// A home's lake keeps its water this far from the sea's, so the two beaches never touch: where +// they did, a unit could step from the sea's beach to the lake's and round the wall. +constexpr int kLakeSeaGap = 7; +// Every home starts identical: fixed wheat and wood beside its lake, a stone deposit, all +// unscaled, outside a clear ring round the swarm. +constexpr int kHomeWheat = 40; +constexpr int kHomeWood = 30; +constexpr int kSwarmClearance = 2; +// Land kept between a lake and any coast, landing or other lake. +constexpr int kLakeShore = 6; +// The commons' heart: its central lake's radius as a share of the commons' radius, and how far +// beyond that shore the orchard stands. +constexpr double kHeartShare = 0.22; +constexpr int kOrchardReach = 5; +// The most the colonies' walks to their causeway landings may differ: this many steps, or a +// third of the longest walk on maps where the walks are long. +constexpr int kLandingSpread = 12; +// A home needs this much depth for a creek, ridges or a band of sand to fit round its lake. +constexpr int kFeatureDepth = 30; + +// What a home is made of, and what the heart of the commons is. One of each per map. +enum HomeKind { Lakeland = 0, Riverside, Highland, Marsh, Barrens, kHomeKinds }; +enum HeartKind { LakeHeart = 0, Crag, IslandHeart, Delta, ForestHeart, kHeartKinds }; + +struct Torus { + int w, h; + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + int offsetX(int from, int to) const { + const int d = x(to - from); + return d > w / 2 ? d - w : d; + } + int offsetY(int from, int to) const { + const int d = y(to - from); + return d > h / 2 ? d - h : d; + } +}; + +// Breadth-first 8-neighbour steps on the torus from every source tile through open tiles; -1 +// where the flood never arrives. +std::vector stepsFrom(const Torus &t, const std::vector &source, + const std::vector &open) { + std::vector dist(size_t(t.w) * t.h, -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % t.w, y = queue[head] / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int n = t.at(x + dx, y + dy); + if (dist[n] < 0 && open[n]) { + dist[n] = dist[queue[head]] + 1; + queue.push_back(n); + } + } + } + return dist; +} + +// A smooth random curve round a ring, periodic in u over [0, 1): a few harmonics with random +// phases, scaled so its peak is one. +struct Profile { + std::array amplitude{}, phase{}; + int first = 1, count = 0; + double at(double u) const { + double v = 0; + for (int h = 0; h < count; ++h) + v += amplitude[h] * std::cos(2 * pi * (first + h) * u + phase[h]); + return v; + } +}; + +Profile rollProfile(GenerationContext &context, const char *stream, int first, int count, + double falloff) { + Profile p; + p.first = first; + p.count = count; + for (int h = 0; h < count; ++h) { + p.amplitude[h] = (0.4 + 0.6 * context.bounded(stream, 1000) / 1000.0) / + std::pow(double(first + h), falloff); + p.phase[h] = 2 * pi * context.bounded(stream, 3600) / 3600.0; + } + double peak = 0; + for (int i = 0; i < 720; ++i) + peak = std::max(peak, std::abs(p.at(i / 720.0))); + if (peak > 0) + for (int h = 0; h < count; ++h) + p.amplitude[h] /= peak; + return p; +} + +// 0 within `flat` tiles of arc from the wedge's middle, 1 beyond `flat + fade`, smooth between. +double awayFromMiddle(double arc, double flat, double fade) { + const double x = std::clamp((arc - flat) / fade, 0.0, 1.0); + return x * x * (3 - 2 * x); +} + +// The numbers every part of the layout is measured from; a pure function of the request, shared +// by validateRequest and the design. +struct Geometry { + int teams, half, rim, strait, causeway; + bool walls; + double commonsRadius, outerRadius, amplitude; + double innerRadius() const { return commonsRadius + strait; } + double depth() const { return outerRadius - innerRadius(); } + // Arc of a home's inner coast beyond its channel, at the nominal radius. + double innerArc() const { + return teams < 2 ? 2 * pi * innerRadius() : 2 * pi * innerRadius() / teams - strait; + } + // Half the arc a wedge spans at radius d, less half the channel. + double arcHalf(double d) const { return teams < 2 ? pi * d : pi * d / teams - strait / 2.0; } +}; + +Geometry geometryFor(const GenerationRequest &r) { + const CityStatesOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + const int side = std::min(1 << r.wDec, 1 << r.hDec); + g.half = side / 2; + g.rim = std::max(kRimMinimum, int(std::lround(kRimShare * g.half))); + g.strait = std::max(4, int(std::lround(o.straitWidth / 100.0 * side))); + g.causeway = o.causewayWidth; + g.walls = o.stoneWalls; + g.commonsRadius = std::lround(o.commonsSize / 100.0 * g.half); + g.outerRadius = g.half - g.rim; + g.amplitude = o.coastRoughness / 100.0; + return g; +} + +// How far in from the strait a home's lake lies: six tenths of the depth, held clear of both +// coasts however far their roughness lets them wander. RadialShape reaches at most 1.3 times its +// radius at the roughness used here. +double lakeOffset(const Geometry &g, double lakeRadius) { + const double reach = 1.3 * lakeRadius + kLakeSeaGap; + const double low = reach + 2.0; + const double high = g.depth() - reach - 2.0; + return std::clamp(0.6 * g.depth(), low, std::max(low, high)); +} + +// A round feature in a wedge's frame: arc offset from the middle, radius from the centre. +struct Blob { + double s, r; + RadialShape shape; + bool holds(double ds, double dr) const { + return std::hypot(ds, dr) < shape.radiusAt(std::atan2(dr, ds)); + } +}; + +struct Home { + double angle; // the wedge's middle, where its causeway and lake lie + double coast; // the commons' coast radius at that angle + int lakeX, lakeY; // its main lake's centre + int landing, shore; // one tile at each end of its causeway: on the commons, on the home +}; + +enum Region : signed char { Sea = 0, Commons = 1, HomeLand = 2 }; + +// The terrain design: a pure function of the request, rebuilt by validateWorld. +struct Layout { + Torus t{1, 1}; + Geometry g{}; + int cx = 0, cy = 0; + double phase = 0; + int homeKind = Lakeland, heartKind = LakeHeart; + double creekSide = 0; // Riverside: which flank the creek runs to, -1 or 1 + double lakeR = 0, islandR = 0, ringR = 0, forestR = 0; // the heart's radii + std::vector homes; + // Per tile. + std::vector region; + std::vector homeOf; // HomeLand tiles: which home, else -1 + std::vector lake, river, ford, sand, ridge, strip, causeway, road, clear; + std::vector radius, angle; + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) { + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.w * t.h, teams = g.teams; + const CityStatesOptions o(request); + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("city-layout", 3600) / 3600.0 * 2 * pi; + const double wedge = 2 * pi / teams; + const double inner = g.innerRadius(), depth = g.depth(); + + // What this map is made of. + L.homeKind = int(context.bounded("city-kind", kHomeKinds)); + L.heartKind = int(context.bounded("city-kind", kHeartKinds)); + if (L.heartKind == Delta && teams < 2) + L.heartKind = LakeHeart; + if (depth < kFeatureDepth && (L.homeKind == Riverside || L.homeKind == Highland || L.homeKind == Barrens)) + L.homeKind = Lakeland; + + // The coasts: bays and headlands round the commons, a ripple on top, bays into the homes' + // flanks, and a bow in every channel. All periodic in the wedge, so every home gets the same. + const Profile coast = rollProfile(context, "city-coast", 1, 4, 1.2); + const Profile ripple = rollProfile(context, "city-coast", 6, 6, 0.6); + const Profile innerBays = rollProfile(context, "city-coast", 1, 3, 1.0); + const Profile outerBays = rollProfile(context, "city-coast", 1, 3, 1.0); + const double coastAmp = kCoastAmplitude * g.amplitude * g.commonsRadius; + const double rippleAmp = kRippleAmplitude * g.amplitude * g.commonsRadius; + const double innerBayAmp = kInnerBayShare * g.amplitude * depth; + const double outerBayAmp = kOuterBayShare * g.amplitude * depth; + const double bend = (context.bounded("city-coast", 2001) / 1000.0 - 1) * kChannelBend; + // The coasts hold flat for a stretch either side of every causeway, measured in tiles of arc so + // the stretch is the same however many homes share the ring, and the flanks' bays begin only + // beyond the lake and its clearance. + const double lakeRadius = std::clamp(0.1 * depth, 2.0, 6.0); + const double lakeReach = 1.3 * lakeRadius; + const double flatArc = g.causeway / 2.0 + kShoulder + kLane + 4; + const double bayArc = lakeReach + kLakeSeaGap + 6; + const auto coastAt = [&](double u, double d) { + const double away = awayFromMiddle(d * std::abs(u - 0.5) * wedge, flatArc, 12); + return g.commonsRadius + (coastAmp * coast.at(u) + rippleAmp * ripple.at(u)) * away; + }; + const auto innerAt = [&](double u, double d) { + return coastAt(u, d) + g.strait + + innerBayAmp * std::max(0.0, innerBays.at(u)) * + awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc, 10); + }; + const auto outerAt = [&](double u, double d) { + return std::min(double(g.half - 3), + g.outerRadius - outerBayAmp * std::max(0.0, outerBays.at(u)) * + awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc - 2, 10)); + }; + const auto bendAt = [&](double d) { + const double x = (d - inner) / std::max(1.0, depth); + return x <= 0 || x >= 1 ? 0.0 : bend * std::sin(pi * x); + }; + + for (int k = 0; k < teams; ++k) { + const double a = L.phase + wedge * (k + 0.5); + L.homes.push_back({a, g.commonsRadius, 0, 0, -1, -1}); + } + + // The home's features in the wedge's frame: lakes first (the main one first), then whatever + // the kind adds. A feature keeps clear of the coasts as bays can bring them, of the channels, + // of the causeway's approach and of other features. + const double mainLakeR = inner + lakeOffset(g, lakeRadius); + std::vector lakes, sandBlobs; + lakes.push_back({0.0, mainLakeR, RadialShape(lakeRadius, 0.3, context, "city-home-lakes")}); + const auto fits = [&](double s0, double r0, double reach, double gap, + const std::vector &others) { + if (std::abs(s0) + reach + gap > g.arcHalf(r0)) + return false; + const double u = 0.5 + s0 / (wedge * r0); + if (r0 - reach - gap < innerAt(u, r0) || r0 + reach + gap > outerAt(u, r0)) + return false; + if (r0 - reach < inner + kLanding + 2 && std::abs(s0) < g.causeway + reach + 4) + return false; + for (const Blob &b : others) + if (std::hypot(s0 - b.s, r0 - b.r) < reach + b.shape.maximumRadius() + gap) + return false; + return true; + }; + if (L.homeKind == Marsh) + for (int attempt = 0; attempt < 400 && lakes.size() < 4; ++attempt) { + const double r0 = inner + 8 + context.bounded("city-home-lakes", 1000) / 1000.0 * (depth - 16); + const double s0 = (context.bounded("city-home-lakes", 2001) / 1000.0 - 1) * 0.6 * g.arcHalf(r0); + RadialShape shape(2.0 + context.bounded("city-home-lakes", 1000) / 1000.0 * 2.0, 0.3, context, + "city-home-lakes"); + if (fits(s0, r0, shape.maximumRadius(), kLakeSeaGap, lakes)) + lakes.push_back({s0, r0, shape}); + } + // Riverside: a creek from the main lake towards one flank of the inner coast, with a ford. + std::vector> creek; + int fordSegment = -1; + if (L.homeKind == Riverside) { + const double side = context.bounded("city-features", 2) ? 1.0 : -1.0; + L.creekSide = side; + const double reachArc = 0.32 * g.arcHalf(mainLakeR); + const double endR = inner + kLakeSeaGap + 6.0; + creek = {{0.0, mainLakeR}, + {side * 0.35 * reachArc, mainLakeR - 0.3 * (mainLakeR - endR)}, + {side * 0.75 * reachArc, mainLakeR - 0.65 * (mainLakeR - endR)}, + {side * reachArc, endR}}; + fordSegment = 1; + } + // Highland: two ridges out from the strait to the sea, a pass through each. + const double ridgeU[2] = {0.28, 0.72}; + const double passR = inner + 0.5 * depth; + // Barrens: both flanks of the home are sand, leaving a strip of grass down the middle from the + // causeway past the lake; the sand keeps clear of every coast so the wall stays on the shore. + const double barrenShare = 0.45; + // Sand patches, in every kind of home. + const double homeArea = wedge * (g.outerRadius * g.outerRadius - inner * inner) / 2; + const int homeSand = int(std::lround(o.sand * homeArea / 8192.0)); + for (int attempt = 0; attempt < 400 && int(sandBlobs.size()) < homeSand; ++attempt) { + const double r0 = inner + 8 + context.bounded("city-sand", 1000) / 1000.0 * (depth - 16); + const double s0 = (context.bounded("city-sand", 2001) / 1000.0 - 1) * 0.75 * g.arcHalf(r0); + RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 4.0, 0.5, context, "city-sand"); + std::vector avoid = lakes; + avoid.insert(avoid.end(), sandBlobs.begin(), sandBlobs.end()); + if (fits(s0, r0, shape.maximumRadius(), 5, avoid)) + sandBlobs.push_back({s0, r0, shape}); + } + + // The heart of the commons. + L.lakeR = std::max(3.0, kHeartShare * g.commonsRadius); + L.ringR = std::max(8.0, kHeartShare * g.commonsRadius); + L.islandR = 0; + if (L.heartKind == Crag) + L.lakeR = std::max(2.5, 0.06 * g.commonsRadius); + if (L.heartKind == IslandHeart) { + L.lakeR = std::max(6.0, 0.3 * g.commonsRadius); + L.islandR = std::max(5.0, 0.12 * g.commonsRadius); + } + L.forestR = L.lakeR + 4 + 0.09 * g.commonsRadius; + const double deltaFordR = 0.6 * g.commonsRadius; + const RadialShape heart(L.lakeR, L.heartKind == IslandHeart ? 0.2 : 0.3, context, "city-heart"); + // Sand on the commons: anywhere between the heart and the shore, off the landings. + struct Patch { + int x, y; + RadialShape shape; + }; + std::vector commonsSand; + const double commonsArea = pi * g.commonsRadius * g.commonsRadius; + const int wantedSand = int(std::lround(o.sand * commonsArea / 8192.0)); + for (int attempt = 0; attempt < 400 && int(commonsSand.size()) < wantedSand; ++attempt) { + const double a = context.bounded("city-sand", 3600) / 3600.0 * 2 * pi; + const double r0 = 0.45 * g.commonsRadius + + context.bounded("city-sand", 1000) / 1000.0 * (0.55 * g.commonsRadius - 12); + RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 5.0, 0.5, context, "city-sand"); + const double reach = shape.maximumRadius(); + const double turn = std::fmod(a - L.phase + 4 * pi, 2 * pi); + const double u = turn / wedge - std::floor(turn / wedge); + if (r0 * std::abs(u - 0.5) * wedge < reach + g.causeway + 4 && r0 > g.commonsRadius - kLanding - reach - 4) + continue; + if (r0 + reach > coastAt(u, r0) - 8) + continue; + bool open = true; + for (const Patch &p : commonsSand) { + const int px = L.cx + int(std::lround(r0 * std::cos(a))), py = L.cy + int(std::lround(r0 * std::sin(a))); + const double dx = t.offsetX(px, p.x), dy = t.offsetY(py, p.y); + open = open && std::hypot(dx, dy) >= reach + p.shape.maximumRadius() + 4; + } + if (open) + commonsSand.push_back({t.x(L.cx + int(std::lround(r0 * std::cos(a)))), + t.y(L.cy + int(std::lround(r0 * std::sin(a)))), shape}); + } + + L.region.assign(n, Sea); + L.homeOf.assign(n, -1); + for (auto *mask : {&L.lake, &L.river, &L.ford, &L.sand, &L.ridge, &L.strip, &L.causeway, &L.road, &L.clear}) + mask->assign(n, 0); + L.radius.assign(n, 0.0); + L.angle.assign(n, 0.0); + const double roadHalf = g.causeway / 2.0; + const double stripHalf = roadHalf + kShoulder; + std::vector landingAlong(teams, -1.0), shoreAlong(teams, 1e9); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const int i = y * t.w + x; + const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); + const double d = std::hypot(double(dx), double(dy)); + const double theta = std::atan2(double(dy), double(dx)); + L.radius[i] = d; + L.angle[i] = theta; + // The wedge frame: which home, how far across it and how far along the arc from its middle. + double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); + if (teams >= 2) + turn = std::fmod(turn - bendAt(d) / std::max(1.0, d) + 4 * pi, 2 * pi); + const int k = std::min(teams - 1, int(turn / wedge)); + const double u = turn / wedge - k; + const double s = d * (u - 0.5) * wedge; + const double toBoundary = teams < 2 ? 1e9 : d * std::min(u, 1 - u) * wedge; + const double toLanding = d * std::abs(u - 0.5) * wedge; + const double c = coastAt(u, d); + const double in = innerAt(u, d), out = outerAt(u, d); + if (d < c) { + L.region[i] = Commons; + } else if (d < in) { + L.region[i] = Sea; + } else if (d < out && toBoundary >= g.strait / 2.0) { + L.region[i] = HomeLand; + L.homeOf[i] = k; + } + // The home's causeway: straight across the strait at the wedge's middle. + const Home &h = L.homes[k]; + const double along = d * std::cos(theta - h.angle), across = std::abs(d * std::sin(theta - h.angle)); + if (across <= stripHalf + kLane && along >= h.coast - 3 && along <= h.coast + g.strait + 3) + L.causeway[i] = 1; + if (across <= stripHalf && along >= h.coast - 3 && along <= h.coast + g.strait + 3) { + L.strip[i] = 1; + if (L.region[i] == Sea) { + L.region[i] = HomeLand; + L.homeOf[i] = k; + } + if (across <= roadHalf) + L.road[i] = 1; + if (along > landingAlong[k] && L.road[i]) { + landingAlong[k] = along; + L.homes[k].shore = i; + } + if (along < shoreAlong[k] && L.road[i]) { + shoreAlong[k] = along; + L.homes[k].landing = i; + } + } + // Both approaches to the causeway are kept clear of deposits. + if (across <= stripHalf + kLane && ((along >= h.coast - kLanding && along < h.coast) || + (along > h.coast + g.strait && along <= h.coast + g.strait + kLanding))) + L.clear[i] = 1; + + // The home's features, the same in every wedge. + if (L.region[i] == HomeLand && !L.strip[i]) { + for (const Blob &b : lakes) + if (b.holds(s - b.s, d - b.r)) + L.lake[i] = 1; + for (size_t seg = 0; seg + 1 < creek.size(); ++seg) { + const double ax = creek[seg].first, ay = creek[seg].second; + const double bx = creek[seg + 1].first, by = creek[seg + 1].second; + const double len2 = (bx - ax) * (bx - ax) + (by - ay) * (by - ay); + const double tt = std::clamp(((s - ax) * (bx - ax) + (d - ay) * (by - ay)) / std::max(1e-6, len2), 0.0, 1.0); + const double dist = std::hypot(s - (ax + tt * (bx - ax)), d - (ay + tt * (by - ay))); + if (dist < 1.6) { + if (int(seg) == fordSegment && std::abs(tt - 0.5) * std::sqrt(len2) < 2.5) { + L.ford[i] = 1; + L.clear[i] = 1; + } else { + L.lake[i] = 1; + } + } else if (int(seg) == fordSegment && dist < 3.5 && std::abs(tt - 0.5) * std::sqrt(len2) < 3.5) { + L.clear[i] = 1; + } + } + if (L.homeKind == Highland && d >= in + 1 && d <= out - 1) + for (double ur : ridgeU) { + const double sr = (ur - 0.5) * wedge * d; + if (std::abs(s - sr) < 1.25) { + if (std::abs(d - passR) < 2.5) + L.clear[i] = 1; + else + L.ridge[i] = 1; + } else if (std::abs(s - sr) < 3.5 && std::abs(d - passR) < 4.5) { + L.clear[i] = 1; + } + } + if (L.homeKind == Barrens && d >= in + 8 && d <= out - 8 && + std::abs(s) > barrenShare * g.arcHalf(d) && std::abs(s) < g.arcHalf(d) - 8) + L.sand[i] = 1; + for (const Blob &b : sandBlobs) + if (b.holds(s - b.s, d - b.r)) + L.sand[i] = 1; + } + // The heart of the commons. + if (L.region[i] == Commons) { + const bool inHeart = d < heart.radiusAt(theta); + if (L.heartKind == IslandHeart) { + if (inHeart && d >= L.islandR) + L.lake[i] = 1; + if (inHeart && d >= L.islandR - 1 && d <= L.lakeR + 2.5 && toLanding < 2) { + L.ford[i] = 1; + L.lake[i] = 0; + } + if (inHeart && d >= L.islandR - 2 && d <= L.lakeR + 3.5 && toLanding < 3.5) + L.clear[i] = 1; + } else if (inHeart) { + L.lake[i] = 1; + } + if (L.heartKind == Crag) { + if (d >= L.ringR && d < L.ringR + 1.5 && toLanding >= 2.5) + L.ridge[i] = 1; + if (d >= L.ringR - 2 && d < L.ringR + 3.5 && toLanding < 4) + L.clear[i] = 1; + } + if (L.heartKind == Delta && teams >= 2 && d >= L.lakeR - 1 && d <= c + 1) { + if (toBoundary < 2) { + if (std::abs(d - deltaFordR) < 2.5) + L.ford[i] = 1; + else + L.river[i] = 1; + } + if (toBoundary < 3.5 && std::abs(d - deltaFordR) < 4) + L.clear[i] = 1; + } + if (!L.lake[i] && !L.river[i] && !L.ford[i] && !L.clear[i] && !L.causeway[i]) + for (const Patch &p : commonsSand) { + const double px = t.offsetX(p.x, x), py = t.offsetY(p.y, y); + if (std::hypot(px, py) < p.shape.radiusAt(std::atan2(py, px))) + L.sand[i] = 1; + } + } + } + // The commons-side end is the end nearest the centre; the home-side end the farthest. + for (int k = 0; k < teams; ++k) { + std::swap(L.homes[k].landing, L.homes[k].shore); + if (L.homes[k].landing < 0 || L.homes[k].shore < 0) { + L.failure = "home " + std::to_string(k) + " has no causeway"; + return L; + } + Home &h = L.homes[k]; + h.lakeX = t.x(L.cx + int(std::lround(mainLakeR * std::cos(h.angle)))); + h.lakeY = t.y(L.cy + int(std::lround(mainLakeR * std::sin(h.angle)))); + } + return L; +} + +// The ground every causeway leads to, by the kind of heart: the island, the inside of the crag, +// or the central lake's shore. +std::vector heartTiles(const Map &map, const Layout &L) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector heart(n, 0); + const double reach = L.heartKind == IslandHeart ? L.islandR + : L.heartKind == Crag ? L.ringR - 1 + : L.lakeR + kOrchardReach + 3; + for (int i = 0; i < n; ++i) + heart[i] = L.region[i] == Commons && L.radius[i] < reach && !map.isWater(i % t.w, i / t.w); + return heart; +} + +// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), +// so every land corner beside water becomes sand. Unlike Map::controlSand() this reads only the +// original terrain, so the result doesn't depend on scan order and water is never eaten away. +void layBeaches(std::vector &terrain, const Torus &t) { + const std::vector original(terrain); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const int i = y * t.w + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[t.at(x + dx, y + dy)] == WATER; + if (shore) + terrain[i] = SAND; + } +} + +// The ground a unit landing from the sea can reach without crossing solid grass: every land tile +// with a sea vertex in the box its four corners touch, and every beach joined to those - a lake's +// beach too, where it meets the sea's. Stone cannot stand on any of it. +std::vector seaMargin(const Map &map, const Layout &L) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector sea(n, 0), margin(n, 0), beach(n, 0); + for (int i = 0; i < n; ++i) + sea[i] = map.getUMTerrain(i % t.w, i / t.w) == WATER && !L.lake[i] && L.region[i] != Commons; + for (int i = 0; i < n; ++i) { + const int x = i % t.w, y = i / t.w; + if (map.isWater(x, y)) + continue; + beach[i] = map.getTerrainType(x, y) != GRASS; + for (int dy = -1; dy <= 2 && !margin[i]; ++dy) + for (int dx = -1; dx <= 2; ++dx) + if (sea[t.at(x + dx, y + dy)]) { + margin[i] = 1; + break; + } + } + const std::vector joined = stepsFrom(t, margin, beach); + for (int i = 0; i < n; ++i) + if (joined[i] >= 0) + margin[i] = 1; + return margin; +} + +// The design's stone, once the terrain is laid: every solid-grass tile of a causeway's shoulders +// outside its road, the ridges and the crag, and round every home a wall on the solid-grass tiles +// that touch the sea's margin. Every step off the beach lands on the wall, so a home is sealed but +// for its road. Rebuilt the same way by validateWorld. +std::vector stoneTiles(const Map &map, const Layout &L) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector stone(n, 0); + if (!L.g.walls) + return stone; + const std::vector margin = seaMargin(map, L); + for (int i = 0; i < n; ++i) { + if (L.road[i] || map.getTerrainType(i % t.w, i / t.w) != GRASS) + continue; + if (L.strip[i] || L.ridge[i]) { + stone[i] = 1; + continue; + } + if (L.homeOf[i] < 0) + continue; + for (int dy = -1; dy <= 1 && !stone[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (margin[t.at(i % t.w + dx, i / t.w + dy)]) { + stone[i] = 1; + break; + } + } + return stone; +} + +// Extra lakes in the commons for fertility, likelier towards the centre; each keeps kLakeShore +// of land to any coast, landing, the central lake and other lakes. Counted per 128x128 of commons. +void carveValleys(std::vector &terrain, const Layout &L, GenerationContext &context, + int perStandardMap) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector water(n, 0), land(n, 0), keep(n, 0); + std::vector ground; + for (int i = 0; i < n; ++i) { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + keep[i] = L.clear[i] || L.causeway[i]; + if (L.region[i] == Commons && land[i]) + ground.push_back(i); + } + if (perStandardMap <= 0 || ground.empty()) + return; + std::vector all(n, 1); + const std::vector shore = stepsFrom(t, water, land); + const std::vector fromKeep = stepsFrom(t, keep, all); + const int wanted = int(std::lround(perStandardMap * double(ground.size()) / 16384.0)); + const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 0.8, 1.6); + struct Lake { + int x, y; + double reach; + }; + std::vector lakes; + for (int attempt = 0; int(lakes.size()) < wanted && attempt < wanted * 80; ++attempt) { + const int at = ground[context.bounded("city-valleys", ground.size())]; + const double inward = 1 - L.radius[at] / std::max(1.0, L.g.commonsRadius); + if (int(context.bounded("city-valleys", 100)) >= 25 + int(75 * inward)) + continue; + const int x = at % t.w, y = at / t.w; + const double stretch = 1.2 + context.bounded("city-valleys", 81) / 100.0; + const double turn = context.bounded("city-valleys", 3600) / 3600.0 * pi; + const RadialShape shape((3 + context.bounded("city-valleys", 4)) * scale, 0.35, context, + "city-valleys"); + const double reach = shape.maximumRadius() * stretch; + if (shore[at] < reach + kLakeShore || fromKeep[at] < reach + kLakeShore) + continue; + bool open = true; + for (const Lake &other : lakes) { + const double gap = reach + other.reach + 2 * kLakeShore; + const double dx = t.offsetX(x, other.x), dy = t.offsetY(y, other.y); + open = open && dx * dx + dy * dy >= gap * gap; + } + if (!open) + continue; + const ShapeTransform transform({double(x), double(y)}, turn, stretch); + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) { + const ShapePoint p = transform.toShape({double(x + dx), double(y + dy)}); + if (std::hypot(p.x, p.y) < shape.radiusAt(std::atan2(p.y, p.x))) + terrain[t.at(x + dx, y + dy)] = WATER; + } + lakes.push_back({x, y, reach}); + } +} + +struct Island { + int x, y; + std::vector tiles; +}; + +// Small islands out in the open sea, each well clear of every coast and of each other, so they are +// only ever reached by swimming. Counted per 128x128 of sea. +std::vector raiseIslands(std::vector &terrain, const Layout &L, + GenerationContext &context, int perStandardMap) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector islands; + if (perStandardMap <= 0) + return islands; + std::vector water(n), land(n); + std::vector sea; + for (int i = 0; i < n; ++i) { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + if (water[i]) + sea.push_back(i); + } + if (sea.empty()) + return islands; + const std::vector offshore = stepsFrom(t, land, water); + const int wanted = int(std::lround(perStandardMap * double(sea.size()) / 16384.0)); + const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 1.0, 1.6); + for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 60; ++attempt) { + const int at = sea[context.bounded("city-islands", sea.size())]; + const int x = at % t.w, y = at / t.w; + const RadialShape shape((4 + context.bounded("city-islands", 3)) * scale, 0.3, context, + "city-islands"); + const double reach = shape.maximumRadius(); + if (offshore[at] < reach + kLakeShore + 1) + continue; + bool open = true; + for (const Island &other : islands) { + const double gap = 2 * reach + kLakeShore + 1; + const double dx = t.offsetX(x, other.x), dy = t.offsetY(y, other.y); + open = open && dx * dx + dy * dy >= gap * gap; + } + if (!open) + continue; + Island island{x, y, {}}; + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (std::hypot(double(dx), double(dy)) < shape.radiusAt(std::atan2(double(dy), double(dx)))) { + const int i = t.at(x + dx, y + dy); + terrain[i] = GRASS; + island.tiles.push_back(i); + } + islands.push_back(std::move(island)); + } + return islands; +} + +// Grows a compact patch of one resource outward from a seed tile, breadth-first over the four +// cardinal neighbours, onto tiles the predicate allows. Returns how many tiles it placed. +template +int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) { + std::vector queued(size_t(t.w) * t.h, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) { + const int i = frontier[head], x = i % t.w, y = i / t.w; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) { + const int n = t.at(x + step[0], y + step[1]); + if (!queued[n]) { + queued[n] = 1; + frontier.push_back(n); + } + } + } + return placed; +} + +bool clearGround(const Map &map, int x, int y) { + return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; +} + +// Splits a set of tiles into wheat and wood by an unrelated noise field, so the two crops form +// separate patches rather than rings sorted by fertility. +void plantFields(Map &map, const Torus &t, std::vector tiles, int wheat, int wood, + HeightMap &split) { + const int total = std::min(int(tiles.size()), wheat + wood); + if (total <= 0) + return; + tiles.resize(total); + std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { + return split.uiLevel(a % t.w, a / t.w, 2048) < split.uiLevel(b % t.w, b / t.w, 2048); + }); + const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); + for (int k = 0; k < total; ++k) + map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheatShare ? CORN : WOOD, 1); +} + +// Every home's kit, identical and unscaled: wheat and wood patches beside its lake, where the lake +// keeps them growing, and a stone deposit further off; then its own scaled ambient farmland, +// outcrops and a grove, as a self-sufficient base needs. +void furnishHomes(Map &map, const Layout &L, GenerationContext &context, + const CityStatesOptions &o) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector reserved(n, 0), water(n), dry(n); + for (int team = 0; team < L.g.teams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + for (int i = 0; i < n; ++i) { + water[i] = map.isWater(i % t.w, i / t.w); + dry[i] = !water[i]; + } + const std::vector shore = stepsFrom(t, water, dry); + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap split(t.w, t.h, context.stream("city-home-split")); + split.makePlain(6); + HeightMap patch(t.w, t.h, context.stream("city-home-patch")); + patch.makePlain(12); + for (int team = 0; team < L.g.teams; ++team) { + const Home &h = L.homes[team]; + const auto eligible = [&](int i) { + return L.homeOf[i] == team && !L.causeway[i] && !L.clear[i] && !reserved[i] && + clearGround(map, i % t.w, i / t.w); + }; + // The kit: wheat and wood beside the lake on the swarm's side, on the flank away from any + // creek, and the quarry behind the lake. + const double reach = std::clamp(0.1 * L.g.depth(), 2.0, 6.0) * 1.3 + 3; + const double flank = L.creekSide != 0 ? -L.creekSide : -1.0; + const auto seedNear = [&](double along, double across, int within) { + const int ax = h.lakeX + int(std::lround(along * std::cos(h.angle) - across * std::sin(h.angle))); + const int ay = h.lakeY + int(std::lround(along * std::sin(h.angle) + across * std::cos(h.angle))); + int seed = -1, nearest = INT_MAX; + for (int dy = -within; dy <= within; ++dy) + for (int dx = -within; dx <= within; ++dx) { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) { + nearest = dx * dx + dy * dy; + seed = i; + } + } + return seed; + }; + if (L.creekSide == 0) { + if (const int seed = seedNear(-0.3 * reach, -reach, 14); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheat, eligible); + if (const int seed = seedNear(-0.3 * reach, reach, 14); seed >= 0) + growPatch(map, t, seed, WOOD, kHomeWood, eligible); + } else { + if (const int seed = seedNear(-0.5 * reach, flank * reach, 14); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheat, eligible); + if (const int seed = seedNear(0.6 * reach, flank * (reach + 1), 14); seed >= 0) + growPatch(map, t, seed, WOOD, kHomeWood, eligible); + } + if (const int seed = seedNear(reach + 8, 0, 12); seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + // Ambient farmland on the home's fertile ground, in patches, then outcrops and a grove. + std::vector ground; + std::vector> farm; + std::vector levels; + for (int i = 0; i < n; ++i) + if (eligible(i)) { + ground.push_back(i); + if (fertility.at(i % t.w, i / t.w) > 0) + levels.push_back(patch(i % t.w, i / t.w)); + } + float cut = 0; + if (!levels.empty()) { + std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, levels.end()); + cut = levels[levels.size() * 45 / 100]; + } + for (int i : ground) { + const std::uint32_t f = fertility.at(i % t.w, i / t.w); + if (f > 0 && patch(i % t.w, i / t.w) >= cut) + farm.push_back({-double(f), i}); + } + std::stable_sort(farm.begin(), farm.end()); + std::vector chosen; + for (const auto &entry : farm) + chosen.push_back(entry.second); + const int area = int(ground.size()); + plantFields(map, t, chosen, int(scaledCount(area * 4 / 100, o.wheat)), + int(scaledCount(area * 2 / 100, o.wood)), split); + const int outcrops = int(scaledCount(std::max(1, area / 2500), o.stone)); + for (int k = 0; k < outcrops; ++k) + for (int attempt = 0; attempt < 100; ++attempt) { + const int at = ground[context.bounded("city-home-stone", ground.size())]; + if (eligible(at)) { + placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, 1); + break; + } + } + const int groves = int(scaledCount(1, o.fruit)); + for (int k = 0; k < groves; ++k) + for (int attempt = 0; attempt < 100; ++attempt) { + const int at = ground[context.bounded("city-home-fruit", ground.size())]; + if (eligible(at)) { + placeResourceClump(map, context, {at % t.w, at / t.w}, + CHERRY + int(context.bounded("city-home-fruit", 3)), 1); + break; + } + } + } +} + +// The commons: farmland on its fertile ground, richer towards the centre as the frontier control +// says; stone outcrops and fruit groves sampled by the same weight; and the orchard of all three +// fruits round the central lake, the heart every causeway leads to. +void stockCommons(Map &map, const Layout &L, GenerationContext &context, + const CityStatesOptions &o) { + const Torus &t = L.t; + const int n = t.w * t.h; + const double frontier = o.frontier / 100.0; + const auto weight = [&](int i) { + const double inward = std::clamp(1 - L.radius[i] / std::max(1.0, L.g.commonsRadius), 0.0, 1.0); + return 1 - frontier + frontier * std::min(1.0, inward / (1 - kHeartShare)); + }; + const auto eligible = [&](int i) { + return L.region[i] == Commons && !L.clear[i] && !L.causeway[i] && + clearGround(map, i % t.w, i / t.w); + }; + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap patch(t.w, t.h, context.stream("city-patch")); + patch.makePlain(12); + HeightMap split(t.w, t.h, context.stream("city-split")); + split.makePlain(6); + std::vector ground; + std::vector levels; + for (int i = 0; i < n; ++i) + if (eligible(i)) { + ground.push_back(i); + if (fertility.at(i % t.w, i / t.w) > 0) + levels.push_back(patch(i % t.w, i / t.w)); + } + if (ground.empty()) + return; + float cut = 0; + if (!levels.empty()) { + std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, levels.end()); + cut = levels[levels.size() * 45 / 100]; + } + std::vector> farm; + for (int i : ground) { + const std::uint32_t f = fertility.at(i % t.w, i / t.w); + if (f > 0 && patch(i % t.w, i / t.w) >= cut) + farm.push_back({-double(f) * weight(i), i}); + } + std::stable_sort(farm.begin(), farm.end()); + std::vector chosen; + for (const auto &entry : farm) + chosen.push_back(entry.second); + const int area = int(ground.size()); + plantFields(map, t, chosen, int(scaledCount(area * 7 / 100, o.wheat)), + int(scaledCount(area * 4 / 100, o.wood)), split); + + const auto pick = [&](const char *stream) { + for (int attempt = 0; attempt < 200; ++attempt) { + const int at = ground[context.bounded(stream, ground.size())]; + if (int(context.bounded(stream, 1000)) < int(weight(at) * 1000) && eligible(at)) + return at; + } + return -1; + }; + const int outcrops = int(scaledCount(std::max(2, area / 2000), o.stone)); + for (int k = 0; k < outcrops; ++k) + if (const int at = pick("city-stone"); at >= 0) + placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, + 1 + int(context.bounded("city-stone", 2))); + const int groves = int(scaledCount(std::max(3, area / 2500), o.fruit)); + for (int k = 0; k < groves; ++k) + if (const int at = pick("city-fruit"); at >= 0) + placeResourceClump(map, context, {at % t.w, at / t.w}, + CHERRY + int(context.bounded("city-fruit", 3)), + 1 + int(context.bounded("city-fruit", 2))); + // The orchard: three groves of the three fruits round the central lake's shore, on the island, + // or inside the crag. + if (scaledCount(1, o.fruit) > 0) { + const double rho = L.heartKind == IslandHeart ? std::max(1.5, 0.45 * L.islandR) + : L.heartKind == Crag ? 0.6 * L.ringR + : L.heartKind == ForestHeart ? L.lakeR + 3 + : L.lakeR + kOrchardReach; + const double spin = context.bounded("city-fruit", 3600) / 3600.0 * 2 * pi; + for (int f = 0; f < 3; ++f) { + const double a = spin + 2 * pi * f / 3; + const int ax = L.cx + int(std::lround(rho * std::cos(a))), ay = L.cy + int(std::lround(rho * std::sin(a))); + int seed = -1, nearest = INT_MAX; + for (int dy = -8; dy <= 8; ++dy) + for (int dx = -8; dx <= 8; ++dx) { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) { + nearest = dx * dx + dy * dy; + seed = i; + } + } + if (seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); + } + } + // The forest heart: a belt of wood round the orchard that has to be cut through, and that the + // lake keeps growing back. + if (L.heartKind == ForestHeart) + for (int i = 0; i < n; ++i) + if (eligible(i) && L.radius[i] >= L.lakeR + 2 && L.radius[i] < L.forestR) + map.setResource(i % t.w, i / t.w, WOOD, 1); +} + +// Each island carries one themed prize, as on Ring world. +void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) { + const int width = map.getW(); + for (const Island &island : islands) { + std::vector grass; + for (int i : island.tiles) + if (map.isGrass(i % width, i / width)) + grass.emplace_back(i % width, i / width); + if (grass.empty()) + continue; + MapGeneratorPoint centre(island.x, island.y); + if (!map.isGrass(centre.x, centre.y)) + centre = grass[context.bounded("city-islands", grass.size())]; + switch (context.bounded("city-islands", 3)) { + case 0: + placeResourceClump(map, context, centre, STONE, 2); + break; + case 1: + placeResourceClump(map, context, centre, CHERRY + int(context.bounded("city-islands", 3)), 2); + break; + default: + placeResourceClump(map, context, centre, CORN, 2); + break; + } + } +} + +// Algae in the shallows along every coast, the strait's, the sea's and the lakes' alike. +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) { + const int n = t.w * t.h; + std::vector water(n), dry(n); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + water[y * t.w + x] = map.isWater(x, y); + dry[y * t.w + x] = !map.isWater(x, y); + } + const std::vector offshore = stepsFrom(t, dry, water); + std::vector shallows; + for (int i = 0; i < n; ++i) + if (offshore[i] >= 2 && offshore[i] <= 4) + shallows.emplace_back(i % t.w, i / t.w); + if (shallows.empty()) + return; + for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) + placeResourceClump(map, context, shallows[context.bounded("city-algae", shallows.size())], + ALGA, 1); +} + +// Clears the ring the swarm's workers walk out through; a causeway's stone beside it stays. +void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t, + const std::vector &line) { + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) { + const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); + if (map.isResource(x, y) && !line[y * t.w + x]) + map.setNoResource(x, y, 1); + } +} + +// Causeways and their approaches hold nothing but the causeways' own stone lines. +void clearRoads(Map &map, const Layout &L, const std::vector &line) { + const Torus &t = L.t; + for (int i = 0; i < t.w * t.h; ++i) + if ((L.causeway[i] || L.clear[i]) && !line[i] && map.isResource(i % t.w, i / t.w)) + map.setNoResource(i % t.w, i / t.w, 1); +} + +// The cheapest walk from any source tile to any goal tile (deposits cost one, open ground nothing; +// water, buildings and the stone lines are impassable), with only the deposits on it cleared. +bool openRoad(Map &map, const Layout &L, const std::vector &line, + const std::vector &sources, const std::vector &goal) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector cost(n, INT_MAX), parent(n, -1); + std::vector done(n, 0); + std::deque queue; + for (int i : sources) { + cost[i] = 0; + queue.push_back(i); + } + int reached = -1; + while (!queue.empty() && reached < 0) { + const int i = queue.front(); + queue.pop_front(); + if (done[i]) + continue; + done[i] = 1; + if (goal[i]) { + reached = i; + break; + } + const int x = i % t.w, y = i / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + if (!dx && !dy) + continue; + const int nx = t.x(x + dx), ny = t.y(y + dy), next = ny * t.w + nx; + if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID || line[next]) + continue; + const int nextCost = cost[i] + (map.isResource(nx, ny) ? 1 : 0); + if (nextCost < cost[next]) { + cost[next] = nextCost; + parent[next] = i; + if (nextCost == cost[i]) + queue.push_front(next); + else + queue.push_back(next); + } + } + } + if (reached < 0) + return false; + for (int i = reached; i >= 0; i = parent[i]) + if (map.isResource(i % t.w, i / t.w)) + map.setNoResource(i % t.w, i / t.w, 1); + return true; +} + +// Deposits may land anywhere, and a band of them could close a home's swarm off from its causeway +// or a landing off from the heart. Keep both walks open, clearing only what stands on the cheapest +// one. Almost always nothing is in the way. +bool openRoads(Map &map, const Layout &L, const std::vector &line, + GenerationContext &context) { + const Torus &t = L.t; + const int n = t.w * t.h; + const std::vector heart = heartTiles(map, L); + for (int k = 0; k < L.g.teams; ++k) { + std::vector workers; + for (int i = 0; i < n; ++i) { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == k) + workers.push_back(i); + } + std::vector shore(n, 0); + shore[L.homes[k].shore] = 1; + if (!openRoad(map, L, line, workers, shore)) { + context.detail = "colony " + std::to_string(k) + " has no way to its causeway"; + return false; + } + if (!openRoad(map, L, line, {L.homes[k].landing}, heart)) { + context.detail = "causeway " + std::to_string(k) + " has no way into the commons"; + return false; + } + } + return true; +} + +bool generate(Game &game, GenerationContext &context) { + context.stage = "city layout"; + const CityStatesOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(GRASS); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.w * t.h; + + context.stage = "city terrain"; + std::vector terrain(n, GRASS); + for (int i = 0; i < n; ++i) { + if (L.region[i] == Sea || L.lake[i] || L.river[i]) + terrain[i] = WATER; + if (L.ford[i]) + terrain[i] = SAND; + if (L.sand[i] && terrain[i] == GRASS) + terrain[i] = SAND; + } + carveValleys(terrain, L, context, o.valleys); + const std::vector islands = raiseIslands(terrain, L, context, o.resourceIslands); + layBeaches(terrain, t); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); + map.rebuildTerrain(); + const std::vector line = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + if (line[i]) + map.setResource(i % t.w, i / t.w, STONE, 1); + + context.stage = "city colonies"; + for (int team = 0; team < teams; ++team) { + const Home &h = L.homes[team]; + std::vector home(n, 0); + for (int i = 0; i < n; ++i) + home[i] = L.homeOf[i] == team && !L.strip[i] && !L.clear[i] && map.isGrass(i % t.w, i / t.w); + // The swarm stands between the causeway's home end and the lake, a short walk from the lake's + // fields whatever the home's depth; placeSettlement measures from the footprint's top-left + // tile, so the anchor is offset to centre the 4x4 there. + const double lakeRadius = std::clamp(0.1 * L.g.depth(), 2.0, 6.0); + const double rho = std::max(h.coast + L.g.strait + 8.0, + h.coast + L.g.strait + lakeOffset(L.g, lakeRadius) - lakeRadius - 9.0); + const MapGeneratorPoint anchor(L.cx + int(std::lround(rho * std::cos(h.angle))) - 2, + L.cy + int(std::lround(rho * std::sin(h.angle))) - 2); + if (!placeSettlement(game, context, team, home, anchor, "city-starts")) + return false; + } + + context.stage = "city resources"; + furnishHomes(map, L, context, o); + stockCommons(map, L, context, o); + stockIslands(map, context, islands); + seedAlgae(map, context, t, o.algae); + clearAroundSwarms(map, context, t, line); + // The kits already put wheat and wood a short walk from every swarm; this is only the backstop, + // and the walls and the causeways' stone are designed and must never be cleared. + guaranteeStartingResources(game, context, 24, 32, 0, &line); + clearAroundSwarms(map, context, t, line); + clearRoads(map, L, line); + context.stage = "city roads"; + return openRoads(map, L, line, context); +} + +std::string validateRequest(const GenerationRequest &r) { + if (r.nbTeams < 1) + return "City states need at least one colony."; + const Geometry g = geometryFor(r); + if (g.depth() < kMinimumDepth || g.innerArc() < g.causeway + kMinimumArc) + return "The commons and the strait leave too little room for the home bases; use a bigger map, " + "fewer colonies, a smaller commons or a narrower strait."; + return ""; +} + +// Checked on the finished world against the rebuilt design: every causeway is open and carries its +// stone lines; every colony can walk to every other and to the heart of the commons; with the +// causeways shut no home reaches another home or the commons at all; and no colony's walk to its +// landing is much longer than another's. Water, buildings and every resource tile block the +// walks; units don't, since they move. +std::string validateWorld(const Game &game, const GenerationContext &context) { + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (!L.failure.empty()) + return "The city design could not be rebuilt: " + L.failure; + const Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (map.getW() != t.w || map.getH() != t.h) + return "The city design does not match the map size."; + const auto where = [&](int i) { + return "(" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")"; + }; + const auto walkable = [&](int i) { + const int x = i % t.w, y = i / t.w; + return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + }; + const std::vector line = stoneTiles(map, L); + for (int i = 0; i < n; ++i) { + if (line[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "The stone at " + where(i) + " is missing."; + if (L.road[i] && !walkable(i)) + return "The causeway at " + where(i) + " is blocked."; + if (L.ford[i] && !walkable(i)) + return "The ford at " + where(i) + " is blocked."; + } + std::vector> workers(std::max(teams, 1)); + for (int i = 0; i < n; ++i) { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(i); + } + if (teams < 1 || workers[0].empty()) + return "Colony 0 has no workers to walk the map."; + std::vector open(n, 0), source(n, 0); + for (int i = 0; i < n; ++i) + open[i] = walkable(i); + for (int i : workers[0]) + source[i] = 1; + const std::vector fromFirst = stepsFrom(t, source, open); + for (int team = 1; team < teams; ++team) { + bool arrived = false; + for (int i : workers[team]) + arrived = arrived || fromFirst[i] >= 0; + if (!arrived) + return "Colony " + std::to_string(team) + " cannot walk to colony 0."; + } + const std::vector heart = heartTiles(map, L); + bool heartReached = false; + for (int i = 0; i < n && !heartReached; ++i) + heartReached = heart[i] && fromFirst[i] >= 0; + if (!heartReached) + return "The heart of the commons cannot be reached on foot."; + + // Shut every causeway road: with the walls up, nothing landing from the sea may get in. + if (L.g.walls) { + const std::vector margin = seaMargin(map, L); + std::vector beach(n, 0), inland(n, 0); + for (int i = 0; i < n; ++i) { + inland[i] = walkable(i) && !L.road[i]; + beach[i] = inland[i] && margin[i]; + } + const std::vector landed = stepsFrom(t, beach, inland); + for (int team = 0; team < teams; ++team) + for (int i : workers[team]) + if (landed[i] >= 0) + return "Colony " + std::to_string(team) + "'s home can be entered from the sea."; + } + // Shut every causeway: no home may reach the commons or another home any other way. + std::vector shut(n, 0); + for (int i = 0; i < n; ++i) + shut[i] = walkable(i) && !L.causeway[i]; + int shortest = INT_MAX, longest = 0; + for (int team = 0; team < teams; ++team) { + std::vector from(n, 0); + for (int i : workers[team]) + from[i] = 1; + const std::vector inside = stepsFrom(t, from, shut); + for (int i = 0; i < n; ++i) + if (inside[i] >= 0 && (L.region[i] == Commons || (L.homeOf[i] >= 0 && L.homeOf[i] != team))) + return "Colony " + std::to_string(team) + " can leave its home without its causeway, at " + + where(i) + "."; + // And every colony's walk to its landing on the commons, over its own causeway. + const std::vector steps = stepsFrom(t, from, open); + const int landing = steps[L.homes[team].landing]; + if (landing < 0) + return "Colony " + std::to_string(team) + " cannot reach its causeway landing."; + shortest = std::min(shortest, landing); + longest = std::max(longest, landing); + } + if (longest - shortest > std::max(kLandingSpread, longest / 3)) + return "The colonies' walks to their landings differ by " + std::to_string(longest - shortest) + + " steps."; + return ""; +} +} // namespace + +CityStatesOptions::CityStatesOptions(const GenerationRequest &r) + : commonsSize(r.option("commons-size")), straitWidth(r.option("strait-width")), + causewayWidth(r.option("causeway-width")), coastRoughness(r.option("coast-roughness")), + valleys(r.option("valleys")), resourceIslands(r.option("resource-islands")), + sand(r.option("sand")), + frontier(r.option("frontier-richness")), stoneWalls(r.option("stone-walls") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) {} + +GeneratorDefinition cityStatesDefinition() { + return {"city-states", + 17, + "City states", + 4, + false, + // The commons' radius as a share of half the shorter side, the strait's width as a + // share of the shorter side, the causeway's road in tiles; valleys per 128x128 of + // commons; resource islands per 128x128 of sea. + {{"commons-size", "Commons size", 30, 65, 5, 55, ControlGroup::Terrain}, + {"strait-width", "Strait width", 3, 8, 1, 4, ControlGroup::Terrain}, + {"causeway-width", "Causeway width", 5, 11, 2, 7, ControlGroup::Layout}, + // Bays and headlands on every coast and the bow in the channels. + {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, + {"valleys", "Valleys", 0, 8, 1, 3, ControlGroup::Terrain}, + // Patches of sand over the homes and the commons, per 64x128 tiles of land. + {"sand", "Sand patches", 0, 8, 1, 3, ControlGroup::Terrain}, + {"resource-islands", "Resource islands", 0, 6, 1, 2, ControlGroup::Resources}, + // How much richer the commons' heart is than its shores. + {"frontier-richness", "Frontier richness", 0, 100, 10, 60, ControlGroup::Resources}, + // Off, no stone: the causeways are plain roads and the homes' coasts are open. + GeneratorControl::toggle("stone-walls", "Stone walls", true, ControlGroup::Layout), + // Every home's ambient fields, outcrops and grove, the commons and the islands' prizes; + // every home's kit and the causeways' stone stay as they are. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/CityStatesGenerator.h b/src/map/generator/generators/CityStatesGenerator.h new file mode 100644 index 000000000..5dfe869ed --- /dev/null +++ b/src/map/generator/generators/CityStatesGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct CityStatesOptions { + int commonsSize, straitWidth, causewayWidth, coastRoughness, valleys, resourceIslands, sand, + frontier; + bool stoneWalls; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit CityStatesOptions(const GenerationRequest &r); +}; +GeneratorDefinition cityStatesDefinition(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index fb01a4bec..d7e35e7dd 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -287,7 +287,7 @@ class MapGeneratorDefaultsTest screen.descriptor.option("test-gap") == 5 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 16); + assert(registry.selectionIndex(101) == 17); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); From 7b221ff64f676606ea9a4121dff54e84b34334db Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 09:46:16 -0400 Subject: [PATCH 083/136] Add the Tidal flats map generator Grass islands standing on a wide expanse of walkable sand, with tide pools and lagoons over the flats and the odd grassy sandbar. Sand carries units freely but holds no building and no deposit, and nothing regrows beside it, so there are open-field battles from the first minute and no forward bases: an army on the flats fights far from any inn or tower while a defender holds a rim of towers on its island's edge, and expansion means taking another island whole. Every home island has a pond, an unscaled kit of wheat and wood beside it and a quarry; every neutral island is an oasis, a pond ringed by wheat out to its shore with a fruit grove or a stone deposit inside, so taking one means clearing it first; an island at the centre carries the orchard of all three fruits. Algae seeds every pool, which is where it regrows. Every island, pool and sandbar is designed in one colony's wedge and stamped into every wedge alike, so any colony count is fair by rotation. Home islands shrink to what the ring, the wrap and the central island leave room for, and the request is refused only when that is too small for a base. validateWorld rebuilds the design and requires every home's pond present and every colony and the central island reachable on foot from colony 0. Legacy id 18. Controls: home island size, extra islands, sandbars, tide pools, lagoons, coast roughness, a central-island switch and the five resource amounts. Strings in all 33 text tables; the framework reference documents the design and regains the City states catalog row that an earlier edit had overwritten; the registry contract test's injected-generator index moves up by one. Verified: 1000/1000 sweep rolls over ten configurations (2 to 12 colonies, 128 to 512 maps) pass generation and validation, MapGeneratorDefaultsTest and CustomGameSetupHarness pass, check_translations.py --strict passes, the client builds, and the map was played and approved by the maintainer over three iterations. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 14 + data/texts.br.txt | 14 + data/texts.ca.txt | 14 + data/texts.cz.txt | 14 + data/texts.de.txt | 14 + data/texts.dk.txt | 14 + data/texts.en.txt | 14 + data/texts.eo.txt | 14 + data/texts.es.txt | 14 + data/texts.eu.txt | 14 + data/texts.fa.txt | 14 + data/texts.fi.txt | 14 + data/texts.fr.txt | 14 + data/texts.gr.txt | 14 + data/texts.hu.txt | 14 + data/texts.id.txt | 14 + data/texts.it.txt | 14 + data/texts.ja.txt | 14 + data/texts.keys.txt | 7 + data/texts.ko.txt | 14 + data/texts.nl.txt | 14 + data/texts.pl.txt | 14 + data/texts.pt.txt | 14 + data/texts.ro.txt | 14 + data/texts.ru.txt | 14 + data/texts.si.txt | 14 + data/texts.sk.txt | 14 + data/texts.sr.txt | 14 + data/texts.sv.txt | 14 + data/texts.tr.txt | 14 + data/texts.uk.txt | 14 + data/texts.vi.txt | 14 + data/texts.zh-cn.txt | 14 + data/texts.zh-tw.txt | 14 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 37 +- src/SConscript | 1 + src/map/generator/core/GenerationRequest.h | 3 +- src/map/generator/core/GeneratorRegistry.cpp | 4 +- .../generators/TidalFlatsGenerator.cpp | 652 ++++++++++++++++++ .../generators/TidalFlatsGenerator.h | 11 + test/MapGeneratorDefaultsTest.cpp | 2 +- 41 files changed, 1172 insertions(+), 7 deletions(-) create mode 100644 src/map/generator/generators/TidalFlatsGenerator.cpp create mode 100644 src/map/generator/generators/TidalFlatsGenerator.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 5ed8c258a..e461e1b90 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1882,6 +1882,20 @@ OpenGL غير متوفر في هذا الإصدار. أسوار حجرية [Sand patches] بقع رملية +[Tidal flats] +السهول المدّية +[Sandbars] +الحواجز الرملية +[Tide pools] +برك المد +[Lagoons] +البحيرات الشاطئية +[Central island] +الجزيرة المركزية +[Tidal flats need at least one colony.] +تحتاج السهول المدّية إلى مستعمرة واحدة على الأقل. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +الجزر الأم لا تتسع لهذه الخريطة؛ استخدم خريطة أكبر أو مستعمرات أقل أو جزرًا أصغر. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] الأراضي المشاع والمضيق لا يتركان مساحة كافية للقواعد؛ استخدم خريطة أكبر أو مستعمرات أقل أو أراضي مشاع أصغر أو مضيقًا أضيق. [Wheat amount] diff --git a/data/texts.br.txt b/data/texts.br.txt index 10b259a86..ed247a21e 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1880,6 +1880,20 @@ Cidades-estado precisam de pelo menos uma colônia. Muralhas de pedra [Sand patches] Manchas de areia +[Tidal flats] +Planícies de maré +[Sandbars] +Bancos de areia +[Tide pools] +Poças de maré +[Lagoons] +Lagoas +[Central island] +Ilha central +[Tidal flats need at least one colony.] +As planícies de maré precisam de pelo menos uma colônia. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +As ilhas natais não cabem neste mapa; use um mapa maior, menos colônias ou ilhas menores. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] A área comum e o estreito deixam pouco espaço para as bases; use um mapa maior, menos colônias, uma área comum menor ou um estreito mais estreito. [Wheat amount] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 0f6f00220..3e9c1e1f1 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1892,6 +1892,20 @@ Les ciutats estat necessiten almenys una colònia. Murs de pedra [Sand patches] Clapes de sorra +[Tidal flats] +Planes de marea +[Sandbars] +Bancs de sorra +[Tide pools] +Basses de marea +[Lagoons] +Llacunes +[Central island] +Illa central +[Tidal flats need at least one colony.] +Les planes de marea necessiten almenys una colònia. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Les illes natals no caben en aquest mapa; fes servir un mapa més gran, menys colònies o illes més petites. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] L'àrea comuna i l'estret deixen massa poc espai per a les bases; fes servir un mapa més gran, menys colònies, una àrea comuna més petita o un estret més estret. [Wheat amount] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 2e2c1a767..4d2926ca6 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1884,6 +1884,20 @@ Městské státy potřebují alespoň jednu kolonii. Kamenné hradby [Sand patches] Písčiny +[Tidal flats] +Přílivové pláně +[Sandbars] +Písečné lavice +[Tide pools] +Přílivové tůně +[Lagoons] +Laguny +[Central island] +Střední ostrov +[Tidal flats need at least one colony.] +Přílivové pláně potřebují alespoň jednu kolonii. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Domovské ostrovy se na tuto mapu nevejdou; použijte větší mapu, méně kolonií nebo menší ostrovy. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Společné území a průliv ponechávají příliš málo místa pro domovské základny; použijte větší mapu, méně kolonií, menší společné území nebo užší průliv. [Wheat amount] diff --git a/data/texts.de.txt b/data/texts.de.txt index 4f5a11714..9f088fa12 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1884,6 +1884,20 @@ Stadtstaaten brauchen mindestens eine Kolonie. Steinmauern [Sand patches] Sandflächen +[Tidal flats] +Wattenmeer +[Sandbars] +Sandbänke +[Tide pools] +Gezeitentümpel +[Lagoons] +Lagunen +[Central island] +Zentrale Insel +[Tidal flats need at least one colony.] +Das Wattenmeer braucht mindestens eine Kolonie. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Die Heimatinseln passen nicht auf diese Karte; verwende eine größere Karte, weniger Kolonien oder kleinere Inseln. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Gemeinschaftsland und Meerenge lassen zu wenig Platz für die Heimatbasen; verwende eine größere Karte, weniger Kolonien, ein kleineres Gemeinschaftsland oder eine schmalere Meerenge. [Wheat amount] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index bf2d2285c..d561c4449 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1960,6 +1960,20 @@ Bystater kræver mindst én koloni. Stenmure [Sand patches] Sandpletter +[Tidal flats] +Vadehav +[Sandbars] +Sandbanker +[Tide pools] +Tidevandspøle +[Lagoons] +Laguner +[Central island] +Central ø +[Tidal flats need at least one colony.] +Vadehavet kræver mindst én koloni. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Hjemøerne passer ikke på dette kort; brug et større kort, færre kolonier eller mindre øer. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Fællesområdet og strædet efterlader for lidt plads til hjembaserne; brug et større kort, færre kolonier, et mindre fællesområde eller et smallere stræde. [Wheat amount] diff --git a/data/texts.en.txt b/data/texts.en.txt index e16a26384..37da93cc1 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1872,6 +1872,20 @@ City states need at least one colony. Stone walls [Sand patches] Sand patches +[Tidal flats] +Tidal flats +[Sandbars] +Sandbars +[Tide pools] +Tide pools +[Lagoons] +Lagoons +[Central island] +Central island +[Tidal flats need at least one colony.] +Tidal flats need at least one colony. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait. [Wheat amount] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 34effa2f1..76e3244d6 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1882,6 +1882,20 @@ Urboŝtatoj bezonas almenaŭ unu kolonion. Ŝtonaj muroj [Sand patches] Sablaj makuloj +[Tidal flats] +Tajdaj ebenaĵoj +[Sandbars] +Sablobenkoj +[Tide pools] +Tajdaj flakoj +[Lagoons] +Lagunoj +[Central island] +Centra insulo +[Tidal flats need at least one colony.] +Tajdaj ebenaĵoj bezonas almenaŭ unu kolonion. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +La hejmaj insuloj ne taŭgas en ĉi tiu mapo; uzu pli grandan mapon, malpli da kolonioj aŭ pli malgrandajn insulojn. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] La komunareo kaj la markolo lasas tro malmulte da spaco por la hejmaj bazoj; uzu pli grandan mapon, malpli da kolonioj, pli malgrandan komunareon aŭ pli mallarĝan markolon. [Wheat amount] diff --git a/data/texts.es.txt b/data/texts.es.txt index 12e22a1a3..0d31b2eef 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1884,6 +1884,20 @@ Las ciudades estado necesitan al menos una colonia. Muros de piedra [Sand patches] Parches de arena +[Tidal flats] +Marismas +[Sandbars] +Bancos de arena +[Tide pools] +Charcas de marea +[Lagoons] +Lagunas +[Central island] +Isla central +[Tidal flats need at least one colony.] +Las marismas necesitan al menos una colonia. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Las islas natales no caben en este mapa; usa un mapa más grande, menos colonias o islas más pequeñas. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] El área común y el estrecho dejan demasiado poco espacio para las bases; usa un mapa más grande, menos colonias, un área común más pequeña o un estrecho más angosto. [Wheat amount] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 6477c0af4..b58f11b08 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1894,6 +1894,20 @@ Hiri-estatuek gutxienez kolonia bat behar dute. Harrizko hormak [Sand patches] Harea-guneak +[Tidal flats] +Marea-lautadak +[Sandbars] +Hondar-bankuak +[Tide pools] +Marea-putzuak +[Lagoons] +Aintzirak +[Central island] +Erdiko uhartea +[Tidal flats need at least one colony.] +Marea-lautadek gutxienez kolonia bat behar dute. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Jatorrizko uharteak ez dira mapa honetan sartzen; erabili mapa handiagoa, kolonia gutxiago edo uharte txikiagoak. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Eremu komunak eta itsasarteak leku gutxiegi uzten diete oinarriei; erabili mapa handiagoa, kolonia gutxiago, eremu komun txikiagoa edo itsasarte estuagoa. [Wheat amount] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 6aeabaec0..075544d2c 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1882,6 +1882,20 @@ OpenGL در این بیلد موجود نیست. دیوارهای سنگی [Sand patches] لکه‌های شنی +[Tidal flats] +پهنه‌های جزر و مدی +[Sandbars] +پشته‌های شنی +[Tide pools] +حوضچه‌های جزر و مدی +[Lagoons] +مرداب‌ها +[Central island] +جزیرهٔ مرکزی +[Tidal flats need at least one colony.] +پهنه‌های جزر و مدی دست‌کم به یک مستعمره نیاز دارند. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +جزیره‌های اصلی در این نقشه جا نمی‌شوند؛ از نقشهٔ بزرگ‌تر، مستعمرات کمتر یا جزیره‌های کوچک‌تر استفاده کنید. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] سرزمین مشترک و تنگه فضای کافی برای پایگاه‌ها باقی نمی‌گذارند؛ از نقشهٔ بزرگ‌تر، مستعمرات کمتر، سرزمین مشترک کوچک‌تر یا تنگهٔ باریک‌تر استفاده کنید. [Wheat amount] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 4cd818073..a95d69252 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1884,6 +1884,20 @@ Kaupunkivaltiot tarvitsevat vähintään yhden siirtokunnan. Kivimuurit [Sand patches] Hiekkaläikät +[Tidal flats] +Vuorovesitasangot +[Sandbars] +Hiekkasärkät +[Tide pools] +Vuorovesilammikot +[Lagoons] +Laguunit +[Central island] +Keskussaari +[Tidal flats need at least one colony.] +Vuorovesitasangot tarvitsevat vähintään yhden siirtokunnan. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Kotisaaret eivät mahdu tälle kartalle; käytä suurempaa karttaa, vähemmän siirtokuntia tai pienempiä saaria. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Yhteisalue ja salmi jättävät liian vähän tilaa kotitukikohdille; käytä suurempaa karttaa, vähemmän siirtokuntia, pienempää yhteisaluetta tai kapeampaa salmea. [Wheat amount] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 7dcd0e0b6..cefb93b98 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1894,6 +1894,20 @@ Les cités-États ont besoin d'au moins une colonie. Murs de pierre [Sand patches] Zones de sable +[Tidal flats] +Estran +[Sandbars] +Bancs de sable +[Tide pools] +Mares de marée +[Lagoons] +Lagunes +[Central island] +Île centrale +[Tidal flats need at least one colony.] +L'estran a besoin d'au moins une colonie. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Les îles natales ne tiennent pas sur cette carte ; utilisez une carte plus grande, moins de colonies ou des îles plus petites. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] La zone commune et le détroit laissent trop peu de place aux bases ; utilisez une carte plus grande, moins de colonies, une zone commune plus petite ou un détroit plus étroit. [Wheat amount] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index fd3348e13..ede972b7d 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1954,6 +1954,20 @@ Cortex Πέτρινα τείχη [Sand patches] Αμμώδεις εκτάσεις +[Tidal flats] +Παλιρροιακές πεδιάδες +[Sandbars] +Αμμοσύρτεις +[Tide pools] +Παλιρροιακές λίμνες +[Lagoons] +Λιμνοθάλασσες +[Central island] +Κεντρικό νησί +[Tidal flats need at least one colony.] +Οι παλιρροιακές πεδιάδες χρειάζονται τουλάχιστον μία αποικία. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Τα νησιά-πατρίδες δεν χωρούν σε αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη, λιγότερες αποικίες ή μικρότερα νησιά. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Η κοινή περιοχή και ο πορθμός αφήνουν πολύ λίγο χώρο για τις βάσεις· χρησιμοποιήστε μεγαλύτερο χάρτη, λιγότερες αποικίες, μικρότερη κοινή περιοχή ή στενότερο πορθμό. [Wheat amount] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 56efdf65f..52eca403c 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1884,6 +1884,20 @@ A városállamokhoz legalább egy kolónia kell. Kőfalak [Sand patches] Homokfoltok +[Tidal flats] +Árapálysíkság +[Sandbars] +Homokpadok +[Tide pools] +Árapálytavak +[Lagoons] +Lagúnák +[Central island] +Középső sziget +[Tidal flats need at least one colony.] +Az árapálysíksághoz legalább egy kolónia kell. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Az otthoni szigetek nem férnek el ezen a térképen; használj nagyobb térképet, kevesebb kolóniát vagy kisebb szigeteket. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] A közös terület és a szoros túl kevés helyet hagy a bázisoknak; használj nagyobb térképet, kevesebb kolóniát, kisebb közös területet vagy keskenyebb szorost. [Wheat amount] diff --git a/data/texts.id.txt b/data/texts.id.txt index 47fd6bbb3..551c64fc3 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1880,6 +1880,20 @@ Negara kota membutuhkan setidaknya satu koloni. Tembok batu [Sand patches] Petak pasir +[Tidal flats] +Dataran pasang surut +[Sandbars] +Gosong pasir +[Tide pools] +Kolam pasang surut +[Lagoons] +Laguna +[Central island] +Pulau tengah +[Tidal flats need at least one colony.] +Dataran pasang surut membutuhkan setidaknya satu koloni. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Pulau-pulau asal tidak muat di peta ini; gunakan peta yang lebih besar, koloni lebih sedikit, atau pulau lebih kecil. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Tanah bersama dan selat menyisakan terlalu sedikit ruang untuk pangkalan; gunakan peta yang lebih besar, koloni lebih sedikit, tanah bersama lebih kecil, atau selat lebih sempit. [Wheat amount] diff --git a/data/texts.it.txt b/data/texts.it.txt index d190590e9..85d7c10ae 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1946,6 +1946,20 @@ Le città-stato hanno bisogno di almeno una colonia. Mura di pietra [Sand patches] Chiazze di sabbia +[Tidal flats] +Piane di marea +[Sandbars] +Banchi di sabbia +[Tide pools] +Pozze di marea +[Lagoons] +Lagune +[Central island] +Isola centrale +[Tidal flats need at least one colony.] +Le piane di marea hanno bisogno di almeno una colonia. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Le isole natali non entrano in questa mappa; usa una mappa più grande, meno colonie o isole più piccole. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] L'area comune e lo stretto lasciano troppo poco spazio alle basi; usa una mappa più grande, meno colonie, un'area comune più piccola o uno stretto più stretto. [Wheat amount] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 51f3feb4a..67e4b906d 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1880,6 +1880,20 @@ Globulation 2 のカーソルを表示します。 石の城壁 [Sand patches] 砂地 +[Tidal flats] +干潟 +[Sandbars] +砂州 +[Tide pools] +潮だまり +[Lagoons] +潟湖 +[Central island] +中央の島 +[Tidal flats need at least one colony.] +干潟には少なくとも一つのコロニーが必要です。 +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +母島がこのマップに収まりません。より大きなマップ、より少ないコロニー、またはより小さな島を使ってください。 [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] 共有地と海峡が本拠地に残す余地が少なすぎます。より大きなマップ、より少ないコロニー、より小さな共有地、またはより狭い海峡を使ってください。 [Wheat amount] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 4350b1c0e..2296f7e14 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -934,6 +934,13 @@ [City states need at least one colony.] [Stone walls] [Sand patches] +[Tidal flats] +[Sandbars] +[Tide pools] +[Lagoons] +[Central island] +[Tidal flats need at least one colony.] +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] [Wheat amount] [Wood amount] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index e2cc64df9..9685b4a0c 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1880,6 +1880,20 @@ Globulation 2 커서를 표시합니다. 돌 성벽 [Sand patches] 모래밭 +[Tidal flats] +갯벌 +[Sandbars] +모래톱 +[Tide pools] +조수 웅덩이 +[Lagoons] +석호 +[Central island] +중앙 섬 +[Tidal flats need at least one colony.] +갯벌에는 최소 한 개의 식민지가 필요합니다. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +본섬이 이 지도에 들어가지 않습니다. 더 큰 지도, 더 적은 식민지 또는 더 작은 섬을 사용하세요. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] 공유지와 해협이 본거지에 남기는 공간이 너무 적습니다. 더 큰 지도, 더 적은 식민지, 더 작은 공유지 또는 더 좁은 해협을 사용하세요. [Wheat amount] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 91a7b6f72..90a7b8e2d 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1908,6 +1908,20 @@ Stadstaten hebben minstens één kolonie nodig. Stenen muren [Sand patches] Zandvlakken +[Tidal flats] +Wadden +[Sandbars] +Zandbanken +[Tide pools] +Getijdenpoelen +[Lagoons] +Lagunes +[Central island] +Centraal eiland +[Tidal flats need at least one colony.] +De wadden hebben minstens één kolonie nodig. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +De thuiseilanden passen niet op deze kaart; gebruik een grotere kaart, minder kolonies of kleinere eilanden. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Het gemeenschappelijke gebied en de zeestraat laten te weinig ruimte voor de thuisbases; gebruik een grotere kaart, minder kolonies, een kleiner gemeenschappelijk gebied of een smallere zeestraat. [Wheat amount] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index ebebe2e11..7697714db 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1884,6 +1884,20 @@ Miasta-państwa potrzebują co najmniej jednej kolonii. Kamienne mury [Sand patches] Płaty piasku +[Tidal flats] +Równiny pływowe +[Sandbars] +Łachy piaskowe +[Tide pools] +Rozlewiska pływowe +[Lagoons] +Laguny +[Central island] +Wyspa środkowa +[Tidal flats need at least one colony.] +Równiny pływowe potrzebują co najmniej jednej kolonii. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Wyspy macierzyste nie mieszczą się na tej mapie; użyj większej mapy, mniejszej liczby kolonii lub mniejszych wysp. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Teren wspólny i cieśnina zostawiają zbyt mało miejsca na bazy; użyj większej mapy, mniejszej liczby kolonii, mniejszego terenu wspólnego lub węższej cieśniny. [Wheat amount] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 757893f9d..de8abfefa 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1890,6 +1890,20 @@ As cidades-estado precisam de pelo menos uma colónia. Muralhas de pedra [Sand patches] Manchas de areia +[Tidal flats] +Planícies de maré +[Sandbars] +Bancos de areia +[Tide pools] +Poças de maré +[Lagoons] +Lagoas +[Central island] +Ilha central +[Tidal flats need at least one colony.] +As planícies de maré precisam de pelo menos uma colónia. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +As ilhas natais não cabem neste mapa; use um mapa maior, menos colónias ou ilhas mais pequenas. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] A área comum e o estreito deixam demasiado pouco espaço para as bases; use um mapa maior, menos colónias, uma área comum mais pequena ou um estreito mais estreito. [Wheat amount] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 43ffa4c45..3494b1d89 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1884,6 +1884,20 @@ Orașele-stat au nevoie de cel puțin o colonie. Ziduri de piatră [Sand patches] Petice de nisip +[Tidal flats] +Câmpii de maree +[Sandbars] +Bancuri de nisip +[Tide pools] +Bălți de maree +[Lagoons] +Lagune +[Central island] +Insula centrală +[Tidal flats need at least one colony.] +Câmpiile de maree au nevoie de cel puțin o colonie. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Insulele natale nu încap pe această hartă; folosește o hartă mai mare, mai puține colonii sau insule mai mici. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Teritoriul comun și strâmtoarea lasă prea puțin loc pentru baze; folosește o hartă mai mare, mai puține colonii, un teritoriu comun mai mic sau o strâmtoare mai îngustă. [Wheat amount] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index e2065b4d0..fb2373faa 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1882,6 +1882,20 @@ OpenGL недоступен в этой сборке. Каменные стены [Sand patches] Песчаные участки +[Tidal flats] +Приливные отмели +[Sandbars] +Песчаные банки +[Tide pools] +Приливные заводи +[Lagoons] +Лагуны +[Central island] +Центральный остров +[Tidal flats need at least one colony.] +Приливным отмелям нужна хотя бы одна колония. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Родные острова не помещаются на этой карте; используйте карту побольше, меньше колоний или острова поменьше. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Общая земля и пролив оставляют слишком мало места для баз; используйте карту побольше, меньше колоний, общую землю поменьше или пролив поуже. [Wheat amount] diff --git a/data/texts.si.txt b/data/texts.si.txt index 78d4f369e..6fd1f165b 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1884,6 +1884,20 @@ Mestne države potrebujejo vsaj eno kolonijo. Kamniti zidovi [Sand patches] Peščene zaplate +[Tidal flats] +Plimske ravnice +[Sandbars] +Peščene sipine +[Tide pools] +Plimski tolmuni +[Lagoons] +Lagune +[Central island] +Osrednji otok +[Tidal flats need at least one colony.] +Plimske ravnice potrebujejo vsaj eno kolonijo. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Domači otoki ne gredo na ta zemljevid; uporabite večji zemljevid, manj kolonij ali manjše otoke. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Skupno ozemlje in ožina puščata premalo prostora za domače baze; uporabite večji zemljevid, manj kolonij, manjše skupno ozemlje ali ožjo ožino. [Wheat amount] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index ee334a154..5f1c585af 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1892,6 +1892,20 @@ Mestské štáty potrebujú aspoň jednu kolóniu. Kamenné hradby [Sand patches] Piesčiny +[Tidal flats] +Prílivové pláne +[Sandbars] +Piesočné lavice +[Tide pools] +Prílivové mláky +[Lagoons] +Lagúny +[Central island] +Stredný ostrov +[Tidal flats need at least one colony.] +Prílivové pláne potrebujú aspoň jednu kolóniu. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Domovské ostrovy sa na túto mapu nezmestia; použite väčšiu mapu, menej kolónií alebo menšie ostrovy. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Spoločné územie a prieliv ponechávajú príliš málo miesta pre domovské základne; použite väčšiu mapu, menej kolónií, menšie spoločné územie alebo užší prieliv. [Wheat amount] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index a5325797c..b7e777353 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1884,6 +1884,20 @@ OpenGL није доступан у овој верзији. Камени зидови [Sand patches] Пешчане површине +[Tidal flats] +Плимске равнице +[Sandbars] +Пешчани спрудови +[Tide pools] +Плимске баре +[Lagoons] +Лагуне +[Central island] +Средишње острво +[Tidal flats need at least one colony.] +Плимским равницама треба бар једна колонија. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Матична острва не стају на ову мапу; користите већу мапу, мање колонија или мања острва. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Заједничко подручје и мореуз остављају премало места за базе; користите већу мапу, мање колонија, мање заједничко подручје или ужи мореуз. [Wheat amount] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 241350812..9715bd0b5 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1894,6 +1894,20 @@ Stadsstater kräver minst en koloni. Stenmurar [Sand patches] Sandfläckar +[Tidal flats] +Tidvattenslätter +[Sandbars] +Sandbankar +[Tide pools] +Tidvattenpölar +[Lagoons] +Laguner +[Central island] +Central ö +[Tidal flats need at least one colony.] +Tidvattenslätter kräver minst en koloni. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Hemöarna får inte plats på den här kartan; använd en större karta, färre kolonier eller mindre öar. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Allmänningen och sundet lämnar för lite plats åt hembaserna; använd en större karta, färre kolonier, en mindre allmänning eller ett smalare sund. [Wheat amount] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 133e54a4c..86ac296f2 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1892,6 +1892,20 @@ Sınır bölgesinin zenginliği Taş duvarlar [Sand patches] Kum lekeleri +[Tidal flats] +Gelgit düzlükleri +[Sandbars] +Kum tepecikleri +[Tide pools] +Gelgit gölcükleri +[Lagoons] +Lagünler +[Central island] +Merkez ada +[Tidal flats need at least one colony.] +Gelgit düzlükleri için en az bir koloni gerekir. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Ana adalar bu haritaya sığmıyor; daha büyük bir harita, daha az koloni ya da daha küçük adalar kullanın. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Ortak alan ve boğaz üslere çok az yer bırakıyor; daha büyük bir harita, daha az koloni, daha küçük bir ortak alan ya da daha dar bir boğaz kullanın. [Wheat amount] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 4408ff9cc..80f59540d 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1880,6 +1880,20 @@ OpenGL недоступний у цій збірці. Кам'яні стіни [Sand patches] Піщані ділянки +[Tidal flats] +Припливні мілини +[Sandbars] +Піщані банки +[Tide pools] +Припливні заводі +[Lagoons] +Лагуни +[Central island] +Центральний острів +[Tidal flats need at least one colony.] +Припливним мілинам потрібна хоча б одна колонія. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Рідні острови не вміщаються на цій карті; використайте більшу карту, менше колоній або менші острови. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Спільна земля і протока залишають замало місця для баз; використайте більшу карту, менше колоній, меншу спільну землю або вужчу протоку. [Wheat amount] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 383b0715c..4921e4920 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1880,6 +1880,20 @@ Thành bang cần ít nhất một thuộc địa. Tường đá [Sand patches] Bãi cát +[Tidal flats] +Bãi triều +[Sandbars] +Cồn cát +[Tide pools] +Vũng triều +[Lagoons] +Đầm phá +[Central island] +Đảo trung tâm +[Tidal flats need at least one colony.] +Bãi triều cần ít nhất một thuộc địa. +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +Các đảo quê hương không vừa với bản đồ này; hãy dùng bản đồ lớn hơn, ít thuộc địa hơn hoặc đảo nhỏ hơn. [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] Vùng đất chung và eo biển để lại quá ít chỗ cho các căn cứ; hãy dùng bản đồ lớn hơn, ít thuộc địa hơn, vùng đất chung nhỏ hơn hoặc eo biển hẹp hơn. [Wheat amount] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 929b32c57..877ed7402 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1880,6 +1880,20 @@ OpenGL 在此版本中不可用。 石墙 [Sand patches] 沙地 +[Tidal flats] +潮间带 +[Sandbars] +沙洲 +[Tide pools] +潮池 +[Lagoons] +潟湖 +[Central island] +中央岛 +[Tidal flats need at least one colony.] +潮间带至少需要一个殖民地。 +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +母岛无法容纳在这张地图上;请使用更大的地图、更少的殖民地或更小的岛屿。 [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] 公地区域与海峡留给基地的空间太少;请使用更大的地图、更少的殖民地、更小的公地区域或更窄的海峡。 [Wheat amount] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 63e5d3e0b..ac1b18b7f 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1956,6 +1956,20 @@ OpenGL 在此版本中不可用。 石牆 [Sand patches] 沙地 +[Tidal flats] +潮間帶 +[Sandbars] +沙洲 +[Tide pools] +潮池 +[Lagoons] +潟湖 +[Central island] +中央島 +[Tidal flats need at least one colony.] +潮間帶至少需要一個殖民地。 +[The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands.] +母島無法容納在這張地圖上;請使用更大的地圖、更少的殖民地或更小的島嶼。 [The commons and the strait leave too little room for the home bases; use a bigger map, fewer colonies, a smaller commons or a narrower strait.] 公地區域與海峽留給基地的空間太少;請使用更大的地圖、更少的殖民地、更小的公地區域或更窄的海峽。 [Wheat amount] diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 552d6480e..434773114 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -41,7 +41,8 @@ reused after a generator is retired. | `stone-highlands` | 14 | Stone highlands | Its own — see below | | `symmetric-arena` | 15 | Symmetric arena | Its own — see below | | `ring-world` | 16 | Ring world | Its own — see below | -| `city-states` | 17 | City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard) and the sea's algae and island prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | +| `city-states` | 17 | City states | Its own — see below | +| `tidal-flats` | 18 | Tidal flats | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -84,7 +85,8 @@ ambient layer but still leaves every colony a start. Maze keeps its explicit den | Stone highlands | The ponds' farmland (wheat and wood) and algae, and how much of the ridgeline is two tiles thick (stone, 0 to 200); kits are unscaled and Fruit still counts groves | Fruit in home valleys (off) | | Symmetric arena | The farmland's wheat and wood, stone outcrops and algae, on top of Resource richness; fruit (0 to 100) keeps that share of the orchard's groves nearest the centre, never fewer than three | Moat (on); Stone in the orchard (on) | | Ring world | The ambient scatter's wheat, wood, stone and fruit, and the shallows' algae; starter patches and island prizes are unscaled | Winding belt (on); Colonies on both coasts (on) | -| City states | The commons' fields and grove, and everything in the New World: farmland, stone outcrops, fruit groves, the heart's orchard and the shallows' algae; every home's kit and its wall are unscaled | Bridges shared by pairs (on): off, every gap between neighbouring compounds gets a bridge | +| City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard) and the sea's algae and island prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | +| Tidal flats | Every island's ambient fields and outcrops and every island's prize; each home's kit is unscaled | Central island (on): off, the middle of the map is flats and there is no orchard | `scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of @@ -156,8 +158,8 @@ fixed resource pass: states' walls; they are treated like terrain, never cleared and never looked past. It wraps each boot tile onto the map first, since the height-field generators' fallback site search can hand over one past the edge. - RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands, Ring world and City states - call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount + RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands, Ring world, City states and + Tidal flats call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount other than 100. Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane down every passage, and its `validateWorld` confirms every colony can still walk to every @@ -464,6 +466,33 @@ causeways stop being the only way in. commons or another home with the causeways shut, and the colonies' walks to their landings within twelve steps of each other. `validateRequest` needs at least 20 tiles of home depth. +## Tidal flats + +Grass islands standing on a wide expanse of walkable sand, with tide pools and lagoons over the +flats and the odd grassy sandbar. Sand carries units freely but holds no building and no deposit, +and nothing regrows beside it, so there are open-field battles from the first minute and no forward +bases: an army on the flats fights far from any inn or tower while a defender holds a rim of towers +on its island's edge, and expansion means taking another island whole. + +- **Layout.** Every home island sits on a ring at 58% of the half side, one per colony evenly + spaced from a random start, with a radius of `home-island-size` percent of the half side or as + much as the ring, the wrap and the central island leave room for. Everything else - + `extra-islands` neutral islands, `sandbars` and `lagoons` per colony and `tide-pools` per + 128×128 of flats - is placed in one wedge's frame, keeping clear of the home island, the wrap, the + central island and each other, and stamped into every wedge alike, so the layout is fair for any + colony count. `coast-roughness` shapes every island. `validateRequest` refuses a map whose home + islands would shrink below nine tiles of radius. +- **Islands.** Each home has a pond at its middle, an unscaled kit of 40 wheat and 30 wood on the + pond's two sides and a quarry towards the map's centre, then scaled ambient farmland on its + fertile ground and an outcrop. Every neutral island is an oasis: a pond, and every other tile + of it under unscaled wheat, so taking one means clearing it first, with one prize inside, a + fruit grove or a stone deposit in turn. With `central-island` (on) an island at the centre carries a pond, + the orchard of all three fruits and a quarry. Algae seeds every pool and lagoon, which is exactly + where it regrows. Nothing is kept clear because the flats hold nothing. +- **Checked, not assumed.** `validateWorld` rebuilds the design and requires every home's pond + present and every colony and the central island reachable on foot from colony 0, with water, + buildings and every resource blocking. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort diff --git a/src/SConscript b/src/SConscript index 42b232f6a..abd3b9947 100644 --- a/src/SConscript +++ b/src/SConscript @@ -191,6 +191,7 @@ map/generator/generators/ShatteredCoastGenerator.cpp map/generator/generators/StoneHighlandsGenerator.cpp map/generator/generators/SwampGenerator.cpp map/generator/generators/SymmetricArenaGenerator.cpp +map/generator/generators/TidalFlatsGenerator.cpp map/generator/generators/UniformGenerator.cpp map/generator/generators/WatershedGenerator.cpp map/generator/shared/Distances.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index ab3fbc917..6ccd40476 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -33,7 +33,8 @@ struct GenerationRequest { eSTONEHIGHLANDS = 14, eSYMMETRICARENA = 15, eRINGWORLD = 16, - eCITYSTATES = 17 + eCITYSTATES = 17, + eTIDALFLATS = 18 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 10ab74817..dfc3d96f1 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -15,6 +15,7 @@ #include "StoneHighlandsGenerator.h" #include "SwampGenerator.h" #include "SymmetricArenaGenerator.h" +#include "TidalFlatsGenerator.h" #include "UniformGenerator.h" #include "WatershedGenerator.h" #include @@ -82,7 +83,8 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), stoneHighlandsDefinition(), symmetricArenaDefinition(), - ringWorldDefinition(), cityStatesDefinition(), ruggedArchipelagoDefinition(), + ringWorldDefinition(), cityStatesDefinition(), tidalFlatsDefinition(), + ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), riverDefinition(), uniformDefinition()}); return registry; diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp new file mode 100644 index 000000000..ae22f3169 --- /dev/null +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "TidalFlatsGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "HeightMap.h" +#include "Resources.h" +#include "Settlements.h" +#include "Unit.h" +#include +#include +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Tidal flats: grass islands standing on a wide expanse of walkable sand, with tide pools and +// lagoons of water over the flats and the odd grassy sandbar. Sand carries units freely but holds +// no building and no deposit, and nothing regrows beside it, so there are open-field battles from +// the first minute and no forward bases: an army on the flats fights far from any inn or tower, +// while a defender holds a rim of towers on the island's edge. Expansion means taking another +// island whole. Every colony's island has a pond inland, fields beside it and a quarry; smaller +// islands round the flats carry a prize each, and an island at the centre of the map carries the +// orchard of all three fruits. +// +// Every island, pool and sandbar is designed in the frame of a colony's wedge - arc across it and +// radius out from the centre - and stamped into every wedge alike, so whatever the roll, every +// colony gets the same neighbourhood turned round the centre, and the layout is fair for any +// colony count. The whole design is a pure function of the request, so validateWorld rebuilds +// it and checks the finished world. +namespace { + +constexpr double pi = 3.14159265358979323846; + +// The home islands' centres lie this far out, as a share of the half side, and keep this much sand +// from each other, from the map's wrap and from every other island. +constexpr double kHomeRing = 0.58; +constexpr int kIslandGap = 8; +// Every home starts identical: wheat and wood beside its pond, a quarry, all unscaled. +constexpr int kHomeWheat = 40; +constexpr int kHomeWood = 30; +constexpr int kSwarmClearance = 2; +// Neutral islands' radii as shares of the home islands' (never below a size that holds a pond and +// a field), and the central island's as a share of the half side. +constexpr double kNeutralLow = 0.45, kNeutralHigh = 0.6; +constexpr double kNeutralMinimum = 7.0; +// Every neutral island is an oasis: a pond, and every other tile of it under wheat, so taking one +// means clearing it first. +constexpr double kCentralShare = 0.13; +// Sandbars are just big enough to hold a tower or an inn. +constexpr double kSandbarLow = 3.0, kSandbarHigh = 4.5; + +struct Torus { + int w, h; + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + int offsetX(int from, int to) const { + const int d = x(to - from); + return d > w / 2 ? d - w : d; + } + int offsetY(int from, int to) const { + const int d = y(to - from); + return d > h / 2 ? d - h : d; + } +}; + +std::vector stepsFrom(const Torus &t, const std::vector &source, + const std::vector &open) { + std::vector dist(size_t(t.w) * t.h, -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) { + const int x = queue[head] % t.w, y = queue[head] / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int n = t.at(x + dx, y + dy); + if (dist[n] < 0 && open[n]) { + dist[n] = dist[queue[head]] + 1; + queue.push_back(n); + } + } + } + return dist; +} + +struct Geometry { + int teams, half; + double homeRing, homeRadius, centralRadius, amplitude; +}; + +Geometry geometryFor(const GenerationRequest &r) { + const TidalFlatsOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; + g.homeRing = kHomeRing * g.half; + g.centralRadius = o.centralIsland ? std::max(8.0, kCentralShare * g.half) : 0.0; + g.amplitude = o.coastRoughness / 100.0 * 0.35; + // The control is the islands' radius as a share of the half side; with many colonies on the + // ring, or a small map, the islands shrink to what the ring, the wrap and the central island + // leave room for, and validateRequest refuses a map where that is too small for a base. + double radius = o.homeIslandSize / 100.0 * g.half; + const double reach = 1 + g.amplitude; + if (g.teams >= 2) + radius = std::min(radius, (2 * g.homeRing * std::sin(pi / g.teams) - kIslandGap) / (2 * reach)); + radius = std::min(radius, (g.half - kIslandGap / 2.0 - g.homeRing) / reach); + if (g.centralRadius > 0) + radius = std::min(radius, (g.homeRing - g.centralRadius * 1.3 - kIslandGap) / reach); + g.homeRadius = std::floor(radius); + return g; +} + +// A round feature in a wedge's frame: arc offset from the wedge's middle, radius from the centre, +// and what it is. Shapes are evaluated in the wedge's frame too, so every wedge gets the same. +enum Kind { HomeIsland = 0, Neutral, Central, Sandbar, Pool, Lagoon }; +struct Blob { + Kind kind; + double s, r, reach, stretch, turn; + int prize; // neutral islands: 0 fruit, 1 stone, 2 wheat + RadialShape shape; + bool holds(double ds, double dr) const { + // Rotate into the shape's own frame and undo its stretch. + const double c = std::cos(turn), sn = std::sin(turn); + const double px = (ds * c + dr * sn) / stretch, py = (-ds * sn + dr * c) * stretch; + return std::hypot(px, py) < shape.radiusAt(std::atan2(py, px)); + } +}; + +struct Layout { + Torus t{1, 1}; + Geometry g{}; + int cx = 0, cy = 0; + double phase = 0; + std::vector blobs; // in the wedge's frame + std::vector homeX, homeY; // every home island's centre + std::vector pondX, pondY; // and its pond's + std::vector grass, water, island; // per tile; island: which home, else -1 below + std::vector islandOf; // -1 flats, -2 central, -3 neutral or sandbar, k a home + std::vector prizeOf; // neutral islands: the prize, else -1 + std::vector radius, angle; + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) { + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.w * t.h, teams = g.teams; + const TidalFlatsOptions o(request); + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("flats-layout", 3600) / 3600.0 * 2 * pi; + const double wedge = 2 * pi / teams; + const double arcHalfAt = [&](double d) { return teams < 2 ? pi * d : pi * d / teams; }(1.0); + + // Everything in one wedge, then stamped into every wedge: the home island in the middle of the + // ring, then neutral islands, sandbars, lagoons and pools that keep clear of each other, of the + // central island and of the map's wrap. + std::vector &blobs = L.blobs; + blobs.push_back({HomeIsland, 0.0, g.homeRing, 0, 1.0, 0.0, -1, + RadialShape(g.homeRadius, g.amplitude, context, "flats-coast")}); + blobs.back().reach = blobs.back().shape.maximumRadius(); + const auto fits = [&](double s0, double r0, double reach, double gap) { + if (r0 - reach < 6 + g.centralRadius * 1.3 + (g.centralRadius > 0 ? gap : 0)) + return false; + if (r0 + reach > g.half - 4) + return false; + if (teams >= 2 && std::abs(s0) + reach + gap / 2 > arcHalfAt * r0) + return false; + for (const Blob &b : blobs) { + const double need = reach + b.reach + std::max(gap, b.kind == HomeIsland ? double(kIslandGap) : gap); + if (std::hypot(s0 - b.s, r0 - b.r) < need) + return false; + } + return true; + }; + const auto scatter = [&](Kind kind, int wanted, double low, double high, double roughness, + double gap, const char *stream, bool stretched) { + for (int attempt = 0, placed = 0; attempt < wanted * 80 && placed < wanted; ++attempt) { + const double r0 = 8 + context.bounded(stream, 1000) / 1000.0 * (g.half - 16); + const double s0 = (context.bounded(stream, 2001) / 1000.0 - 1) * arcHalfAt * r0; + const double radius = low + context.bounded(stream, 1000) / 1000.0 * (high - low); + const double stretch = stretched ? 1.2 + context.bounded(stream, 81) / 100.0 : 1.0; + const double turn = stretched ? context.bounded(stream, 3600) / 3600.0 * pi : 0.0; + RadialShape shape(radius, roughness, context, stream); + const double reach = shape.maximumRadius() * stretch; + if (!fits(s0, r0, reach, gap)) + continue; + blobs.push_back({kind, s0, r0, reach, stretch, turn, placed % 2, shape}); + ++placed; + } + }; + scatter(Neutral, o.extraIslands, std::max(kNeutralMinimum, kNeutralLow * g.homeRadius), + std::max(kNeutralMinimum + 2, kNeutralHigh * g.homeRadius), 0.35, kIslandGap, + "flats-islands", false); + scatter(Sandbar, o.sandbars, kSandbarLow, kSandbarHigh, 0.3, 6, "flats-sandbars", false); + scatter(Lagoon, o.lagoons, 8.0, 14.0, 0.35, 6, "flats-lagoons", true); + const double wedgeArea = wedge * g.half * g.half / 2; + scatter(Pool, int(std::lround(o.tidePools * wedgeArea / 16384.0)), 1.5, 4.0, 0.4, 4, + "flats-pools", false); + const RadialShape central(std::max(1.0, g.centralRadius), g.amplitude * 0.6, context, "flats-central"); + + L.grass.assign(n, 0); + L.water.assign(n, 0); + L.islandOf.assign(n, -1); + L.prizeOf.assign(n, -1); + L.radius.assign(n, 0.0); + L.angle.assign(n, 0.0); + L.homeX.assign(teams, 0); + L.homeY.assign(teams, 0); + L.pondX.assign(teams, 0); + L.pondY.assign(teams, 0); + for (int k = 0; k < teams; ++k) { + const double a = L.phase + wedge * (k + 0.5); + L.homeX[k] = t.x(L.cx + int(std::lround(g.homeRing * std::cos(a)))); + L.homeY[k] = t.y(L.cy + int(std::lround(g.homeRing * std::sin(a)))); + L.pondX[k] = L.homeX[k]; + L.pondY[k] = L.homeY[k]; + } + const double pondRadius = std::clamp(0.2 * g.homeRadius, 3.0, 6.0); + const RadialShape pond(pondRadius, 0.3, context, "flats-ponds"); + const RadialShape oasis(2.5, 0.3, context, "flats-ponds"); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const int i = y * t.w + x; + const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); + const double d = std::hypot(double(dx), double(dy)); + const double theta = std::atan2(double(dy), double(dx)); + L.radius[i] = d; + L.angle[i] = theta; + if (g.centralRadius > 0 && d < central.radiusAt(theta)) { + L.grass[i] = 1; + L.islandOf[i] = -2; + if (d < std::max(1.5, 0.22 * g.centralRadius)) + L.water[i] = 1; + continue; + } + const double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); + const int k = std::min(teams - 1, int(turn / wedge)); + const double u = turn / wedge - k; + const double s = d * (u - 0.5) * wedge; + for (const Blob &b : blobs) { + if (!b.holds(s - b.s, d - b.r)) + continue; + if (b.kind == Pool || b.kind == Lagoon) { + L.water[i] = 1; + L.grass[i] = 0; + L.islandOf[i] = -1; + } else { + L.grass[i] = 1; + L.islandOf[i] = b.kind == HomeIsland ? k : -3; + if (b.kind == Neutral) + L.prizeOf[i] = b.prize; + if (b.kind == HomeIsland && + std::hypot(s - b.s, d - b.r) < pond.radiusAt(std::atan2(d - b.r, s - b.s))) + L.water[i] = 1; + if (b.kind == Neutral && + std::hypot(s - b.s, d - b.r) < oasis.radiusAt(std::atan2(d - b.r, s - b.s))) + L.water[i] = 1; + } + } + } + return L; +} + +// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), +// so every land corner beside water becomes sand. +void layBeaches(std::vector &terrain, const Torus &t) { + const std::vector original(terrain); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { + const int i = y * t.w + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[t.at(x + dx, y + dy)] == WATER; + if (shore) + terrain[i] = SAND; + } +} + +template +int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) { + std::vector queued(size_t(t.w) * t.h, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) { + const int i = frontier[head], x = i % t.w, y = i / t.w; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) { + const int m = t.at(x + step[0], y + step[1]); + if (!queued[m]) { + queued[m] = 1; + frontier.push_back(m); + } + } + } + return placed; +} + +bool clearGround(const Map &map, int x, int y) { + return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; +} + +// The nearest eligible tile to a point, within a box. +template +int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) { + int seed = -1, nearest = INT_MAX; + for (int dy = -within; dy <= within; ++dy) + for (int dx = -within; dx <= within; ++dx) { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) { + nearest = dx * dx + dy * dy; + seed = i; + } + } + return seed; +} + +// Every home's kit, identical and unscaled: wheat and wood beside the pond on the side away from +// the swarm, a quarry on the island's rim towards the centre of the map; then its own scaled +// ambient farmland and outcrops. +void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const TidalFlatsOptions &o) { + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector reserved(n, 0); + for (int team = 0; team < L.g.teams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap split(t.w, t.h, context.stream("flats-home-split")); + split.makePlain(6); + const double pondRadius = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 3; + for (int team = 0; team < L.g.teams; ++team) { + const auto eligible = [&](int i) { + return L.islandOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); + }; + const double a = std::atan2(double(t.offsetY(L.cy, L.pondY[team])), double(t.offsetX(L.cx, L.pondX[team]))); + const auto at = [&](double along, double across) { + return std::make_pair(L.pondX[team] + int(std::lround(along * std::cos(a) - across * std::sin(a))), + L.pondY[team] + int(std::lround(along * std::sin(a) + across * std::cos(a)))); + }; + // Wheat and wood on the two sides of the pond, the quarry towards the map's centre. + if (const auto [x, y] = at(0, -pondRadius); true) + if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheat, eligible); + if (const auto [x, y] = at(0, pondRadius); true) + if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) + growPatch(map, t, seed, WOOD, kHomeWood, eligible); + if (const auto [x, y] = at(-(pondRadius + 4), 0); true) + if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + // Ambient farmland on the island's fertile ground, split two to one. + std::vector ground; + std::vector> farm; + for (int i = 0; i < n; ++i) + if (eligible(i)) { + ground.push_back(i); + if (const std::uint32_t f = fertility.at(i % t.w, i / t.w); f > 0) + farm.push_back({-double(f), i}); + } + std::stable_sort(farm.begin(), farm.end()); + const int area = int(ground.size()); + const int wheat = int(scaledCount(area * 4 / 100, o.wheat)), wood = int(scaledCount(area * 2 / 100, o.wood)); + const int total = std::min(int(farm.size()), wheat + wood); + std::vector chosen; + for (int k = 0; k < total; ++k) + chosen.push_back(farm[k].second); + std::stable_sort(chosen.begin(), chosen.end(), [&](int p, int q) { + return split.uiLevel(p % t.w, p / t.w, 2048) < split.uiLevel(q % t.w, q / t.w, 2048); + }); + const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); + for (int k = 0; k < total; ++k) + map.setResource(chosen[k] % t.w, chosen[k] / t.w, k < wheatShare ? CORN : WOOD, 1); + const int outcrops = int(scaledCount(1, o.stone)); + for (int k = 0; k < outcrops && !ground.empty(); ++k) + for (int attempt = 0; attempt < 100; ++attempt) { + const int at2 = ground[context.bounded("flats-home-stone", ground.size())]; + if (eligible(at2)) { + placeResourceClump(map, context, {at2 % t.w, at2 / t.w}, STONE, 1); + break; + } + } + } +} + +// Every neutral island is an oasis, its pond ringed by wheat to its shores, with one prize inside, +// a fruit grove or a stone deposit in turn; the central island carries the orchard of all three +// fruits round its pond, with a quarry. +void stockIslands(Map &map, const Layout &L, GenerationContext &context, const TidalFlatsOptions &o) { + const Torus &t = L.t; + const int n = t.w * t.h; + // Every neutral island is its own component of grass; each gets its prize at its middle. + std::vector seen(n, 0); + for (int start = 0; start < n; ++start) { + if (seen[start] || L.islandOf[start] != -3 || L.prizeOf[start] < 0) + continue; + std::vector tiles{start}; + seen[start] = 1; + for (size_t head = 0; head < tiles.size(); ++head) { + const int i = tiles[head]; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + const int m = t.at(i % t.w + dx, i / t.w + dy); + if (!seen[m] && L.islandOf[m] == -3 && L.prizeOf[m] >= 0) { + seen[m] = 1; + tiles.push_back(m); + } + } + } + double sx = 0, sy = 0; + for (int i : tiles) { + sx += t.offsetX(start, i % t.w); + sy += t.offsetY(start / t.w, i / t.w); + } + const int mx = t.x(start % t.w + int(std::lround(sx / tiles.size()))); + const int my = t.y(start / t.w + int(std::lround(sy / tiles.size()))); + const auto eligible = [&](int i) { return L.islandOf[i] == -3 && clearGround(map, i % t.w, i / t.w); }; + // The prize, on the far side of the pond from the map's centre. + const double a = std::atan2(double(t.offsetY(L.cy, my)), double(t.offsetX(L.cx, mx))); + if (const int seed = seedNear(t, mx + int(std::lround(6 * std::cos(a))), my + int(std::lround(6 * std::sin(a))), 8, eligible); + seed >= 0) { + const MapGeneratorPoint centre(seed % t.w, seed / t.w); + if (L.prizeOf[start] == 0) { + if (scaledCount(1, o.fruit) > 0) + placeResourceClump(map, context, centre, CHERRY + int(context.bounded("flats-prizes", 3)), 2); + } else if (scaledCount(1, o.stone) > 0) { + placeResourceClump(map, context, centre, STONE, 2); + } + } + // The oasis: every other tile of the island under wheat, unscaled, so it has to be cleared. + for (int i : tiles) + if (eligible(i) && map.isResourceAllowed(i % t.w, i / t.w, CORN)) + map.setResource(i % t.w, i / t.w, CORN, 1); + } + if (L.g.centralRadius > 0) { + const auto eligible = [&](int i) { return L.islandOf[i] == -2 && clearGround(map, i % t.w, i / t.w); }; + const double rho = std::max(1.5, 0.22 * L.g.centralRadius) + 4; + const double spin = context.bounded("flats-prizes", 3600) / 3600.0 * 2 * pi; + if (scaledCount(1, o.fruit) > 0) + for (int f = 0; f < 3; ++f) { + const double a = spin + 2 * pi * f / 3; + const int seed = seedNear(t, L.cx + int(std::lround(rho * std::cos(a))), + L.cy + int(std::lround(rho * std::sin(a))), 8, eligible); + if (seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); + } + if (scaledCount(1, o.stone) > 0) + if (const int seed = seedNear(t, L.cx + int(std::lround((L.g.centralRadius - 4) * std::cos(spin + pi / 3))), + L.cy + int(std::lround((L.g.centralRadius - 4) * std::sin(spin + pi / 3))), 8, eligible); + seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + } +} + +// Algae in every pool and lagoon: water beside sand is exactly where it regrows. +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) { + const int n = t.w * t.h; + std::vector water; + for (int i = 0; i < n; ++i) + if (map.isWater(i % t.w, i / t.w)) + water.emplace_back(i % t.w, i / t.w); + if (water.empty()) + return; + for (int clump = 0; clump < scaledCount(int(water.size()) / 40, algaePercent); ++clump) + placeResourceClump(map, context, water[context.bounded("flats-algae", water.size())], ALGA, 1); +} + +void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t) { + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) { + const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); + if (map.isResource(x, y)) + map.setNoResource(x, y, 1); + } +} + +bool generate(Game &game, GenerationContext &context) { + context.stage = "flats layout"; + const TidalFlatsOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(SAND); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.w * t.h; + + context.stage = "flats terrain"; + std::vector terrain(n, SAND); + for (int i = 0; i < n; ++i) { + if (L.grass[i]) + terrain[i] = GRASS; + if (L.water[i]) + terrain[i] = WATER; + } + layBeaches(terrain, t); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); + map.rebuildTerrain(); + + context.stage = "flats colonies"; + for (int team = 0; team < teams; ++team) { + std::vector home(n, 0); + for (int i = 0; i < n; ++i) + home[i] = L.islandOf[i] == team && map.isGrass(i % t.w, i / t.w); + // The swarm stands on the far side of the pond from the map's centre, a short walk from the + // fields; placeSettlement measures from the footprint's top-left tile. + const double a = std::atan2(double(t.offsetY(L.cy, L.homeY[team])), double(t.offsetX(L.cx, L.homeX[team]))); + const double back = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 6; + const MapGeneratorPoint anchor(L.pondX[team] + int(std::lround(back * std::cos(a))) - 2, + L.pondY[team] + int(std::lround(back * std::sin(a))) - 2); + if (!placeSettlement(game, context, team, home, anchor, "flats-starts")) + return false; + } + + context.stage = "flats resources"; + furnishHomes(map, L, context, o); + stockIslands(map, L, context, o); + seedAlgae(map, context, t, o.algae); + clearAroundSwarms(map, context, t); + guaranteeStartingResources(game, context, 24, 32, 0); + clearAroundSwarms(map, context, t); + return true; +} + +std::string validateRequest(const GenerationRequest &r) { + if (r.nbTeams < 1) + return "Tidal flats need at least one colony."; + const Geometry g = geometryFor(r); + if (g.homeRadius < 9) + return "The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands."; + return ""; +} + +// Checked on the finished world: every home island still carries its pond, and colony 0 can walk +// to every other colony and onto the central island, with water, buildings and every resource +// blocking; the flats themselves hold nothing, so that walk is always the open one. +std::string validateWorld(const Game &game, const GenerationContext &context) { + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (!L.failure.empty()) + return "The flats design could not be rebuilt: " + L.failure; + const Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (map.getW() != t.w || map.getH() != t.h) + return "The flats design does not match the map size."; + for (int k = 0; k < teams; ++k) { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(L.pondX[k] + dx), t.y(L.pondY[k] + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s island has lost its pond."; + } + std::vector> workers(std::max(teams, 1)); + for (int i = 0; i < n; ++i) { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(i); + } + if (teams < 1 || workers[0].empty()) + return "Colony 0 has no workers to walk the flats."; + std::vector open(n, 0), source(n, 0); + for (int i = 0; i < n; ++i) { + const int x = i % t.w, y = i / t.w; + open[i] = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + } + for (int i : workers[0]) + source[i] = 1; + const std::vector steps = stepsFrom(t, source, open); + for (int team = 1; team < teams; ++team) { + bool arrived = false; + for (int i : workers[team]) + arrived = arrived || steps[i] >= 0; + if (!arrived) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 over the flats."; + } + if (L.g.centralRadius > 0) { + bool arrived = false; + for (int i = 0; i < n && !arrived; ++i) + arrived = L.islandOf[i] == -2 && steps[i] >= 0; + if (!arrived) + return "The central island cannot be reached over the flats."; + } + return ""; +} +} // namespace + +TidalFlatsOptions::TidalFlatsOptions(const GenerationRequest &r) + : homeIslandSize(r.option("home-island-size")), extraIslands(r.option("extra-islands")), + sandbars(r.option("sandbars")), tidePools(r.option("tide-pools")), lagoons(r.option("lagoons")), + coastRoughness(r.option("coast-roughness")), centralIsland(r.option("central-island") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} + +GeneratorDefinition tidalFlatsDefinition() { + return {"tidal-flats", + 18, + "Tidal flats", + 3, + false, + // The home islands' radius as a share of the half side; extra islands, sandbars and + // lagoons per colony; tide pools per 128x128 of flats. + {{"home-island-size", "Home island size", 14, 26, 1, 20, ControlGroup::Terrain}, + {"extra-islands", "Extra islands", 0, 4, 1, 3, ControlGroup::Terrain}, + {"sandbars", "Sandbars", 0, 4, 1, 3, ControlGroup::Terrain}, + {"tide-pools", "Tide pools", 0, 12, 1, 8, ControlGroup::Terrain}, + {"lagoons", "Lagoons", 0, 4, 1, 3, ControlGroup::Terrain}, + {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, + // Off, the middle of the map is flats like the rest, and there is no orchard. + GeneratorControl::toggle("central-island", "Central island", true, ControlGroup::Layout), + // Every island's ambient fields and outcrops and every prize; each home's kit is unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/TidalFlatsGenerator.h b/src/map/generator/generators/TidalFlatsGenerator.h new file mode 100644 index 000000000..5f4382e93 --- /dev/null +++ b/src/map/generator/generators/TidalFlatsGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct TidalFlatsOptions { + int homeIslandSize, extraIslands, sandbars, tidePools, lagoons, coastRoughness; + bool centralIsland; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit TidalFlatsOptions(const GenerationRequest &r); +}; +GeneratorDefinition tidalFlatsDefinition(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index d7e35e7dd..b86cf3e78 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -287,7 +287,7 @@ class MapGeneratorDefaultsTest screen.descriptor.option("test-gap") == 5 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 17); + assert(registry.selectionIndex(101) == 18); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); From adf81b19b52e72cbf4dec3bd3040c4b1b4075844 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 12:44:54 -0400 Subject: [PATCH 084/136] Pick a landscape by sight in the custom game lobby The Landscape dropdown becomes a chooser field that opens LandscapePickerScreen, a full-window modal showing every playable landscape as a freshly generated map at the draft's current size and colony count, with numbered colony markers, a Regenerate all button, and selection by click, arrows and Return. The picker takes only localized names and generation requests, so the editor and multiplayer lobby can run it later. LandscapePreviewer rolls the previews on background threads (hardware threads minus one, at most four), up to three seeds per landscape before a tile reports no preview, and hands back the seed behind each map shown. The lobby then rolls that one seed instead of sampling five, so the map a player picked is the map the preview shows and the match starts on; any later edit drops the remembered seed. MapThumbnail gains an in-memory Map overload and the lobby's preview drawing is shared with the picker. Two engine changes make off-thread generation safe. The synchronized random generator behind syncRand() is now thread_local: the simulation runs on one thread, so its sequence is unchanged, while a worker seeds its own stream inside GenerationService::generate without racing the menu's live colony on the UI thread. And libusl's static per-type NativeValue prototype, which every Game construction points at its own heap and repopulates, is now updated under a recursive mutex; two games constructed at once corrupted each other's heaps. Four new strings in all 33 text tables; the framework reference and the development notes describe the picker, the seed handoff and the per-thread stream. Verified: CustomGameSetupHarness passes headless and in the visual runs at 640x480 and 1000x700 (three times each), with new coverage of the picker's previews, selection, regeneration and the lobby playing the very seed shown; MapGeneratorDefaultsTest and the preferences modes pass; headless --nox replays from the previous and the new client binary are byte-identical over 1200 ticks; a ThreadSanitizer build of the visual harness reports only the pre-existing VoiceRecorder teardown race; check_translations.py --strict passes. Linux and Windows builds are left to CI. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 8 + data/texts.br.txt | 8 + data/texts.ca.txt | 8 + data/texts.cz.txt | 8 + data/texts.de.txt | 8 + data/texts.dk.txt | 8 + data/texts.en.txt | 8 + data/texts.eo.txt | 8 + data/texts.es.txt | 8 + data/texts.eu.txt | 8 + data/texts.fa.txt | 8 + data/texts.fi.txt | 8 + data/texts.fr.txt | 8 + data/texts.gr.txt | 8 + data/texts.hu.txt | 8 + data/texts.id.txt | 8 + data/texts.it.txt | 8 + data/texts.ja.txt | 8 + data/texts.keys.txt | 4 + data/texts.ko.txt | 8 + data/texts.nl.txt | 8 + data/texts.pl.txt | 8 + data/texts.pt.txt | 8 + data/texts.ro.txt | 8 + data/texts.ru.txt | 8 + data/texts.si.txt | 8 + data/texts.sk.txt | 8 + data/texts.sr.txt | 8 + data/texts.sv.txt | 8 + data/texts.tr.txt | 8 + data/texts.uk.txt | 8 + data/texts.vi.txt | 8 + data/texts.zh-cn.txt | 8 + data/texts.zh-tw.txt | 8 + docs/development-notes.md | 4 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 12 + libusl/src/native.h | 12 + src/CustomGameScreen.cpp | 77 ++++-- src/CustomGameScreen.h | 8 + src/LandscapePickerScreen.cpp | 227 ++++++++++++++++++ src/LandscapePickerScreen.h | 60 +++++ src/LandscapePreviewer.cpp | 136 +++++++++++ src/LandscapePreviewer.h | 71 ++++++ src/LobbyControls.h | 12 + src/LobbyMapPreview.h | 67 +++--- src/SConscript | 2 + src/Utilities.cpp | 3 +- src/Utilities.h | 6 +- src/map/io/MapThumbnail.cpp | 11 +- src/map/io/MapThumbnail.h | 4 + test/CustomGameSetupHarness.cpp | 166 ++++++++++++- 51 files changed, 1083 insertions(+), 63 deletions(-) create mode 100644 src/LandscapePickerScreen.cpp create mode 100644 src/LandscapePickerScreen.h create mode 100644 src/LandscapePreviewer.cpp create mode 100644 src/LandscapePreviewer.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index e461e1b90..9279949ac 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1378,6 +1378,14 @@ Cortex خرائط مسبقة الصنع [Preview ready. Start plays this exact map.] المعاينة جاهزة. بدء اللعبة يستخدم هذه الخريطة الدقيقة. +[Regenerate all] +إعادة إنشاء الكل +[Generating preview...] +جارٍ إنشاء المعاينة... +[Preview unavailable] +المعاينة غير متاحة +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +تُعرض كل تضاريس كخريطة حقيقية بحجمك الحالي وعدد مستعمراتك. اختر واحدة للعب الخريطة المعروضة. [Purple] بنفسجي [Quick clash] diff --git a/data/texts.br.txt b/data/texts.br.txt index ed247a21e..c274c25b9 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1376,6 +1376,14 @@ Jogadores e equipes Mapas prontos [Preview ready. Start plays this exact map.] Visualização pronta. Iniciar o jogo usa exatamente este mapa. +[Regenerate all] +Gerar tudo novamente +[Generating preview...] +Gerando visualização... +[Preview unavailable] +Visualização indisponível +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Cada paisagem é mostrada como um mapa real com seu tamanho e número de colônias atuais. Use uma para jogar o mapa mostrado. [Purple] Roxo [Quick clash] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 3e9c1e1f1..48170c0d8 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1388,6 +1388,14 @@ Jugadors i equips Mapes predissenyats [Preview ready. Start plays this exact map.] Vista prèvia a punt. Per començar el joc s'utilitza aquest mapa exacte. +[Regenerate all] +Regenera-ho tot +[Generating preview...] +S'està generant la vista prèvia... +[Preview unavailable] +Vista prèvia no disponible +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Cada paisatge es mostra com un mapa real amb la mida i el nombre de colònies actuals. Tria'n un per jugar el mapa mostrat. [Purple] Lila [Quick clash] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 4d2926ca6..f1921464e 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1380,6 +1380,14 @@ Hráči a týmy Předpřipravené mapy [Preview ready. Start plays this exact map.] Náhled připraven. Při spuštění hry se používá přesně tato mapa. +[Regenerate all] +Znovu vygenerovat vše +[Generating preview...] +Generuje se náhled... +[Preview unavailable] +Náhled není k dispozici +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Každá krajina je zobrazena jako skutečná mapa ve vaší aktuální velikosti a počtu kolonií. Vyberte jednu a zahrajte si zobrazenou mapu. [Purple] Fialová [Quick clash] diff --git a/data/texts.de.txt b/data/texts.de.txt index 9f088fa12..9271f3201 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1380,6 +1380,14 @@ Spieler & Teams Vorgefertigte Karten [Preview ready. Start plays this exact map.] Vorschau bereit. Beim Starten des Spiels wird genau diese Karte verwendet. +[Regenerate all] +Alle neu generieren +[Generating preview...] +Vorschau wird erstellt... +[Preview unavailable] +Keine Vorschau verfügbar +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Jede Landschaft wird als echte Karte in deiner aktuellen Größe und Kolonienzahl gezeigt. Wähle eine, um die gezeigte Karte zu spielen. [Purple] Lila [Quick clash] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index d561c4449..6bdb47da9 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1456,6 +1456,14 @@ Spillere og hold Forudarbejdede kort [Preview ready. Start plays this exact map.] Forhåndsvisning klar. Start af spillet bruger dette nøjagtige kort. +[Regenerate all] +Generer alle igen +[Generating preview...] +Genererer forhåndsvisning... +[Preview unavailable] +Forhåndsvisning ikke tilgængelig +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Hvert landskab vises som et rigtigt kort med din nuværende størrelse og dit antal kolonier. Vælg et for at spille det viste kort. [Purple] Lilla [Quick clash] diff --git a/data/texts.en.txt b/data/texts.en.txt index 37da93cc1..3d3a73017 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1378,6 +1378,14 @@ Players & Teams Premade maps [Preview ready. Start plays this exact map.] Preview ready. Start plays this exact map. +[Regenerate all] +Regenerate all +[Generating preview...] +Generating preview... +[Preview unavailable] +Preview unavailable +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown. [Purple] Purple [Quick clash] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 76e3244d6..edb19a5cb 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1378,6 +1378,14 @@ Ludantoj kaj Teamoj Antaŭfaritaj mapoj [Preview ready. Start plays this exact map.] Antaŭrigardo preta. Komenci la ludon uzas ĉi tiun precizan mapon. +[Regenerate all] +Regeneri ĉion +[Generating preview...] +Kreante antaŭprezenton... +[Preview unavailable] +Antaŭprezento ne disponebla +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Ĉiu pejzaĝo estas montrata kiel vera mapo laŭ via nuna grandeco kaj nombro de kolonioj. Elektu unu por ludi la montritan mapon. [Purple] Purpura [Quick clash] diff --git a/data/texts.es.txt b/data/texts.es.txt index 0d31b2eef..343dc5c8d 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1380,6 +1380,14 @@ Jugadores y equipos Mapas prediseñados [Preview ready. Start plays this exact map.] Vista previa lista. Al iniciar el juego se utiliza este mapa exacto. +[Regenerate all] +Regenerar todo +[Generating preview...] +Generando vista previa... +[Preview unavailable] +Vista previa no disponible +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Cada paisaje se muestra como un mapa real con tu tamaño y número de colonias actuales. Usa uno para jugar el mapa mostrado. [Purple] Púrpura [Quick clash] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index b58f11b08..491460080 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1390,6 +1390,14 @@ Jokalariak eta taldeak Aurrez egindako mapak [Preview ready. Start plays this exact map.] Aurrebista prest. Jokoa hastean mapa zehatz hau erabiltzen da. +[Regenerate all] +Sortu berriro guztiak +[Generating preview...] +Aurrebista sortzen... +[Preview unavailable] +Aurrebista ez dago erabilgarri +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Paisaia bakoitza benetako mapa gisa erakusten da zure uneko tamaina eta kolonia kopuruarekin. Aukeratu bat erakutsitako mapa jokatzeko. [Purple] Morea [Quick clash] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 075544d2c..9b2e4a9b2 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1378,6 +1378,14 @@ Cortex نقشه های از پیش ساخته شده [Preview ready. Start plays this exact map.] پیش نمایش آماده است. شروع بازی دقیقاً از این نقشه استفاده می کند. +[Regenerate all] +ایجاد دوباره همه +[Generating preview...] +در حال ایجاد پیش‌نمایش... +[Preview unavailable] +پیش‌نمایش در دسترس نیست +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +هر چشم‌انداز به صورت یک نقشه واقعی با اندازه و تعداد مستعمرات فعلی شما نشان داده می‌شود. یکی را انتخاب کنید تا نقشه نشان داده شده را بازی کنید. [Purple] بنفش [Quick clash] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index a95d69252..d3132c56c 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1380,6 +1380,14 @@ Pelaajat & Joukkueet Valmiit kartat [Preview ready. Start plays this exact map.] Esikatselu valmis. Pelin aloittaminen käyttää tätä tarkkaa karttaa. +[Regenerate all] +Luo kaikki uudelleen +[Generating preview...] +Luodaan esikatselua... +[Preview unavailable] +Esikatselu ei ole saatavilla +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Jokainen maisema näytetään oikeana karttana nykyisellä koolla ja siirtokuntien määrällä. Valitse yksi pelataksesi näytetyn kartan. [Purple] Violetti [Quick clash] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index cefb93b98..57ce25fad 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1390,6 +1390,14 @@ Joueurs et équipes Cartes prédéfinies [Preview ready. Start plays this exact map.] Aperçu prêt. Le démarrage du jeu utilise cette carte exacte. +[Regenerate all] +Tout régénérer +[Generating preview...] +Génération de l'aperçu... +[Preview unavailable] +Aperçu indisponible +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Chaque paysage est présenté comme une vraie carte à votre taille et votre nombre de colonies actuels. Choisissez-en un pour jouer la carte affichée. [Purple] Violet [Quick clash] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index ede972b7d..8468ae2f1 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1450,6 +1450,14 @@ Cortex Προκατασκευασμένοι χάρτες [Preview ready. Start plays this exact map.] Έτοιμη η προεπισκόπηση. Η έναρξη του παιχνιδιού χρησιμοποιεί αυτόν ακριβώς τον χάρτη. +[Regenerate all] +Αναδημιουργία όλων +[Generating preview...] +Δημιουργία προεπισκόπησης... +[Preview unavailable] +Η προεπισκόπηση δεν είναι διαθέσιμη +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Κάθε τοπίο εμφανίζεται ως πραγματικός χάρτης στο τρέχον μέγεθος και αριθμό αποικιών σας. Επιλέξτε ένα για να παίξετε τον χάρτη που εμφανίζεται. [Purple] Μωβ [Quick clash] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 52eca403c..a06ff2f67 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1380,6 +1380,14 @@ Játékosok és csapatok Előre elkészített térképek [Preview ready. Start plays this exact map.] Az előnézet készen áll. A játék elindítása pontosan ezt a térképet használja. +[Regenerate all] +Összes újragenerálása +[Generating preview...] +Előnézet készítése... +[Preview unavailable] +Az előnézet nem érhető el +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Minden táj valódi térképként jelenik meg a jelenlegi mérettel és kolóniaszámmal. Válassz egyet a megjelenített térkép játszásához. [Purple] Lila [Quick clash] diff --git a/data/texts.id.txt b/data/texts.id.txt index 551c64fc3..f40714ca3 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1376,6 +1376,14 @@ Pemain & Tim Peta yang sudah jadi [Preview ready. Start plays this exact map.] Pratinjau siap. Memulai permainan menggunakan peta persis ini. +[Regenerate all] +Buat ulang semua +[Generating preview...] +Membuat pratinjau... +[Preview unavailable] +Pratinjau tidak tersedia +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Setiap lanskap ditampilkan sebagai peta sungguhan dengan ukuran dan jumlah koloni Anda saat ini. Pilih satu untuk memainkan peta yang ditampilkan. [Purple] Ungu [Quick clash] diff --git a/data/texts.it.txt b/data/texts.it.txt index 85d7c10ae..11e512361 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1442,6 +1442,14 @@ Giocatori e squadre Mappe predefinite [Preview ready. Start plays this exact map.] Anteprima pronta. L'avvio del gioco utilizza questa mappa esatta. +[Regenerate all] +Rigenera tutto +[Generating preview...] +Generazione dell'anteprima... +[Preview unavailable] +Anteprima non disponibile +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Ogni paesaggio è mostrato come una mappa reale con le dimensioni e il numero di colonie attuali. Scegline uno per giocare la mappa mostrata. [Purple] Viola [Quick clash] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 67e4b906d..aff028ba3 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1376,6 +1376,14 @@ AIの命令なし 既製のマップ [Preview ready. Start plays this exact map.] プレビューの準備ができました。ゲームを開始すると、この正確なマップが使用されます。 +[Regenerate all] +すべて再生成 +[Generating preview...] +プレビューを生成しています... +[Preview unavailable] +プレビューを利用できません +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +各地形は、現在のサイズとコロニー数で実際に生成したマップとして表示されます。1 つ選ぶと、表示されたマップでプレイできます。 [Purple] 紫 [Quick clash] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 2296f7e14..4a6fdf581 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -687,6 +687,10 @@ [Players & Teams] [Premade maps] [Preview ready. Start plays this exact map.] +[Regenerate all] +[Generating preview...] +[Preview unavailable] +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] [Purple] [Quick clash] [Random map] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 9685b4a0c..42133432b 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1376,6 +1376,14 @@ AI 명령 없음 미리 만들어진 지도 [Preview ready. Start plays this exact map.] 미리보기가 준비되었습니다. 게임을 시작할 때 이 정확한 지도를 사용합니다. +[Regenerate all] +모두 다시 생성 +[Generating preview...] +미리보기 생성 중... +[Preview unavailable] +미리보기를 사용할 수 없음 +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +각 지형은 현재 크기와 식민지 수로 실제 생성된 지도로 표시됩니다. 하나를 선택하면 표시된 지도로 플레이합니다. [Purple] 보라색 [Quick clash] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 90a7b8e2d..99400593d 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1404,6 +1404,14 @@ Spelers & Teams Vooraf gemaakte kaarten [Preview ready. Start plays this exact map.] Voorbeeld klaar. Bij het starten van het spel wordt deze exacte kaart gebruikt. +[Regenerate all] +Alles opnieuw genereren +[Generating preview...] +Voorbeeld wordt gemaakt... +[Preview unavailable] +Geen voorbeeld beschikbaar +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Elk landschap wordt getoond als een echte kaart met je huidige grootte en aantal kolonies. Kies er een om de getoonde kaart te spelen. [Purple] Paars [Quick clash] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 7697714db..ff91657d3 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1380,6 +1380,14 @@ Gracze i drużyny Gotowe mapy [Preview ready. Start plays this exact map.] Podgląd gotowy. Rozpoczęcie gry korzysta z dokładnie tej mapy. +[Regenerate all] +Wygeneruj wszystko ponownie +[Generating preview...] +Generowanie podglądu... +[Preview unavailable] +Podgląd niedostępny +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Każdy krajobraz jest pokazany jako prawdziwa mapa w twoim bieżącym rozmiarze i liczbie kolonii. Wybierz jeden, aby zagrać na pokazanej mapie. [Purple] Fioletowy [Quick clash] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index de8abfefa..be11d2d8e 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1386,6 +1386,14 @@ Jogadores e equipas Mapas prontos [Preview ready. Start plays this exact map.] Visualização pronta. Iniciar o jogo usa exatamente este mapa. +[Regenerate all] +Regenerar tudo +[Generating preview...] +A gerar a pré-visualização... +[Preview unavailable] +Pré-visualização indisponível +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Cada paisagem é mostrada como um mapa real com o seu tamanho e número de colónias atuais. Use uma para jogar o mapa mostrado. [Purple] Roxo [Quick clash] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 3494b1d89..12a4c413a 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1380,6 +1380,14 @@ Jucători și echipe Hărți prefabricate [Preview ready. Start plays this exact map.] Previzualizare gata. Pornirea jocului folosește exact această hartă. +[Regenerate all] +Regenerează tot +[Generating preview...] +Se generează previzualizarea... +[Preview unavailable] +Previzualizare indisponibilă +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Fiecare peisaj este afișat ca o hartă reală la dimensiunea și numărul de colonii curente. Alegeți unul pentru a juca harta afișată. [Purple] Violet [Quick clash] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index fb2373faa..297b3f74f 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1378,6 +1378,14 @@ Cortex Готовые карты [Preview ready. Start plays this exact map.] Предварительный просмотр готов. В начале игры используется именно эта карта. +[Regenerate all] +Сгенерировать всё заново +[Generating preview...] +Создание предварительного просмотра... +[Preview unavailable] +Предварительный просмотр недоступен +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Каждый ландшафт показан как настоящая карта с вашим текущим размером и числом колоний. Выберите один, чтобы сыграть на показанной карте. [Purple] Фиолетовый [Quick clash] diff --git a/data/texts.si.txt b/data/texts.si.txt index 6fd1f165b..bdf387586 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1380,6 +1380,14 @@ Igralci in ekipe Pripravljeni zemljevidi [Preview ready. Start plays this exact map.] Predogled pripravljen. Začetek igre uporablja točno ta zemljevid. +[Regenerate all] +Znova ustvari vse +[Generating preview...] +Ustvarjanje predogleda ... +[Preview unavailable] +Predogled ni na voljo +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Vsaka pokrajina je prikazana kot pravi zemljevid v vaši trenutni velikosti in številu kolonij. Izberite eno, da igrate prikazani zemljevid. [Purple] Vijolična [Quick clash] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 5f1c585af..9778c9e3d 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1388,6 +1388,14 @@ Hráči a tímy Predpripravené mapy [Preview ready. Start plays this exact map.] Ukážka pripravená. Pri spustení hry sa používa presne táto mapa. +[Regenerate all] +Znova vygenerovať všetko +[Generating preview...] +Generuje sa náhľad... +[Preview unavailable] +Náhľad nie je k dispozícii +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Každá krajina je zobrazená ako skutočná mapa vo vašej aktuálnej veľkosti a počte kolónií. Vyberte jednu a zahrajte si zobrazenú mapu. [Purple] Fialová [Quick clash] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index b7e777353..4995ee881 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1380,6 +1380,14 @@ Cortex Унапред припремљене карте [Preview ready. Start plays this exact map.] Преглед спреман. Покретање игре користи ову тачну мапу. +[Regenerate all] +Поново генериши све +[Generating preview...] +Прављење прегледа... +[Preview unavailable] +Преглед није доступан +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Сваки предео је приказан као права мапа у вашој тренутној величини и броју колонија. Изаберите један да бисте играли приказану мапу. [Purple] Љубичаста [Quick clash] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 9715bd0b5..731ca8ef0 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1390,6 +1390,14 @@ Spelare och lag Färdiga kartor [Preview ready. Start plays this exact map.] Förhandsvisning klar. Starta spelet använder denna exakta karta. +[Regenerate all] +Generera om alla +[Generating preview...] +Skapar förhandsvisning... +[Preview unavailable] +Förhandsvisning saknas +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Varje landskap visas som en riktig karta med din nuvarande storlek och ditt antal kolonier. Välj ett för att spela den visade kartan. [Purple] Lila [Quick clash] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 86ac296f2..a77b71c6e 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1388,6 +1388,14 @@ Oyuncular ve Takımlar Hazır haritalar [Preview ready. Start plays this exact map.] Önizleme hazır. Oyunun başlatılması tam olarak bu haritayı kullanır. +[Regenerate all] +Tümünü yeniden oluştur +[Generating preview...] +Önizleme oluşturuluyor... +[Preview unavailable] +Önizleme kullanılamıyor +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Her arazi, mevcut boyutunuz ve koloni sayınızla gerçek bir harita olarak gösterilir. Gösterilen haritayı oynamak için birini seçin. [Purple] Mor [Quick clash] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 80f59540d..80fad3b48 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1376,6 +1376,14 @@ Ysagoon Online Gaming Готові карти [Preview ready. Start plays this exact map.] Попередній перегляд готовий. Початок гри використовує саме цю карту. +[Regenerate all] +Згенерувати все заново +[Generating preview...] +Створення попереднього перегляду... +[Preview unavailable] +Попередній перегляд недоступний +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Кожен ландшафт показано як справжню карту з вашим поточним розміром і кількістю колоній. Виберіть один, щоб зіграти на показаній карті. [Purple] Фіолетовий [Quick clash] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 4921e4920..ab250af85 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1376,6 +1376,14 @@ Người chơi & Đội Bản đồ được tạo sẵn [Preview ready. Start plays this exact map.] Xem trước đã sẵn sàng. Bắt đầu trò chơi sử dụng bản đồ chính xác này. +[Regenerate all] +Tạo lại tất cả +[Generating preview...] +Đang tạo bản xem trước... +[Preview unavailable] +Không có bản xem trước +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +Mỗi địa hình được hiển thị như một bản đồ thật với kích thước và số thuộc địa hiện tại của bạn. Chọn một để chơi bản đồ được hiển thị. [Purple] Màu tím [Quick clash] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 877ed7402..ba469eea3 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1376,6 +1376,14 @@ AI策略与对抗 预制地图 [Preview ready. Start plays this exact map.] 预览准备就绪。开始游戏时使用这张精确的地图。 +[Regenerate all] +全部重新生成 +[Generating preview...] +正在生成预览... +[Preview unavailable] +预览不可用 +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +每种地貌都以当前大小和殖民地数量实际生成的地图显示。选择一个即可游玩所显示的地图。 [Purple] 紫色 [Quick clash] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index ac1b18b7f..b990c3881 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1452,6 +1452,14 @@ AI策略與對抗 預製地圖 [Preview ready. Start plays this exact map.] 預覽準備就緒。開始遊戲時使用這張精確的地圖。 +[Regenerate all] +全部重新生成 +[Generating preview...] +正在產生預覽... +[Preview unavailable] +預覽無法使用 +[Each landscape is shown as a real map at your current size and colony count. Use one to play the map shown.] +每種地貌都以目前大小和殖民地數量實際產生的地圖顯示。選擇一個即可遊玩所顯示的地圖。 [Purple] 紫色 [Quick clash] diff --git a/docs/development-notes.md b/docs/development-notes.md index 5baab5b79..f55faa57c 100644 --- a/docs/development-notes.md +++ b/docs/development-notes.md @@ -55,6 +55,10 @@ For timing and scheduling, start with `src/Game_sync.cpp` and `src/EngineRun.cpp - Use `Utilities::syncRand()` for simulation randomness. Keep iteration and tie breaking deterministic; never depend on pointer ordering, hash-table iteration, thread scheduling or wall-clock budgets for simulation decisions. +- `syncRand()`'s generator is `thread_local`: the simulation runs on one thread, so its + sequence is unchanged, and a background thread that generates maps (the lobby's + landscape previews) seeds its own stream without racing the UI thread's menu colony. + A new thread starts from the default seed; seed it before relying on its sequence. - For behavior-preserving refactors and optimizations, compare base and changed builds using identical saves/maps, seeds, settings and orders. Compare per-tick state/checksums as well as replay bytes: matching orders alone do not prove that diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 434773114..9e227daf9 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -211,6 +211,18 @@ used by the map editor's regeneration action. Generation is deterministic and th pure function of the finished map (asserted in `map-generator-defaults-test`), which is what makes "roll several, keep the best" and "regenerate the winning seed later" both sound. +The lobby's Landscape field opens `LandscapePickerScreen`, a full-window modal that shows every +playable landscape as a freshly generated map at the draft's current size and colony count, with +a Regenerate all button. `LandscapePreviewer` rolls those previews on background threads (one +roll per landscape, up to three seeds before a tile reports no preview) and hands back the seed +each shown map came from; the lobby then rolls that one seed instead of sampling five, so the map +a player picked by sight is the map the preview shows and the match starts on. Any later edit to +the draft drops the remembered seed and returns to candidate sampling. The picker takes only a +list of localized names and requests, so the editor or a multiplayer lobby can run it too. +Background rolls are safe because `syncRand()`'s state is per thread: a worker seeds its own +stream inside `GenerationService::generate` and never touches the menu's live colony on the UI +thread. + ## Fjord continent The richest generator, and the one most of this framework's resource work was proven against: diff --git a/libusl/src/native.h b/libusl/src/native.h index bb1bc5a1f..512e59215 100644 --- a/libusl/src/native.h +++ b/libusl/src/native.h @@ -5,6 +5,7 @@ #include "code.h" #include "types.h" +#include #include #include #include @@ -48,6 +49,16 @@ struct NativeValuePrototype: Prototype } }; +/// Every heap shares one static prototype per native type, and each NativeValue points it at +/// its own heap and re-adds its methods there. Two games constructed on different threads +/// would allocate those methods into each other's heaps, so that update is serialized. +/// Recursive, since a prototype's methods may themselves construct native values. +inline std::recursive_mutex& nativePrototypeMutex() +{ + static std::recursive_mutex mutex; + return mutex; +} + template struct NativeValue: Value { @@ -57,6 +68,7 @@ struct NativeValue: Value Value(heap, &prototype), value(value) { + std::lock_guard lock(nativePrototypeMutex()); prototype.heap = heap; prototype.initialize(); } diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 5e224a0c1..b14a66bb4 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -6,6 +6,7 @@ #include "AINames.h" #include "CustomGameScreen.h" #include "LobbyMapPreview.h" +#include "LandscapePickerScreen.h" #include "Game.h" #include "GenerationService.h" #include "GlobalContainer.h" @@ -25,13 +26,6 @@ namespace { std::string tr(const std::string &s) { return Toolkit::getStringTable()->getString("[" + s + "]"); } const Uint32 A = ALIGN_SCREEN_CENTERED; -const std::vector methods = [] -{ - std::vector result; - for (int m : GeneratorRegistry::builtins().methods(false)) - result.emplace_back(GenerationRequest::methodName(m)); - return result; -}(); std::vector localized(std::vector v) { for (auto &s : v) @@ -288,6 +282,47 @@ int CustomGameScreen::choose(const std::string &title, const std::vector> CustomGameScreen::landscapeEntries() const +{ + std::vector> entries; + for (int method : GeneratorRegistry::builtins().methods(false)) + { + auto draft = setup; + draft.generatorHistory.select(draft.generator, method); + draft.generator.nbTeams = setup.capacity; + entries.emplace_back(method, draft.generator); + } + return entries; +} +void CustomGameScreen::chooseLandscape() +{ + const auto entries = landscapeEntries(); + std::vector shown; + int selected = 0; + for (const auto &[method, request] : entries) + { + if (method == setup.generator.method) + selected = int(shown.size()); + shown.push_back({tr(GenerationRequest::methodName(method)), request}); + } + LandscapePickerScreen picker(tr("Landscape"), std::move(shown), selected); + const int result = picker.execute(globalContainer->gfx, 40); + if (result == QUIT_APPLICATION) + endExecute(QUIT_APPLICATION); + else if (result >= 0 && result < int(entries.size())) + applyLandscape(entries[result].first, picker.chosenSeed()); +} +void CustomGameScreen::applyLandscape(int method, std::optional seed) +{ + setup.generatorHistory.select(setup.generator, method); + chosenSeed = seed; + ++setup.mapRevision; + invalidate(); + // An explicit choice, not an edit in progress: preview it now rather than after the debounce. + previewDue = SDL_GetTicks(); +} void CustomGameScreen::invalidate() { validMap = false; @@ -396,16 +431,21 @@ bool CustomGameScreen::generateMap() const auto rootSeed = GenerationContext::randomSeed(); GenerationResult generationResult; setup.generator.nbTeams = setup.capacity; - // Some rolls cannot fit every starting colony, and among those that can, some hand - // one colony far better ground than another. Roll the whole budget either way and - // keep the best-scoring world rather than the first that fits. + // A map the landscape picker showed is rolled once, exactly as shown. Otherwise some + // rolls cannot fit every starting colony, and among those that can, some hand one + // colony far better ground than another: roll the whole budget either way and keep the + // best-scoring world rather than the first that fits. + const auto shown = chosenSeed; + chosenSeed.reset(); double bestScore = -1.0; - for (int attempt = 0; attempt < GenerationService::kSampledCandidates; ++attempt) + const int candidates = shown ? 1 : GenerationService::kSampledCandidates; + for (int attempt = 0; attempt < candidates; ++attempt) { auto roll = std::make_unique(nullptr); auto request = setup.generator; request.seed = - GenerationContext::deriveSeed(rootSeed, "attempt/" + std::to_string(attempt)); + shown ? *shown + : GenerationContext::deriveSeed(rootSeed, "attempt/" + std::to_string(attempt)); const auto rollResult = generator.generate(*roll, request); std::cout << "Map generation: " << rollResult.diagnostic() << std::endl; if (!rollResult || roll->teamsCount() != setup.capacity) @@ -869,20 +909,14 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) auto &g = setup.generator; auto changed = [this] { + chosenSeed.reset(); ++setup.mapRevision; invalidate(); }; ui.text(x + 4, yy, tr("Landscape"), "little", leftW - 20, true); yy += 18; - ui.dropdown("generator/landscape", {x, yy, leftW - 16, 30}, localized(methods), - GeneratorRegistry::builtins().selectionIndex(g.method, false), - [this, changed](int value) - { - setup.generatorHistory.select( - setup.generator, - GeneratorRegistry::builtins().methods(false).at(value)); - changed(); - }); + ui.chooser("generator/landscape", {x, yy, leftW - 16, 30}, + tr(GenerationRequest::methodName(g.method)), [this] { chooseLandscape(); }); yy += 40; auto discrete = [&](const GenerationRequest::Control &c, const std::string &id) { @@ -990,6 +1024,7 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) reset.seed = setup.generator.seed; setup.generator = reset; setup.setCapacity(reset.nbTeams); + chosenSeed.reset(); ++setup.mapRevision; invalidate(); }, diff --git a/src/CustomGameScreen.h b/src/CustomGameScreen.h index 8158bebdc..2dca956f3 100644 --- a/src/CustomGameScreen.h +++ b/src/CustomGameScreen.h @@ -3,9 +3,12 @@ #include "CustomGameSetup.h" #include "Glob2Screen.h" #include "MapHeader.h" +#include #include +#include #include class LobbyControls; +class LandscapePickerScreen; class CustomGameChoiceScreen : public Glob2Screen { friend struct CustomGameSetupHarness; @@ -61,6 +64,8 @@ class CustomGameScreen : public Glob2TabScreen unsigned previewRevision = ~0u; bool previewPending = false; Uint32 previewDue = 0; + // A seed the landscape picker showed a map for: the next preview reproduces that map. + std::optional chosenSeed; bool validMap = false, userMaps = false; bool separateMapLibraries = true; int currentTab = 0; @@ -79,6 +84,9 @@ class CustomGameScreen : public Glob2TabScreen void listMaps(); bool loadMap(const std::string &path); bool generateMap(); + std::vector> landscapeEntries() const; + void chooseLandscape(); + void applyLandscape(int method, std::optional seed); int choose(const std::string &, const std::vector &, int, bool profiles = false, const std::vector &enabled = {}); void invalidate(); diff --git a/src/LandscapePickerScreen.cpp b/src/LandscapePickerScreen.cpp new file mode 100644 index 000000000..206fd071c --- /dev/null +++ b/src/LandscapePickerScreen.cpp @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "LandscapePickerScreen.h" +#include "GlobalContainer.h" +#include "LobbyControls.h" +#include +#include +#include + +namespace +{ +std::string tr(const std::string &s) { return Toolkit::getStringTable()->getString("[" + s + "]"); } +} // namespace + +std::vector +LandscapePickerScreen::requestsOf(const std::vector &entries) +{ + std::vector requests; + for (const auto &entry : entries) + requests.push_back(entry.request); + return requests; +} + +LandscapePickerScreen::LandscapePickerScreen(const std::string &title, std::vector entries, + int selected) + : title(title), entries(std::move(entries)), tiles(this->entries.size()), + selected(std::clamp(selected, 0, std::max(0, int(this->entries.size()) - 1))), + previewer(requestsOf(this->entries)) +{ + gfx = globalContainer->gfx; + controls = new LobbyControls(); + controls->render = [this] { render(); }; + controls->focus = "landscape/" + std::to_string(this->selected); + addWidget(controls); +} + +LandscapePickerScreen::~LandscapePickerScreen() = default; + +void LandscapePickerScreen::onAction(Widget *, Action, int, int) {} + +std::optional LandscapePickerScreen::chosenSeed() const +{ + if (selected < 0 || selected >= int(tiles.size())) + return {}; + const auto &preview = tiles[selected].preview; + if (preview.state != LandscapePreviewer::State::Ready) + return {}; + return preview.seed; +} + +void LandscapePickerScreen::select(int index) +{ + if (index < 0 || index >= int(entries.size())) + return; + selected = index; + controls->focus = "landscape/" + std::to_string(index); + reveal = true; +} + +void LandscapePickerScreen::confirm() +{ + if (selected >= 0 && selected < int(entries.size())) + endExecute(selected); +} + +void LandscapePickerScreen::onTimer(Uint32) { refresh(); } + +void LandscapePickerScreen::refresh() +{ + for (std::size_t i = 0; i < tiles.size(); ++i) + { + auto &tile = tiles[i]; + const unsigned revision = previewer.revision(i); + if (revision == tile.revision) + continue; + tile.revision = revision; + tile.preview = previewer.preview(i); + tile.surface.reset(); + if (tile.preview.state == LandscapePreviewer::State::Ready) + { + // Surfaces belong to the UI thread; the worker only hands over pixels. + tile.surface = std::make_unique(MapPreview::PreviewSize, + MapPreview::PreviewSize); + tile.preview.thumbnail.loadIntoSurface(tile.surface.get()); + } + } +} + +void LandscapePickerScreen::onSDLEvent(SDL_Event *event) +{ + if (event->type == SDL_KEYDOWN && !controls->popup.open) + { + const auto key = event->key.keysym.sym; + if (key == SDLK_ESCAPE) + { + endExecute(CANCEL); + return; + } + const int step = key == SDLK_LEFT ? -1 + : key == SDLK_RIGHT ? 1 + : key == SDLK_UP ? -columns + : key == SDLK_DOWN ? columns + : 0; + if (step != 0) + { + select(std::clamp(selected + step, 0, int(entries.size()) - 1)); + return; + } + // Return on a tile, or with nothing focused, confirms; on a button it presses that. + if (key == SDLK_RETURN && + (controls->focus.empty() || controls->focus == "landscape/" + std::to_string(selected))) + { + confirm(); + return; + } + } + controls->handle(event); +} + +void LandscapePickerScreen::render() +{ + auto &ui = *controls; + const int width = gfx->getW(), height = gfx->getH(); + const int w = std::min(width - 32, 1120), x = (width - w) / 2; + const bool compact = width < 800; + ui.setScreenPosition(0, 0); + ui.setDimensions(width, height); + ui.box({x - 8, 8, w + 16, height - 16}, Color(232, 237, 218), 8); + ui.text(x + 8, 20, title, compact ? "standard" : "menu", w - 16); + if (previewer.busy()) + { + const std::string status = tr("Generating preview...") + " " + + std::to_string(previewer.finished()) + " / " + + std::to_string(previewer.size()); + const int sw = Toolkit::getFont("little")->getStringWidth(status); + ui.text(x + w - 8 - sw, compact ? 26 : 30, status, "little", sw + 2, true); + } + const int subtitleY = compact ? 46 : 56; + const int top = subtitleY + 6 + + ui.paragraph(x + 8, subtitleY, w - 16, + tr("Each landscape is shown as a real map at your current size " + "and colony count. Use one to play the map shown.")); + const int bottom = height - 66; + const int gap = 12, scrollbar = 12; + const int tileMin = compact ? 176 : 200; + columns = std::clamp((w - scrollbar + gap) / (tileMin + gap), 1, + std::max(1, int(entries.size()))); + const int tileW = (w - scrollbar - gap * (columns - 1)) / columns; + const int image = tileW - 16; + const int nameH = Toolkit::getFont("standard")->getStringHeight("Ag"); + const int noteH = Toolkit::getFont("little")->getStringHeight("Ag"); + const int tileH = 8 + image + 8 + nameH + 4 + noteH + 8; + const int stride = tileH + gap; + const int rows = (int(entries.size()) + columns - 1) / columns; + auto ®ion = ui.regions[30]; + if (reveal && selected >= 0) + { + // Keep the selection in view, for keyboard moves and the initial choice. + const int ty = (selected / columns) * stride, viewH = bottom - top; + region.maximum = std::max(0, rows * stride - gap + 4 - viewH); + if (ty < region.offset) + region.offset = ty; + else if (ty + tileH > region.offset + viewH) + region.offset = ty + tileH - viewH; + region.offset = std::clamp(region.offset, 0, region.maximum); + reveal = false; + } + ui.beginRegion(30, {x, top, w, bottom - top}); + const int offset = region.offset; + for (std::size_t i = 0; i < entries.size(); ++i) + { + const int column = int(i) % columns, row = int(i) / columns; + const SDL_Rect r{x + column * (tileW + gap), top + row * stride - offset, tileW, tileH}; + const bool current = int(i) == selected; + ui.button( + "landscape/" + std::to_string(i), r, "", + [this, i] + { + if (int(i) == selected) + confirm(); + else + select(int(i)); + }, + current); + const SDL_Rect frame{r.x + 8, r.y + 8, image, image}; + ui.box(frame, Color(211, 223, 197), 3); + const auto &tile = tiles[i]; + std::string note; + if (tile.preview.state == LandscapePreviewer::State::Ready && tile.surface) + { + SDL_Rect map = frame; + if (tile.preview.width >= tile.preview.height) + map.h = std::max(1, image * tile.preview.height / tile.preview.width); + else + map.w = std::max(1, image * tile.preview.width / tile.preview.height); + map.x += (image - map.w) / 2; + map.y += (image - map.h) / 2; + drawMapThumbnail(ui.surface(), map, tile.surface.get(), tile.preview.width, + tile.preview.height, tile.preview.starts, compact ? 12 : 14); + note = std::to_string(tile.preview.width) + " x " + + std::to_string(tile.preview.height) + " / " + + std::to_string(tile.preview.starts.size()) + " " + tr("colonies"); + } + else + { + note = tr(tile.preview.state == LandscapePreviewer::State::Failed + ? "Preview unavailable" + : "Generating preview..."); + ui.paragraph(frame.x + 12, frame.y + image / 2 - noteH, image - 24, note); + } + ui.text(r.x + 8, r.y + 8 + image + 8, entries[i].name, "standard", tileW - 16); + ui.text(r.x + 8, r.y + 8 + image + 8 + nameH + 4, note, "little", tileW - 16, true); + } + ui.endRegion(rows * stride - gap + 4); + ui.button( + "landscape/back", {x, height - 55, 100, 34}, tr("Back"), [this] { endExecute(CANCEL); }, + false, true, true); + const int regenerateW = compact ? 150 : 190; + ui.button( + "landscape/regenerate", {x + 110, height - 55, regenerateW, 34}, tr("Regenerate all"), + [this] { previewer.regenerate(); }); + const int useX = x + 110 + regenerateW + 10; + const bool valid = selected >= 0 && selected < int(entries.size()); + ui.button( + "landscape/use", {useX, height - 55, x + w - useX, 34}, + tr("Use") + (valid ? " " + entries[selected].name : ""), [this] { confirm(); }, true, + valid); +} diff --git a/src/LandscapePickerScreen.h b/src/LandscapePickerScreen.h new file mode 100644 index 000000000..eca634c56 --- /dev/null +++ b/src/LandscapePickerScreen.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "Glob2Screen.h" +#include "LandscapePreviewer.h" +#include +#include +#include +#include +#include +class LobbyControls; + +/// A full-window modal showing every landscape as a freshly generated map, for picking one by +/// sight. execute() returns the chosen entry's index, CANCEL, or QUIT_APPLICATION; chosenSeed() +/// then reproduces the map that was shown for it. Entries are just a localized name and the +/// request to preview, so any screen that selects a landscape can run it. +class LandscapePickerScreen : public Glob2Screen +{ + friend struct CustomGameSetupHarness; + + public: + enum + { + CANCEL = -2 + }; + struct Entry + { + std::string name; + GenerationRequest request; + }; + LandscapePickerScreen(const std::string &title, std::vector entries, int selected); + ~LandscapePickerScreen() override; + void onAction(Widget *, Action, int, int) override; + void onSDLEvent(SDL_Event *) override; + void onTimer(Uint32 tick) override; + int selection() const { return selected; } + /// The seed behind the preview shown for the selection, once that preview is ready. + std::optional chosenSeed() const; + bool busy() const { return previewer.busy(); } + + private: + struct Tile + { + LandscapePreviewer::Preview preview; + std::unique_ptr surface; + unsigned revision = ~0u; + }; + static std::vector requestsOf(const std::vector &); + void render(); + void refresh(); + void select(int index); + void confirm(); + std::string title; + std::vector entries; + std::vector tiles; + int selected, columns = 1; + bool reveal = true; + LobbyControls *controls; + LandscapePreviewer previewer; +}; diff --git a/src/LandscapePreviewer.cpp b/src/LandscapePreviewer.cpp new file mode 100644 index 000000000..94da14d97 --- /dev/null +++ b/src/LandscapePreviewer.cpp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "LandscapePreviewer.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GenerationService.h" +#include +#include + +LandscapePreviewer::LandscapePreviewer(std::vector requests, int threads) + : requests(std::move(requests)) +{ + slots.resize(this->requests.size()); + seeds.resize(this->requests.size()); + if (threads <= 0) + { + const unsigned cores = std::thread::hardware_concurrency(); + threads = int(std::clamp(cores == 0 ? 1u : cores - 1, 1u, 4u)); + } + threads = int(std::min(threads, std::max(1, this->requests.size()))); + regenerate(); + for (int i = 0; i < threads; ++i) + workers.emplace_back([this] { work(); }); +} + +LandscapePreviewer::~LandscapePreviewer() +{ + { + std::lock_guard lock(mutex); + stopping = true; + } + wake.notify_all(); + for (auto &worker : workers) + worker.join(); +} + +void LandscapePreviewer::regenerate() +{ + { + std::lock_guard lock(mutex); + ++pass; + next = 0; + const auto root = GenerationContext::randomSeed(); + for (std::size_t i = 0; i < slots.size(); ++i) + { + seeds[i] = GenerationContext::deriveSeed(root, "landscape/" + std::to_string(i)); + slots[i].state = State::Pending; + ++slots[i].revision; + } + } + wake.notify_all(); +} + +unsigned LandscapePreviewer::revision(std::size_t index) const +{ + std::lock_guard lock(mutex); + return slots.at(index).revision; +} + +LandscapePreviewer::Preview LandscapePreviewer::preview(std::size_t index) const +{ + std::lock_guard lock(mutex); + return slots.at(index); +} + +bool LandscapePreviewer::busy() const +{ + std::lock_guard lock(mutex); + return std::any_of(slots.begin(), slots.end(), [](const Preview &p) + { return p.state == State::Pending || p.state == State::Generating; }); +} + +int LandscapePreviewer::finished() const +{ + std::lock_guard lock(mutex); + return int(std::count_if(slots.begin(), slots.end(), [](const Preview &p) + { return p.state == State::Ready || p.state == State::Failed; })); +} + +LandscapePreviewer::Preview LandscapePreviewer::roll(const GenerationRequest &request, + std::uint32_t rootSeed) +{ + Preview result; + GenerationService generator; + for (int attempt = 0; attempt < kAttempts; ++attempt) + { + auto roll = request; + roll.seed = GenerationContext::deriveSeed(rootSeed, "attempt/" + std::to_string(attempt)); + Game game(nullptr); + const auto outcome = generator.generate(game, roll); + if (!outcome || game.teamsCount() != request.nbTeams) + { + result.detail = outcome ? "Could not place every colony" : outcome.diagnostic(); + // A request the generator refuses outright fails the same way on every seed. + if (outcome.error == GenerationError::InvalidRequest) + break; + continue; + } + result.thumbnail.loadFromMap(game.map); + for (int i = 0; i < game.teamsCount(); ++i) + result.starts.push_back( + {game.teams[i]->startPosX, game.teams[i]->startPosY, game.teams[i]->color}); + result.seed = roll.seed; + result.width = game.map.getW(); + result.height = game.map.getH(); + result.state = State::Ready; + result.detail.clear(); + return result; + } + std::cerr << "Landscape preview: " << result.detail << std::endl; + result.state = State::Failed; + return result; +} + +void LandscapePreviewer::work() +{ + std::unique_lock lock(mutex); + for (;;) + { + wake.wait(lock, [this] { return stopping || next < requests.size(); }); + if (stopping) + return; + const std::size_t index = next++; + const unsigned myPass = pass; + slots[index].state = State::Generating; + ++slots[index].revision; + const GenerationRequest request = requests[index]; + const std::uint32_t seed = seeds[index]; + lock.unlock(); + Preview result = roll(request, seed); + lock.lock(); + if (pass != myPass) + continue; // regenerate() superseded this roll while it ran + result.revision = slots[index].revision + 1; + slots[index] = std::move(result); + } +} diff --git a/src/LandscapePreviewer.h b/src/LandscapePreviewer.h new file mode 100644 index 000000000..8bda145c1 --- /dev/null +++ b/src/LandscapePreviewer.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "LobbyMapPreview.h" +#include "MapThumbnail.h" +#include +#include +#include +#include +#include +#include + +/// Rolls one preview map per request on background threads, so a picker can show every +/// landscape side by side while the UI keeps running. A slot is rolled with derived seeds until +/// a roll succeeds or kAttempts are spent, and the seed that succeeded is reported, so a caller +/// can regenerate exactly the map it showed. Generation is deterministic per seed, and each +/// thread has its own synchronized random stream, so any number of workers may roll at once. +class LandscapePreviewer +{ + public: + enum class State + { + Pending, + Generating, + Ready, + Failed + }; + struct Preview + { + State state = State::Pending; + MapThumbnail thumbnail; + std::vector starts; + std::uint32_t seed = 0; + int width = 0, height = 0; + /// The last generation diagnostic, when Failed. + std::string detail; + /// Increments on every state change, so a viewer polling cheaply knows when to copy. + unsigned revision = 0; + }; + static constexpr int kAttempts = 3; + /// threads <= 0 picks a count from the hardware, leaving a core to the UI. + explicit LandscapePreviewer(std::vector requests, int threads = 0); + ~LandscapePreviewer(); + LandscapePreviewer(const LandscapePreviewer &) = delete; + LandscapePreviewer &operator=(const LandscapePreviewer &) = delete; + /// Rolls every request again with fresh seeds. Rolls already under way finish, then are + /// dropped rather than shown. + void regenerate(); + std::size_t size() const { return requests.size(); } + unsigned revision(std::size_t index) const; + Preview preview(std::size_t index) const; + /// True while any slot is still pending or being rolled. + bool busy() const; + /// Slots that have reached Ready or Failed in the current pass. + int finished() const; + int threadCount() const { return int(workers.size()); } + /// One slot's roll, on the calling thread; what the workers run. + static Preview roll(const GenerationRequest &request, std::uint32_t rootSeed); + + private: + void work(); + std::vector requests; + std::vector slots; + std::vector seeds; + unsigned pass = 0; + std::size_t next = 0; + bool stopping = false; + mutable std::mutex mutex; + std::condition_variable wake; + std::vector workers; +}; diff --git a/src/LobbyControls.h b/src/LobbyControls.h index 4da8116e9..f068bd7e6 100644 --- a/src/LobbyControls.h +++ b/src/LobbyControls.h @@ -175,6 +175,18 @@ class LobbyControls : public GAGGUI::RectangularWidget box({r.x + r.w - 18, r.y + 4, 14, r.h - 8}, panel, 2); text(r.x + r.w - 15, r.y + (r.h - 12) / 2, "v", "little", 12, true); } + // A field whose choice is made in a fuller view elsewhere (a modal): shows the current + // value and, in the dropdown's chevron slot, an ellipsis. + void chooser(const std::string &id, SDL_Rect r, const std::string &label, Callback action, + bool enabled = true) + { + button(id, r, "", std::move(action), false, enabled); + int fh = GAGCore::Toolkit::getFont("standard")->getStringHeight("Ag"); + text(r.x + 9, r.y + (r.h - fh) / 2, label, "standard", r.w - 44, !enabled); + surface()->drawRect(r.x, r.y, r.w, r.h, line); + box({r.x + r.w - 26, r.y + 4, 22, r.h - 8}, panel, 2); + text(r.x + r.w - 21, r.y + (r.h - 12) / 2 - 3, "...", "little", 18, true); + } void segments(const std::string &id, SDL_Rect r, const std::vector &options, int selected, std::function apply, const std::vector &enabled = {}, const char *font = "standard") diff --git a/src/LobbyMapPreview.h b/src/LobbyMapPreview.h index dcddecbed..12290ac27 100644 --- a/src/LobbyMapPreview.h +++ b/src/LobbyMapPreview.h @@ -6,14 +6,47 @@ #include #include +/// A colony's starting position and colour, drawn as a numbered marker over a map preview. +struct MapStart +{ + int x, y; + Color color; +}; + +/// Draws a 128x128 letterboxed thumbnail cropped to the map's own aspect ratio into r, then a +/// numbered marker per colony start. Shared by the lobby's preview and the landscape picker. +inline void drawMapThumbnail(DrawableSurface *target, SDL_Rect r, DrawableSurface *thumbnail, + int mapW, int mapH, const std::vector &starts, + int marker = 16) +{ + if (!thumbnail || mapW <= 0 || mapH <= 0) + return; + // Thumbnails letterbox rectangular maps inside a 128x128 image. Crop that padding before + // scaling, so terrain and colony markers share the same coordinate system. + int extent, cropW, cropH, cropX, cropY; + Utilities::computeMinimapData(MapPreview::PreviewSize, mapW, mapH, &extent, &cropW, &cropH, + &cropX, &cropY); + target->drawSurface(r.x, r.y, r.w, r.h, thumbnail, cropX, cropY, cropW, cropH); + auto font = Toolkit::getFont(marker >= 16 ? "standard" : "little"); + for (size_t i = 0; i < starts.size(); ++i) + { + const int startX = (starts[i].x % mapW + mapW) % mapW; + const int startY = (starts[i].y % mapH + mapH) % mapH; + int px = r.x + std::clamp(startX * r.w / mapW, 2, std::max(2, r.w - marker - 2)); + int py = r.y + std::clamp(startY * r.h / mapH, 2, std::max(2, r.h - marker - 2)); + target->drawFilledRect(px - 2, py - 2, marker + 4, marker + 4, 20, 30, 20); + target->drawFilledRect(px, py, marker, marker, starts[i].color); + font->pushStyle(Font::Style(Font::STYLE_NORMAL, Color(0, 0, 0))); + const std::string label = std::to_string(i + 1); + target->drawString(px + (marker - font->getStringWidth(label)) / 2, py, font, label); + font->popStyle(); + } +} + class LobbyMapPreview : public MapPreview { public: - struct Start - { - int x, y; - Color color; - }; + using Start = MapStart; std::vector starts; LobbyMapPreview() : MapPreview(430, 115, ALIGN_SCREEN_CENTERED, ALIGN_SCREEN_CENTERED) { w = h = 180; } void paint() override @@ -22,27 +55,7 @@ class LobbyMapPreview : public MapPreview return; int x, y, w, h; getScreenPos(&x, &y, &w, &h); - auto target = parent->getSurface(); - // Thumbnails letterbox rectangular maps inside a 128x128 image. Crop - // that padding before scaling, so terrain and colony markers share - // the same coordinate system. - int extent, cropW, cropH, cropX, cropY; - Utilities::computeMinimapData(PreviewSize, getLastWidth(), getLastHeight(), - &extent, &cropW, &cropH, &cropX, &cropY); - target->drawSurface(x, y, w, h, surface, cropX, cropY, cropW, cropH); - auto font = Toolkit::getFont("standard"); - for (size_t i = 0; i < starts.size(); ++i) - { - const int mapW = getLastWidth(), mapH = getLastHeight(); - const int startX = (starts[i].x % mapW + mapW) % mapW; - const int startY = (starts[i].y % mapH + mapH) % mapH; - int px = x + std::clamp(startX * w / mapW, 2, std::max(2, w - 18)); - int py = y + std::clamp(startY * h / mapH, 2, std::max(2, h - 18)); - target->drawFilledRect(px - 2, py - 2, 20, 20, 20, 30, 20); - target->drawFilledRect(px, py, 16, 16, starts[i].color); - font->pushStyle(Font::Style(Font::STYLE_NORMAL, Color(0, 0, 0))); - target->drawString(px + 3, py, font, std::to_string(i + 1)); - font->popStyle(); - } + drawMapThumbnail(parent->getSurface(), {x, y, w, h}, surface, getLastWidth(), + getLastHeight(), starts); } }; diff --git a/src/SConscript b/src/SConscript index abd3b9947..cc2633ef1 100644 --- a/src/SConscript +++ b/src/SConscript @@ -168,6 +168,8 @@ KeyboardManager.cpp LANFindScreen.cpp LANGameInformation.cpp LANMenuScreen.cpp +LandscapePickerScreen.cpp +LandscapePreviewer.cpp MainMenuScreen.cpp map/Map.cpp map/generator/compatibility/LegacyGenerationDescriptor.cpp diff --git a/src/Utilities.cpp b/src/Utilities.cpp index 68631ebf7..bd31fe415 100644 --- a/src/Utilities.cpp +++ b/src/Utilities.cpp @@ -26,8 +26,7 @@ using ssize_t = SSIZE_T; #endif -//Mersenne twister implementation -boost::mt19937 randomGenerator; +thread_local boost::mt19937 randomGenerator; int distSquare(int x1, int y1, int x2, int y2) { diff --git a/src/Utilities.h b/src/Utilities.h index 83d773c9b..7f8f3d5fd 100644 --- a/src/Utilities.h +++ b/src/Utilities.h @@ -18,8 +18,10 @@ namespace GAGCore class OutputStream; } -//Mersenne twister implementation -extern boost::mt19937 randomGenerator; +// The synchronized gameplay stream. One state per thread: the simulation only ever runs on +// one thread, so its sequence is unchanged, while a background map generation (which seeds +// this stream itself) neither disturbs nor races the menu's live colony on the UI thread. +extern thread_local boost::mt19937 randomGenerator; inline Uint32 syncRand(void) { diff --git a/src/map/io/MapThumbnail.cpp b/src/map/io/MapThumbnail.cpp index 5760f21b8..79722b13b 100644 --- a/src/map/io/MapThumbnail.cpp +++ b/src/map/io/MapThumbnail.cpp @@ -60,12 +60,17 @@ void MapThumbnail::loadFromMap(const std::string& map) delete stream; if (!good) return; - - // set values + loadFromMap(map); + } +} + +void MapThumbnail::loadFromMap(const Map& map) +{ + loaded = true; + { lastW = map.getW(); lastH = map.getH(); - // TODO : put this thumbnail code in a function int H[3]= { 0, 90, 0 }; int E[3]= { 0, 40, 120 }; int S[3]= { 170, 170, 0 }; diff --git a/src/map/io/MapThumbnail.h b/src/map/io/MapThumbnail.h index 5fe81d696..728cac6cd 100644 --- a/src/map/io/MapThumbnail.h +++ b/src/map/io/MapThumbnail.h @@ -7,6 +7,7 @@ #include #include "SDL_net.h" +class Map; namespace GAGCore { class DrawableSurface; @@ -23,6 +24,9 @@ class MapThumbnail ///Loads the thumbnail from the map with the given map name void loadFromMap(const std::string& map); + + ///Renders the thumbnail from a map already in memory + void loadFromMap(const Map& map); ///Encodes this thumbnail into a stream void encodeData(GAGCore::OutputStream* stream) const; diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index cd02a0942..f754ebf4c 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -7,6 +7,7 @@ #include "Engine.h" #include "FrontendTheme.h" #include "GlobalContainer.h" +#include "LandscapePickerScreen.h" #include "LobbyControls.h" #include "LobbyMapCatalog.h" #include "LobbyMapPreview.h" @@ -23,6 +24,7 @@ #include #include #include +#include #include GlobalContainer *globalContainer = nullptr; @@ -688,15 +690,11 @@ struct CustomGameSetupHarness screen.onTimer(screen.previewDue); assert(screen.validMap); - // Drive the same dropdown/steppers used by players, including measured - // five-unit steps. + // Apply a landscape the way the picker's result does, then drive the same steppers used + // by players, including measured five-unit steps. The picker itself is exercised below. auto landscape = [&](int method) { - clickControl("generator/landscape"); - for (size_t i = 0; i < GeneratorRegistry::builtins().methods(false).size(); ++i) - keyEvent(SDLK_UP); - for (int i = 0; i < GeneratorRegistry::builtins().selectionIndex(method, false); ++i) - keyEvent(SDLK_DOWN); - keyEvent(SDLK_RETURN); + screen.applyLandscape(method, std::nullopt); + paint(); assert(screen.setup.generator.method == method); }; landscape(MapGenerationDescriptor::eCONCRETEISLANDS); @@ -818,6 +816,158 @@ struct CustomGameSetupHarness SDL_FreeSurface(rgba); } puts("PASS rectangular aspect ratios, cropped terrain and colony marker pixels"); + // The landscape picker: every playable landscape previewed on background threads, + // selection by click and keys, regeneration with fresh seeds, and the lobby then + // playing exactly the map that was shown. + { + screen.setup.generatorHistory.select(screen.setup.generator, + GenerationRequest::eCONTESTEDCOMMONS); + screen.setup.generator.wDec = screen.setup.generator.hDec = 8; + screen.setup.setCapacity(4); + screen.invalidate(); + paint(); + assert(std::any_of(screen.controls->hits.begin(), screen.controls->hits.end(), + [](const auto &h) { return h.id == "generator/landscape"; })); + const auto entries = screen.landscapeEntries(); + assert(entries.size() == GeneratorRegistry::builtins().methods(false).size()); + for (const auto &[method, request] : entries) + assert(request.method == method && request.nbTeams == 4 && request.wDec == 8 && + request.hDec == 8); + std::vector shown; + for (const auto &[method, request] : entries) + shown.push_back({GenerationRequest::methodName(method), request}); + const int current = + GeneratorRegistry::builtins().selectionIndex(screen.setup.generator.method, false); + LandscapePickerScreen picker("Landscape", shown, current); + picker.dispatchInit(); + assert(picker.previewer.threadCount() >= 1 && picker.busy()); + picker.dispatchPaint(false); // placeholders while every tile is still pending + globalContainer->gfx->printScreen(output + "/landscape-picker-pending.bmp"); + auto settle = [&] { + const Uint32 deadline = SDL_GetTicks() + 120000; + while (picker.busy()) { + assert(Sint32(SDL_GetTicks() - deadline) < 0); + SDL_Delay(10); + picker.dispatchTimer(SDL_GetTicks()); + } + picker.dispatchTimer(SDL_GetTicks()); + picker.dispatchPaint(false); + }; + auto pick = [&](const std::string &id) { + picker.dispatchPaint(false); + auto find = [&] { + return std::find_if(picker.controls->hits.begin(), picker.controls->hits.end(), + [&](const auto &h) { return h.id == id; }); + }; + auto hit = find(); + assert(hit != picker.controls->hits.end()); + if (hit->region >= 0) { + // Tiles below the fold are clipped, so bring one into view before clicking it. + auto ®ion = picker.controls->regions[hit->region]; + if (hit->box.y < region.box.y) + picker.controls->scroll(hit->region, hit->box.y - region.box.y); + else if (hit->box.y + hit->box.h > region.box.y + region.box.h) + picker.controls->scroll(hit->region, + hit->box.y + hit->box.h - region.box.y - region.box.h); + picker.dispatchPaint(false); + hit = find(); + } + const auto r = hit->box; + for (auto type : {SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP}) { + SDL_Event e = {}; + e.type = type; + e.button.button = SDL_BUTTON_LEFT; + e.button.x = r.x + r.w / 2; + e.button.y = r.y + r.h / 2; + picker.dispatchEvents(&e); + } + picker.dispatchPaint(false); + }; + auto pickerKey = [&](SDL_Keycode key) { + SDL_Event e = {}; + e.type = SDL_KEYDOWN; + e.key.keysym.sym = key; + picker.dispatchEvents(&e); + picker.dispatchPaint(false); + }; + settle(); + globalContainer->gfx->printScreen(output + "/landscape-picker.bmp"); + auto seedsShown = [&] { + std::vector seeds; + for (const auto &tile : picker.tiles) { + assert(tile.preview.state == LandscapePreviewer::State::Ready && tile.surface && + tile.preview.width == 256 && tile.preview.height == 256 && + tile.preview.starts.size() == 4); + seeds.push_back(tile.preview.seed); + } + assert(std::set(seeds.begin(), seeds.end()).size() == seeds.size()); + return seeds; + }; + const auto first = seedsShown(); + assert(picker.selection() == current && picker.chosenSeed() == first[current]); + // A click selects; arrows move by one tile or one row; Escape cancels. + const int other = (current + 1) % int(shown.size()); + pick("landscape/" + std::to_string(other)); + assert(picker.selection() == other && picker.returnCode == 0); + pickerKey(SDLK_LEFT); + assert(picker.selection() == std::max(0, other - 1)); + pickerKey(SDLK_DOWN); + assert(picker.selection() == + std::min(int(shown.size()) - 1, std::max(0, other - 1) + picker.columns)); + pickerKey(SDLK_ESCAPE); + assert(picker.returnCode == LandscapePickerScreen::CANCEL); + // Regenerate all rolls every landscape again with fresh seeds. + pick("landscape/regenerate"); + assert(picker.busy()); + settle(); + const auto second = seedsShown(); + for (size_t i = 0; i < first.size(); ++i) + assert(first[i] != second[i]); + // Return confirms the selection, as does clicking the selected tile. + pick("landscape/" + std::to_string(other)); + pickerKey(SDLK_RETURN); + assert(picker.returnCode == other); + pick("landscape/" + std::to_string(other)); + assert(picker.returnCode == other); + pick("landscape/use"); + assert(picker.returnCode == other); + globalContainer->gfx->printScreen(output + "/landscape-picker-selected.bmp"); + // The lobby then rolls the very seed the picker showed: same starts, same map header. + const auto seed = *picker.chosenSeed(); + const auto starts = picker.tiles[other].preview.starts; + screen.applyLandscape(entries[other].first, seed); + assert(screen.previewPending && screen.chosenSeed == seed && + screen.setup.generator.method == entries[other].first); + screen.onTimer(screen.previewDue); + assert(screen.validMap && !screen.chosenSeed); + assert(screen.preview->starts.size() == starts.size()); + for (size_t i = 0; i < starts.size(); ++i) + assert(screen.preview->starts[i].x == starts[i].x && + screen.preview->starts[i].y == starts[i].y && + screen.preview->starts[i].color.r == starts[i].color.r && + screen.preview->starts[i].color.g == starts[i].color.g && + screen.preview->starts[i].color.b == starts[i].color.b); + { + Game shownMap(nullptr); + GAGCore::BinaryInputStream in( + Toolkit::getFileManager()->openInputStreamBackend(screen.snapshot)); + assert(shownMap.load(&in) && shownMap.gameHeader.getRandomSeed() == seed); + } + capture("landscape-applied"); + // An edit after the pick drops the shown seed: the next preview samples candidates again. + screen.applyLandscape(entries[other].first, seed); + assert(screen.chosenSeed == seed); + clickControl("generator/width"); + assert(screen.controls->popup.open); + keyEvent(SDLK_UP); + keyEvent(SDLK_RETURN); + assert(!screen.chosenSeed && screen.previewPending); + screen.onTimer(screen.previewDue); + assert(screen.validMap); + std::cout << "PASS landscape picker: " << shown.size() << " previews on " + << picker.previewer.threadCount() + << " threads, selection, regeneration and the shown map played\n"; + } screen.setup.generatorHistory.select(screen.setup.generator, MapGenerationDescriptor::eRIVER); screen.setup.setCapacity(12); screen.setup.generator.wDec = screen.setup.generator.hDec = 8; From 517ea16d92b306e06727e489e2f516c214c18048 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 12:53:40 -0400 Subject: [PATCH 085/136] Let Contested commons spread any colony count on any map size Contested commons places its colonies and the shared island with the whole-region best-response search in shared/Regions, which scored every tile against every other point for every point: map area times points squared per pass, behind a fixed budget of 20 million distance evaluations. That budget bought four passes at 8 colonies on a 256 map and one pass at 512, and the search never converges that fast, so the landscape failed on every seed past four colonies at 256 and at any colony count at 512, in the lobby as much as in the new landscape picker. The search now keeps, per tile, the nearest and second-nearest weighted distance over all points and which points they belong to, refreshed as each point moves (a full recompute only where a departing point held one of the two slots), so a point's score at a tile is one read: the same minimum the loop computed, scanned in the same row-major order under the same strict-improvement rule, so every seed that generated before produces the same map. The evaluation budget goes; the 200-pass cap remains the convergence guard. The local 7x7 search is untouched. The generator's revision stays at 2: no map that a seed produced before changes, only seeds that failed now generate. Verified: 147 baseline configurations across every generator (all methods at defaults, seeds 1-5; Contested commons at 128, 256, 512x256 and 2-4 colonies, 30 seeds at 256/4) give identical study statistics and map hashes before and after; the 8 that failed before now generate; Contested commons at 256 with 8 colonies takes about 0.2 s and at 512 with 12 colonies about 1 s per roll, against 0.8 s and 4.5 s with only the budget lifted. MapGeneratorDefaultsTest gains a regression rolling 256/8, 512/4 and 512/12 on three seeds each; it fails on the previous code with the budget diagnostic and passes now. CustomGameSetupHarness passes headless and in the visual run. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 2 +- src/map/generator/shared/Regions.cpp | 160 +++++++++++++++--- test/MapGeneratorDefaultsTest.cpp | 16 ++ 3 files changed, 150 insertions(+), 28 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 9e227daf9..1ebed408b 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -17,7 +17,7 @@ leans on the shared modules below only where doing so is actually less work than | Cross-control and dimension constraints | `GeneratorDefinition::validateRequest` | A pure check run before generation; no silent settings rewrites | | Jagged islands, stretched/rotated continents, coastline shape | `shared/Geometry` (`ShapeTransform`, `RadialShape`, `stampShape`) | An invertible shape transform and a seeded radial shape with a per-angle `radiusAt()` query — checking a specific candidate against its own angle is exact, where a single global "furthest possible" bound is only ever a conservative overestimate | | Distances through corridors, connected components, resource-role adjacency | `shared/Topology`, `shared/Distances` | Graph distances and grid-component labelling with explicit wrapping/neighbor policy | -| Whole-region or local point dispersion (Contested Commons, Concrete Islands, Isles) | `shared/Regions` | Paired weight/point shuffling, a bounded local search and an opt-in whole-region search with an explicit pass/evaluation budget rather than silently treating an unfinished search as converged | +| Whole-region or local point dispersion (Contested Commons, Concrete Islands, Isles) | `shared/Regions` | Paired weight/point shuffling, a bounded local search and an opt-in whole-region search that scores every tile from its two nearest weighted distances, kept current as points move, under an explicit pass budget rather than silently treating an unfinished search as converged | | Settlements restricted to a home island or room | `shared/Settlements` | Whole-footprint mask, nearest legal anchor, exact worker count, per-colony diagnostics | | Generator-specific connectivity guarantees | `GeneratorDefinition::validateWorld` | Runs after structural checks, against the actual finished terrain/movement mask | diff --git a/src/map/generator/shared/Regions.cpp b/src/map/generator/shared/Regions.cpp index 159a49c12..994ad31b9 100644 --- a/src/map/generator/shared/Regions.cpp +++ b/src/map/generator/shared/Regions.cpp @@ -14,6 +14,7 @@ #include #include #include +#include using namespace MapGeneration; namespace MapGeneration @@ -147,10 +148,105 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, sources.clear(); obstacles.clear(); + // A point's score at a tile is the smallest weighted squared distance to any other point. + // The whole-region search reads that off two per-tile fields, the nearest and second-nearest + // weighted distance over every point (a point's own entry is skipped by taking the second), + // kept current as points move, instead of looping over every other point at every tile for + // every point, which cost map area times points squared per pass and, behind a fixed + // evaluation budget, refused any map past four colonies at 256 or any colony count at 512. + // The values are the same minimums, so a pass makes the same moves as that loop did. + struct NearestTwo + { + Map ↦ + const std::vector &points; + const std::vector &weights; + std::vector first, second; + std::vector firstId, secondId; + NearestTwo(Map &map, const std::vector &points, + const std::vector &weights) + : map(map), points(points), weights(weights), first(map.getW() * map.getH()), + second(first.size()), firstId(first.size()), secondId(first.size()) + { + for (std::size_t t = 0; t < first.size(); ++t) + rebuild(t); + } + std::int64_t value(int x, int y, unsigned j) const + { + return std::int64_t(map.warpDistSquare(x, y, points[j].x, points[j].y)) * weights[j]; + } + void rebuild(std::size_t t) + { + const int x = int(t % map.getW()), y = int(t / map.getW()); + first[t] = second[t] = std::numeric_limits::max(); + firstId[t] = secondId[t] = -1; + for (unsigned j = 0; j < points.size(); ++j) + offer(t, value(x, y, j), int(j)); + } + void offer(std::size_t t, std::int64_t v, int id) + { + if (v < first[t]) + { + second[t] = first[t]; + secondId[t] = firstId[t]; + first[t] = v; + firstId[t] = id; + } + else if (v < second[t]) + { + second[t] = v; + secondId[t] = id; + } + } + std::int64_t excluding(std::size_t t, int id) const + { + return firstId[t] == id ? second[t] : first[t]; + } + // Point id now stands at its new position: refresh every tile's pair. + void moved(int id) + { + const int w = map.getW(); + for (std::size_t t = 0; t < first.size(); ++t) + { + const std::int64_t v = value(int(t % w), int(t / w), id); + if (firstId[t] == id) + { + if (v <= second[t]) + first[t] = v; + else + rebuild(t); // its successor is unknown + } + else if (secondId[t] == id) + { + if (v < first[t]) + { + second[t] = first[t]; + secondId[t] = firstId[t]; + first[t] = v; + firstId[t] = id; + } + else if (v <= second[t]) + second[t] = v; + else + rebuild(t); + } + else + offer(t, v, id); + } + } + }; + std::unique_ptr nearest; + std::vector occupants; + if (search == PointSearch::WholeRegion) + { + nearest = std::make_unique(map, points, weights); + occupants.assign(grid.size(), 0); + for (const auto &p : points) + ++occupants[p.y * map.getW() + p.x]; + } + bool cont = true; std::int64_t minDist = std::numeric_limits::max(); int passes = 0; - std::uint64_t evaluations = 0; while (cont) { if (++passes > maxPasses) @@ -159,6 +255,37 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, bool changed = false; for (unsigned int i = 0; i < points.size(); ++i) { + if (search == PointSearch::WholeRegion) + { + const int w = map.getW(); + const std::size_t home = points[i].y * w + points[i].x; + std::int64_t best = nearest->excluding(home, int(i)); + minDist = std::min(best, minDist); + std::size_t bestTile = home; + // Row-major over the grid, updating on strict improvement only, so the same + // candidate wins an exact tie as before. + for (std::size_t t = 0; t < grid.size(); ++t) + { + if (t == home || grid[t] != areaN || occupants[t] != 0) + continue; + const std::int64_t score = nearest->excluding(t, int(i)); + if (score > best) + { + best = score; + bestTile = t; + } + } + if (bestTile != home) + { + changed = true; + --occupants[home]; + ++occupants[bestTile]; + points[i].x = int(bestTile % w); + points[i].y = int(bestTile / w); + nearest->moved(int(i)); + } + continue; + } std::int64_t best = std::numeric_limits::max(); for (unsigned int j = 0; j < points.size(); ++j) { @@ -190,9 +317,6 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, invalid = true; break; } - if (search == PointSearch::WholeRegion && ++evaluations > 20000000) - throw GenerationFailure( - "Point dispersion exhausted its distance-evaluation budget"); std::int64_t dist = std::int64_t(map.warpDistSquare(nx, ny, points[j].x, points[j].y)) * weights[j]; @@ -207,29 +331,11 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, best_y = ny; } }; - if (search == PointSearch::WholeRegion) - { - // A full-grid scan: nesting the row coordinate outside the column coordinate - // keeps it within the grain of grid's row-major layout instead of striding - // across it a full row at a time on every step. This can change which - // exactly-tied candidate wins a placement (best only updates on strict - // improvement), but never which candidates exist or how good the eventual - // choice is - already documented as bounded best-response, not a single - // guaranteed optimum, so a different tie winner is not a different guarantee. - for (int ny = 0; ny < map.getH(); ++ny) - for (int nx = 0; nx < map.getW(); ++nx) - tryCandidate(nx, ny); - } - else - { - // PointSearch::Local's window is the 7x7 area right around the point itself - - // small enough that traversal order was never going to matter for cache - // behavior - so its enumeration order is left exactly as it was. - for (int dx = -3; dx <= 3; ++dx) - for (int dy = -3; dy <= 3; ++dy) - tryCandidate(map.normalizeX(points[i].x + dx), - map.normalizeY(points[i].y + dy)); - } + // PointSearch::Local's window is the 7x7 area right around the point itself. + for (int dx = -3; dx <= 3; ++dx) + for (int dy = -3; dy <= 3; ++dy) + tryCandidate(map.normalizeX(points[i].x + dx), + map.normalizeY(points[i].y + dy)); if (best_x != -1) { if (best != orig) diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index b86cf3e78..7a592425b 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -173,6 +173,22 @@ class MapGeneratorDefaultsTest assert(failure.error == GenerationError::InvalidRequest && fresh.teamsCount() == 0); } } + // Contested commons spreads its colonies with the whole-region search, which used to run + // out of a fixed evaluation budget past four colonies at 256 and at any count at 512. + for (auto [dims, teams] : {std::pair{8, 8}, std::pair{9, 4}, std::pair{9, 12}}) + for (std::uint32_t seed = 1; seed <= 3; ++seed) + { + D crowded; + crowded.setMethodDefaults(D::eCONTESTEDCOMMONS); + crowded.wDec = crowded.hDec = dims; + crowded.nbTeams = teams; + crowded.seed = seed; + Game world(nullptr); + const auto outcome = service.generate(world, crowded); + if (!outcome) + std::cerr << outcome.diagnostic() << std::endl; + assert(outcome && world.teamsCount() == teams); + } // Scoped RNG restoration must also hold when placement fails. setSyncRandSeed(711); auto savedFailureRng = randomGenerator; From 57e424cdefd0cadea7b6098da08753d46fc67cfb Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 13:48:53 -0400 Subject: [PATCH 086/136] Enforce generator revisions with golden maps and sweep every colony count MapGeneratorGoldenTest keeps the revision rule honest. A checked-in table records, per platform, the fingerprint (terrain, resources and colony starts) every registered generator produces for three seeds at 256, one at 128 and 512, and one each with two and eight colonies, keyed by the generator's revision. The check fails any map that changed at an unchanged revision and any generator whose revision moved without the table following; --update rewrites this platform's rows and refuses to record a changed map under an unchanged revision unless forced; --print writes the rows to stdout so a platform's rows can be bootstrapped from a CI log. Generation is deterministic per platform, not across platforms, so rows carry the platform they were made on and a platform without rows reports rather than fails; macOS arm64 rows are committed here. --sweep rolls every playable landscape at the colony counts and sizes the lobby offers, five seeds at 128 and 256 and three at 512 as the lobby's best-of-five stands in for, prints the success rate per cell and fails any valid cell where no seed generated. Its first run found Shattered coast seating 12 colonies on 256 on two seeds of five and Concrete islands at 12 colonies on four of five; the Contested commons budget failure fixed yesterday would have been a 0/5 here. The contract test gains two toolkit oracles: the point dispersion ends at a mutual best response checked against a brute-force score, on a ragged region, for both search modes, and the distance flood matches a Chebyshev oracle on the torus, with a sealing obstacle ring and repeated sources. The map fingerprint moves to the shared checks header and now covers colony starts. CI builds and runs the golden check, print and sweep on both Linux images; the framework reference, the development notes and the test README describe the rules. Verified: MapGeneratorDefaultsTest passes; the golden check compares 126 rows with 0 failures and the sweep reports 0 failing cells in 54 s, both on macOS arm64. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .github/workflows/build.yml | 7 +- docs/development-notes.md | 4 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 22 +- src/SConscript | 3 +- test/MapGeneratorDefaultsTest.cpp | 38 +- test/MapGeneratorFrameworkChecks.h | 160 +++++++- test/MapGeneratorGoldenTest.cpp | 365 ++++++++++++++++++ test/README.md | 9 + test/map-generator-golden.txt | 128 ++++++ 9 files changed, 696 insertions(+), 40 deletions(-) create mode 100644 test/MapGeneratorGoldenTest.cpp create mode 100644 test/map-generator-golden.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7261f80a..e7d3cdc2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,8 +107,13 @@ jobs: - name: Build and run custom-game setup regressions run: | - scons -j$(nproc) release=1 server=0 custom-setup-test map-generator-defaults-test + scons -j$(nproc) release=1 server=0 custom-setup-test map-generator-defaults-test map-generator-golden-test timeout 60s ./build/src/MapGeneratorDefaultsTest glob2-map-defaults-ci + # Golden maps: rows exist per platform; a platform without rows reports and passes, + # and --print shows the rows it would record so they can be committed from the log. + timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci + timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --print + timeout 600s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --sweep timeout 180s ./build/src/CustomGameSetupHarness mkdir -p artifacts/custom-game-ci timeout 180s xvfb-run -a -s '-screen 0 1024x768x24' ./build/src/CustomGameSetupHarness artifacts/custom-game-ci > artifacts/custom-game-ci/ui.log 2>&1 diff --git a/docs/development-notes.md b/docs/development-notes.md index f55faa57c..e41ce4ea9 100644 --- a/docs/development-notes.md +++ b/docs/development-notes.md @@ -99,6 +99,10 @@ For timing and scheduling, start with `src/Game_sync.cpp` and `src/EngineRun.cpp - A new regression harness only protects the codebase once `.github/workflows/build.yml` actually builds and runs it; one that only runs by hand, once, is not a regression test. +- A map generator's `revision` is enforced by `MapGeneratorGoldenTest`: a seed's map changing + while the revision stays fails the check, so bump the revision and run `--update` together + (see the framework reference). `--sweep` there is the first thing to run after touching + colony placement; a cell it fails is a "Generation failed" a player would see. - When fixing a bug, confirm the regression actually fails against the unpatched code before trusting that it passes against the fix. A test that passes either way is not testing the bug. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 1ebed408b..a86717a82 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -531,10 +531,26 @@ on its island's edge, and expansion means taking another island whole. controls as JSON, with each control's `kind` (`range` or `toggle`). - `test/MapGeneratorDefaultsTest.cpp` builds to `MapGeneratorDefaultsTest`, asserting the registry's and every control's contract: discrete domains, shape bounds, topology, home - footprints, exact worker counts, seed repeatability and RNG stream isolation, and the - lobby/editor UI's own control-editing behavior. + footprints, exact worker counts, seed repeatability and RNG stream isolation, the + lobby/editor UI's own control-editing behavior, and the shared toolkit's own guarantees: + the point dispersion ends at a mutual best response checked against a brute-force score, + and the distance flood matches a Chebyshev oracle on the torus, with obstacles and repeated + sources. +- `test/MapGeneratorGoldenTest.cpp` builds to `MapGeneratorGoldenTest` and keeps the revision + rule honest. `test/map-generator-golden.txt` records, per platform, the fingerprint (terrain, + resources and colony starts) every registered generator produces for three seeds at 256, one + at 128 and 512, and one each with two and eight colonies, keyed by the generator's revision. + `MapGeneratorGoldenTest ` regenerates this platform's rows and fails on any map that + changed at an unchanged revision or any generator whose revision moved without the table + following; `--update` rewrites this platform's rows and refuses (without `--force`) to record + a changed map under an unchanged revision; `--print` writes the rows to stdout, which is how + a platform's rows are first bootstrapped from a CI log. Generation is deterministic per + platform, not across platforms, so rows carry the platform they were made on. `--sweep` rolls + every playable landscape at the colony counts and sizes the lobby offers, five seeds at 128 + and 256 and three at 512, prints the success rate per cell and fails any valid cell where no + seed generated: that is what a player would see as a failed generation. CI runs all three. - The cppunit suite under `test/` (`scons && ./TestsRunner`) covers the rest of the engine and - must stay green alongside both of the above. + must stay green alongside all of the above. Generators validate their own construction results rather than trusting the geometry to always succeed: a moat must connect to land at both bridge ends, jagged outlines must leave legal diff --git a/src/SConscript b/src/SConscript index a3eb0d649..bbb607b72 100644 --- a/src/SConscript +++ b/src/SConscript @@ -594,7 +594,8 @@ if not env['server']: # Map analysis and UI contract tests use production generators and widgets. if not env['server']: - for alias, target in [('map-generator-study', 'MapGeneratorStudy'), ('map-generator-defaults-test', 'MapGeneratorDefaultsTest')]: + for alias, target in [('map-generator-study', 'MapGeneratorStudy'), ('map-generator-defaults-test', 'MapGeneratorDefaultsTest'), + ('map-generator-golden-test', 'MapGeneratorGoldenTest')]: sources = [source for source in source_files if source != 'Glob2.cpp'] sources += local.Object(target + '.o', '#test/' + target + '.cpp') local.Alias(alias, local.Program(target, sources)) diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 7a592425b..cdf992589 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -81,36 +81,6 @@ class MapGeneratorDefaultsTest s.dispatchEvents(&event); } } - static std::uint64_t fingerprint(const Game &game) - { - std::uint64_t hash = 14695981039346656037ull; - auto add = [&](unsigned v) - { - hash ^= v; - hash *= 1099511628211ull; - }; - for (int y = 0; y < game.map.getH(); ++y) - for (int x = 0; x < game.map.getW(); ++x) - { - add(game.map.getUMTerrain(x, y)); - add(game.map.getTerrain(x, y)); - add(game.map.getResource(x, y).type); - add(game.map.getResource(x, y).amount); - } - for (int i = 0; i < game.teamsCount(); ++i) - { - add(game.teams[i]->startPosX); - add(game.teams[i]->startPosY); - for (int j = 0; j < Unit::MAX_COUNT; ++j) - if (auto *unit = game.teams[i]->myUnits[j]) - { - add(j); - add(unit->posX); - add(unit->posY); - } - } - return hash; - } static void generationContracts() { globalsInit(); @@ -126,7 +96,7 @@ class MapGeneratorDefaultsTest Game first(nullptr); auto a = service.generate(first, request); assert(randomGenerator == surrounding); - auto hash = fingerprint(first); + auto hash = mapFingerprint(first); auto checksum = first.checkSum(nullptr, nullptr, nullptr, true); D intervening; intervening.setMethodDefaults(D::eISLANDS); @@ -135,17 +105,17 @@ class MapGeneratorDefaultsTest service.generate(other, intervening); Game repeat(nullptr); auto b = service.generate(repeat, request); - if (bool(a) != bool(b) || a.stage != b.stage || hash != fingerprint(repeat)) + if (bool(a) != bool(b) || a.stage != b.stage || hash != mapFingerprint(repeat)) std::fprintf(stderr, "Not repeatable after an intervening map: generator %d (%s, %s)\n", method, a.diagnostic().c_str(), b.diagnostic().c_str()); - assert(bool(a) == bool(b) && a.stage == b.stage && hash == fingerprint(repeat)); + assert(bool(a) == bool(b) && a.stage == b.stage && hash == mapFingerprint(repeat)); assert(checksum == repeat.checkSum(nullptr, nullptr, nullptr, true)); assert(request.seed == 22001 && request.options == DWithDefaults(method).options); if (a) { auto rejected = service.generate(first, request); assert(rejected.error == GenerationError::NonEmptyTarget); - assert(fingerprint(first) == hash); + assert(mapFingerprint(first) == hash); } if (method != D::eUNIFORM) for (auto dimensions : {std::pair{9, 7}, std::pair{7, 9}}) diff --git a/test/MapGeneratorFrameworkChecks.h b/test/MapGeneratorFrameworkChecks.h index cad4f85e7..f8e750a2c 100644 --- a/test/MapGeneratorFrameworkChecks.h +++ b/test/MapGeneratorFrameworkChecks.h @@ -8,13 +8,171 @@ #include #include #include "GenerationValidation.h" +#include "Distances.h" #include "Geometry.h" +#include "Regions.h" #include "Settlements.h" #include "Topology.h" #include +#include +#include -static void frameworkChecks() +/// Everything a generator decides that a player can see: terrain, resources and where each +/// colony starts. Two maps with the same fingerprint are the same map for golden comparisons. +inline std::uint64_t mapFingerprint(const Game &game) { + std::uint64_t hash = 14695981039346656037ull; + auto add = [&](unsigned v) + { + hash ^= v; + hash *= 1099511628211ull; + }; + add(game.map.getW()); + add(game.map.getH()); + for (int y = 0; y < game.map.getH(); ++y) + for (int x = 0; x < game.map.getW(); ++x) + { + add(game.map.getUMTerrain(x, y)); + add(game.map.getTerrain(x, y)); + add(game.map.getResource(x, y).type); + add(game.map.getResource(x, y).amount); + } + add(game.teamsCount()); + for (int i = 0; i < game.teamsCount(); ++i) + { + add(game.teams[i]->startPosX); + add(game.teams[i]->startPosY); + } + return hash; +} + +// The whole-region dispersion ends where no point can improve its own score, the smallest +// weighted squared distance to any other point, by moving anywhere in the region; the local +// search, anywhere within its 7x7 window. Checked against a brute-force score, so the +// incremental nearest-pair bookkeeping behind the whole-region search has an oracle. +inline void dispersionChecks() +{ + using namespace MapGeneration; + for (int variant = 0; variant < 4; ++variant) + { + const bool whole = variant % 2 == 0; + const int count = variant < 2 ? 5 : 3; + Game game(nullptr); + game.map.setSize(5, 5); + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + std::vector grid(size_t(w) * h, 0); + // Two blocked bands make the region ragged, so "anywhere in the region" is a real test. + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if ((x >= 10 && x < 14 && y < 20) || (y >= 24 && y < 27 && x >= 6)) + grid[y * w + x] = 1; + auto run = [&](std::uint32_t seed, std::vector &points, + std::vector &weights) + { + GenerationRequest request; + request.seed = seed; + GenerationContext context(request); + points.assign(count, MapGeneratorPoint(0, 0)); + weights.assign(count, 2); + weights.back() = 1; + return splitUpPoints(map, context, grid, 0, points, weights, + whole ? PointSearch::WholeRegion : PointSearch::Local); + }; + std::vector points, again; + std::vector weights, weightsAgain; + const int spread = run(1234 + variant, points, weights); + run(1234 + variant, again, weightsAgain); + assert(spread > 0); + for (int i = 0; i < count; ++i) + assert(points[i].x == again[i].x && points[i].y == again[i].y && + weights[i] == weightsAgain[i]); + auto score = [&](int i, int x, int y) + { + std::int64_t best = std::numeric_limits::max(); + for (int j = 0; j < count; ++j) + if (j != i) + best = std::min(best, std::int64_t(map.warpDistSquare(x, y, points[j].x, + points[j].y)) * + weights[j]); + return best; + }; + std::set> occupied; + for (int i = 0; i < count; ++i) + { + assert(grid[points[i].y * w + points[i].x] == 0); + assert(occupied.insert({points[i].x, points[i].y}).second); + } + for (int i = 0; i < count; ++i) + { + const std::int64_t own = score(i, points[i].x, points[i].y); + auto consider = [&](int x, int y) + { + if (grid[y * w + x] != 0 || occupied.count({x, y})) + return; + assert(score(i, x, y) <= own); + }; + if (whole) + { + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + consider(x, y); + } + else + for (int dx = -3; dx <= 3; ++dx) + for (int dy = -3; dy <= 3; ++dy) + consider(map.normalizeX(points[i].x + dx), map.normalizeY(points[i].y + dy)); + } + } +} + +// computeDistances floods eight-connected over the torus: a source reads 1, each ring one +// more, obstacles -1 and never crossed, unreached tiles 0. +inline void distanceChecks() +{ + using namespace MapGeneration; + Game game(nullptr); + game.map.setSize(5, 4); + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + std::vector sources{{3, 3}, {30, 12}}, none, wall; + std::vector heights; + computeDistances(map, sources, none, heights); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + int nearest = std::numeric_limits::max(); + for (const auto &s : sources) + { + const int dx = std::min(std::abs(x - s.x), w - std::abs(x - s.x)); + const int dy = std::min(std::abs(y - s.y), h - std::abs(y - s.y)); + nearest = std::min(nearest, std::max(dx, dy)); + } + assert(heights[y * w + x] == nearest + 1); + } + // A ring of obstacles around the first source holds its flood; the second still spreads. + std::vector one{{3, 3}}; + for (int dx = -1; dx <= 1; ++dx) + for (int dy = -1; dy <= 1; ++dy) + if (dx || dy) + wall.push_back({3 + dx, 3 + dy}); + computeDistances(map, one, wall, heights); + assert(heights[3 * w + 3] == 1); + for (const auto &o : wall) + assert(heights[o.y * w + o.x] == -1); + assert(heights[0] == 0 && heights[10 * w + 10] == 0); + // Repeated sources are tolerated and change nothing. + std::vector twice{{3, 3}, {3, 3}, {30, 12}}; + std::vector repeated; + computeDistances(map, twice, none, repeated); + computeDistances(map, sources, none, heights); + assert(repeated == heights); +} + +inline void frameworkChecks() +{ + dispersionChecks(); + distanceChecks(); using namespace MapGeneration; GeneratorControl cells{"cell", "Cell", 4, 16, 1, 8, ControlGroup::Layout, false, false, {4, 8, 16}}; diff --git a/test/MapGeneratorGoldenTest.cpp b/test/MapGeneratorGoldenTest.cpp new file mode 100644 index 000000000..a32855df0 --- /dev/null +++ b/test/MapGeneratorGoldenTest.cpp @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Golden maps and a colony-count sweep for every registered generator. +// +// MapGeneratorGoldenTest check this platform's rows of the table +// MapGeneratorGoldenTest --update regenerate this platform's rows +// MapGeneratorGoldenTest --print print this platform's rows to stdout +// MapGeneratorGoldenTest --sweep every playable landscape at the colony +// counts and sizes the lobby offers +// +// The table (test/map-generator-golden.txt) records, per platform, the fingerprint of the map +// each generator produces for a few seeds, sizes and colony counts, keyed by the generator's +// revision. A generator whose output changes without its revision moving fails the check; a +// revision bump makes --update accept the new maps. Generation is deterministic per platform, +// not across platforms, so rows carry the platform they were made on and a platform without +// rows is reported, not failed. +#include "Game.h" +#include "GenerationContext.h" +#include "GenerationService.h" +#include "GeneratorRegistry.h" +#include "GlobalContainer.h" +#include "MapGeneratorFrameworkChecks.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +GlobalContainer *globalContainer = nullptr; + +namespace +{ +const char *const kTablePath = "test/map-generator-golden.txt"; + +std::string platformTag() +{ +#if defined(_WIN32) + const char *os = "windows"; +#elif defined(__APPLE__) + const char *os = "macos"; +#elif defined(__linux__) + const char *os = "linux"; +#else + const char *os = "other"; +#endif +#if defined(__aarch64__) || defined(_M_ARM64) + const char *arch = "arm64"; +#elif defined(__x86_64__) || defined(_M_X64) + const char *arch = "x86_64"; +#else + const char *arch = "unknown"; +#endif + return std::string(os) + "-" + arch; +} + +struct Row +{ + std::string platform; + int id = 0; + unsigned revision = 0; + int wDec = 0, hDec = 0, teams = 0; + std::uint32_t seed = 0; + std::string status; // ok, invalid or failed + std::uint64_t hash = 0; + std::string key() const + { + return platform + " " + std::to_string(id) + " " + std::to_string(wDec) + " " + + std::to_string(hDec) + " " + std::to_string(teams) + " " + std::to_string(seed); + } +}; + +std::vector readTable(const std::string &path) +{ + std::vector rows; + std::ifstream in(path); + std::string line; + while (std::getline(in, line)) + { + if (line.empty() || line[0] == '#') + continue; + std::istringstream fields(line); + Row row; + if (fields >> row.platform >> row.id >> row.revision >> row.wDec >> row.hDec >> + row.teams >> row.seed >> row.status >> row.hash) + rows.push_back(row); + else + throw std::runtime_error("Malformed golden row: " + line); + } + return rows; +} + +void writeTable(const std::string &path, const std::vector &rows) +{ + std::ofstream out(path); + out << "# Golden map fingerprints per generator: platform id revision wDec hDec teams seed" + " status hash\n" + "# Regenerate this platform's rows with MapGeneratorGoldenTest --update" + " after a revision bump.\n"; + for (const auto &r : rows) + out << r.platform << ' ' << r.id << ' ' << r.revision << ' ' << r.wDec << ' ' << r.hDec + << ' ' << r.teams << ' ' << r.seed << ' ' << r.status << ' ' << r.hash << '\n'; +} + +// One roll, as the lobby and editor would ask for it. +Row roll(int id, int wDec, int hDec, int teams, std::uint32_t seed) +{ + Row row; + row.platform = platformTag(); + row.id = id; + row.revision = GeneratorRegistry::builtins().at(id).revision; + row.wDec = wDec; + row.hDec = hDec; + row.teams = teams; + row.seed = seed; + GenerationRequest request; + request.setMethodDefaults(id); + request.wDec = wDec; + request.hDec = hDec; + request.nbTeams = teams; + request.seed = seed; + Game game(nullptr); + const auto result = GenerationService().generate(game, request); + const bool seated = GeneratorRegistry::builtins().at(id).hasStartingColonies + ? game.teamsCount() == teams + : game.teamsCount() == 1; + if (result && seated) + { + row.status = "ok"; + row.hash = mapFingerprint(game); + } + else + row.status = result.error == GenerationError::InvalidRequest ? "invalid" : "failed"; + return row; +} + +// The rows this platform keeps: every registered generator at its own defaults on the three +// lobby sizes, and at 256 with two and with eight colonies. +std::vector goldenRows() +{ + std::vector rows; + for (int id : GeneratorRegistry::builtins().methods(true)) + { + GenerationRequest defaults; + defaults.setMethodDefaults(id); + for (std::uint32_t seed = 1; seed <= 3; ++seed) + rows.push_back(roll(id, 8, 8, defaults.nbTeams, seed)); + rows.push_back(roll(id, 7, 7, defaults.nbTeams, 1)); + rows.push_back(roll(id, 9, 9, defaults.nbTeams, 1)); + rows.push_back(roll(id, 8, 8, 2, 1)); + rows.push_back(roll(id, 8, 8, 8, 1)); + } + return rows; +} + +int check(const std::string &path) +{ + const auto table = readTable(path); + const auto platform = platformTag(); + std::map expected; + std::map revisions; + for (const auto &r : table) + { + revisions[r.id] = r.revision; + if (r.platform == platform) + expected[r.key()] = r; + } + int failures = 0; + for (int id : GeneratorRegistry::builtins().methods(true)) + { + const auto &definition = GeneratorRegistry::builtins().at(id); + auto it = revisions.find(id); + if (it == revisions.end()) + { + std::printf("MISSING generator %s (%d): no golden rows; run --update\n", + definition.id, id); + ++failures; + } + else if (it->second != definition.revision) + { + std::printf("STALE generator %s (%d): table revision %u, registered %u; run --update\n", + definition.id, id, it->second, definition.revision); + ++failures; + } + } + if (expected.empty()) + { + std::printf("No golden rows for %s; nothing to compare. Run --update on this platform" + " and commit its rows.\n", + platform.c_str()); + return failures ? 1 : 0; + } + int compared = 0; + for (const auto &[key, row] : expected) + { + if (row.revision != GeneratorRegistry::builtins().at(row.id).revision) + continue; // already reported as stale + const Row now = roll(row.id, row.wDec, row.hDec, row.teams, row.seed); + ++compared; + if (now.status != row.status || now.hash != row.hash) + { + std::printf("CHANGED %s (%d) %dx%d %d colonies seed %u: was %s %llu, now %s %llu at" + " the same revision %u\n", + GeneratorRegistry::builtins().at(row.id).id, row.id, 1 << row.wDec, + 1 << row.hDec, row.teams, row.seed, row.status.c_str(), + (unsigned long long)row.hash, now.status.c_str(), + (unsigned long long)now.hash, row.revision); + ++failures; + } + } + std::printf("%s: %d golden rows compared, %d failures\n", platform.c_str(), compared, + failures); + return failures ? 1 : 0; +} + +int update(const std::string &path, bool toStdout, bool force) +{ + std::vector table; + try + { + table = readTable(path); + } + catch (const std::exception &error) + { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + const auto platform = platformTag(); + std::map previous; + for (const auto &r : table) + if (r.platform == platform) + previous[r.key()] = r; + const auto fresh = goldenRows(); + int silent = 0; + for (const auto &row : fresh) + { + auto it = previous.find(row.key()); + if (it == previous.end() || it->second.revision != row.revision) + continue; + if (it->second.status != row.status || it->second.hash != row.hash) + { + std::printf("%s (%d) %dx%d %d colonies seed %u changed at unchanged revision %u\n", + GeneratorRegistry::builtins().at(row.id).id, row.id, 1 << row.wDec, + 1 << row.hDec, row.teams, row.seed, row.revision); + ++silent; + } + } + if (silent && !force) + { + std::printf("%d rows changed without a revision bump. Bump the generator's revision, or" + " pass --force if the change is intended to stay unversioned.\n", + silent); + return 1; + } + std::vector merged; + for (const auto &r : table) + if (r.platform != platform) + merged.push_back(r); + merged.insert(merged.end(), fresh.begin(), fresh.end()); + std::stable_sort(merged.begin(), merged.end(), + [](const Row &a, const Row &b) + { + if (a.platform != b.platform) + return a.platform < b.platform; + if (a.id != b.id) + return a.id < b.id; + return a.key() < b.key(); + }); + if (toStdout) + { + for (const auto &r : fresh) + std::cout << r.platform << ' ' << r.id << ' ' << r.revision << ' ' << r.wDec << ' ' + << r.hDec << ' ' << r.teams << ' ' << r.seed << ' ' << r.status << ' ' + << r.hash << '\n'; + return 0; + } + writeTable(path, merged); + std::printf("Wrote %zu rows for %s to %s\n", fresh.size(), platform.c_str(), path.c_str()); + return 0; +} + +// The lobby offers 64 to 512 and 1 to 12 colonies and rolls five seeds, keeping the best. At +// its 256 default every playable landscape must seat every colony count it accepts on at +// least one of five seeds; the small and large sizes are checked at the counts a player is +// likely to ask for. The per-cell rates are printed so a landscape that only just scrapes by +// is visible before it starts failing. +int sweep() +{ + struct Cell + { + int wDec, teams; + std::vector seeds; + }; + const std::vector five{1, 2, 3, 4, 5}, three{1, 2, 3}; + const std::vector cells = { + {7, 2, five}, {7, 4, five}, {8, 2, five}, {8, 3, five}, {8, 4, five}, + {8, 6, five}, {8, 8, five}, {8, 12, five}, {9, 4, three}, {9, 12, three}, + }; + int failures = 0; + for (int id : GeneratorRegistry::builtins().methods(false)) + { + const auto &definition = GeneratorRegistry::builtins().at(id); + std::string line; + for (const auto &cell : cells) + { + int ok = 0; + bool invalid = false; + for (auto seed : cell.seeds) + { + const Row row = roll(id, cell.wDec, cell.wDec, cell.teams, seed); + if (row.status == "invalid") + invalid = true; + else if (row.status == "ok") + ++ok; + } + char cellText[48]; + if (invalid) + std::snprintf(cellText, sizeof cellText, " %d/%d:-", 1 << cell.wDec, cell.teams); + else + std::snprintf(cellText, sizeof cellText, " %d/%d:%d/%zu", 1 << cell.wDec, + cell.teams, ok, cell.seeds.size()); + line += cellText; + if (!invalid && ok == 0) + { + std::printf("FAIL %s (%d) at %dx%d with %d colonies: no seed of %zu generated\n", + definition.id, id, 1 << cell.wDec, 1 << cell.wDec, cell.teams, + cell.seeds.size()); + ++failures; + } + } + std::printf("%-20s%s\n", definition.id, line.c_str()); + } + std::printf("sweep: %d failing combinations\n", failures); + return failures ? 1 : 0; +} +} // namespace + +int main(int argc, char **argv) +{ + if (argc < 2) + { + std::fprintf(stderr, "usage: %s [--update [--force]|--print|--sweep]\n", + argv[0]); + return 2; + } + SDL_SetMainReady(); + GlobalContainer globals(argv[1]); + globalContainer = &globals; + globals.runNoX = true; + globals.settings.rememberUnit = false; + globals.load(); + std::string mode = argc > 2 ? argv[2] : ""; + const bool force = argc > 3 && std::strcmp(argv[3], "--force") == 0; + if (mode == "--sweep") + return sweep(); + if (mode == "--update") + return update(kTablePath, false, force); + if (mode == "--print") + return update(kTablePath, true, false); + return check(kTablePath); +} diff --git a/test/README.md b/test/README.md index 7b062af32..ed22ad2d6 100644 --- a/test/README.md +++ b/test/README.md @@ -41,6 +41,15 @@ resource types, all three base terrains, overlapping strokes, and all four wrapped map corners. A whole-map oracle checks both removal and preservation. These are headless map-operation tests; they do not drive editor mouse events. +## Map generator golden maps and colony sweep + +From the repository root, `scons -j8 release=1 server=0 map-generator-golden-test` builds +`build/src/MapGeneratorGoldenTest`. Run it as `./build/src/MapGeneratorGoldenTest ` +to compare this platform's rows of `test/map-generator-golden.txt` against fresh rolls, with +`--update` after a revision bump, `--print` to bootstrap a platform's rows from a log, and +`--sweep` to roll every playable landscape at the lobby's colony counts and sizes. The +framework reference under `docs/map-generators/` describes the rules it enforces. + ## Map subclass test pattern Pattern used by `MapQueryTest.cpp` (commit `2d42c340`). Lets you write tests against `Map`'s predicates with a minimal link surface — no `globalContainer`, no real `Sector` array, no transitive pull of `Bullet` / `Team` / `Building` / `Unit` into the test binary. diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt new file mode 100644 index 000000000..abcda0463 --- /dev/null +++ b/test/map-generator-golden.txt @@ -0,0 +1,128 @@ +# Golden map fingerprints per generator: platform id revision wDec hDec teams seed status hash +# Regenerate this platform's rows with MapGeneratorGoldenTest --update after a revision bump. +macos-arm64 0 1 7 7 4 1 ok 3707480985526347828 +macos-arm64 0 1 8 8 2 1 ok 1882570766644760050 +macos-arm64 0 1 8 8 4 1 ok 1882570766644760050 +macos-arm64 0 1 8 8 4 2 ok 13897470516389625799 +macos-arm64 0 1 8 8 4 3 ok 3342469568135303530 +macos-arm64 0 1 8 8 8 1 ok 1882570766644760050 +macos-arm64 0 1 9 9 4 1 ok 9537656230795372076 +macos-arm64 1 2 7 7 4 1 ok 2056248083838577766 +macos-arm64 1 2 8 8 2 1 ok 4513806375703215835 +macos-arm64 1 2 8 8 4 1 ok 15266936846292760467 +macos-arm64 1 2 8 8 4 2 ok 14755082496502706273 +macos-arm64 1 2 8 8 4 3 ok 7766703425182182688 +macos-arm64 1 2 8 8 8 1 ok 15211747697609312326 +macos-arm64 1 2 9 9 4 1 ok 13823901920601588993 +macos-arm64 2 2 7 7 4 1 ok 14759329394348325807 +macos-arm64 2 2 8 8 2 1 ok 1334307081035450250 +macos-arm64 2 2 8 8 4 1 ok 12800649820831069509 +macos-arm64 2 2 8 8 4 2 ok 16247178421364950126 +macos-arm64 2 2 8 8 4 3 ok 8829998086451332904 +macos-arm64 2 2 8 8 8 1 ok 3393915502379318686 +macos-arm64 2 2 9 9 4 1 ok 11934512450853997163 +macos-arm64 3 2 7 7 4 1 ok 8448347508778630572 +macos-arm64 3 2 8 8 2 1 ok 18055009306025368179 +macos-arm64 3 2 8 8 4 1 ok 9615729526066216419 +macos-arm64 3 2 8 8 4 2 ok 8908276107237789891 +macos-arm64 3 2 8 8 4 3 ok 7682200520761145636 +macos-arm64 3 2 8 8 8 1 ok 16434396702117749324 +macos-arm64 3 2 9 9 4 1 ok 528287643730928710 +macos-arm64 4 2 7 7 4 1 ok 11202276671897504324 +macos-arm64 4 2 8 8 2 1 ok 9701754549885880600 +macos-arm64 4 2 8 8 4 1 ok 11152758684431740930 +macos-arm64 4 2 8 8 4 2 ok 12437321240535810528 +macos-arm64 4 2 8 8 4 3 ok 9751465076302685550 +macos-arm64 4 2 8 8 8 1 ok 12077514013485761639 +macos-arm64 4 2 9 9 4 1 ok 1854948760916881435 +macos-arm64 5 1 7 7 4 1 ok 13850702411921678476 +macos-arm64 5 1 8 8 2 1 ok 16875811593585503047 +macos-arm64 5 1 8 8 4 1 ok 15081591080055302398 +macos-arm64 5 1 8 8 4 2 ok 14333463520897961954 +macos-arm64 5 1 8 8 4 3 ok 1927162392993477983 +macos-arm64 5 1 8 8 8 1 ok 1417569272582492023 +macos-arm64 5 1 9 9 4 1 ok 6078319397555065000 +macos-arm64 6 2 7 7 4 1 ok 10962798937752830582 +macos-arm64 6 2 8 8 2 1 ok 13908422176345369582 +macos-arm64 6 2 8 8 4 1 ok 12156463255653919960 +macos-arm64 6 2 8 8 4 2 ok 8488290807097875252 +macos-arm64 6 2 8 8 4 3 ok 13493861953378784496 +macos-arm64 6 2 8 8 8 1 ok 5023916742560360564 +macos-arm64 6 2 9 9 4 1 ok 18148498138515235675 +macos-arm64 7 2 7 7 4 1 ok 278576360011260021 +macos-arm64 7 2 8 8 2 1 ok 8574781324117286872 +macos-arm64 7 2 8 8 4 1 ok 15788142514004224693 +macos-arm64 7 2 8 8 4 2 ok 5712377136822184348 +macos-arm64 7 2 8 8 4 3 ok 13319374393828491671 +macos-arm64 7 2 8 8 8 1 failed 0 +macos-arm64 7 2 9 9 4 1 ok 1708935503910148886 +macos-arm64 8 1 7 7 4 1 ok 6473018063863562378 +macos-arm64 8 1 8 8 2 1 ok 12651074798552895253 +macos-arm64 8 1 8 8 4 1 ok 5644802038968384258 +macos-arm64 8 1 8 8 4 2 ok 14702577180534027034 +macos-arm64 8 1 8 8 4 3 ok 293881615974475555 +macos-arm64 8 1 8 8 8 1 ok 13875648332759742026 +macos-arm64 8 1 9 9 4 1 ok 14481987140521717300 +macos-arm64 9 2 7 7 4 1 ok 2277607816406551252 +macos-arm64 9 2 8 8 2 1 ok 6984614415357599638 +macos-arm64 9 2 8 8 4 1 ok 1045725267928572154 +macos-arm64 9 2 8 8 4 2 ok 1933790560431647187 +macos-arm64 9 2 8 8 4 3 ok 5877899263698877100 +macos-arm64 9 2 8 8 8 1 ok 4204618854271989380 +macos-arm64 9 2 9 9 4 1 ok 17370525382688177735 +macos-arm64 11 11 7 7 4 1 ok 7099819568130086231 +macos-arm64 11 11 8 8 2 1 ok 6302098072410457647 +macos-arm64 11 11 8 8 4 1 ok 35825363079579543 +macos-arm64 11 11 8 8 4 2 ok 4919401921601200783 +macos-arm64 11 11 8 8 4 3 ok 4565939337693971842 +macos-arm64 11 11 8 8 8 1 ok 906849352031588290 +macos-arm64 11 11 9 9 4 1 ok 1888712339707412248 +macos-arm64 12 10 7 7 4 1 ok 7388571519173228055 +macos-arm64 12 10 8 8 2 1 ok 15699276765293966482 +macos-arm64 12 10 8 8 4 1 ok 11621882014804934120 +macos-arm64 12 10 8 8 4 2 ok 8547038603337966082 +macos-arm64 12 10 8 8 4 3 ok 4736503892792565969 +macos-arm64 12 10 8 8 8 1 ok 4268435493852729962 +macos-arm64 12 10 9 9 4 1 ok 13231033850504282907 +macos-arm64 13 1 7 7 4 1 ok 8112226036058122700 +macos-arm64 13 1 8 8 2 1 ok 9154120752318257696 +macos-arm64 13 1 8 8 4 1 ok 2078930885184706426 +macos-arm64 13 1 8 8 4 2 ok 13659661778465349112 +macos-arm64 13 1 8 8 4 3 ok 13134881442491555389 +macos-arm64 13 1 8 8 8 1 ok 16484172863350979963 +macos-arm64 13 1 9 9 4 1 ok 9971144189844359399 +macos-arm64 14 1 7 7 4 1 ok 8352155152496438549 +macos-arm64 14 1 8 8 2 1 ok 2194436084179284477 +macos-arm64 14 1 8 8 4 1 ok 2921791490199882621 +macos-arm64 14 1 8 8 4 2 ok 2778819455373451420 +macos-arm64 14 1 8 8 4 3 ok 10572830601036884107 +macos-arm64 14 1 8 8 8 1 ok 8166217628013975417 +macos-arm64 14 1 9 9 4 1 ok 10894948886516768346 +macos-arm64 15 1 7 7 4 1 ok 18061507907613243474 +macos-arm64 15 1 8 8 2 1 ok 14205129108449246087 +macos-arm64 15 1 8 8 4 1 ok 1822623745155517541 +macos-arm64 15 1 8 8 4 2 ok 13627699444976556672 +macos-arm64 15 1 8 8 4 3 ok 4558451268999198918 +macos-arm64 15 1 8 8 8 1 ok 6662980182189545293 +macos-arm64 15 1 9 9 4 1 ok 4317157755929348617 +macos-arm64 16 1 7 7 4 1 ok 10199323229909488086 +macos-arm64 16 1 8 8 2 1 ok 17554114376208289941 +macos-arm64 16 1 8 8 4 1 ok 11072127706920976238 +macos-arm64 16 1 8 8 4 2 ok 10075510292789336147 +macos-arm64 16 1 8 8 4 3 ok 4396075304858481954 +macos-arm64 16 1 8 8 8 1 ok 699982766957804005 +macos-arm64 16 1 9 9 4 1 ok 12471630520837885284 +macos-arm64 17 4 7 7 4 1 ok 4983285384533269693 +macos-arm64 17 4 8 8 2 1 ok 15410170018519406689 +macos-arm64 17 4 8 8 4 1 ok 1496908417910164065 +macos-arm64 17 4 8 8 4 2 ok 11861326676775640834 +macos-arm64 17 4 8 8 4 3 ok 17308555237519110064 +macos-arm64 17 4 8 8 8 1 ok 7621305293371525278 +macos-arm64 17 4 9 9 4 1 ok 11272053320214136168 +macos-arm64 18 3 7 7 4 1 ok 2131984367767490485 +macos-arm64 18 3 8 8 2 1 ok 16443395861378427243 +macos-arm64 18 3 8 8 4 1 ok 9678987861891776728 +macos-arm64 18 3 8 8 4 2 ok 10102518421633888991 +macos-arm64 18 3 8 8 4 3 ok 5379511258847874570 +macos-arm64 18 3 8 8 8 1 ok 12075523445877924400 +macos-arm64 18 3 9 9 4 1 ok 8538100221992578386 From 84c30a05b024f39d986b520ecdf0d0927f0bbc05 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 13:51:01 -0400 Subject: [PATCH 087/136] Seat every colony on a crowded Shattered coast Shattered coast gives each colony the widest grass patch that keeps a spacing of one colony's share of the grass from every colony already placed, largest patch first. On a crowded map the last colonies can find every remaining patch too close and the roll failed with "Cannot space every colony on grass": at 256x256, 12 colonies seated on two seeds of five and 8 colonies on four, which the new sweep reported. A colony that finds no patch at full spacing now accepts one at half the spacing, then a quarter, before the roll gives up. A map that seats every colony at full spacing never reaches the relaxation, so its starts are unchanged, and the revision stays at 2. Verified: the golden check reports exactly one changed row, the 256x256 8-colony seed 1 roll that failed before and generates now, and 125 rows identical; that row is updated. The sweep now seats Shattered coast on every seed at every cell, 5/5 at 256 with 8 and with 12 colonies. MapGeneratorDefaultsTest passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generators/ShatteredCoastGenerator.cpp | 26 ++++++++++++++----- test/map-generator-golden.txt | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 119c20b93..c8800ccf0 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -550,12 +550,14 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast int *bootX = context.bootX.data(); int *bootY = context.bootY.data(); - // TODO: First pass to find the number of available places. - for (int team = 0; team < nbTeams; team++) + // Each colony takes the widest grass patch that keeps its distance from the ones already + // placed. The patches are taken largest first, so on a crowded map the last colonies can + // find every remaining patch too close; rather than refuse the map, such a colony accepts a + // nearer patch, down to half the spacing, then a quarter. A map that seats every colony at + // full spacing never reaches the relaxation, so its starts are as they always were. + auto widestPatch = [&](int team, int spacingSquare, int &maxX, int &maxY) { int maxSurface = 0; - int maxX = 0; - int maxY = 0; for (int y = 0; y < h; y++) { int width = 0; @@ -585,7 +587,7 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast bool farEnough = true; for (int ti = 0; ti < team; ti++) if (map.warpDistSquare(centerX, centerY, bootX[ti], bootY[ti]) < - minDistSquare) + spacingSquare) { farEnough = false; break; @@ -603,6 +605,19 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast } } } + return maxSurface; + }; + for (int team = 0; team < nbTeams; team++) + { + int maxSurface = 0; + int maxX = 0; + int maxY = 0; + for (int spacing : {minDistSquare, minDistSquare / 2, minDistSquare / 4}) + { + maxSurface = widestPatch(team, spacing, maxX, maxY); + if (maxSurface > 0) + break; + } if (maxSurface <= 0) { @@ -610,7 +625,6 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast context.detail = "Cannot space every colony on grass"; return false; } - assert(maxSurface); bootX[team] = maxX; bootY[team] = maxY; diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index abcda0463..a0b0a0dc0 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -54,7 +54,7 @@ macos-arm64 7 2 8 8 2 1 ok 8574781324117286872 macos-arm64 7 2 8 8 4 1 ok 15788142514004224693 macos-arm64 7 2 8 8 4 2 ok 5712377136822184348 macos-arm64 7 2 8 8 4 3 ok 13319374393828491671 -macos-arm64 7 2 8 8 8 1 failed 0 +macos-arm64 7 2 8 8 8 1 ok 8121618781306643342 macos-arm64 7 2 9 9 4 1 ok 1708935503910148886 macos-arm64 8 1 7 7 4 1 ok 6473018063863562378 macos-arm64 8 1 8 8 2 1 ok 12651074798552895253 From 4f7be2cd1dcef2eacea12e204e209302dd393fe3 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 13:54:57 -0400 Subject: [PATCH 088/136] Format the map generator tree with one clang-format configuration The generator tree had grown two styles: tabs and Allman braces in the core, the shared toolkit and the older generators, two-space K&R in ten newer generators, Resources and two core files. A .clang-format at the repository root now describes the house style (tabs, Allman, 100 columns, right-aligned pointers, access specifiers half a level out), chosen so that files already in that style move by a handful of lines each; the whole tree, its tests and the lobby's landscape picker are reformatted to it. Comments are not reflowed and includes are not reordered, so the diff is layout only. The development reference says how to keep it that way. Verified: the golden table compares 126 rows with 0 failures and MapGeneratorDefaultsTest passes on the reformatted tree, and clang-format --dry-run -Werror accepts every file in scope. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .clang-format | 31 + docs/development-notes.md | 6 +- src/LandscapePickerScreen.cpp | 32 +- src/NewMapScreen.h | 4 +- .../LegacyGenerationDescriptor.cpp | 20 +- src/map/generator/core/GenerationContext.cpp | 5 +- src/map/generator/core/GenerationRequest.h | 110 +- .../generator/core/GenerationValidation.cpp | 4 +- src/map/generator/core/GeneratorControls.cpp | 15 +- src/map/generator/core/GeneratorRegistry.cpp | 123 +- .../generators/CityStatesGenerator.cpp | 2572 ++++++------ .../generators/CityStatesGenerator.h | 13 +- .../generators/ConcreteIslandsGenerator.cpp | 35 +- .../generators/ContestedCommonsGenerator.cpp | 6 +- .../generators/CraterLakesGenerator.cpp | 5 +- .../generators/FjordContinentGenerator.cpp | 1333 +++--- .../generator/generators/IslandsGenerator.cpp | 5 +- .../generator/generators/IslesGenerator.cpp | 38 +- src/map/generator/generators/IslesGenerator.h | 7 +- .../generator/generators/MazeGenerator.cpp | 1291 +++--- src/map/generator/generators/MazeGenerator.h | 11 +- .../generators/RingWorldGenerator.cpp | 1572 ++++---- .../generator/generators/RiverGenerator.cpp | 14 +- src/map/generator/generators/RiverGenerator.h | 4 +- .../generators/RuggedArchipelagoGenerator.cpp | 37 +- .../generators/ShatteredCoastGenerator.cpp | 38 +- .../generators/ShatteredCoastGenerator.h | 5 +- .../generators/StoneHighlandsGenerator.cpp | 2343 ++++++----- .../generators/StoneHighlandsGenerator.h | 11 +- .../generators/SymmetricArenaGenerator.cpp | 2001 ++++----- .../generators/SymmetricArenaGenerator.h | 11 +- .../generators/TidalFlatsGenerator.cpp | 1166 +++--- .../generators/TidalFlatsGenerator.h | 11 +- .../generators/WatershedGenerator.cpp | 3573 +++++++++-------- .../generator/generators/WatershedGenerator.h | 11 +- src/map/generator/shared/Distances.cpp | 2 +- src/map/generator/shared/HeightMap.h | 10 +- src/map/generator/shared/Noise.cpp | 10 +- src/map/generator/shared/Regions.cpp | 18 +- src/map/generator/shared/Resources.cpp | 1066 ++--- src/map/generator/shared/Resources.h | 38 +- src/map/generator/shared/StartQuality.cpp | 26 +- src/map/generator/shared/StartQuality.h | 7 +- .../generator/shared/StartingPositions.cpp | 2 +- src/map/generator/shared/Terrain.cpp | 16 +- test/MapGeneratorDefaultsTest.cpp | 93 +- test/MapGeneratorFrameworkChecks.h | 36 +- test/MapGeneratorGoldenTest.cpp | 13 +- test/MapGeneratorStudy.cpp | 42 +- 49 files changed, 9559 insertions(+), 8283 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..cdc787394 --- /dev/null +++ b/.clang-format @@ -0,0 +1,31 @@ +# Matches the style the engine and the generator core already use: tabs, Allman braces, +# 100 columns, right-aligned pointers and references, access specifiers half a level out. +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +TabWidth: 4 +UseTab: ForContinuationAndIndentation +ContinuationIndentWidth: 4 +ColumnLimit: 100 +BreakBeforeBraces: Allman +AccessModifierOffset: -2 +PointerAlignment: Right +ReferenceAlignment: Right +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortLambdasOnASingleLine: All +AllowShortBlocksOnASingleLine: Never +AlignAfterOpenBracket: Align +AlignOperands: Align +BinPackArguments: true +BinPackParameters: true +NamespaceIndentation: None +IndentCaseLabels: false +SortIncludes: false +ReflowComments: false +FixNamespaceComments: true +SpaceAfterCStyleCast: false +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 diff --git a/docs/development-notes.md b/docs/development-notes.md index e41ce4ea9..69eebc6e3 100644 --- a/docs/development-notes.md +++ b/docs/development-notes.md @@ -121,7 +121,11 @@ For timing and scheduling, start with `src/Game_sync.cpp` and `src/EngineRun.cpp ## Local conventions Preserve other contributors' work; use an isolated checkout when work overlaps. -Use PascalCase C++ filenames (not snake_case) and `#pragma once`. Case-only renames +Use PascalCase C++ filenames (not snake_case) and `#pragma once`. The map generator tree +(`src/map/generator/`, its tests and the lobby's landscape picker) is kept in the style +`.clang-format` at the repository root describes: tabs, Allman braces, 100 columns. Run +`clang-format -i` on the files you touch there (`pip install clang-format` gives a current +binary), and `clang-format --dry-run -Werror` over the tree to check it. Case-only renames on macOS need an intermediate filename, e.g. `git mv Foo.cpp temp.cpp` then `git mv temp.cpp foo.cpp`. Keep comments terse and about the current code; put change history and rationale in commit messages; omit tombstone or “moved to” comments. diff --git a/src/LandscapePickerScreen.cpp b/src/LandscapePickerScreen.cpp index 206fd071c..121a4e600 100644 --- a/src/LandscapePickerScreen.cpp +++ b/src/LandscapePickerScreen.cpp @@ -8,11 +8,13 @@ namespace { -std::string tr(const std::string &s) { return Toolkit::getStringTable()->getString("[" + s + "]"); } +std::string tr(const std::string &s) +{ + return Toolkit::getStringTable()->getString("[" + s + "]"); +} } // namespace -std::vector -LandscapePickerScreen::requestsOf(const std::vector &entries) +std::vector LandscapePickerScreen::requestsOf(const std::vector &entries) { std::vector requests; for (const auto &entry : entries) @@ -62,7 +64,10 @@ void LandscapePickerScreen::confirm() endExecute(selected); } -void LandscapePickerScreen::onTimer(Uint32) { refresh(); } +void LandscapePickerScreen::onTimer(Uint32) +{ + refresh(); +} void LandscapePickerScreen::refresh() { @@ -78,8 +83,8 @@ void LandscapePickerScreen::refresh() if (tile.preview.state == LandscapePreviewer::State::Ready) { // Surfaces belong to the UI thread; the worker only hands over pixels. - tile.surface = std::make_unique(MapPreview::PreviewSize, - MapPreview::PreviewSize); + tile.surface = + std::make_unique(MapPreview::PreviewSize, MapPreview::PreviewSize); tile.preview.thumbnail.loadIntoSurface(tile.surface.get()); } } @@ -95,10 +100,10 @@ void LandscapePickerScreen::onSDLEvent(SDL_Event *event) endExecute(CANCEL); return; } - const int step = key == SDLK_LEFT ? -1 + const int step = key == SDLK_LEFT ? -1 : key == SDLK_RIGHT ? 1 - : key == SDLK_UP ? -columns - : key == SDLK_DOWN ? columns + : key == SDLK_UP ? -columns + : key == SDLK_DOWN ? columns : 0; if (step != 0) { @@ -142,8 +147,8 @@ void LandscapePickerScreen::render() const int bottom = height - 66; const int gap = 12, scrollbar = 12; const int tileMin = compact ? 176 : 200; - columns = std::clamp((w - scrollbar + gap) / (tileMin + gap), 1, - std::max(1, int(entries.size()))); + columns = + std::clamp((w - scrollbar + gap) / (tileMin + gap), 1, std::max(1, int(entries.size()))); const int tileW = (w - scrollbar - gap * (columns - 1)) / columns; const int image = tileW - 16; const int nameH = Toolkit::getFont("standard")->getStringHeight("Ag"); @@ -215,9 +220,8 @@ void LandscapePickerScreen::render() "landscape/back", {x, height - 55, 100, 34}, tr("Back"), [this] { endExecute(CANCEL); }, false, true, true); const int regenerateW = compact ? 150 : 190; - ui.button( - "landscape/regenerate", {x + 110, height - 55, regenerateW, 34}, tr("Regenerate all"), - [this] { previewer.regenerate(); }); + ui.button("landscape/regenerate", {x + 110, height - 55, regenerateW, 34}, tr("Regenerate all"), + [this] { previewer.regenerate(); }); const int useX = x + 110 + regenerateW + 10; const bool valid = selected >= 0 && selected < int(entries.size()); ui.button( diff --git a/src/NewMapScreen.h b/src/NewMapScreen.h index dd9cf4ced..8e036f36d 100644 --- a/src/NewMapScreen.h +++ b/src/NewMapScreen.h @@ -35,8 +35,8 @@ class NewMapScreen : public Glob2Screen struct ControlWidget { GenerationRequest::Control definition; - int method; // -1 for shared controls - Number *number; // range controls + int method; // -1 for shared controls + Number *number; // range controls OnOffButton *toggle; // toggle controls, shown as a check button Text *label; Widget *field() const; diff --git a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp index c973ecff7..814ed4b36 100644 --- a/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp +++ b/src/map/generator/compatibility/LegacyGenerationDescriptor.cpp @@ -88,8 +88,14 @@ void GeneratorControl::set(D &d, int v) const d.logRepeatAreaTimes = normalize(v); // Else: no legacy field for this option: nothing to store. } -const std::vector &D::controls(Method m) { return GenerationRequest::controls(m); } -const std::vector &D::sharedControls() { return sharedGeneratorControls(); } +const std::vector &D::controls(Method m) +{ + return GenerationRequest::controls(m); +} +const std::vector &D::sharedControls() +{ + return sharedGeneratorControls(); +} const D::Control &D::control(Method m, const char *label) { for (const auto &c : controls(m)) @@ -100,7 +106,10 @@ const D::Control &D::control(Method m, const char *label) return c; throw std::invalid_argument("Unknown legacy control"); } -const char *D::methodName(Method m) { return GenerationRequest::methodName(m); } +const char *D::methodName(Method m) +{ + return GenerationRequest::methodName(m); +} D::MapGenerationDescriptor() { terrainType = GRASS; @@ -126,7 +135,10 @@ void D::setMethodDefaults(Method m) for (const auto &c : controls(m)) c.set(*this, c.defaultValue); } -bool D::hasTerrainWeight() const { return fromLegacyDescriptor(*this, 0).hasTerrainWeight(); } +bool D::hasTerrainWeight() const +{ + return fromLegacyDescriptor(*this, 0).hasTerrainWeight(); +} GenerationRequest fromLegacyDescriptor(const D &d, std::uint32_t seed) { GenerationRequest r; diff --git a/src/map/generator/core/GenerationContext.cpp b/src/map/generator/core/GenerationContext.cpp index cd7e3737e..01e9f3da6 100644 --- a/src/map/generator/core/GenerationContext.cpp +++ b/src/map/generator/core/GenerationContext.cpp @@ -21,7 +21,10 @@ std::mt19937 &GenerationContext::stream(const std::string &name) { return streams.try_emplace(name, deriveSeed(request.seed, name)).first->second; } -std::uint32_t GenerationContext::randomSeed() { return std::random_device{}(); } +std::uint32_t GenerationContext::randomSeed() +{ + return std::random_device{}(); +} std::uint32_t GenerationContext::bounded(const std::string &name, std::uint32_t bound) { diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index 6ccd40476..7b6190bc1 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -8,62 +8,62 @@ #include #include class GeneratorRegistry; -struct GenerationRequest { - enum Method : int { - /// No terrain (terrain undefined) - eNONE = -1, - /// Uniform terrain (all of one type. completely unstructured) - eUNIFORM = 0, - /// swamp-like terrain with water here and land there - eSWAMP = 1, - /// a more or less winding river - eRIVER = 2, - /// islands that have organic shape and no passage from one to the next - eISLANDS = 3, - /// all connected land with round lakes - eCRATERLAKES = 4, - eCONCRETEISLANDS = 5, - eISLES = 6, - eOLDRANDOM = 7, - eOLDISLANDS = 8, - eCONTESTEDCOMMONS = 9, - eMAZE = 11, - eFJORDCONTINENT = 12, - eWATERSHED = 13, - eSTONEHIGHLANDS = 14, - eSYMMETRICARENA = 15, - eRINGWORLD = 16, - eCITYSTATES = 17, - eTIDALFLATS = 18 - }; +struct GenerationRequest +{ + enum Method : int + { + /// No terrain (terrain undefined) + eNONE = -1, + /// Uniform terrain (all of one type. completely unstructured) + eUNIFORM = 0, + /// swamp-like terrain with water here and land there + eSWAMP = 1, + /// a more or less winding river + eRIVER = 2, + /// islands that have organic shape and no passage from one to the next + eISLANDS = 3, + /// all connected land with round lakes + eCRATERLAKES = 4, + eCONCRETEISLANDS = 5, + eISLES = 6, + eOLDRANDOM = 7, + eOLDISLANDS = 8, + eCONTESTEDCOMMONS = 9, + eMAZE = 11, + eFJORDCONTINENT = 12, + eWATERSHED = 13, + eSTONEHIGHLANDS = 14, + eSYMMETRICARENA = 15, + eRINGWORLD = 16, + eCITYSTATES = 17, + eTIDALFLATS = 18 + }; - using Control = GeneratorControl; - using ControlGroup = ::ControlGroup; - GenerationRequest(); - int method = eUNIFORM; // stable registry ID, never a selection index - int wDec = 0, hDec = 0, nbTeams = 0, nbWorkers = 0; - TerrainType terrainType = GRASS; - std::uint32_t seed = 0; - std::map options; - // Legacy resource quantities remain supported by the compatibility adapter. - std::array resourceAmounts{}; - int option(const std::string &id) const { return options.at(id); } - void setMethodDefaults(int method); - void setMethodDefaults(int method, const GeneratorRegistry ®istry); - bool hasTerrainWeight() const; - bool hasTerrainWeight(const std::vector &definitions) const; - static const std::vector &controls(int method); - static const std::vector &sharedControls() { - return sharedGeneratorControls(); - } - static const Control &control(int method, const std::string &id); - static const char *methodName(int method); + using Control = GeneratorControl; + using ControlGroup = ::ControlGroup; + GenerationRequest(); + int method = eUNIFORM; // stable registry ID, never a selection index + int wDec = 0, hDec = 0, nbTeams = 0, nbWorkers = 0; + TerrainType terrainType = GRASS; + std::uint32_t seed = 0; + std::map options; + // Legacy resource quantities remain supported by the compatibility adapter. + std::array resourceAmounts{}; + int option(const std::string &id) const { return options.at(id); } + void setMethodDefaults(int method); + void setMethodDefaults(int method, const GeneratorRegistry ®istry); + bool hasTerrainWeight() const; + bool hasTerrainWeight(const std::vector &definitions) const; + static const std::vector &controls(int method); + static const std::vector &sharedControls() { return sharedGeneratorControls(); } + static const Control &control(int method, const std::string &id); + static const char *methodName(int method); }; -class GenerationHistory { - std::map settings; +class GenerationHistory +{ + std::map settings; -public: - void select(GenerationRequest ¤t, int method); - void select(GenerationRequest ¤t, int method, - const GeneratorRegistry ®istry); + public: + void select(GenerationRequest ¤t, int method); + void select(GenerationRequest ¤t, int method, const GeneratorRegistry ®istry); }; diff --git a/src/map/generator/core/GenerationValidation.cpp b/src/map/generator/core/GenerationValidation.cpp index cd35c0a25..8087fd4e9 100644 --- a/src/map/generator/core/GenerationValidation.cpp +++ b/src/map/generator/core/GenerationValidation.cpp @@ -48,8 +48,8 @@ std::string validateGeneratedWorld(const Game &g, const GenerationRequest &r, const Team *team = g.teams[i]; if (!team) return "Missing colony"; - if (team->startPosX < 0 || team->startPosX >= g.map.getW() || - team->startPosY < 0 || team->startPosY >= g.map.getH()) + if (team->startPosX < 0 || team->startPosX >= g.map.getW() || team->startPosY < 0 || + team->startPosY >= g.map.getH()) return "Starting position outside map bounds"; int workers = 0; bool swarm = false; diff --git a/src/map/generator/core/GeneratorControls.cpp b/src/map/generator/core/GeneratorControls.cpp index 93e6d7af1..21fe80984 100644 --- a/src/map/generator/core/GeneratorControls.cpp +++ b/src/map/generator/core/GeneratorControls.cpp @@ -45,8 +45,14 @@ int GeneratorControl::indexOf(int value) const const auto domain = values(); return int(std::lower_bound(domain.begin(), domain.end(), normalize(value)) - domain.begin()); } -int GeneratorControl::valueAt(int index) const { return values().at(index); } -int GeneratorControl::displayValue(int value) const { return powerOfTwo ? (1 << value) : value; } +int GeneratorControl::valueAt(int index) const +{ + return values().at(index); +} +int GeneratorControl::displayValue(int value) const +{ + return powerOfTwo ? (1 << value) : value; +} int GeneratorControl::get(const GenerationRequest &r) const { if (id == "width") @@ -118,7 +124,10 @@ const GeneratorControl &GenerationRequest::control(int method, const std::string return c; throw std::invalid_argument("Unknown generator control: " + id); } -bool GenerationRequest::hasTerrainWeight() const { return hasTerrainWeight(controls(method)); } +bool GenerationRequest::hasTerrainWeight() const +{ + return hasTerrainWeight(controls(method)); +} bool GenerationRequest::hasTerrainWeight(const std::vector &definitions) const { int total = 0; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index dfc3d96f1..e4b11beb9 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -22,70 +22,71 @@ #include #include GeneratorRegistry::GeneratorRegistry(std::vector values) - : definitions(std::move(values)) { - std::set numbers; - std::set ids; - for (const auto &d : definitions) { - if (!d.id || !*d.id || !d.nameKey || d.legacyId < 0 || !d.generate || - !numbers.insert(d.legacyId).second || !ids.insert(d.id).second) - throw std::invalid_argument("Invalid generator registration"); - std::set controls; - for (const auto &c : sharedGeneratorControls()) - controls.insert(c.id); - for (const auto &c : d.controls) - if (c.id.empty() || !c.label || c.step <= 0 || c.maximum < c.minimum || - (c.allowedValues.empty() && - (std::int64_t(c.maximum) - c.minimum) % c.step) || - (!c.allowedValues.empty() && - (c.allowedValues.front() != c.minimum || - c.allowedValues.back() != c.maximum || - !std::is_sorted(c.allowedValues.begin(), c.allowedValues.end()) || - std::adjacent_find(c.allowedValues.begin(), - c.allowedValues.end()) != - c.allowedValues.end())) || - (c.powerOfTwo && (c.minimum < 0 || c.maximum > 30)) || - (c.isToggle() && - (c.minimum != 0 || c.maximum != 1 || c.step != 1 || c.powerOfTwo || - c.terrainWeight || !c.allowedValues.empty())) || - c.normalize(c.defaultValue) != c.defaultValue || - !controls.insert(c.id).second) - throw std::invalid_argument("Invalid generator control"); - } + : definitions(std::move(values)) +{ + std::set numbers; + std::set ids; + for (const auto &d : definitions) + { + if (!d.id || !*d.id || !d.nameKey || d.legacyId < 0 || !d.generate || + !numbers.insert(d.legacyId).second || !ids.insert(d.id).second) + throw std::invalid_argument("Invalid generator registration"); + std::set controls; + for (const auto &c : sharedGeneratorControls()) + controls.insert(c.id); + for (const auto &c : d.controls) + if (c.id.empty() || !c.label || c.step <= 0 || c.maximum < c.minimum || + (c.allowedValues.empty() && (std::int64_t(c.maximum) - c.minimum) % c.step) || + (!c.allowedValues.empty() && + (c.allowedValues.front() != c.minimum || c.allowedValues.back() != c.maximum || + !std::is_sorted(c.allowedValues.begin(), c.allowedValues.end()) || + std::adjacent_find(c.allowedValues.begin(), c.allowedValues.end()) != + c.allowedValues.end())) || + (c.powerOfTwo && (c.minimum < 0 || c.maximum > 30)) || + (c.isToggle() && (c.minimum != 0 || c.maximum != 1 || c.step != 1 || c.powerOfTwo || + c.terrainWeight || !c.allowedValues.empty())) || + c.normalize(c.defaultValue) != c.defaultValue || !controls.insert(c.id).second) + throw std::invalid_argument("Invalid generator control"); + } } -const GeneratorDefinition *GeneratorRegistry::find(int id) const { - for (const auto &d : definitions) - if (d.legacyId == id) - return &d; - return nullptr; +const GeneratorDefinition *GeneratorRegistry::find(int id) const +{ + for (const auto &d : definitions) + if (d.legacyId == id) + return &d; + return nullptr; } -const GeneratorDefinition &GeneratorRegistry::at(int id) const { - if (auto *d = find(id)) - return *d; - throw std::invalid_argument("Unknown generator ID"); +const GeneratorDefinition &GeneratorRegistry::at(int id) const +{ + if (auto *d = find(id)) + return *d; + throw std::invalid_argument("Unknown generator ID"); } -std::vector GeneratorRegistry::methods(bool editor) const { - std::vector result; - for (const auto &d : definitions) - if (editor || !d.editorOnly) - result.push_back(d.legacyId); - return result; +std::vector GeneratorRegistry::methods(bool editor) const +{ + std::vector result; + for (const auto &d : definitions) + if (editor || !d.editorOnly) + result.push_back(d.legacyId); + return result; } -int GeneratorRegistry::selectionIndex(int id, bool editor) const { - auto ids = methods(editor); - auto it = std::find(ids.begin(), ids.end(), id); - return it == ids.end() ? -1 : int(it - ids.begin()); +int GeneratorRegistry::selectionIndex(int id, bool editor) const +{ + auto ids = methods(editor); + auto it = std::find(ids.begin(), ids.end(), id); + return it == ids.end() ? -1 : int(it - ids.begin()); } -const GeneratorRegistry &GeneratorRegistry::builtins() { - // This is also the product-facing catalog order. Keep the most polished and - // distinctive playable maps first; numeric IDs remain stable compatibility - // identifiers and do not determine presentation order. - static const GeneratorRegistry registry( - {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), - shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), - stoneHighlandsDefinition(), symmetricArenaDefinition(), - ringWorldDefinition(), cityStatesDefinition(), tidalFlatsDefinition(), - ruggedArchipelagoDefinition(), - concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), - swampDefinition(), riverDefinition(), uniformDefinition()}); - return registry; +const GeneratorRegistry &GeneratorRegistry::builtins() +{ + // This is also the product-facing catalog order. Keep the most polished and + // distinctive playable maps first; numeric IDs remain stable compatibility + // identifiers and do not determine presentation order. + static const GeneratorRegistry registry( + {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), + shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), + stoneHighlandsDefinition(), symmetricArenaDefinition(), ringWorldDefinition(), + cityStatesDefinition(), tidalFlatsDefinition(), ruggedArchipelagoDefinition(), + concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), + swampDefinition(), riverDefinition(), uniformDefinition()}); + return registry; } diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index f19de9324..4ee834b15 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -41,7 +41,8 @@ using namespace MapGeneration; // The whole design is a pure function of the request, so validateWorld rebuilds it and checks the // finished world. Terrain is written straight to the undermap with an order-independent beach // pass, as Ring world does. -namespace { +namespace +{ constexpr double pi = 3.14159265358979323846; @@ -89,1184 +90,1379 @@ constexpr int kLandingSpread = 12; constexpr int kFeatureDepth = 30; // What a home is made of, and what the heart of the commons is. One of each per map. -enum HomeKind { Lakeland = 0, Riverside, Highland, Marsh, Barrens, kHomeKinds }; -enum HeartKind { LakeHeart = 0, Crag, IslandHeart, Delta, ForestHeart, kHeartKinds }; +enum HomeKind +{ + Lakeland = 0, + Riverside, + Highland, + Marsh, + Barrens, + kHomeKinds +}; +enum HeartKind +{ + LakeHeart = 0, + Crag, + IslandHeart, + Delta, + ForestHeart, + kHeartKinds +}; -struct Torus { - int w, h; - int x(int v) const { return ((v % w) + w) % w; } - int y(int v) const { return ((v % h) + h) % h; } - int at(int px, int py) const { return y(py) * w + x(px); } - int offsetX(int from, int to) const { - const int d = x(to - from); - return d > w / 2 ? d - w : d; - } - int offsetY(int from, int to) const { - const int d = y(to - from); - return d > h / 2 ? d - h : d; - } +struct Torus +{ + int w, h; + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + int offsetX(int from, int to) const + { + const int d = x(to - from); + return d > w / 2 ? d - w : d; + } + int offsetY(int from, int to) const + { + const int d = y(to - from); + return d > h / 2 ? d - h : d; + } }; // Breadth-first 8-neighbour steps on the torus from every source tile through open tiles; -1 // where the flood never arrives. std::vector stepsFrom(const Torus &t, const std::vector &source, - const std::vector &open) { - std::vector dist(size_t(t.w) * t.h, -1); - std::vector queue; - queue.reserve(dist.size()); - for (size_t i = 0; i < dist.size(); ++i) - if (source[i]) { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % t.w, y = queue[head] / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int n = t.at(x + dx, y + dy); - if (dist[n] < 0 && open[n]) { - dist[n] = dist[queue[head]] + 1; - queue.push_back(n); - } - } - } - return dist; + const std::vector &open) +{ + std::vector dist(size_t(t.w) * t.h, -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) + { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % t.w, y = queue[head] / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int n = t.at(x + dx, y + dy); + if (dist[n] < 0 && open[n]) + { + dist[n] = dist[queue[head]] + 1; + queue.push_back(n); + } + } + } + return dist; } // A smooth random curve round a ring, periodic in u over [0, 1): a few harmonics with random // phases, scaled so its peak is one. -struct Profile { - std::array amplitude{}, phase{}; - int first = 1, count = 0; - double at(double u) const { - double v = 0; - for (int h = 0; h < count; ++h) - v += amplitude[h] * std::cos(2 * pi * (first + h) * u + phase[h]); - return v; - } +struct Profile +{ + std::array amplitude{}, phase{}; + int first = 1, count = 0; + double at(double u) const + { + double v = 0; + for (int h = 0; h < count; ++h) + v += amplitude[h] * std::cos(2 * pi * (first + h) * u + phase[h]); + return v; + } }; Profile rollProfile(GenerationContext &context, const char *stream, int first, int count, - double falloff) { - Profile p; - p.first = first; - p.count = count; - for (int h = 0; h < count; ++h) { - p.amplitude[h] = (0.4 + 0.6 * context.bounded(stream, 1000) / 1000.0) / - std::pow(double(first + h), falloff); - p.phase[h] = 2 * pi * context.bounded(stream, 3600) / 3600.0; - } - double peak = 0; - for (int i = 0; i < 720; ++i) - peak = std::max(peak, std::abs(p.at(i / 720.0))); - if (peak > 0) - for (int h = 0; h < count; ++h) - p.amplitude[h] /= peak; - return p; + double falloff) +{ + Profile p; + p.first = first; + p.count = count; + for (int h = 0; h < count; ++h) + { + p.amplitude[h] = (0.4 + 0.6 * context.bounded(stream, 1000) / 1000.0) / + std::pow(double(first + h), falloff); + p.phase[h] = 2 * pi * context.bounded(stream, 3600) / 3600.0; + } + double peak = 0; + for (int i = 0; i < 720; ++i) + peak = std::max(peak, std::abs(p.at(i / 720.0))); + if (peak > 0) + for (int h = 0; h < count; ++h) + p.amplitude[h] /= peak; + return p; } // 0 within `flat` tiles of arc from the wedge's middle, 1 beyond `flat + fade`, smooth between. -double awayFromMiddle(double arc, double flat, double fade) { - const double x = std::clamp((arc - flat) / fade, 0.0, 1.0); - return x * x * (3 - 2 * x); +double awayFromMiddle(double arc, double flat, double fade) +{ + const double x = std::clamp((arc - flat) / fade, 0.0, 1.0); + return x * x * (3 - 2 * x); } // The numbers every part of the layout is measured from; a pure function of the request, shared // by validateRequest and the design. -struct Geometry { - int teams, half, rim, strait, causeway; - bool walls; - double commonsRadius, outerRadius, amplitude; - double innerRadius() const { return commonsRadius + strait; } - double depth() const { return outerRadius - innerRadius(); } - // Arc of a home's inner coast beyond its channel, at the nominal radius. - double innerArc() const { - return teams < 2 ? 2 * pi * innerRadius() : 2 * pi * innerRadius() / teams - strait; - } - // Half the arc a wedge spans at radius d, less half the channel. - double arcHalf(double d) const { return teams < 2 ? pi * d : pi * d / teams - strait / 2.0; } +struct Geometry +{ + int teams, half, rim, strait, causeway; + bool walls; + double commonsRadius, outerRadius, amplitude; + double innerRadius() const { return commonsRadius + strait; } + double depth() const { return outerRadius - innerRadius(); } + // Arc of a home's inner coast beyond its channel, at the nominal radius. + double innerArc() const + { + return teams < 2 ? 2 * pi * innerRadius() : 2 * pi * innerRadius() / teams - strait; + } + // Half the arc a wedge spans at radius d, less half the channel. + double arcHalf(double d) const { return teams < 2 ? pi * d : pi * d / teams - strait / 2.0; } }; -Geometry geometryFor(const GenerationRequest &r) { - const CityStatesOptions o(r); - Geometry g; - g.teams = std::max(1, r.nbTeams); - const int side = std::min(1 << r.wDec, 1 << r.hDec); - g.half = side / 2; - g.rim = std::max(kRimMinimum, int(std::lround(kRimShare * g.half))); - g.strait = std::max(4, int(std::lround(o.straitWidth / 100.0 * side))); - g.causeway = o.causewayWidth; - g.walls = o.stoneWalls; - g.commonsRadius = std::lround(o.commonsSize / 100.0 * g.half); - g.outerRadius = g.half - g.rim; - g.amplitude = o.coastRoughness / 100.0; - return g; +Geometry geometryFor(const GenerationRequest &r) +{ + const CityStatesOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + const int side = std::min(1 << r.wDec, 1 << r.hDec); + g.half = side / 2; + g.rim = std::max(kRimMinimum, int(std::lround(kRimShare * g.half))); + g.strait = std::max(4, int(std::lround(o.straitWidth / 100.0 * side))); + g.causeway = o.causewayWidth; + g.walls = o.stoneWalls; + g.commonsRadius = std::lround(o.commonsSize / 100.0 * g.half); + g.outerRadius = g.half - g.rim; + g.amplitude = o.coastRoughness / 100.0; + return g; } // How far in from the strait a home's lake lies: six tenths of the depth, held clear of both // coasts however far their roughness lets them wander. RadialShape reaches at most 1.3 times its // radius at the roughness used here. -double lakeOffset(const Geometry &g, double lakeRadius) { - const double reach = 1.3 * lakeRadius + kLakeSeaGap; - const double low = reach + 2.0; - const double high = g.depth() - reach - 2.0; - return std::clamp(0.6 * g.depth(), low, std::max(low, high)); +double lakeOffset(const Geometry &g, double lakeRadius) +{ + const double reach = 1.3 * lakeRadius + kLakeSeaGap; + const double low = reach + 2.0; + const double high = g.depth() - reach - 2.0; + return std::clamp(0.6 * g.depth(), low, std::max(low, high)); } // A round feature in a wedge's frame: arc offset from the middle, radius from the centre. -struct Blob { - double s, r; - RadialShape shape; - bool holds(double ds, double dr) const { - return std::hypot(ds, dr) < shape.radiusAt(std::atan2(dr, ds)); - } +struct Blob +{ + double s, r; + RadialShape shape; + bool holds(double ds, double dr) const + { + return std::hypot(ds, dr) < shape.radiusAt(std::atan2(dr, ds)); + } }; -struct Home { - double angle; // the wedge's middle, where its causeway and lake lie - double coast; // the commons' coast radius at that angle - int lakeX, lakeY; // its main lake's centre - int landing, shore; // one tile at each end of its causeway: on the commons, on the home +struct Home +{ + double angle; // the wedge's middle, where its causeway and lake lie + double coast; // the commons' coast radius at that angle + int lakeX, lakeY; // its main lake's centre + int landing, shore; // one tile at each end of its causeway: on the commons, on the home }; -enum Region : signed char { Sea = 0, Commons = 1, HomeLand = 2 }; +enum Region : signed char +{ + Sea = 0, + Commons = 1, + HomeLand = 2 +}; // The terrain design: a pure function of the request, rebuilt by validateWorld. -struct Layout { - Torus t{1, 1}; - Geometry g{}; - int cx = 0, cy = 0; - double phase = 0; - int homeKind = Lakeland, heartKind = LakeHeart; - double creekSide = 0; // Riverside: which flank the creek runs to, -1 or 1 - double lakeR = 0, islandR = 0, ringR = 0, forestR = 0; // the heart's radii - std::vector homes; - // Per tile. - std::vector region; - std::vector homeOf; // HomeLand tiles: which home, else -1 - std::vector lake, river, ford, sand, ridge, strip, causeway, road, clear; - std::vector radius, angle; - std::string failure; +struct Layout +{ + Torus t{1, 1}; + Geometry g{}; + int cx = 0, cy = 0; + double phase = 0; + int homeKind = Lakeland, heartKind = LakeHeart; + double creekSide = 0; // Riverside: which flank the creek runs to, -1 or 1 + double lakeR = 0, islandR = 0, ringR = 0, forestR = 0; // the heart's radii + std::vector homes; + // Per tile. + std::vector region; + std::vector homeOf; // HomeLand tiles: which home, else -1 + std::vector lake, river, ford, sand, ridge, strip, causeway, road, clear; + std::vector radius, angle; + std::string failure; }; -Layout design(const GenerationRequest &request, GenerationContext &context) { - Layout L; - L.t = {1 << request.wDec, 1 << request.hDec}; - L.g = geometryFor(request); - const Torus &t = L.t; - const Geometry &g = L.g; - const int n = t.w * t.h, teams = g.teams; - const CityStatesOptions o(request); - L.cx = t.w / 2; - L.cy = t.h / 2; - L.phase = context.bounded("city-layout", 3600) / 3600.0 * 2 * pi; - const double wedge = 2 * pi / teams; - const double inner = g.innerRadius(), depth = g.depth(); +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.w * t.h, teams = g.teams; + const CityStatesOptions o(request); + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("city-layout", 3600) / 3600.0 * 2 * pi; + const double wedge = 2 * pi / teams; + const double inner = g.innerRadius(), depth = g.depth(); - // What this map is made of. - L.homeKind = int(context.bounded("city-kind", kHomeKinds)); - L.heartKind = int(context.bounded("city-kind", kHeartKinds)); - if (L.heartKind == Delta && teams < 2) - L.heartKind = LakeHeart; - if (depth < kFeatureDepth && (L.homeKind == Riverside || L.homeKind == Highland || L.homeKind == Barrens)) - L.homeKind = Lakeland; + // What this map is made of. + L.homeKind = int(context.bounded("city-kind", kHomeKinds)); + L.heartKind = int(context.bounded("city-kind", kHeartKinds)); + if (L.heartKind == Delta && teams < 2) + L.heartKind = LakeHeart; + if (depth < kFeatureDepth && + (L.homeKind == Riverside || L.homeKind == Highland || L.homeKind == Barrens)) + L.homeKind = Lakeland; - // The coasts: bays and headlands round the commons, a ripple on top, bays into the homes' - // flanks, and a bow in every channel. All periodic in the wedge, so every home gets the same. - const Profile coast = rollProfile(context, "city-coast", 1, 4, 1.2); - const Profile ripple = rollProfile(context, "city-coast", 6, 6, 0.6); - const Profile innerBays = rollProfile(context, "city-coast", 1, 3, 1.0); - const Profile outerBays = rollProfile(context, "city-coast", 1, 3, 1.0); - const double coastAmp = kCoastAmplitude * g.amplitude * g.commonsRadius; - const double rippleAmp = kRippleAmplitude * g.amplitude * g.commonsRadius; - const double innerBayAmp = kInnerBayShare * g.amplitude * depth; - const double outerBayAmp = kOuterBayShare * g.amplitude * depth; - const double bend = (context.bounded("city-coast", 2001) / 1000.0 - 1) * kChannelBend; - // The coasts hold flat for a stretch either side of every causeway, measured in tiles of arc so - // the stretch is the same however many homes share the ring, and the flanks' bays begin only - // beyond the lake and its clearance. - const double lakeRadius = std::clamp(0.1 * depth, 2.0, 6.0); - const double lakeReach = 1.3 * lakeRadius; - const double flatArc = g.causeway / 2.0 + kShoulder + kLane + 4; - const double bayArc = lakeReach + kLakeSeaGap + 6; - const auto coastAt = [&](double u, double d) { - const double away = awayFromMiddle(d * std::abs(u - 0.5) * wedge, flatArc, 12); - return g.commonsRadius + (coastAmp * coast.at(u) + rippleAmp * ripple.at(u)) * away; - }; - const auto innerAt = [&](double u, double d) { - return coastAt(u, d) + g.strait + - innerBayAmp * std::max(0.0, innerBays.at(u)) * - awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc, 10); - }; - const auto outerAt = [&](double u, double d) { - return std::min(double(g.half - 3), - g.outerRadius - outerBayAmp * std::max(0.0, outerBays.at(u)) * - awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc - 2, 10)); - }; - const auto bendAt = [&](double d) { - const double x = (d - inner) / std::max(1.0, depth); - return x <= 0 || x >= 1 ? 0.0 : bend * std::sin(pi * x); - }; + // The coasts: bays and headlands round the commons, a ripple on top, bays into the homes' + // flanks, and a bow in every channel. All periodic in the wedge, so every home gets the same. + const Profile coast = rollProfile(context, "city-coast", 1, 4, 1.2); + const Profile ripple = rollProfile(context, "city-coast", 6, 6, 0.6); + const Profile innerBays = rollProfile(context, "city-coast", 1, 3, 1.0); + const Profile outerBays = rollProfile(context, "city-coast", 1, 3, 1.0); + const double coastAmp = kCoastAmplitude * g.amplitude * g.commonsRadius; + const double rippleAmp = kRippleAmplitude * g.amplitude * g.commonsRadius; + const double innerBayAmp = kInnerBayShare * g.amplitude * depth; + const double outerBayAmp = kOuterBayShare * g.amplitude * depth; + const double bend = (context.bounded("city-coast", 2001) / 1000.0 - 1) * kChannelBend; + // The coasts hold flat for a stretch either side of every causeway, measured in tiles of arc so + // the stretch is the same however many homes share the ring, and the flanks' bays begin only + // beyond the lake and its clearance. + const double lakeRadius = std::clamp(0.1 * depth, 2.0, 6.0); + const double lakeReach = 1.3 * lakeRadius; + const double flatArc = g.causeway / 2.0 + kShoulder + kLane + 4; + const double bayArc = lakeReach + kLakeSeaGap + 6; + const auto coastAt = [&](double u, double d) + { + const double away = awayFromMiddle(d * std::abs(u - 0.5) * wedge, flatArc, 12); + return g.commonsRadius + (coastAmp * coast.at(u) + rippleAmp * ripple.at(u)) * away; + }; + const auto innerAt = [&](double u, double d) + { + return coastAt(u, d) + g.strait + + innerBayAmp * std::max(0.0, innerBays.at(u)) * + awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc, 10); + }; + const auto outerAt = [&](double u, double d) + { + return std::min(double(g.half - 3), + g.outerRadius - + outerBayAmp * std::max(0.0, outerBays.at(u)) * + awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc - 2, 10)); + }; + const auto bendAt = [&](double d) + { + const double x = (d - inner) / std::max(1.0, depth); + return x <= 0 || x >= 1 ? 0.0 : bend * std::sin(pi * x); + }; - for (int k = 0; k < teams; ++k) { - const double a = L.phase + wedge * (k + 0.5); - L.homes.push_back({a, g.commonsRadius, 0, 0, -1, -1}); - } + for (int k = 0; k < teams; ++k) + { + const double a = L.phase + wedge * (k + 0.5); + L.homes.push_back({a, g.commonsRadius, 0, 0, -1, -1}); + } - // The home's features in the wedge's frame: lakes first (the main one first), then whatever - // the kind adds. A feature keeps clear of the coasts as bays can bring them, of the channels, - // of the causeway's approach and of other features. - const double mainLakeR = inner + lakeOffset(g, lakeRadius); - std::vector lakes, sandBlobs; - lakes.push_back({0.0, mainLakeR, RadialShape(lakeRadius, 0.3, context, "city-home-lakes")}); - const auto fits = [&](double s0, double r0, double reach, double gap, - const std::vector &others) { - if (std::abs(s0) + reach + gap > g.arcHalf(r0)) - return false; - const double u = 0.5 + s0 / (wedge * r0); - if (r0 - reach - gap < innerAt(u, r0) || r0 + reach + gap > outerAt(u, r0)) - return false; - if (r0 - reach < inner + kLanding + 2 && std::abs(s0) < g.causeway + reach + 4) - return false; - for (const Blob &b : others) - if (std::hypot(s0 - b.s, r0 - b.r) < reach + b.shape.maximumRadius() + gap) - return false; - return true; - }; - if (L.homeKind == Marsh) - for (int attempt = 0; attempt < 400 && lakes.size() < 4; ++attempt) { - const double r0 = inner + 8 + context.bounded("city-home-lakes", 1000) / 1000.0 * (depth - 16); - const double s0 = (context.bounded("city-home-lakes", 2001) / 1000.0 - 1) * 0.6 * g.arcHalf(r0); - RadialShape shape(2.0 + context.bounded("city-home-lakes", 1000) / 1000.0 * 2.0, 0.3, context, - "city-home-lakes"); - if (fits(s0, r0, shape.maximumRadius(), kLakeSeaGap, lakes)) - lakes.push_back({s0, r0, shape}); - } - // Riverside: a creek from the main lake towards one flank of the inner coast, with a ford. - std::vector> creek; - int fordSegment = -1; - if (L.homeKind == Riverside) { - const double side = context.bounded("city-features", 2) ? 1.0 : -1.0; - L.creekSide = side; - const double reachArc = 0.32 * g.arcHalf(mainLakeR); - const double endR = inner + kLakeSeaGap + 6.0; - creek = {{0.0, mainLakeR}, - {side * 0.35 * reachArc, mainLakeR - 0.3 * (mainLakeR - endR)}, - {side * 0.75 * reachArc, mainLakeR - 0.65 * (mainLakeR - endR)}, - {side * reachArc, endR}}; - fordSegment = 1; - } - // Highland: two ridges out from the strait to the sea, a pass through each. - const double ridgeU[2] = {0.28, 0.72}; - const double passR = inner + 0.5 * depth; - // Barrens: both flanks of the home are sand, leaving a strip of grass down the middle from the - // causeway past the lake; the sand keeps clear of every coast so the wall stays on the shore. - const double barrenShare = 0.45; - // Sand patches, in every kind of home. - const double homeArea = wedge * (g.outerRadius * g.outerRadius - inner * inner) / 2; - const int homeSand = int(std::lround(o.sand * homeArea / 8192.0)); - for (int attempt = 0; attempt < 400 && int(sandBlobs.size()) < homeSand; ++attempt) { - const double r0 = inner + 8 + context.bounded("city-sand", 1000) / 1000.0 * (depth - 16); - const double s0 = (context.bounded("city-sand", 2001) / 1000.0 - 1) * 0.75 * g.arcHalf(r0); - RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 4.0, 0.5, context, "city-sand"); - std::vector avoid = lakes; - avoid.insert(avoid.end(), sandBlobs.begin(), sandBlobs.end()); - if (fits(s0, r0, shape.maximumRadius(), 5, avoid)) - sandBlobs.push_back({s0, r0, shape}); - } + // The home's features in the wedge's frame: lakes first (the main one first), then whatever + // the kind adds. A feature keeps clear of the coasts as bays can bring them, of the channels, + // of the causeway's approach and of other features. + const double mainLakeR = inner + lakeOffset(g, lakeRadius); + std::vector lakes, sandBlobs; + lakes.push_back({0.0, mainLakeR, RadialShape(lakeRadius, 0.3, context, "city-home-lakes")}); + const auto fits = + [&](double s0, double r0, double reach, double gap, const std::vector &others) + { + if (std::abs(s0) + reach + gap > g.arcHalf(r0)) + return false; + const double u = 0.5 + s0 / (wedge * r0); + if (r0 - reach - gap < innerAt(u, r0) || r0 + reach + gap > outerAt(u, r0)) + return false; + if (r0 - reach < inner + kLanding + 2 && std::abs(s0) < g.causeway + reach + 4) + return false; + for (const Blob &b : others) + if (std::hypot(s0 - b.s, r0 - b.r) < reach + b.shape.maximumRadius() + gap) + return false; + return true; + }; + if (L.homeKind == Marsh) + for (int attempt = 0; attempt < 400 && lakes.size() < 4; ++attempt) + { + const double r0 = + inner + 8 + context.bounded("city-home-lakes", 1000) / 1000.0 * (depth - 16); + const double s0 = + (context.bounded("city-home-lakes", 2001) / 1000.0 - 1) * 0.6 * g.arcHalf(r0); + RadialShape shape(2.0 + context.bounded("city-home-lakes", 1000) / 1000.0 * 2.0, 0.3, + context, "city-home-lakes"); + if (fits(s0, r0, shape.maximumRadius(), kLakeSeaGap, lakes)) + lakes.push_back({s0, r0, shape}); + } + // Riverside: a creek from the main lake towards one flank of the inner coast, with a ford. + std::vector> creek; + int fordSegment = -1; + if (L.homeKind == Riverside) + { + const double side = context.bounded("city-features", 2) ? 1.0 : -1.0; + L.creekSide = side; + const double reachArc = 0.32 * g.arcHalf(mainLakeR); + const double endR = inner + kLakeSeaGap + 6.0; + creek = {{0.0, mainLakeR}, + {side * 0.35 * reachArc, mainLakeR - 0.3 * (mainLakeR - endR)}, + {side * 0.75 * reachArc, mainLakeR - 0.65 * (mainLakeR - endR)}, + {side * reachArc, endR}}; + fordSegment = 1; + } + // Highland: two ridges out from the strait to the sea, a pass through each. + const double ridgeU[2] = {0.28, 0.72}; + const double passR = inner + 0.5 * depth; + // Barrens: both flanks of the home are sand, leaving a strip of grass down the middle from the + // causeway past the lake; the sand keeps clear of every coast so the wall stays on the shore. + const double barrenShare = 0.45; + // Sand patches, in every kind of home. + const double homeArea = wedge * (g.outerRadius * g.outerRadius - inner * inner) / 2; + const int homeSand = int(std::lround(o.sand * homeArea / 8192.0)); + for (int attempt = 0; attempt < 400 && int(sandBlobs.size()) < homeSand; ++attempt) + { + const double r0 = inner + 8 + context.bounded("city-sand", 1000) / 1000.0 * (depth - 16); + const double s0 = (context.bounded("city-sand", 2001) / 1000.0 - 1) * 0.75 * g.arcHalf(r0); + RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 4.0, 0.5, context, + "city-sand"); + std::vector avoid = lakes; + avoid.insert(avoid.end(), sandBlobs.begin(), sandBlobs.end()); + if (fits(s0, r0, shape.maximumRadius(), 5, avoid)) + sandBlobs.push_back({s0, r0, shape}); + } - // The heart of the commons. - L.lakeR = std::max(3.0, kHeartShare * g.commonsRadius); - L.ringR = std::max(8.0, kHeartShare * g.commonsRadius); - L.islandR = 0; - if (L.heartKind == Crag) - L.lakeR = std::max(2.5, 0.06 * g.commonsRadius); - if (L.heartKind == IslandHeart) { - L.lakeR = std::max(6.0, 0.3 * g.commonsRadius); - L.islandR = std::max(5.0, 0.12 * g.commonsRadius); - } - L.forestR = L.lakeR + 4 + 0.09 * g.commonsRadius; - const double deltaFordR = 0.6 * g.commonsRadius; - const RadialShape heart(L.lakeR, L.heartKind == IslandHeart ? 0.2 : 0.3, context, "city-heart"); - // Sand on the commons: anywhere between the heart and the shore, off the landings. - struct Patch { - int x, y; - RadialShape shape; - }; - std::vector commonsSand; - const double commonsArea = pi * g.commonsRadius * g.commonsRadius; - const int wantedSand = int(std::lround(o.sand * commonsArea / 8192.0)); - for (int attempt = 0; attempt < 400 && int(commonsSand.size()) < wantedSand; ++attempt) { - const double a = context.bounded("city-sand", 3600) / 3600.0 * 2 * pi; - const double r0 = 0.45 * g.commonsRadius + - context.bounded("city-sand", 1000) / 1000.0 * (0.55 * g.commonsRadius - 12); - RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 5.0, 0.5, context, "city-sand"); - const double reach = shape.maximumRadius(); - const double turn = std::fmod(a - L.phase + 4 * pi, 2 * pi); - const double u = turn / wedge - std::floor(turn / wedge); - if (r0 * std::abs(u - 0.5) * wedge < reach + g.causeway + 4 && r0 > g.commonsRadius - kLanding - reach - 4) - continue; - if (r0 + reach > coastAt(u, r0) - 8) - continue; - bool open = true; - for (const Patch &p : commonsSand) { - const int px = L.cx + int(std::lround(r0 * std::cos(a))), py = L.cy + int(std::lround(r0 * std::sin(a))); - const double dx = t.offsetX(px, p.x), dy = t.offsetY(py, p.y); - open = open && std::hypot(dx, dy) >= reach + p.shape.maximumRadius() + 4; - } - if (open) - commonsSand.push_back({t.x(L.cx + int(std::lround(r0 * std::cos(a)))), - t.y(L.cy + int(std::lround(r0 * std::sin(a)))), shape}); - } + // The heart of the commons. + L.lakeR = std::max(3.0, kHeartShare * g.commonsRadius); + L.ringR = std::max(8.0, kHeartShare * g.commonsRadius); + L.islandR = 0; + if (L.heartKind == Crag) + L.lakeR = std::max(2.5, 0.06 * g.commonsRadius); + if (L.heartKind == IslandHeart) + { + L.lakeR = std::max(6.0, 0.3 * g.commonsRadius); + L.islandR = std::max(5.0, 0.12 * g.commonsRadius); + } + L.forestR = L.lakeR + 4 + 0.09 * g.commonsRadius; + const double deltaFordR = 0.6 * g.commonsRadius; + const RadialShape heart(L.lakeR, L.heartKind == IslandHeart ? 0.2 : 0.3, context, "city-heart"); + // Sand on the commons: anywhere between the heart and the shore, off the landings. + struct Patch + { + int x, y; + RadialShape shape; + }; + std::vector commonsSand; + const double commonsArea = pi * g.commonsRadius * g.commonsRadius; + const int wantedSand = int(std::lround(o.sand * commonsArea / 8192.0)); + for (int attempt = 0; attempt < 400 && int(commonsSand.size()) < wantedSand; ++attempt) + { + const double a = context.bounded("city-sand", 3600) / 3600.0 * 2 * pi; + const double r0 = 0.45 * g.commonsRadius + context.bounded("city-sand", 1000) / 1000.0 * + (0.55 * g.commonsRadius - 12); + RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 5.0, 0.5, context, + "city-sand"); + const double reach = shape.maximumRadius(); + const double turn = std::fmod(a - L.phase + 4 * pi, 2 * pi); + const double u = turn / wedge - std::floor(turn / wedge); + if (r0 * std::abs(u - 0.5) * wedge < reach + g.causeway + 4 && + r0 > g.commonsRadius - kLanding - reach - 4) + continue; + if (r0 + reach > coastAt(u, r0) - 8) + continue; + bool open = true; + for (const Patch &p : commonsSand) + { + const int px = L.cx + int(std::lround(r0 * std::cos(a))), + py = L.cy + int(std::lround(r0 * std::sin(a))); + const double dx = t.offsetX(px, p.x), dy = t.offsetY(py, p.y); + open = open && std::hypot(dx, dy) >= reach + p.shape.maximumRadius() + 4; + } + if (open) + commonsSand.push_back({t.x(L.cx + int(std::lround(r0 * std::cos(a)))), + t.y(L.cy + int(std::lround(r0 * std::sin(a)))), shape}); + } - L.region.assign(n, Sea); - L.homeOf.assign(n, -1); - for (auto *mask : {&L.lake, &L.river, &L.ford, &L.sand, &L.ridge, &L.strip, &L.causeway, &L.road, &L.clear}) - mask->assign(n, 0); - L.radius.assign(n, 0.0); - L.angle.assign(n, 0.0); - const double roadHalf = g.causeway / 2.0; - const double stripHalf = roadHalf + kShoulder; - std::vector landingAlong(teams, -1.0), shoreAlong(teams, 1e9); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); - const double d = std::hypot(double(dx), double(dy)); - const double theta = std::atan2(double(dy), double(dx)); - L.radius[i] = d; - L.angle[i] = theta; - // The wedge frame: which home, how far across it and how far along the arc from its middle. - double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); - if (teams >= 2) - turn = std::fmod(turn - bendAt(d) / std::max(1.0, d) + 4 * pi, 2 * pi); - const int k = std::min(teams - 1, int(turn / wedge)); - const double u = turn / wedge - k; - const double s = d * (u - 0.5) * wedge; - const double toBoundary = teams < 2 ? 1e9 : d * std::min(u, 1 - u) * wedge; - const double toLanding = d * std::abs(u - 0.5) * wedge; - const double c = coastAt(u, d); - const double in = innerAt(u, d), out = outerAt(u, d); - if (d < c) { - L.region[i] = Commons; - } else if (d < in) { - L.region[i] = Sea; - } else if (d < out && toBoundary >= g.strait / 2.0) { - L.region[i] = HomeLand; - L.homeOf[i] = k; - } - // The home's causeway: straight across the strait at the wedge's middle. - const Home &h = L.homes[k]; - const double along = d * std::cos(theta - h.angle), across = std::abs(d * std::sin(theta - h.angle)); - if (across <= stripHalf + kLane && along >= h.coast - 3 && along <= h.coast + g.strait + 3) - L.causeway[i] = 1; - if (across <= stripHalf && along >= h.coast - 3 && along <= h.coast + g.strait + 3) { - L.strip[i] = 1; - if (L.region[i] == Sea) { - L.region[i] = HomeLand; - L.homeOf[i] = k; - } - if (across <= roadHalf) - L.road[i] = 1; - if (along > landingAlong[k] && L.road[i]) { - landingAlong[k] = along; - L.homes[k].shore = i; - } - if (along < shoreAlong[k] && L.road[i]) { - shoreAlong[k] = along; - L.homes[k].landing = i; - } - } - // Both approaches to the causeway are kept clear of deposits. - if (across <= stripHalf + kLane && ((along >= h.coast - kLanding && along < h.coast) || - (along > h.coast + g.strait && along <= h.coast + g.strait + kLanding))) - L.clear[i] = 1; + L.region.assign(n, Sea); + L.homeOf.assign(n, -1); + for (auto *mask : + {&L.lake, &L.river, &L.ford, &L.sand, &L.ridge, &L.strip, &L.causeway, &L.road, &L.clear}) + mask->assign(n, 0); + L.radius.assign(n, 0.0); + L.angle.assign(n, 0.0); + const double roadHalf = g.causeway / 2.0; + const double stripHalf = roadHalf + kShoulder; + std::vector landingAlong(teams, -1.0), shoreAlong(teams, 1e9); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); + const double d = std::hypot(double(dx), double(dy)); + const double theta = std::atan2(double(dy), double(dx)); + L.radius[i] = d; + L.angle[i] = theta; + // The wedge frame: which home, how far across it and how far along the arc from its middle. + double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); + if (teams >= 2) + turn = std::fmod(turn - bendAt(d) / std::max(1.0, d) + 4 * pi, 2 * pi); + const int k = std::min(teams - 1, int(turn / wedge)); + const double u = turn / wedge - k; + const double s = d * (u - 0.5) * wedge; + const double toBoundary = teams < 2 ? 1e9 : d * std::min(u, 1 - u) * wedge; + const double toLanding = d * std::abs(u - 0.5) * wedge; + const double c = coastAt(u, d); + const double in = innerAt(u, d), out = outerAt(u, d); + if (d < c) + { + L.region[i] = Commons; + } + else if (d < in) + { + L.region[i] = Sea; + } + else if (d < out && toBoundary >= g.strait / 2.0) + { + L.region[i] = HomeLand; + L.homeOf[i] = k; + } + // The home's causeway: straight across the strait at the wedge's middle. + const Home &h = L.homes[k]; + const double along = d * std::cos(theta - h.angle), + across = std::abs(d * std::sin(theta - h.angle)); + if (across <= stripHalf + kLane && along >= h.coast - 3 && + along <= h.coast + g.strait + 3) + L.causeway[i] = 1; + if (across <= stripHalf && along >= h.coast - 3 && along <= h.coast + g.strait + 3) + { + L.strip[i] = 1; + if (L.region[i] == Sea) + { + L.region[i] = HomeLand; + L.homeOf[i] = k; + } + if (across <= roadHalf) + L.road[i] = 1; + if (along > landingAlong[k] && L.road[i]) + { + landingAlong[k] = along; + L.homes[k].shore = i; + } + if (along < shoreAlong[k] && L.road[i]) + { + shoreAlong[k] = along; + L.homes[k].landing = i; + } + } + // Both approaches to the causeway are kept clear of deposits. + if (across <= stripHalf + kLane && + ((along >= h.coast - kLanding && along < h.coast) || + (along > h.coast + g.strait && along <= h.coast + g.strait + kLanding))) + L.clear[i] = 1; - // The home's features, the same in every wedge. - if (L.region[i] == HomeLand && !L.strip[i]) { - for (const Blob &b : lakes) - if (b.holds(s - b.s, d - b.r)) - L.lake[i] = 1; - for (size_t seg = 0; seg + 1 < creek.size(); ++seg) { - const double ax = creek[seg].first, ay = creek[seg].second; - const double bx = creek[seg + 1].first, by = creek[seg + 1].second; - const double len2 = (bx - ax) * (bx - ax) + (by - ay) * (by - ay); - const double tt = std::clamp(((s - ax) * (bx - ax) + (d - ay) * (by - ay)) / std::max(1e-6, len2), 0.0, 1.0); - const double dist = std::hypot(s - (ax + tt * (bx - ax)), d - (ay + tt * (by - ay))); - if (dist < 1.6) { - if (int(seg) == fordSegment && std::abs(tt - 0.5) * std::sqrt(len2) < 2.5) { - L.ford[i] = 1; - L.clear[i] = 1; - } else { - L.lake[i] = 1; - } - } else if (int(seg) == fordSegment && dist < 3.5 && std::abs(tt - 0.5) * std::sqrt(len2) < 3.5) { - L.clear[i] = 1; - } - } - if (L.homeKind == Highland && d >= in + 1 && d <= out - 1) - for (double ur : ridgeU) { - const double sr = (ur - 0.5) * wedge * d; - if (std::abs(s - sr) < 1.25) { - if (std::abs(d - passR) < 2.5) - L.clear[i] = 1; - else - L.ridge[i] = 1; - } else if (std::abs(s - sr) < 3.5 && std::abs(d - passR) < 4.5) { - L.clear[i] = 1; - } - } - if (L.homeKind == Barrens && d >= in + 8 && d <= out - 8 && - std::abs(s) > barrenShare * g.arcHalf(d) && std::abs(s) < g.arcHalf(d) - 8) - L.sand[i] = 1; - for (const Blob &b : sandBlobs) - if (b.holds(s - b.s, d - b.r)) - L.sand[i] = 1; - } - // The heart of the commons. - if (L.region[i] == Commons) { - const bool inHeart = d < heart.radiusAt(theta); - if (L.heartKind == IslandHeart) { - if (inHeart && d >= L.islandR) - L.lake[i] = 1; - if (inHeart && d >= L.islandR - 1 && d <= L.lakeR + 2.5 && toLanding < 2) { - L.ford[i] = 1; - L.lake[i] = 0; - } - if (inHeart && d >= L.islandR - 2 && d <= L.lakeR + 3.5 && toLanding < 3.5) - L.clear[i] = 1; - } else if (inHeart) { - L.lake[i] = 1; - } - if (L.heartKind == Crag) { - if (d >= L.ringR && d < L.ringR + 1.5 && toLanding >= 2.5) - L.ridge[i] = 1; - if (d >= L.ringR - 2 && d < L.ringR + 3.5 && toLanding < 4) - L.clear[i] = 1; - } - if (L.heartKind == Delta && teams >= 2 && d >= L.lakeR - 1 && d <= c + 1) { - if (toBoundary < 2) { - if (std::abs(d - deltaFordR) < 2.5) - L.ford[i] = 1; - else - L.river[i] = 1; - } - if (toBoundary < 3.5 && std::abs(d - deltaFordR) < 4) - L.clear[i] = 1; - } - if (!L.lake[i] && !L.river[i] && !L.ford[i] && !L.clear[i] && !L.causeway[i]) - for (const Patch &p : commonsSand) { - const double px = t.offsetX(p.x, x), py = t.offsetY(p.y, y); - if (std::hypot(px, py) < p.shape.radiusAt(std::atan2(py, px))) - L.sand[i] = 1; - } - } - } - // The commons-side end is the end nearest the centre; the home-side end the farthest. - for (int k = 0; k < teams; ++k) { - std::swap(L.homes[k].landing, L.homes[k].shore); - if (L.homes[k].landing < 0 || L.homes[k].shore < 0) { - L.failure = "home " + std::to_string(k) + " has no causeway"; - return L; - } - Home &h = L.homes[k]; - h.lakeX = t.x(L.cx + int(std::lround(mainLakeR * std::cos(h.angle)))); - h.lakeY = t.y(L.cy + int(std::lround(mainLakeR * std::sin(h.angle)))); - } - return L; + // The home's features, the same in every wedge. + if (L.region[i] == HomeLand && !L.strip[i]) + { + for (const Blob &b : lakes) + if (b.holds(s - b.s, d - b.r)) + L.lake[i] = 1; + for (size_t seg = 0; seg + 1 < creek.size(); ++seg) + { + const double ax = creek[seg].first, ay = creek[seg].second; + const double bx = creek[seg + 1].first, by = creek[seg + 1].second; + const double len2 = (bx - ax) * (bx - ax) + (by - ay) * (by - ay); + const double tt = std::clamp(((s - ax) * (bx - ax) + (d - ay) * (by - ay)) / + std::max(1e-6, len2), + 0.0, 1.0); + const double dist = + std::hypot(s - (ax + tt * (bx - ax)), d - (ay + tt * (by - ay))); + if (dist < 1.6) + { + if (int(seg) == fordSegment && std::abs(tt - 0.5) * std::sqrt(len2) < 2.5) + { + L.ford[i] = 1; + L.clear[i] = 1; + } + else + { + L.lake[i] = 1; + } + } + else if (int(seg) == fordSegment && dist < 3.5 && + std::abs(tt - 0.5) * std::sqrt(len2) < 3.5) + { + L.clear[i] = 1; + } + } + if (L.homeKind == Highland && d >= in + 1 && d <= out - 1) + for (double ur : ridgeU) + { + const double sr = (ur - 0.5) * wedge * d; + if (std::abs(s - sr) < 1.25) + { + if (std::abs(d - passR) < 2.5) + L.clear[i] = 1; + else + L.ridge[i] = 1; + } + else if (std::abs(s - sr) < 3.5 && std::abs(d - passR) < 4.5) + { + L.clear[i] = 1; + } + } + if (L.homeKind == Barrens && d >= in + 8 && d <= out - 8 && + std::abs(s) > barrenShare * g.arcHalf(d) && std::abs(s) < g.arcHalf(d) - 8) + L.sand[i] = 1; + for (const Blob &b : sandBlobs) + if (b.holds(s - b.s, d - b.r)) + L.sand[i] = 1; + } + // The heart of the commons. + if (L.region[i] == Commons) + { + const bool inHeart = d < heart.radiusAt(theta); + if (L.heartKind == IslandHeart) + { + if (inHeart && d >= L.islandR) + L.lake[i] = 1; + if (inHeart && d >= L.islandR - 1 && d <= L.lakeR + 2.5 && toLanding < 2) + { + L.ford[i] = 1; + L.lake[i] = 0; + } + if (inHeart && d >= L.islandR - 2 && d <= L.lakeR + 3.5 && toLanding < 3.5) + L.clear[i] = 1; + } + else if (inHeart) + { + L.lake[i] = 1; + } + if (L.heartKind == Crag) + { + if (d >= L.ringR && d < L.ringR + 1.5 && toLanding >= 2.5) + L.ridge[i] = 1; + if (d >= L.ringR - 2 && d < L.ringR + 3.5 && toLanding < 4) + L.clear[i] = 1; + } + if (L.heartKind == Delta && teams >= 2 && d >= L.lakeR - 1 && d <= c + 1) + { + if (toBoundary < 2) + { + if (std::abs(d - deltaFordR) < 2.5) + L.ford[i] = 1; + else + L.river[i] = 1; + } + if (toBoundary < 3.5 && std::abs(d - deltaFordR) < 4) + L.clear[i] = 1; + } + if (!L.lake[i] && !L.river[i] && !L.ford[i] && !L.clear[i] && !L.causeway[i]) + for (const Patch &p : commonsSand) + { + const double px = t.offsetX(p.x, x), py = t.offsetY(p.y, y); + if (std::hypot(px, py) < p.shape.radiusAt(std::atan2(py, px))) + L.sand[i] = 1; + } + } + } + // The commons-side end is the end nearest the centre; the home-side end the farthest. + for (int k = 0; k < teams; ++k) + { + std::swap(L.homes[k].landing, L.homes[k].shore); + if (L.homes[k].landing < 0 || L.homes[k].shore < 0) + { + L.failure = "home " + std::to_string(k) + " has no causeway"; + return L; + } + Home &h = L.homes[k]; + h.lakeX = t.x(L.cx + int(std::lround(mainLakeR * std::cos(h.angle)))); + h.lakeY = t.y(L.cy + int(std::lround(mainLakeR * std::sin(h.angle)))); + } + return L; } // The ground every causeway leads to, by the kind of heart: the island, the inside of the crag, // or the central lake's shore. -std::vector heartTiles(const Map &map, const Layout &L) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector heart(n, 0); - const double reach = L.heartKind == IslandHeart ? L.islandR - : L.heartKind == Crag ? L.ringR - 1 - : L.lakeR + kOrchardReach + 3; - for (int i = 0; i < n; ++i) - heart[i] = L.region[i] == Commons && L.radius[i] < reach && !map.isWater(i % t.w, i / t.w); - return heart; +std::vector heartTiles(const Map &map, const Layout &L) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector heart(n, 0); + const double reach = L.heartKind == IslandHeart ? L.islandR + : L.heartKind == Crag ? L.ringR - 1 + : L.lakeR + kOrchardReach + 3; + for (int i = 0; i < n; ++i) + heart[i] = L.region[i] == Commons && L.radius[i] < reach && !map.isWater(i % t.w, i / t.w); + return heart; } // Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), // so every land corner beside water becomes sand. Unlike Map::controlSand() this reads only the // original terrain, so the result doesn't depend on scan order and water is never eaten away. -void layBeaches(std::vector &terrain, const Torus &t) { - const std::vector original(terrain); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - if (original[i] == WATER) - continue; - bool shore = false; - for (int dy = -1; dy <= 1 && !shore; ++dy) - for (int dx = -1; dx <= 1 && !shore; ++dx) - shore = original[t.at(x + dx, y + dy)] == WATER; - if (shore) - terrain[i] = SAND; - } +void layBeaches(std::vector &terrain, const Torus &t) +{ + const std::vector original(terrain); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[t.at(x + dx, y + dy)] == WATER; + if (shore) + terrain[i] = SAND; + } } // The ground a unit landing from the sea can reach without crossing solid grass: every land tile // with a sea vertex in the box its four corners touch, and every beach joined to those - a lake's // beach too, where it meets the sea's. Stone cannot stand on any of it. -std::vector seaMargin(const Map &map, const Layout &L) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector sea(n, 0), margin(n, 0), beach(n, 0); - for (int i = 0; i < n; ++i) - sea[i] = map.getUMTerrain(i % t.w, i / t.w) == WATER && !L.lake[i] && L.region[i] != Commons; - for (int i = 0; i < n; ++i) { - const int x = i % t.w, y = i / t.w; - if (map.isWater(x, y)) - continue; - beach[i] = map.getTerrainType(x, y) != GRASS; - for (int dy = -1; dy <= 2 && !margin[i]; ++dy) - for (int dx = -1; dx <= 2; ++dx) - if (sea[t.at(x + dx, y + dy)]) { - margin[i] = 1; - break; - } - } - const std::vector joined = stepsFrom(t, margin, beach); - for (int i = 0; i < n; ++i) - if (joined[i] >= 0) - margin[i] = 1; - return margin; +std::vector seaMargin(const Map &map, const Layout &L) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector sea(n, 0), margin(n, 0), beach(n, 0); + for (int i = 0; i < n; ++i) + sea[i] = + map.getUMTerrain(i % t.w, i / t.w) == WATER && !L.lake[i] && L.region[i] != Commons; + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + if (map.isWater(x, y)) + continue; + beach[i] = map.getTerrainType(x, y) != GRASS; + for (int dy = -1; dy <= 2 && !margin[i]; ++dy) + for (int dx = -1; dx <= 2; ++dx) + if (sea[t.at(x + dx, y + dy)]) + { + margin[i] = 1; + break; + } + } + const std::vector joined = stepsFrom(t, margin, beach); + for (int i = 0; i < n; ++i) + if (joined[i] >= 0) + margin[i] = 1; + return margin; } // The design's stone, once the terrain is laid: every solid-grass tile of a causeway's shoulders // outside its road, the ridges and the crag, and round every home a wall on the solid-grass tiles // that touch the sea's margin. Every step off the beach lands on the wall, so a home is sealed but // for its road. Rebuilt the same way by validateWorld. -std::vector stoneTiles(const Map &map, const Layout &L) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector stone(n, 0); - if (!L.g.walls) - return stone; - const std::vector margin = seaMargin(map, L); - for (int i = 0; i < n; ++i) { - if (L.road[i] || map.getTerrainType(i % t.w, i / t.w) != GRASS) - continue; - if (L.strip[i] || L.ridge[i]) { - stone[i] = 1; - continue; - } - if (L.homeOf[i] < 0) - continue; - for (int dy = -1; dy <= 1 && !stone[i]; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (margin[t.at(i % t.w + dx, i / t.w + dy)]) { - stone[i] = 1; - break; - } - } - return stone; +std::vector stoneTiles(const Map &map, const Layout &L) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector stone(n, 0); + if (!L.g.walls) + return stone; + const std::vector margin = seaMargin(map, L); + for (int i = 0; i < n; ++i) + { + if (L.road[i] || map.getTerrainType(i % t.w, i / t.w) != GRASS) + continue; + if (L.strip[i] || L.ridge[i]) + { + stone[i] = 1; + continue; + } + if (L.homeOf[i] < 0) + continue; + for (int dy = -1; dy <= 1 && !stone[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (margin[t.at(i % t.w + dx, i / t.w + dy)]) + { + stone[i] = 1; + break; + } + } + return stone; } // Extra lakes in the commons for fertility, likelier towards the centre; each keeps kLakeShore // of land to any coast, landing, the central lake and other lakes. Counted per 128x128 of commons. void carveValleys(std::vector &terrain, const Layout &L, GenerationContext &context, - int perStandardMap) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector water(n, 0), land(n, 0), keep(n, 0); - std::vector ground; - for (int i = 0; i < n; ++i) { - water[i] = terrain[i] == WATER; - land[i] = !water[i]; - keep[i] = L.clear[i] || L.causeway[i]; - if (L.region[i] == Commons && land[i]) - ground.push_back(i); - } - if (perStandardMap <= 0 || ground.empty()) - return; - std::vector all(n, 1); - const std::vector shore = stepsFrom(t, water, land); - const std::vector fromKeep = stepsFrom(t, keep, all); - const int wanted = int(std::lround(perStandardMap * double(ground.size()) / 16384.0)); - const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 0.8, 1.6); - struct Lake { - int x, y; - double reach; - }; - std::vector lakes; - for (int attempt = 0; int(lakes.size()) < wanted && attempt < wanted * 80; ++attempt) { - const int at = ground[context.bounded("city-valleys", ground.size())]; - const double inward = 1 - L.radius[at] / std::max(1.0, L.g.commonsRadius); - if (int(context.bounded("city-valleys", 100)) >= 25 + int(75 * inward)) - continue; - const int x = at % t.w, y = at / t.w; - const double stretch = 1.2 + context.bounded("city-valleys", 81) / 100.0; - const double turn = context.bounded("city-valleys", 3600) / 3600.0 * pi; - const RadialShape shape((3 + context.bounded("city-valleys", 4)) * scale, 0.35, context, - "city-valleys"); - const double reach = shape.maximumRadius() * stretch; - if (shore[at] < reach + kLakeShore || fromKeep[at] < reach + kLakeShore) - continue; - bool open = true; - for (const Lake &other : lakes) { - const double gap = reach + other.reach + 2 * kLakeShore; - const double dx = t.offsetX(x, other.x), dy = t.offsetY(y, other.y); - open = open && dx * dx + dy * dy >= gap * gap; - } - if (!open) - continue; - const ShapeTransform transform({double(x), double(y)}, turn, stretch); - const int r = int(std::ceil(reach)); - for (int dy = -r; dy <= r; ++dy) - for (int dx = -r; dx <= r; ++dx) { - const ShapePoint p = transform.toShape({double(x + dx), double(y + dy)}); - if (std::hypot(p.x, p.y) < shape.radiusAt(std::atan2(p.y, p.x))) - terrain[t.at(x + dx, y + dy)] = WATER; - } - lakes.push_back({x, y, reach}); - } + int perStandardMap) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector water(n, 0), land(n, 0), keep(n, 0); + std::vector ground; + for (int i = 0; i < n; ++i) + { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + keep[i] = L.clear[i] || L.causeway[i]; + if (L.region[i] == Commons && land[i]) + ground.push_back(i); + } + if (perStandardMap <= 0 || ground.empty()) + return; + std::vector all(n, 1); + const std::vector shore = stepsFrom(t, water, land); + const std::vector fromKeep = stepsFrom(t, keep, all); + const int wanted = int(std::lround(perStandardMap * double(ground.size()) / 16384.0)); + const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 0.8, 1.6); + struct Lake + { + int x, y; + double reach; + }; + std::vector lakes; + for (int attempt = 0; int(lakes.size()) < wanted && attempt < wanted * 80; ++attempt) + { + const int at = ground[context.bounded("city-valleys", ground.size())]; + const double inward = 1 - L.radius[at] / std::max(1.0, L.g.commonsRadius); + if (int(context.bounded("city-valleys", 100)) >= 25 + int(75 * inward)) + continue; + const int x = at % t.w, y = at / t.w; + const double stretch = 1.2 + context.bounded("city-valleys", 81) / 100.0; + const double turn = context.bounded("city-valleys", 3600) / 3600.0 * pi; + const RadialShape shape((3 + context.bounded("city-valleys", 4)) * scale, 0.35, context, + "city-valleys"); + const double reach = shape.maximumRadius() * stretch; + if (shore[at] < reach + kLakeShore || fromKeep[at] < reach + kLakeShore) + continue; + bool open = true; + for (const Lake &other : lakes) + { + const double gap = reach + other.reach + 2 * kLakeShore; + const double dx = t.offsetX(x, other.x), dy = t.offsetY(y, other.y); + open = open && dx * dx + dy * dy >= gap * gap; + } + if (!open) + continue; + const ShapeTransform transform({double(x), double(y)}, turn, stretch); + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + { + const ShapePoint p = transform.toShape({double(x + dx), double(y + dy)}); + if (std::hypot(p.x, p.y) < shape.radiusAt(std::atan2(p.y, p.x))) + terrain[t.at(x + dx, y + dy)] = WATER; + } + lakes.push_back({x, y, reach}); + } } -struct Island { - int x, y; - std::vector tiles; +struct Island +{ + int x, y; + std::vector tiles; }; // Small islands out in the open sea, each well clear of every coast and of each other, so they are // only ever reached by swimming. Counted per 128x128 of sea. std::vector raiseIslands(std::vector &terrain, const Layout &L, - GenerationContext &context, int perStandardMap) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector islands; - if (perStandardMap <= 0) - return islands; - std::vector water(n), land(n); - std::vector sea; - for (int i = 0; i < n; ++i) { - water[i] = terrain[i] == WATER; - land[i] = !water[i]; - if (water[i]) - sea.push_back(i); - } - if (sea.empty()) - return islands; - const std::vector offshore = stepsFrom(t, land, water); - const int wanted = int(std::lround(perStandardMap * double(sea.size()) / 16384.0)); - const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 1.0, 1.6); - for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 60; ++attempt) { - const int at = sea[context.bounded("city-islands", sea.size())]; - const int x = at % t.w, y = at / t.w; - const RadialShape shape((4 + context.bounded("city-islands", 3)) * scale, 0.3, context, - "city-islands"); - const double reach = shape.maximumRadius(); - if (offshore[at] < reach + kLakeShore + 1) - continue; - bool open = true; - for (const Island &other : islands) { - const double gap = 2 * reach + kLakeShore + 1; - const double dx = t.offsetX(x, other.x), dy = t.offsetY(y, other.y); - open = open && dx * dx + dy * dy >= gap * gap; - } - if (!open) - continue; - Island island{x, y, {}}; - const int r = int(std::ceil(reach)); - for (int dy = -r; dy <= r; ++dy) - for (int dx = -r; dx <= r; ++dx) - if (std::hypot(double(dx), double(dy)) < shape.radiusAt(std::atan2(double(dy), double(dx)))) { - const int i = t.at(x + dx, y + dy); - terrain[i] = GRASS; - island.tiles.push_back(i); - } - islands.push_back(std::move(island)); - } - return islands; + GenerationContext &context, int perStandardMap) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector islands; + if (perStandardMap <= 0) + return islands; + std::vector water(n), land(n); + std::vector sea; + for (int i = 0; i < n; ++i) + { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + if (water[i]) + sea.push_back(i); + } + if (sea.empty()) + return islands; + const std::vector offshore = stepsFrom(t, land, water); + const int wanted = int(std::lround(perStandardMap * double(sea.size()) / 16384.0)); + const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 1.0, 1.6); + for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 60; ++attempt) + { + const int at = sea[context.bounded("city-islands", sea.size())]; + const int x = at % t.w, y = at / t.w; + const RadialShape shape((4 + context.bounded("city-islands", 3)) * scale, 0.3, context, + "city-islands"); + const double reach = shape.maximumRadius(); + if (offshore[at] < reach + kLakeShore + 1) + continue; + bool open = true; + for (const Island &other : islands) + { + const double gap = 2 * reach + kLakeShore + 1; + const double dx = t.offsetX(x, other.x), dy = t.offsetY(y, other.y); + open = open && dx * dx + dy * dy >= gap * gap; + } + if (!open) + continue; + Island island{x, y, {}}; + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (std::hypot(double(dx), double(dy)) < + shape.radiusAt(std::atan2(double(dy), double(dx)))) + { + const int i = t.at(x + dx, y + dy); + terrain[i] = GRASS; + island.tiles.push_back(i); + } + islands.push_back(std::move(island)); + } + return islands; } // Grows a compact patch of one resource outward from a seed tile, breadth-first over the four // cardinal neighbours, onto tiles the predicate allows. Returns how many tiles it placed. template -int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) { - std::vector queued(size_t(t.w) * t.h, 0); - std::vector frontier{seed}; - queued[seed] = 1; - int placed = 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - for (size_t head = 0; head < frontier.size() && placed < count; ++head) { - const int i = frontier[head], x = i % t.w, y = i / t.w; - if (!eligible(i) || !map.isResourceAllowed(x, y, type)) - continue; - map.setResource(x, y, type, 1); - ++placed; - for (const auto &step : steps) { - const int n = t.at(x + step[0], y + step[1]); - if (!queued[n]) { - queued[n] = 1; - frontier.push_back(n); - } - } - } - return placed; +int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) +{ + std::vector queued(size_t(t.w) * t.h, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) + { + const int i = frontier[head], x = i % t.w, y = i / t.w; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) + { + const int n = t.at(x + step[0], y + step[1]); + if (!queued[n]) + { + queued[n] = 1; + frontier.push_back(n); + } + } + } + return placed; } -bool clearGround(const Map &map, int x, int y) { - return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID; +bool clearGround(const Map &map, int x, int y) +{ + return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; } // Splits a set of tiles into wheat and wood by an unrelated noise field, so the two crops form // separate patches rather than rings sorted by fertility. void plantFields(Map &map, const Torus &t, std::vector tiles, int wheat, int wood, - HeightMap &split) { - const int total = std::min(int(tiles.size()), wheat + wood); - if (total <= 0) - return; - tiles.resize(total); - std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { - return split.uiLevel(a % t.w, a / t.w, 2048) < split.uiLevel(b % t.w, b / t.w, 2048); - }); - const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); - for (int k = 0; k < total; ++k) - map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheatShare ? CORN : WOOD, 1); + HeightMap &split) +{ + const int total = std::min(int(tiles.size()), wheat + wood); + if (total <= 0) + return; + tiles.resize(total); + std::stable_sort( + tiles.begin(), tiles.end(), [&](int a, int b) + { return split.uiLevel(a % t.w, a / t.w, 2048) < split.uiLevel(b % t.w, b / t.w, 2048); }); + const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); + for (int k = 0; k < total; ++k) + map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheatShare ? CORN : WOOD, 1); } // Every home's kit, identical and unscaled: wheat and wood patches beside its lake, where the lake // keeps them growing, and a stone deposit further off; then its own scaled ambient farmland, // outcrops and a grove, as a self-sufficient base needs. -void furnishHomes(Map &map, const Layout &L, GenerationContext &context, - const CityStatesOptions &o) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector reserved(n, 0), water(n), dry(n); - for (int team = 0; team < L.g.teams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; - for (int i = 0; i < n; ++i) { - water[i] = map.isWater(i % t.w, i / t.w); - dry[i] = !water[i]; - } - const std::vector shore = stepsFrom(t, water, dry); - const Fertility::Field fertility = Fertility::forMap(map, false); - HeightMap split(t.w, t.h, context.stream("city-home-split")); - split.makePlain(6); - HeightMap patch(t.w, t.h, context.stream("city-home-patch")); - patch.makePlain(12); - for (int team = 0; team < L.g.teams; ++team) { - const Home &h = L.homes[team]; - const auto eligible = [&](int i) { - return L.homeOf[i] == team && !L.causeway[i] && !L.clear[i] && !reserved[i] && - clearGround(map, i % t.w, i / t.w); - }; - // The kit: wheat and wood beside the lake on the swarm's side, on the flank away from any - // creek, and the quarry behind the lake. - const double reach = std::clamp(0.1 * L.g.depth(), 2.0, 6.0) * 1.3 + 3; - const double flank = L.creekSide != 0 ? -L.creekSide : -1.0; - const auto seedNear = [&](double along, double across, int within) { - const int ax = h.lakeX + int(std::lround(along * std::cos(h.angle) - across * std::sin(h.angle))); - const int ay = h.lakeY + int(std::lround(along * std::sin(h.angle) + across * std::cos(h.angle))); - int seed = -1, nearest = INT_MAX; - for (int dy = -within; dy <= within; ++dy) - for (int dx = -within; dx <= within; ++dx) { - const int i = t.at(ax + dx, ay + dy); - if (eligible(i) && dx * dx + dy * dy < nearest) { - nearest = dx * dx + dy * dy; - seed = i; - } - } - return seed; - }; - if (L.creekSide == 0) { - if (const int seed = seedNear(-0.3 * reach, -reach, 14); seed >= 0) - growPatch(map, t, seed, CORN, kHomeWheat, eligible); - if (const int seed = seedNear(-0.3 * reach, reach, 14); seed >= 0) - growPatch(map, t, seed, WOOD, kHomeWood, eligible); - } else { - if (const int seed = seedNear(-0.5 * reach, flank * reach, 14); seed >= 0) - growPatch(map, t, seed, CORN, kHomeWheat, eligible); - if (const int seed = seedNear(0.6 * reach, flank * (reach + 1), 14); seed >= 0) - growPatch(map, t, seed, WOOD, kHomeWood, eligible); - } - if (const int seed = seedNear(reach + 8, 0, 12); seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); - // Ambient farmland on the home's fertile ground, in patches, then outcrops and a grove. - std::vector ground; - std::vector> farm; - std::vector levels; - for (int i = 0; i < n; ++i) - if (eligible(i)) { - ground.push_back(i); - if (fertility.at(i % t.w, i / t.w) > 0) - levels.push_back(patch(i % t.w, i / t.w)); - } - float cut = 0; - if (!levels.empty()) { - std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, levels.end()); - cut = levels[levels.size() * 45 / 100]; - } - for (int i : ground) { - const std::uint32_t f = fertility.at(i % t.w, i / t.w); - if (f > 0 && patch(i % t.w, i / t.w) >= cut) - farm.push_back({-double(f), i}); - } - std::stable_sort(farm.begin(), farm.end()); - std::vector chosen; - for (const auto &entry : farm) - chosen.push_back(entry.second); - const int area = int(ground.size()); - plantFields(map, t, chosen, int(scaledCount(area * 4 / 100, o.wheat)), - int(scaledCount(area * 2 / 100, o.wood)), split); - const int outcrops = int(scaledCount(std::max(1, area / 2500), o.stone)); - for (int k = 0; k < outcrops; ++k) - for (int attempt = 0; attempt < 100; ++attempt) { - const int at = ground[context.bounded("city-home-stone", ground.size())]; - if (eligible(at)) { - placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, 1); - break; - } - } - const int groves = int(scaledCount(1, o.fruit)); - for (int k = 0; k < groves; ++k) - for (int attempt = 0; attempt < 100; ++attempt) { - const int at = ground[context.bounded("city-home-fruit", ground.size())]; - if (eligible(at)) { - placeResourceClump(map, context, {at % t.w, at / t.w}, - CHERRY + int(context.bounded("city-home-fruit", 3)), 1); - break; - } - } - } +void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const CityStatesOptions &o) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector reserved(n, 0), water(n), dry(n); + for (int team = 0; team < L.g.teams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + for (int i = 0; i < n; ++i) + { + water[i] = map.isWater(i % t.w, i / t.w); + dry[i] = !water[i]; + } + const std::vector shore = stepsFrom(t, water, dry); + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap split(t.w, t.h, context.stream("city-home-split")); + split.makePlain(6); + HeightMap patch(t.w, t.h, context.stream("city-home-patch")); + patch.makePlain(12); + for (int team = 0; team < L.g.teams; ++team) + { + const Home &h = L.homes[team]; + const auto eligible = [&](int i) + { + return L.homeOf[i] == team && !L.causeway[i] && !L.clear[i] && !reserved[i] && + clearGround(map, i % t.w, i / t.w); + }; + // The kit: wheat and wood beside the lake on the swarm's side, on the flank away from any + // creek, and the quarry behind the lake. + const double reach = std::clamp(0.1 * L.g.depth(), 2.0, 6.0) * 1.3 + 3; + const double flank = L.creekSide != 0 ? -L.creekSide : -1.0; + const auto seedNear = [&](double along, double across, int within) + { + const int ax = + h.lakeX + int(std::lround(along * std::cos(h.angle) - across * std::sin(h.angle))); + const int ay = + h.lakeY + int(std::lround(along * std::sin(h.angle) + across * std::cos(h.angle))); + int seed = -1, nearest = INT_MAX; + for (int dy = -within; dy <= within; ++dy) + for (int dx = -within; dx <= within; ++dx) + { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) + { + nearest = dx * dx + dy * dy; + seed = i; + } + } + return seed; + }; + if (L.creekSide == 0) + { + if (const int seed = seedNear(-0.3 * reach, -reach, 14); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheat, eligible); + if (const int seed = seedNear(-0.3 * reach, reach, 14); seed >= 0) + growPatch(map, t, seed, WOOD, kHomeWood, eligible); + } + else + { + if (const int seed = seedNear(-0.5 * reach, flank * reach, 14); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheat, eligible); + if (const int seed = seedNear(0.6 * reach, flank * (reach + 1), 14); seed >= 0) + growPatch(map, t, seed, WOOD, kHomeWood, eligible); + } + if (const int seed = seedNear(reach + 8, 0, 12); seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + // Ambient farmland on the home's fertile ground, in patches, then outcrops and a grove. + std::vector ground; + std::vector> farm; + std::vector levels; + for (int i = 0; i < n; ++i) + if (eligible(i)) + { + ground.push_back(i); + if (fertility.at(i % t.w, i / t.w) > 0) + levels.push_back(patch(i % t.w, i / t.w)); + } + float cut = 0; + if (!levels.empty()) + { + std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, + levels.end()); + cut = levels[levels.size() * 45 / 100]; + } + for (int i : ground) + { + const std::uint32_t f = fertility.at(i % t.w, i / t.w); + if (f > 0 && patch(i % t.w, i / t.w) >= cut) + farm.push_back({-double(f), i}); + } + std::stable_sort(farm.begin(), farm.end()); + std::vector chosen; + for (const auto &entry : farm) + chosen.push_back(entry.second); + const int area = int(ground.size()); + plantFields(map, t, chosen, int(scaledCount(area * 4 / 100, o.wheat)), + int(scaledCount(area * 2 / 100, o.wood)), split); + const int outcrops = int(scaledCount(std::max(1, area / 2500), o.stone)); + for (int k = 0; k < outcrops; ++k) + for (int attempt = 0; attempt < 100; ++attempt) + { + const int at = ground[context.bounded("city-home-stone", ground.size())]; + if (eligible(at)) + { + placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, 1); + break; + } + } + const int groves = int(scaledCount(1, o.fruit)); + for (int k = 0; k < groves; ++k) + for (int attempt = 0; attempt < 100; ++attempt) + { + const int at = ground[context.bounded("city-home-fruit", ground.size())]; + if (eligible(at)) + { + placeResourceClump(map, context, {at % t.w, at / t.w}, + CHERRY + int(context.bounded("city-home-fruit", 3)), 1); + break; + } + } + } } // The commons: farmland on its fertile ground, richer towards the centre as the frontier control // says; stone outcrops and fruit groves sampled by the same weight; and the orchard of all three // fruits round the central lake, the heart every causeway leads to. -void stockCommons(Map &map, const Layout &L, GenerationContext &context, - const CityStatesOptions &o) { - const Torus &t = L.t; - const int n = t.w * t.h; - const double frontier = o.frontier / 100.0; - const auto weight = [&](int i) { - const double inward = std::clamp(1 - L.radius[i] / std::max(1.0, L.g.commonsRadius), 0.0, 1.0); - return 1 - frontier + frontier * std::min(1.0, inward / (1 - kHeartShare)); - }; - const auto eligible = [&](int i) { - return L.region[i] == Commons && !L.clear[i] && !L.causeway[i] && - clearGround(map, i % t.w, i / t.w); - }; - const Fertility::Field fertility = Fertility::forMap(map, false); - HeightMap patch(t.w, t.h, context.stream("city-patch")); - patch.makePlain(12); - HeightMap split(t.w, t.h, context.stream("city-split")); - split.makePlain(6); - std::vector ground; - std::vector levels; - for (int i = 0; i < n; ++i) - if (eligible(i)) { - ground.push_back(i); - if (fertility.at(i % t.w, i / t.w) > 0) - levels.push_back(patch(i % t.w, i / t.w)); - } - if (ground.empty()) - return; - float cut = 0; - if (!levels.empty()) { - std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, levels.end()); - cut = levels[levels.size() * 45 / 100]; - } - std::vector> farm; - for (int i : ground) { - const std::uint32_t f = fertility.at(i % t.w, i / t.w); - if (f > 0 && patch(i % t.w, i / t.w) >= cut) - farm.push_back({-double(f) * weight(i), i}); - } - std::stable_sort(farm.begin(), farm.end()); - std::vector chosen; - for (const auto &entry : farm) - chosen.push_back(entry.second); - const int area = int(ground.size()); - plantFields(map, t, chosen, int(scaledCount(area * 7 / 100, o.wheat)), - int(scaledCount(area * 4 / 100, o.wood)), split); +void stockCommons(Map &map, const Layout &L, GenerationContext &context, const CityStatesOptions &o) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + const double frontier = o.frontier / 100.0; + const auto weight = [&](int i) + { + const double inward = + std::clamp(1 - L.radius[i] / std::max(1.0, L.g.commonsRadius), 0.0, 1.0); + return 1 - frontier + frontier * std::min(1.0, inward / (1 - kHeartShare)); + }; + const auto eligible = [&](int i) + { + return L.region[i] == Commons && !L.clear[i] && !L.causeway[i] && + clearGround(map, i % t.w, i / t.w); + }; + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap patch(t.w, t.h, context.stream("city-patch")); + patch.makePlain(12); + HeightMap split(t.w, t.h, context.stream("city-split")); + split.makePlain(6); + std::vector ground; + std::vector levels; + for (int i = 0; i < n; ++i) + if (eligible(i)) + { + ground.push_back(i); + if (fertility.at(i % t.w, i / t.w) > 0) + levels.push_back(patch(i % t.w, i / t.w)); + } + if (ground.empty()) + return; + float cut = 0; + if (!levels.empty()) + { + std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, levels.end()); + cut = levels[levels.size() * 45 / 100]; + } + std::vector> farm; + for (int i : ground) + { + const std::uint32_t f = fertility.at(i % t.w, i / t.w); + if (f > 0 && patch(i % t.w, i / t.w) >= cut) + farm.push_back({-double(f) * weight(i), i}); + } + std::stable_sort(farm.begin(), farm.end()); + std::vector chosen; + for (const auto &entry : farm) + chosen.push_back(entry.second); + const int area = int(ground.size()); + plantFields(map, t, chosen, int(scaledCount(area * 7 / 100, o.wheat)), + int(scaledCount(area * 4 / 100, o.wood)), split); - const auto pick = [&](const char *stream) { - for (int attempt = 0; attempt < 200; ++attempt) { - const int at = ground[context.bounded(stream, ground.size())]; - if (int(context.bounded(stream, 1000)) < int(weight(at) * 1000) && eligible(at)) - return at; - } - return -1; - }; - const int outcrops = int(scaledCount(std::max(2, area / 2000), o.stone)); - for (int k = 0; k < outcrops; ++k) - if (const int at = pick("city-stone"); at >= 0) - placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, - 1 + int(context.bounded("city-stone", 2))); - const int groves = int(scaledCount(std::max(3, area / 2500), o.fruit)); - for (int k = 0; k < groves; ++k) - if (const int at = pick("city-fruit"); at >= 0) - placeResourceClump(map, context, {at % t.w, at / t.w}, - CHERRY + int(context.bounded("city-fruit", 3)), - 1 + int(context.bounded("city-fruit", 2))); - // The orchard: three groves of the three fruits round the central lake's shore, on the island, - // or inside the crag. - if (scaledCount(1, o.fruit) > 0) { - const double rho = L.heartKind == IslandHeart ? std::max(1.5, 0.45 * L.islandR) - : L.heartKind == Crag ? 0.6 * L.ringR - : L.heartKind == ForestHeart ? L.lakeR + 3 - : L.lakeR + kOrchardReach; - const double spin = context.bounded("city-fruit", 3600) / 3600.0 * 2 * pi; - for (int f = 0; f < 3; ++f) { - const double a = spin + 2 * pi * f / 3; - const int ax = L.cx + int(std::lround(rho * std::cos(a))), ay = L.cy + int(std::lround(rho * std::sin(a))); - int seed = -1, nearest = INT_MAX; - for (int dy = -8; dy <= 8; ++dy) - for (int dx = -8; dx <= 8; ++dx) { - const int i = t.at(ax + dx, ay + dy); - if (eligible(i) && dx * dx + dy * dy < nearest) { - nearest = dx * dx + dy * dy; - seed = i; - } - } - if (seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); - } - } - // The forest heart: a belt of wood round the orchard that has to be cut through, and that the - // lake keeps growing back. - if (L.heartKind == ForestHeart) - for (int i = 0; i < n; ++i) - if (eligible(i) && L.radius[i] >= L.lakeR + 2 && L.radius[i] < L.forestR) - map.setResource(i % t.w, i / t.w, WOOD, 1); + const auto pick = [&](const char *stream) + { + for (int attempt = 0; attempt < 200; ++attempt) + { + const int at = ground[context.bounded(stream, ground.size())]; + if (int(context.bounded(stream, 1000)) < int(weight(at) * 1000) && eligible(at)) + return at; + } + return -1; + }; + const int outcrops = int(scaledCount(std::max(2, area / 2000), o.stone)); + for (int k = 0; k < outcrops; ++k) + if (const int at = pick("city-stone"); at >= 0) + placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, + 1 + int(context.bounded("city-stone", 2))); + const int groves = int(scaledCount(std::max(3, area / 2500), o.fruit)); + for (int k = 0; k < groves; ++k) + if (const int at = pick("city-fruit"); at >= 0) + placeResourceClump(map, context, {at % t.w, at / t.w}, + CHERRY + int(context.bounded("city-fruit", 3)), + 1 + int(context.bounded("city-fruit", 2))); + // The orchard: three groves of the three fruits round the central lake's shore, on the island, + // or inside the crag. + if (scaledCount(1, o.fruit) > 0) + { + const double rho = L.heartKind == IslandHeart ? std::max(1.5, 0.45 * L.islandR) + : L.heartKind == Crag ? 0.6 * L.ringR + : L.heartKind == ForestHeart ? L.lakeR + 3 + : L.lakeR + kOrchardReach; + const double spin = context.bounded("city-fruit", 3600) / 3600.0 * 2 * pi; + for (int f = 0; f < 3; ++f) + { + const double a = spin + 2 * pi * f / 3; + const int ax = L.cx + int(std::lround(rho * std::cos(a))), + ay = L.cy + int(std::lround(rho * std::sin(a))); + int seed = -1, nearest = INT_MAX; + for (int dy = -8; dy <= 8; ++dy) + for (int dx = -8; dx <= 8; ++dx) + { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) + { + nearest = dx * dx + dy * dy; + seed = i; + } + } + if (seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); + } + } + // The forest heart: a belt of wood round the orchard that has to be cut through, and that the + // lake keeps growing back. + if (L.heartKind == ForestHeart) + for (int i = 0; i < n; ++i) + if (eligible(i) && L.radius[i] >= L.lakeR + 2 && L.radius[i] < L.forestR) + map.setResource(i % t.w, i / t.w, WOOD, 1); } // Each island carries one themed prize, as on Ring world. -void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) { - const int width = map.getW(); - for (const Island &island : islands) { - std::vector grass; - for (int i : island.tiles) - if (map.isGrass(i % width, i / width)) - grass.emplace_back(i % width, i / width); - if (grass.empty()) - continue; - MapGeneratorPoint centre(island.x, island.y); - if (!map.isGrass(centre.x, centre.y)) - centre = grass[context.bounded("city-islands", grass.size())]; - switch (context.bounded("city-islands", 3)) { - case 0: - placeResourceClump(map, context, centre, STONE, 2); - break; - case 1: - placeResourceClump(map, context, centre, CHERRY + int(context.bounded("city-islands", 3)), 2); - break; - default: - placeResourceClump(map, context, centre, CORN, 2); - break; - } - } +void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) +{ + const int width = map.getW(); + for (const Island &island : islands) + { + std::vector grass; + for (int i : island.tiles) + if (map.isGrass(i % width, i / width)) + grass.emplace_back(i % width, i / width); + if (grass.empty()) + continue; + MapGeneratorPoint centre(island.x, island.y); + if (!map.isGrass(centre.x, centre.y)) + centre = grass[context.bounded("city-islands", grass.size())]; + switch (context.bounded("city-islands", 3)) + { + case 0: + placeResourceClump(map, context, centre, STONE, 2); + break; + case 1: + placeResourceClump(map, context, centre, + CHERRY + int(context.bounded("city-islands", 3)), 2); + break; + default: + placeResourceClump(map, context, centre, CORN, 2); + break; + } + } } // Algae in the shallows along every coast, the strait's, the sea's and the lakes' alike. -void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) { - const int n = t.w * t.h; - std::vector water(n), dry(n); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - water[y * t.w + x] = map.isWater(x, y); - dry[y * t.w + x] = !map.isWater(x, y); - } - const std::vector offshore = stepsFrom(t, dry, water); - std::vector shallows; - for (int i = 0; i < n; ++i) - if (offshore[i] >= 2 && offshore[i] <= 4) - shallows.emplace_back(i % t.w, i / t.w); - if (shallows.empty()) - return; - for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) - placeResourceClump(map, context, shallows[context.bounded("city-algae", shallows.size())], - ALGA, 1); +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) +{ + const int n = t.w * t.h; + std::vector water(n), dry(n); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + water[y * t.w + x] = map.isWater(x, y); + dry[y * t.w + x] = !map.isWater(x, y); + } + const std::vector offshore = stepsFrom(t, dry, water); + std::vector shallows; + for (int i = 0; i < n; ++i) + if (offshore[i] >= 2 && offshore[i] <= 4) + shallows.emplace_back(i % t.w, i / t.w); + if (shallows.empty()) + return; + for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) + placeResourceClump(map, context, shallows[context.bounded("city-algae", shallows.size())], + ALGA, 1); } // Clears the ring the swarm's workers walk out through; a causeway's stone beside it stays. void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t, - const std::vector &line) { - for (int team = 0; team < context.request.nbTeams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) { - const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); - if (map.isResource(x, y) && !line[y * t.w + x]) - map.setNoResource(x, y, 1); - } + const std::vector &line) +{ + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + { + const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); + if (map.isResource(x, y) && !line[y * t.w + x]) + map.setNoResource(x, y, 1); + } } // Causeways and their approaches hold nothing but the causeways' own stone lines. -void clearRoads(Map &map, const Layout &L, const std::vector &line) { - const Torus &t = L.t; - for (int i = 0; i < t.w * t.h; ++i) - if ((L.causeway[i] || L.clear[i]) && !line[i] && map.isResource(i % t.w, i / t.w)) - map.setNoResource(i % t.w, i / t.w, 1); +void clearRoads(Map &map, const Layout &L, const std::vector &line) +{ + const Torus &t = L.t; + for (int i = 0; i < t.w * t.h; ++i) + if ((L.causeway[i] || L.clear[i]) && !line[i] && map.isResource(i % t.w, i / t.w)) + map.setNoResource(i % t.w, i / t.w, 1); } // The cheapest walk from any source tile to any goal tile (deposits cost one, open ground nothing; // water, buildings and the stone lines are impassable), with only the deposits on it cleared. bool openRoad(Map &map, const Layout &L, const std::vector &line, - const std::vector &sources, const std::vector &goal) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector cost(n, INT_MAX), parent(n, -1); - std::vector done(n, 0); - std::deque queue; - for (int i : sources) { - cost[i] = 0; - queue.push_back(i); - } - int reached = -1; - while (!queue.empty() && reached < 0) { - const int i = queue.front(); - queue.pop_front(); - if (done[i]) - continue; - done[i] = 1; - if (goal[i]) { - reached = i; - break; - } - const int x = i % t.w, y = i / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (!dx && !dy) - continue; - const int nx = t.x(x + dx), ny = t.y(y + dy), next = ny * t.w + nx; - if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID || line[next]) - continue; - const int nextCost = cost[i] + (map.isResource(nx, ny) ? 1 : 0); - if (nextCost < cost[next]) { - cost[next] = nextCost; - parent[next] = i; - if (nextCost == cost[i]) - queue.push_front(next); - else - queue.push_back(next); - } - } - } - if (reached < 0) - return false; - for (int i = reached; i >= 0; i = parent[i]) - if (map.isResource(i % t.w, i / t.w)) - map.setNoResource(i % t.w, i / t.w, 1); - return true; + const std::vector &sources, const std::vector &goal) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector cost(n, INT_MAX), parent(n, -1); + std::vector done(n, 0); + std::deque queue; + for (int i : sources) + { + cost[i] = 0; + queue.push_back(i); + } + int reached = -1; + while (!queue.empty() && reached < 0) + { + const int i = queue.front(); + queue.pop_front(); + if (done[i]) + continue; + done[i] = 1; + if (goal[i]) + { + reached = i; + break; + } + const int x = i % t.w, y = i / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int nx = t.x(x + dx), ny = t.y(y + dy), next = ny * t.w + nx; + if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID || line[next]) + continue; + const int nextCost = cost[i] + (map.isResource(nx, ny) ? 1 : 0); + if (nextCost < cost[next]) + { + cost[next] = nextCost; + parent[next] = i; + if (nextCost == cost[i]) + queue.push_front(next); + else + queue.push_back(next); + } + } + } + if (reached < 0) + return false; + for (int i = reached; i >= 0; i = parent[i]) + if (map.isResource(i % t.w, i / t.w)) + map.setNoResource(i % t.w, i / t.w, 1); + return true; } // Deposits may land anywhere, and a band of them could close a home's swarm off from its causeway // or a landing off from the heart. Keep both walks open, clearing only what stands on the cheapest // one. Almost always nothing is in the way. bool openRoads(Map &map, const Layout &L, const std::vector &line, - GenerationContext &context) { - const Torus &t = L.t; - const int n = t.w * t.h; - const std::vector heart = heartTiles(map, L); - for (int k = 0; k < L.g.teams; ++k) { - std::vector workers; - for (int i = 0; i < n; ++i) { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == k) - workers.push_back(i); - } - std::vector shore(n, 0); - shore[L.homes[k].shore] = 1; - if (!openRoad(map, L, line, workers, shore)) { - context.detail = "colony " + std::to_string(k) + " has no way to its causeway"; - return false; - } - if (!openRoad(map, L, line, {L.homes[k].landing}, heart)) { - context.detail = "causeway " + std::to_string(k) + " has no way into the commons"; - return false; - } - } - return true; + GenerationContext &context) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + const std::vector heart = heartTiles(map, L); + for (int k = 0; k < L.g.teams; ++k) + { + std::vector workers; + for (int i = 0; i < n; ++i) + { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == k) + workers.push_back(i); + } + std::vector shore(n, 0); + shore[L.homes[k].shore] = 1; + if (!openRoad(map, L, line, workers, shore)) + { + context.detail = "colony " + std::to_string(k) + " has no way to its causeway"; + return false; + } + if (!openRoad(map, L, line, {L.homes[k].landing}, heart)) + { + context.detail = "causeway " + std::to_string(k) + " has no way into the commons"; + return false; + } + } + return true; } -bool generate(Game &game, GenerationContext &context) { - context.stage = "city layout"; - const CityStatesOptions o(context.request); - Map &map = game.map; - const int teams = context.request.nbTeams; - map.makeHomogenMap(GRASS); - for (int i = 0; i < teams; ++i) - game.addTeam(); - const Layout L = design(context.request, context); - if (!L.failure.empty()) { - context.detail = L.failure; - return false; - } - const Torus &t = L.t; - const int n = t.w * t.h; +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "city layout"; + const CityStatesOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(GRASS); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.w * t.h; - context.stage = "city terrain"; - std::vector terrain(n, GRASS); - for (int i = 0; i < n; ++i) { - if (L.region[i] == Sea || L.lake[i] || L.river[i]) - terrain[i] = WATER; - if (L.ford[i]) - terrain[i] = SAND; - if (L.sand[i] && terrain[i] == GRASS) - terrain[i] = SAND; - } - carveValleys(terrain, L, context, o.valleys); - const std::vector islands = raiseIslands(terrain, L, context, o.resourceIslands); - layBeaches(terrain, t); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); - map.rebuildTerrain(); - const std::vector line = stoneTiles(map, L); - for (int i = 0; i < n; ++i) - if (line[i]) - map.setResource(i % t.w, i / t.w, STONE, 1); + context.stage = "city terrain"; + std::vector terrain(n, GRASS); + for (int i = 0; i < n; ++i) + { + if (L.region[i] == Sea || L.lake[i] || L.river[i]) + terrain[i] = WATER; + if (L.ford[i]) + terrain[i] = SAND; + if (L.sand[i] && terrain[i] == GRASS) + terrain[i] = SAND; + } + carveValleys(terrain, L, context, o.valleys); + const std::vector islands = raiseIslands(terrain, L, context, o.resourceIslands); + layBeaches(terrain, t); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); + map.rebuildTerrain(); + const std::vector line = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + if (line[i]) + map.setResource(i % t.w, i / t.w, STONE, 1); - context.stage = "city colonies"; - for (int team = 0; team < teams; ++team) { - const Home &h = L.homes[team]; - std::vector home(n, 0); - for (int i = 0; i < n; ++i) - home[i] = L.homeOf[i] == team && !L.strip[i] && !L.clear[i] && map.isGrass(i % t.w, i / t.w); - // The swarm stands between the causeway's home end and the lake, a short walk from the lake's - // fields whatever the home's depth; placeSettlement measures from the footprint's top-left - // tile, so the anchor is offset to centre the 4x4 there. - const double lakeRadius = std::clamp(0.1 * L.g.depth(), 2.0, 6.0); - const double rho = std::max(h.coast + L.g.strait + 8.0, - h.coast + L.g.strait + lakeOffset(L.g, lakeRadius) - lakeRadius - 9.0); - const MapGeneratorPoint anchor(L.cx + int(std::lround(rho * std::cos(h.angle))) - 2, - L.cy + int(std::lround(rho * std::sin(h.angle))) - 2); - if (!placeSettlement(game, context, team, home, anchor, "city-starts")) - return false; - } + context.stage = "city colonies"; + for (int team = 0; team < teams; ++team) + { + const Home &h = L.homes[team]; + std::vector home(n, 0); + for (int i = 0; i < n; ++i) + home[i] = + L.homeOf[i] == team && !L.strip[i] && !L.clear[i] && map.isGrass(i % t.w, i / t.w); + // The swarm stands between the causeway's home end and the lake, a short walk from the lake's + // fields whatever the home's depth; placeSettlement measures from the footprint's top-left + // tile, so the anchor is offset to centre the 4x4 there. + const double lakeRadius = std::clamp(0.1 * L.g.depth(), 2.0, 6.0); + const double rho = + std::max(h.coast + L.g.strait + 8.0, + h.coast + L.g.strait + lakeOffset(L.g, lakeRadius) - lakeRadius - 9.0); + const MapGeneratorPoint anchor(L.cx + int(std::lround(rho * std::cos(h.angle))) - 2, + L.cy + int(std::lround(rho * std::sin(h.angle))) - 2); + if (!placeSettlement(game, context, team, home, anchor, "city-starts")) + return false; + } - context.stage = "city resources"; - furnishHomes(map, L, context, o); - stockCommons(map, L, context, o); - stockIslands(map, context, islands); - seedAlgae(map, context, t, o.algae); - clearAroundSwarms(map, context, t, line); - // The kits already put wheat and wood a short walk from every swarm; this is only the backstop, - // and the walls and the causeways' stone are designed and must never be cleared. - guaranteeStartingResources(game, context, 24, 32, 0, &line); - clearAroundSwarms(map, context, t, line); - clearRoads(map, L, line); - context.stage = "city roads"; - return openRoads(map, L, line, context); + context.stage = "city resources"; + furnishHomes(map, L, context, o); + stockCommons(map, L, context, o); + stockIslands(map, context, islands); + seedAlgae(map, context, t, o.algae); + clearAroundSwarms(map, context, t, line); + // The kits already put wheat and wood a short walk from every swarm; this is only the backstop, + // and the walls and the causeways' stone are designed and must never be cleared. + guaranteeStartingResources(game, context, 24, 32, 0, &line); + clearAroundSwarms(map, context, t, line); + clearRoads(map, L, line); + context.stage = "city roads"; + return openRoads(map, L, line, context); } -std::string validateRequest(const GenerationRequest &r) { - if (r.nbTeams < 1) - return "City states need at least one colony."; - const Geometry g = geometryFor(r); - if (g.depth() < kMinimumDepth || g.innerArc() < g.causeway + kMinimumArc) - return "The commons and the strait leave too little room for the home bases; use a bigger map, " - "fewer colonies, a smaller commons or a narrower strait."; - return ""; +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 1) + return "City states need at least one colony."; + const Geometry g = geometryFor(r); + if (g.depth() < kMinimumDepth || g.innerArc() < g.causeway + kMinimumArc) + return "The commons and the strait leave too little room for the home bases; use a bigger " + "map, " + "fewer colonies, a smaller commons or a narrower strait."; + return ""; } // Checked on the finished world against the rebuilt design: every causeway is open and carries its @@ -1274,144 +1470,156 @@ std::string validateRequest(const GenerationRequest &r) { // causeways shut no home reaches another home or the commons at all; and no colony's walk to its // landing is much longer than another's. Water, buildings and every resource tile block the // walks; units don't, since they move. -std::string validateWorld(const Game &game, const GenerationContext &context) { - GenerationContext replay(context.request); - const Layout L = design(context.request, replay); - if (!L.failure.empty()) - return "The city design could not be rebuilt: " + L.failure; - const Map &map = game.map; - const Torus &t = L.t; - const int n = t.w * t.h, teams = context.request.nbTeams; - if (map.getW() != t.w || map.getH() != t.h) - return "The city design does not match the map size."; - const auto where = [&](int i) { - return "(" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")"; - }; - const auto walkable = [&](int i) { - const int x = i % t.w, y = i / t.w; - return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - }; - const std::vector line = stoneTiles(map, L); - for (int i = 0; i < n; ++i) { - if (line[i] && map.getResource(i % t.w, i / t.w).type != STONE) - return "The stone at " + where(i) + " is missing."; - if (L.road[i] && !walkable(i)) - return "The causeway at " + where(i) + " is blocked."; - if (L.ford[i] && !walkable(i)) - return "The ford at " + where(i) + " is blocked."; - } - std::vector> workers(std::max(teams, 1)); - for (int i = 0; i < n; ++i) { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(i); - } - if (teams < 1 || workers[0].empty()) - return "Colony 0 has no workers to walk the map."; - std::vector open(n, 0), source(n, 0); - for (int i = 0; i < n; ++i) - open[i] = walkable(i); - for (int i : workers[0]) - source[i] = 1; - const std::vector fromFirst = stepsFrom(t, source, open); - for (int team = 1; team < teams; ++team) { - bool arrived = false; - for (int i : workers[team]) - arrived = arrived || fromFirst[i] >= 0; - if (!arrived) - return "Colony " + std::to_string(team) + " cannot walk to colony 0."; - } - const std::vector heart = heartTiles(map, L); - bool heartReached = false; - for (int i = 0; i < n && !heartReached; ++i) - heartReached = heart[i] && fromFirst[i] >= 0; - if (!heartReached) - return "The heart of the commons cannot be reached on foot."; +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (!L.failure.empty()) + return "The city design could not be rebuilt: " + L.failure; + const Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (map.getW() != t.w || map.getH() != t.h) + return "The city design does not match the map size."; + const auto where = [&](int i) + { return "(" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")"; }; + const auto walkable = [&](int i) + { + const int x = i % t.w, y = i / t.w; + return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + }; + const std::vector line = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + { + if (line[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "The stone at " + where(i) + " is missing."; + if (L.road[i] && !walkable(i)) + return "The causeway at " + where(i) + " is blocked."; + if (L.ford[i] && !walkable(i)) + return "The ford at " + where(i) + " is blocked."; + } + std::vector> workers(std::max(teams, 1)); + for (int i = 0; i < n; ++i) + { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(i); + } + if (teams < 1 || workers[0].empty()) + return "Colony 0 has no workers to walk the map."; + std::vector open(n, 0), source(n, 0); + for (int i = 0; i < n; ++i) + open[i] = walkable(i); + for (int i : workers[0]) + source[i] = 1; + const std::vector fromFirst = stepsFrom(t, source, open); + for (int team = 1; team < teams; ++team) + { + bool arrived = false; + for (int i : workers[team]) + arrived = arrived || fromFirst[i] >= 0; + if (!arrived) + return "Colony " + std::to_string(team) + " cannot walk to colony 0."; + } + const std::vector heart = heartTiles(map, L); + bool heartReached = false; + for (int i = 0; i < n && !heartReached; ++i) + heartReached = heart[i] && fromFirst[i] >= 0; + if (!heartReached) + return "The heart of the commons cannot be reached on foot."; - // Shut every causeway road: with the walls up, nothing landing from the sea may get in. - if (L.g.walls) { - const std::vector margin = seaMargin(map, L); - std::vector beach(n, 0), inland(n, 0); - for (int i = 0; i < n; ++i) { - inland[i] = walkable(i) && !L.road[i]; - beach[i] = inland[i] && margin[i]; - } - const std::vector landed = stepsFrom(t, beach, inland); - for (int team = 0; team < teams; ++team) - for (int i : workers[team]) - if (landed[i] >= 0) - return "Colony " + std::to_string(team) + "'s home can be entered from the sea."; - } - // Shut every causeway: no home may reach the commons or another home any other way. - std::vector shut(n, 0); - for (int i = 0; i < n; ++i) - shut[i] = walkable(i) && !L.causeway[i]; - int shortest = INT_MAX, longest = 0; - for (int team = 0; team < teams; ++team) { - std::vector from(n, 0); - for (int i : workers[team]) - from[i] = 1; - const std::vector inside = stepsFrom(t, from, shut); - for (int i = 0; i < n; ++i) - if (inside[i] >= 0 && (L.region[i] == Commons || (L.homeOf[i] >= 0 && L.homeOf[i] != team))) - return "Colony " + std::to_string(team) + " can leave its home without its causeway, at " + - where(i) + "."; - // And every colony's walk to its landing on the commons, over its own causeway. - const std::vector steps = stepsFrom(t, from, open); - const int landing = steps[L.homes[team].landing]; - if (landing < 0) - return "Colony " + std::to_string(team) + " cannot reach its causeway landing."; - shortest = std::min(shortest, landing); - longest = std::max(longest, landing); - } - if (longest - shortest > std::max(kLandingSpread, longest / 3)) - return "The colonies' walks to their landings differ by " + std::to_string(longest - shortest) + - " steps."; - return ""; + // Shut every causeway road: with the walls up, nothing landing from the sea may get in. + if (L.g.walls) + { + const std::vector margin = seaMargin(map, L); + std::vector beach(n, 0), inland(n, 0); + for (int i = 0; i < n; ++i) + { + inland[i] = walkable(i) && !L.road[i]; + beach[i] = inland[i] && margin[i]; + } + const std::vector landed = stepsFrom(t, beach, inland); + for (int team = 0; team < teams; ++team) + for (int i : workers[team]) + if (landed[i] >= 0) + return "Colony " + std::to_string(team) + + "'s home can be entered from the sea."; + } + // Shut every causeway: no home may reach the commons or another home any other way. + std::vector shut(n, 0); + for (int i = 0; i < n; ++i) + shut[i] = walkable(i) && !L.causeway[i]; + int shortest = INT_MAX, longest = 0; + for (int team = 0; team < teams; ++team) + { + std::vector from(n, 0); + for (int i : workers[team]) + from[i] = 1; + const std::vector inside = stepsFrom(t, from, shut); + for (int i = 0; i < n; ++i) + if (inside[i] >= 0 && + (L.region[i] == Commons || (L.homeOf[i] >= 0 && L.homeOf[i] != team))) + return "Colony " + std::to_string(team) + + " can leave its home without its causeway, at " + where(i) + "."; + // And every colony's walk to its landing on the commons, over its own causeway. + const std::vector steps = stepsFrom(t, from, open); + const int landing = steps[L.homes[team].landing]; + if (landing < 0) + return "Colony " + std::to_string(team) + " cannot reach its causeway landing."; + shortest = std::min(shortest, landing); + longest = std::max(longest, landing); + } + if (longest - shortest > std::max(kLandingSpread, longest / 3)) + return "The colonies' walks to their landings differ by " + + std::to_string(longest - shortest) + " steps."; + return ""; } } // namespace CityStatesOptions::CityStatesOptions(const GenerationRequest &r) - : commonsSize(r.option("commons-size")), straitWidth(r.option("strait-width")), - causewayWidth(r.option("causeway-width")), coastRoughness(r.option("coast-roughness")), - valleys(r.option("valleys")), resourceIslands(r.option("resource-islands")), - sand(r.option("sand")), - frontier(r.option("frontier-richness")), stoneWalls(r.option("stone-walls") != 0), - wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), - stone(r.option("stone-amount")), algae(r.option("algae-amount")), - fruit(r.option("fruit-amount")) {} + : commonsSize(r.option("commons-size")), straitWidth(r.option("strait-width")), + causewayWidth(r.option("causeway-width")), coastRoughness(r.option("coast-roughness")), + valleys(r.option("valleys")), resourceIslands(r.option("resource-islands")), + sand(r.option("sand")), frontier(r.option("frontier-richness")), + stoneWalls(r.option("stone-walls") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} -GeneratorDefinition cityStatesDefinition() { - return {"city-states", - 17, - "City states", - 4, - false, - // The commons' radius as a share of half the shorter side, the strait's width as a - // share of the shorter side, the causeway's road in tiles; valleys per 128x128 of - // commons; resource islands per 128x128 of sea. - {{"commons-size", "Commons size", 30, 65, 5, 55, ControlGroup::Terrain}, - {"strait-width", "Strait width", 3, 8, 1, 4, ControlGroup::Terrain}, - {"causeway-width", "Causeway width", 5, 11, 2, 7, ControlGroup::Layout}, - // Bays and headlands on every coast and the bow in the channels. - {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, - {"valleys", "Valleys", 0, 8, 1, 3, ControlGroup::Terrain}, - // Patches of sand over the homes and the commons, per 64x128 tiles of land. - {"sand", "Sand patches", 0, 8, 1, 3, ControlGroup::Terrain}, - {"resource-islands", "Resource islands", 0, 6, 1, 2, ControlGroup::Resources}, - // How much richer the commons' heart is than its shores. - {"frontier-richness", "Frontier richness", 0, 100, 10, 60, ControlGroup::Resources}, - // Off, no stone: the causeways are plain roads and the homes' coasts are open. - GeneratorControl::toggle("stone-walls", "Stone walls", true, ControlGroup::Layout), - // Every home's ambient fields, outcrops and grove, the commons and the islands' prizes; - // every home's kit and the causeways' stone stay as they are. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - validateRequest, - validateWorld}; +GeneratorDefinition cityStatesDefinition() +{ + return { + "city-states", + 17, + "City states", + 4, + false, + // The commons' radius as a share of half the shorter side, the strait's width as a + // share of the shorter side, the causeway's road in tiles; valleys per 128x128 of + // commons; resource islands per 128x128 of sea. + {{"commons-size", "Commons size", 30, 65, 5, 55, ControlGroup::Terrain}, + {"strait-width", "Strait width", 3, 8, 1, 4, ControlGroup::Terrain}, + {"causeway-width", "Causeway width", 5, 11, 2, 7, ControlGroup::Layout}, + // Bays and headlands on every coast and the bow in the channels. + {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, + {"valleys", "Valleys", 0, 8, 1, 3, ControlGroup::Terrain}, + // Patches of sand over the homes and the commons, per 64x128 tiles of land. + {"sand", "Sand patches", 0, 8, 1, 3, ControlGroup::Terrain}, + {"resource-islands", "Resource islands", 0, 6, 1, 2, ControlGroup::Resources}, + // How much richer the commons' heart is than its shores. + {"frontier-richness", "Frontier richness", 0, 100, 10, 60, ControlGroup::Resources}, + // Off, no stone: the causeways are plain roads and the homes' coasts are open. + GeneratorControl::toggle("stone-walls", "Stone walls", true, ControlGroup::Layout), + // Every home's ambient fields, outcrops and grove, the commons and the islands' prizes; + // every home's kit and the causeways' stone stay as they are. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; } diff --git a/src/map/generator/generators/CityStatesGenerator.h b/src/map/generator/generators/CityStatesGenerator.h index 5dfe869ed..399665b1b 100644 --- a/src/map/generator/generators/CityStatesGenerator.h +++ b/src/map/generator/generators/CityStatesGenerator.h @@ -2,11 +2,12 @@ #pragma once #include "GenerationRequest.h" #include "GeneratorDefinition.h" -struct CityStatesOptions { - int commonsSize, straitWidth, causewayWidth, coastRoughness, valleys, resourceIslands, sand, - frontier; - bool stoneWalls; - int wheat, wood, stone, algae, fruit; // percentages of the default amounts - explicit CityStatesOptions(const GenerationRequest &r); +struct CityStatesOptions +{ + int commonsSize, straitWidth, causewayWidth, coastRoughness, valleys, resourceIslands, sand, + frontier; + bool stoneWalls; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit CityStatesOptions(const GenerationRequest &r); }; GeneratorDefinition cityStatesDefinition(); diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.cpp b/src/map/generator/generators/ConcreteIslandsGenerator.cpp index 95d23dcd2..7f04e729c 100644 --- a/src/map/generator/generators/ConcreteIslandsGenerator.cpp +++ b/src/map/generator/generators/ConcreteIslandsGenerator.cpp @@ -198,21 +198,22 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition concreteIslandsDefinition() { - return {"concrete-islands", - 5, - "Concrete islands", - 1, - false, - {{"channel-width", "Channel width", 5, 8, 1, 5, ControlGroup::Terrain, false}, - {"extra-islands", "Extra islands", 0, 6, 1, 3, ControlGroup::Terrain, false}, - // Off, islands meet their channels without a band of sand. - GeneratorControl::toggle("sandy-beaches", "Sandy beaches", true, ControlGroup::Terrain), - // Wheat and wood scale each colony's fields and the neutral islands' wheat; stone - // each colony's deposits; algae the channels'; fruit the neutral islands'. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate}; + return { + "concrete-islands", + 5, + "Concrete islands", + 1, + false, + {{"channel-width", "Channel width", 5, 8, 1, 5, ControlGroup::Terrain, false}, + {"extra-islands", "Extra islands", 0, 6, 1, 3, ControlGroup::Terrain, false}, + // Off, islands meet their channels without a band of sand. + GeneratorControl::toggle("sandy-beaches", "Sandy beaches", true, ControlGroup::Terrain), + // Wheat and wood scale each colony's fields and the neutral islands' wheat; stone + // each colony's deposits; algae the channels'; fruit the neutral islands'. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate}; } diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp index f67fecadb..7c9fcfdce 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.cpp +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -225,7 +225,8 @@ static bool generate(Game &game, GenerationContext &context) if (options.algae < 100) seeded = seeded && int(context.bounded("commons-algae", 100)) < options.algae; else if (options.algae > 100) - seeded = seeded || int(context.bounded("commons-algae", 300)) < options.algae - 100; + seeded = + seeded || int(context.bounded("commons-algae", 300)) < options.algae - 100; if (seeded) game.map.setResource(x, y, ALGA, 1); } @@ -464,7 +465,8 @@ GeneratorDefinition contestedCommonsDefinition() // Off, no bridge crosses the commons' moat. GeneratorControl::toggle("moat-bridges", "Moat bridges", true, ControlGroup::Layout), // Off, the home islands and the commons are smooth rounds. - GeneratorControl::toggle("jagged-coasts", "Jagged coastlines", true, ControlGroup::Terrain), + GeneratorControl::toggle("jagged-coasts", "Jagged coastlines", true, + ControlGroup::Terrain), // How many of the commons' zones are wheat, wood and fruit, the size of its quarry and // the moat's algae. Every home island's own starter fields stay as they are. GeneratorControl::percentage("wheat-amount", "Wheat amount"), diff --git a/src/map/generator/generators/CraterLakesGenerator.cpp b/src/map/generator/generators/CraterLakesGenerator.cpp index 9aa9b15b0..74e33d1e8 100644 --- a/src/map/generator/generators/CraterLakesGenerator.cpp +++ b/src/map/generator/generators/CraterLakesGenerator.cpp @@ -9,9 +9,8 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const CraterLakesOptions options(context.request); - HeightFieldOptions terrain{ - options.water, options.sand, options.grass, options.desert, - options.smoothing, options.fruit, options.repeat, false}; + HeightFieldOptions terrain{options.water, options.sand, options.grass, options.desert, + options.smoothing, options.fruit, options.repeat, false}; readResourceControls(terrain, context.request); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 30a00702d..31ae99043 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -14,132 +14,139 @@ #include #include using namespace MapGeneration; -namespace { +namespace +{ constexpr double pi = 3.14159265358979323846; -double randomAngle(GenerationContext &context) { - return context.bounded("layout", 3600) / 3600.0 * 2 * pi; +double randomAngle(GenerationContext &context) +{ + return context.bounded("layout", 3600) / 3600.0 * 2 * pi; } -void createJaggedIsland(Map &map, GenerationContext &context, - std::vector &grid, int area, int x, int y, - int radius, double roughness) { - RadialShape shape(radius, roughness, context, "outliers", 1.4); - stampShape(grid, map.getW(), map.getH(), area, - ShapeTransform({double(x), double(y)}, 0), shape, true); +void createJaggedIsland(Map &map, GenerationContext &context, std::vector &grid, int area, + int x, int y, int radius, double roughness) +{ + RadialShape shape(radius, roughness, context, "outliers", 1.4); + stampShape(grid, map.getW(), map.getH(), area, ShapeTransform({double(x), double(y)}, 0), shape, + true); } bool placeBankClump(Map &map, GenerationContext &context, - const std::vector ¢erline, - double progress, int side, int resourceType) { - if (centerline.size() < 3) - return false; - const int target = int(progress * (centerline.size() - 1)); - for (int offset = 0; offset < int(centerline.size()); ++offset) { - const int signedOffset = offset % 2 ? -(offset + 1) / 2 : offset / 2; - const int i = std::max( - 1, std::min(int(centerline.size()) - 2, target + signedOffset)); - int tx = centerline[i + 1].x - centerline[i - 1].x; - int ty = centerline[i + 1].y - centerline[i - 1].y; - if (tx > map.getW() / 2) - tx -= map.getW(); - else if (tx < -map.getW() / 2) - tx += map.getW(); - if (ty > map.getH() / 2) - ty -= map.getH(); - else if (ty < -map.getH() / 2) - ty += map.getH(); - const double length = std::sqrt(double(tx * tx + ty * ty)); - if (length < 0.5) - continue; - const double nx = -ty / length * side, ny = tx / length * side; - for (int distance = 2; distance <= 16; ++distance) { - MapGeneratorPoint anchor( - map.normalizeX(centerline[i].x + int(std::lround(nx * distance))), - map.normalizeY(centerline[i].y + int(std::lround(ny * distance)))); - if (map.isResourceAllowed(anchor.x, anchor.y, resourceType) && - placeResourceClump(map, context, anchor, resourceType, 2) >= 3) - return true; - } - } - return false; + const std::vector ¢erline, double progress, int side, + int resourceType) +{ + if (centerline.size() < 3) + return false; + const int target = int(progress * (centerline.size() - 1)); + for (int offset = 0; offset < int(centerline.size()); ++offset) + { + const int signedOffset = offset % 2 ? -(offset + 1) / 2 : offset / 2; + const int i = std::max(1, std::min(int(centerline.size()) - 2, target + signedOffset)); + int tx = centerline[i + 1].x - centerline[i - 1].x; + int ty = centerline[i + 1].y - centerline[i - 1].y; + if (tx > map.getW() / 2) + tx -= map.getW(); + else if (tx < -map.getW() / 2) + tx += map.getW(); + if (ty > map.getH() / 2) + ty -= map.getH(); + else if (ty < -map.getH() / 2) + ty += map.getH(); + const double length = std::sqrt(double(tx * tx + ty * ty)); + if (length < 0.5) + continue; + const double nx = -ty / length * side, ny = tx / length * side; + for (int distance = 2; distance <= 16; ++distance) + { + MapGeneratorPoint anchor( + map.normalizeX(centerline[i].x + int(std::lround(nx * distance))), + map.normalizeY(centerline[i].y + int(std::lround(ny * distance)))); + if (map.isResourceAllowed(anchor.x, anchor.y, resourceType) && + placeResourceClump(map, context, anchor, resourceType, 2) >= 3) + return true; + } + } + return false; } // Shared read-only geometry computed once at the start of generation and threaded through // every step below: the continent's shape, its lake and fjord targets, and where along the // rim each team is aimed. -struct FjordLayout { - int W, H, nbTeams; - ShapeTransform xf; - RadialShape coast; - double coreR, lakeR, fjordInnerR, maxStretch; - bool hasLake, lakeConnected; - std::vector teamTheta; +struct FjordLayout +{ + int W, H, nbTeams; + ShapeTransform xf; + RadialShape coast; + double coreR, lakeR, fjordInnerR, maxStretch; + bool hasLake, lakeConnected; + std::vector teamTheta; }; FjordLayout computeLayout(Game &game, GenerationContext &context, - const FjordContinentOptions &options, int nbTeams) { - const int W = game.map.getW(); - const int H = game.map.getH(); - - const double rotation = randomAngle(context) * 0.5; - const double elongation = - 1.0 + context.bounded("layout", 1000) / 1000.0 * 0.3; - ShapeTransform xf({W / 2.0, H / 2.0}, rotation, elongation); - - const double baseR = options.continentSize / 100.0 * std::min(W, H); - RadialShape coast(baseR, options.roughness / 100.0, context, "coast", 1.4); - // Widened from 0.19 to make room for the central lake and its resource ring below without - // starving the fjords of length - coreR is still where every fjord stops short, just a little - // further out than before. - const double coreR = baseR * 0.23; - // lake-size is a percentage of coreR; 0 disables the lake entirely. lake-connected decides - // whether the fjords actually cut through to it (every peninsula then water-isolated from its - // neighbors, boats required) or stop short of it behind a solid land ring (every peninsula - // stays mutually land-connected, verified explicitly in verifyConnectivity rather than - // assumed) - both are legitimate map styles, so both stay available rather than picking one - // permanently. - const double lakeR = coreR * (options.lakeSize / 100.0); - const bool hasLake = lakeR > 0.5; - const bool lakeConnected = hasLake && options.lakeConnected != 0; - // Where every fjord actually stops: coreR in disconnected mode. In connected mode, well inside - // the lake (0.4x its radius) rather than exactly on its edge, so the centerline crosses the - // lake's real boundary with margin to spare - the extra length beyond the boundary just carves - // more water inside ground that's already the lake, which is harmless. - const double fjordInnerR = lakeConnected ? lakeR * 0.4 : coreR; - const double maxStretch = std::max(elongation, 1.0 / elongation); - - // One angular sector per team, evenly spaced with a little jitter so it - // doesn't look mechanical. Deciding sectors up front (rather than deriving - // neighbor order from wherever seed points happen to land) is what guarantees - // every team gets pushed out to its own tip instead of sometimes landing near - // the shared middle. - std::vector teamTheta(nbTeams); - { - double baseRotation = randomAngle(context); - double slot = 2 * pi / nbTeams; - for (int i = 0; i < nbTeams; ++i) { - double jitter = - ((context.bounded("layout", 2001)) / 1000.0 - 1.0) * 0.2 * slot; - teamTheta[i] = baseRotation + i * slot + jitter; - } - } - - return FjordLayout{W, H, nbTeams, xf, coast, - coreR, lakeR, fjordInnerR, maxStretch, - hasLake, lakeConnected, teamTheta}; + const FjordContinentOptions &options, int nbTeams) +{ + const int W = game.map.getW(); + const int H = game.map.getH(); + + const double rotation = randomAngle(context) * 0.5; + const double elongation = 1.0 + context.bounded("layout", 1000) / 1000.0 * 0.3; + ShapeTransform xf({W / 2.0, H / 2.0}, rotation, elongation); + + const double baseR = options.continentSize / 100.0 * std::min(W, H); + RadialShape coast(baseR, options.roughness / 100.0, context, "coast", 1.4); + // Widened from 0.19 to make room for the central lake and its resource ring below without + // starving the fjords of length - coreR is still where every fjord stops short, just a little + // further out than before. + const double coreR = baseR * 0.23; + // lake-size is a percentage of coreR; 0 disables the lake entirely. lake-connected decides + // whether the fjords actually cut through to it (every peninsula then water-isolated from its + // neighbors, boats required) or stop short of it behind a solid land ring (every peninsula + // stays mutually land-connected, verified explicitly in verifyConnectivity rather than + // assumed) - both are legitimate map styles, so both stay available rather than picking one + // permanently. + const double lakeR = coreR * (options.lakeSize / 100.0); + const bool hasLake = lakeR > 0.5; + const bool lakeConnected = hasLake && options.lakeConnected != 0; + // Where every fjord actually stops: coreR in disconnected mode. In connected mode, well inside + // the lake (0.4x its radius) rather than exactly on its edge, so the centerline crosses the + // lake's real boundary with margin to spare - the extra length beyond the boundary just carves + // more water inside ground that's already the lake, which is harmless. + const double fjordInnerR = lakeConnected ? lakeR * 0.4 : coreR; + const double maxStretch = std::max(elongation, 1.0 / elongation); + + // One angular sector per team, evenly spaced with a little jitter so it + // doesn't look mechanical. Deciding sectors up front (rather than deriving + // neighbor order from wherever seed points happen to land) is what guarantees + // every team gets pushed out to its own tip instead of sometimes landing near + // the shared middle. + std::vector teamTheta(nbTeams); + { + double baseRotation = randomAngle(context); + double slot = 2 * pi / nbTeams; + for (int i = 0; i < nbTeams; ++i) + { + double jitter = ((context.bounded("layout", 2001)) / 1000.0 - 1.0) * 0.2 * slot; + teamTheta[i] = baseRotation + i * slot + jitter; + } + } + + return FjordLayout{W, H, nbTeams, xf, coast, coreR, + lakeR, fjordInnerR, maxStretch, hasLake, lakeConnected, teamTheta}; } // 1) Stamp the continent. -void stampContinent(Game &game, const FjordLayout &layout) { - for (int y = 0; y < layout.H; ++y) { - for (int x = 0; x < layout.W; ++x) { - const auto shaped = layout.xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - double theta = atan2(v, u); - double r = sqrt(u * u + v * v); - if (r < layout.coast.radiusAt(theta)) - game.map.setUMatPos(x, y, GRASS, 1); - } - } +void stampContinent(Game &game, const FjordLayout &layout) +{ + for (int y = 0; y < layout.H; ++y) + { + for (int x = 0; x < layout.W; ++x) + { + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + double theta = atan2(v, u); + double r = sqrt(u * u + v * v); + if (r < layout.coast.radiusAt(theta)) + game.map.setUMatPos(x, y, GRASS, 1); + } + } } // 2) Carve a fjord between every pair of angularly-neighboring teams: a @@ -149,74 +156,78 @@ void stampContinent(Game &game, const FjordLayout &layout) { // peninsula "finger" hangs off. Lateral wiggle is enveloped to zero at both // ends so the mouth stays aligned with the sector boundary and the tip stays // on the radial line. -std::vector> -carveFjords(Game &game, GenerationContext &context, const FjordLayout &layout, - const FjordContinentOptions &options) { - std::vector> fjordCenterlines(layout.nbTeams); - if (layout.nbTeams < 2) - return fjordCenterlines; - for (int k = 0; k < layout.nbTeams; ++k) { - int i = k; - int j = (k + 1) % layout.nbTeams; - double ti = layout.teamTheta[i]; - double tj = layout.teamTheta[j]; - double d = fmod(tj - ti, 2 * pi); - if (d < 0) - d += 2 * pi; - double midTheta = ti + d / 2.0; - - double mouthR = layout.coast.radiusAt(midTheta) + 3.0; - double mouthU = mouthR * cos(midTheta), mouthV = mouthR * sin(midTheta); - double tipU = layout.fjordInnerR * cos(midTheta), - tipV = layout.fjordInnerR * sin(midTheta); - double perpU = -sin(midTheta), perpV = cos(midTheta); - - double gap = std::min(d, 2 * pi - d); - double amplitude = gap * mouthR * 0.17; - double phase = randomAngle(context); - double mouthWidth = - options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; - // Map::controlSand (MapTerrain.cpp) converts any water tile with a grass neighbor in its - // own 3x3 neighborhood to sand - not just a coastal decoration, it can erase a channel - // outright. The disconnected-mode tip (0.65, ~1.3 tiles across) is entirely coastal by that - // rule and gets sanded over - an intentional dead end there, but it would silently close - // the one connection lake-connected mode needs open. A channel needs a surviving center row - // not touching grass on either side to stay water at all, which takes a radius of at least - // ~1.5; this targets comfortably above that floor. - double tipWidth = layout.lakeConnected ? std::max(2.5, mouthWidth * 0.6) : 0.65; - - int steps = std::max(24, (int)(mouthR - layout.fjordInnerR)); - for (int s = 0; s <= steps; ++s) { - double t = double(s) / steps; - double baseU = mouthU + t * (tipU - mouthU); - double baseV = mouthV + t * (tipV - mouthV); - double envelope = sin(pi * t); - double lateral = amplitude * sin(2 * pi * t + phase) * envelope; - double u = baseU + lateral * perpU; - double v = baseV + lateral * perpV; - double width = mouthWidth * (1 - t) + tipWidth * t; - - const auto mapped = layout.xf.toMap({u, v}); - double mx = mapped.x, my = mapped.y; - int rad = (int)ceil(width * layout.maxStretch) + 1; - int ix = (int)lround(mx), iy = (int)lround(my); - fjordCenterlines[k].push_back(MapGeneratorPoint( - game.map.normalizeX(ix), game.map.normalizeY(iy))); - for (int dy = -rad; dy <= rad; ++dy) { - for (int dx = -rad; dx <= rad; ++dx) { - int nx = game.map.normalizeX(ix + dx); - int ny = game.map.normalizeY(iy + dy); - const auto shaped = layout.xf.toShape({double(nx), double(ny)}); - double u2 = shaped.x, v2 = shaped.y; - double dd = (u2 - u) * (u2 - u) + (v2 - v) * (v2 - v); - if (dd <= width * width) { - game.map.setUMatPos(nx, ny, WATER, 1); - } - } - } - } - } - return fjordCenterlines; +std::vector> carveFjords(Game &game, GenerationContext &context, + const FjordLayout &layout, + const FjordContinentOptions &options) +{ + std::vector> fjordCenterlines(layout.nbTeams); + if (layout.nbTeams < 2) + return fjordCenterlines; + for (int k = 0; k < layout.nbTeams; ++k) + { + int i = k; + int j = (k + 1) % layout.nbTeams; + double ti = layout.teamTheta[i]; + double tj = layout.teamTheta[j]; + double d = fmod(tj - ti, 2 * pi); + if (d < 0) + d += 2 * pi; + double midTheta = ti + d / 2.0; + + double mouthR = layout.coast.radiusAt(midTheta) + 3.0; + double mouthU = mouthR * cos(midTheta), mouthV = mouthR * sin(midTheta); + double tipU = layout.fjordInnerR * cos(midTheta), tipV = layout.fjordInnerR * sin(midTheta); + double perpU = -sin(midTheta), perpV = cos(midTheta); + + double gap = std::min(d, 2 * pi - d); + double amplitude = gap * mouthR * 0.17; + double phase = randomAngle(context); + double mouthWidth = options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; + // Map::controlSand (MapTerrain.cpp) converts any water tile with a grass neighbor in its + // own 3x3 neighborhood to sand - not just a coastal decoration, it can erase a channel + // outright. The disconnected-mode tip (0.65, ~1.3 tiles across) is entirely coastal by that + // rule and gets sanded over - an intentional dead end there, but it would silently close + // the one connection lake-connected mode needs open. A channel needs a surviving center row + // not touching grass on either side to stay water at all, which takes a radius of at least + // ~1.5; this targets comfortably above that floor. + double tipWidth = layout.lakeConnected ? std::max(2.5, mouthWidth * 0.6) : 0.65; + + int steps = std::max(24, (int)(mouthR - layout.fjordInnerR)); + for (int s = 0; s <= steps; ++s) + { + double t = double(s) / steps; + double baseU = mouthU + t * (tipU - mouthU); + double baseV = mouthV + t * (tipV - mouthV); + double envelope = sin(pi * t); + double lateral = amplitude * sin(2 * pi * t + phase) * envelope; + double u = baseU + lateral * perpU; + double v = baseV + lateral * perpV; + double width = mouthWidth * (1 - t) + tipWidth * t; + + const auto mapped = layout.xf.toMap({u, v}); + double mx = mapped.x, my = mapped.y; + int rad = (int)ceil(width * layout.maxStretch) + 1; + int ix = (int)lround(mx), iy = (int)lround(my); + fjordCenterlines[k].push_back( + MapGeneratorPoint(game.map.normalizeX(ix), game.map.normalizeY(iy))); + for (int dy = -rad; dy <= rad; ++dy) + { + for (int dx = -rad; dx <= rad; ++dx) + { + int nx = game.map.normalizeX(ix + dx); + int ny = game.map.normalizeY(iy + dy); + const auto shaped = layout.xf.toShape({double(nx), double(ny)}); + double u2 = shaped.x, v2 = shaped.y; + double dd = (u2 - u) * (u2 - u) + (v2 - v) * (v2 - v); + if (dd <= width * width) + { + game.map.setUMatPos(nx, ny, WATER, 1); + } + } + } + } + } + return fjordCenterlines; } // 3) A lake at the very center every fjord points toward (lake-size 0 skips this entirely). @@ -226,125 +237,131 @@ carveFjords(Game &game, GenerationContext &context, const FjordLayout &layout, // well inside lakeR (fjordInnerR, computed in computeLayout), so this carve is what actually // opens each fjord into the lake - the fjords' own tapered tips would otherwise be too narrow // to reliably merge with it. -void carveLake(Game &game, const FjordLayout &layout, bool sandyShore) { - if (!layout.hasLake) - return; - for (int y = 0; y < layout.H; ++y) { - for (int x = 0; x < layout.W; ++x) { - const auto shaped = layout.xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= layout.lakeR * layout.lakeR) - game.map.setUMatPos(x, y, WATER, 1); - } - } - - // A sandy no-man's-land ring just outside the lake - wider than controlSand's own thin - // coastal fringe would give it, so the open ground around the lake reads as a deliberate - // contested space rather than an ordinary beach. Only touches tiles the lake/fjord carving - // above left as land, so it never overwrites water. Without it, the lake has an ordinary - // beach and grass to build and farm on right up to it. - if (!sandyShore) - return; - const double sandOuterR = layout.lakeR + layout.lakeR * 0.5; - for (int y = 0; y < layout.H; ++y) { - for (int x = 0; x < layout.W; ++x) { - if (game.map.isWater(x, y)) - continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= sandOuterR * sandOuterR) - game.map.setUMatPos(x, y, SAND, 1); - } - } +void carveLake(Game &game, const FjordLayout &layout, bool sandyShore) +{ + if (!layout.hasLake) + return; + for (int y = 0; y < layout.H; ++y) + { + for (int x = 0; x < layout.W; ++x) + { + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= layout.lakeR * layout.lakeR) + game.map.setUMatPos(x, y, WATER, 1); + } + } + + // A sandy no-man's-land ring just outside the lake - wider than controlSand's own thin + // coastal fringe would give it, so the open ground around the lake reads as a deliberate + // contested space rather than an ordinary beach. Only touches tiles the lake/fjord carving + // above left as land, so it never overwrites water. Without it, the lake has an ordinary + // beach and grass to build and farm on right up to it. + if (!sandyShore) + return; + const double sandOuterR = layout.lakeR + layout.lakeR * 0.5; + for (int y = 0; y < layout.H; ++y) + { + for (int x = 0; x < layout.W; ++x) + { + if (game.map.isWater(x, y)) + continue; + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= sandOuterR * sandOuterR) + game.map.setUMatPos(x, y, SAND, 1); + } + } } // 4) A couple of small, unconnected resource islands out in the open sea -- // purely a bonus for whoever explores, never touching the mainland or each // other. void placeOutlierIslands(Game &game, GenerationContext &context, const FjordLayout &layout, - const FjordContinentOptions &options, std::vector &grid, - int &areaNumber) { - // A single global "mainland reach" bound has to stay safe in whichever single direction - // roughness and elongation happen to push the coastline furthest, and that alone can exceed - // half the map (continent-size up to 40%, roughness up to 35%, elongation up to 1.3x) on a - // majority of rolls regardless of map size - a global bound leaves islands unplaced almost - // always, not just at small sizes. Each island's own center is independently randomized, - // though, so it only needs to be safe in the coastline's direction at its own location, not - // everywhere. Checking each candidate directly against coast.radiusAt() there (the same - // per-point technique placeOpenSeaAlgae's placement already uses for its own shoreline check, - // via the same xf.toShape transform) replaces the pessimistic global bound with an exact - // local one, so an island can land close to a narrow stretch of coast even while the - // coastline bulges out far away in some other direction. - double halfMapMargin = std::min(layout.W, layout.H) / 2.0 - 6.0; - // No cap beyond the control's own range here - resource-islands now goes up to 20 for - // players who want an island-heavy map, and each one is still an independent best-effort - // placement (a request that can't all fit in the margin just places as many as do). - int outlierCount = options.resourceIslands + int(context.bounded("layout", 2)); - std::vector outlierCenters; - std::vector outlierRadii; - for (int oi = 0; oi < outlierCount; ++oi) { - int islandRadius = 5 + context.bounded("layout", 4); - bool placed = false; - for (int attempt = 0; attempt < 60 && !placed; ++attempt) { - double theta = randomAngle(context); - double r = (context.bounded("layout", 1000)) / 1000.0 * halfMapMargin; - int cx = game.map.normalizeX((int)lround(layout.W / 2.0 + r * cos(theta))); - int cy = game.map.normalizeY((int)lround(layout.H / 2.0 + r * sin(theta))); - - const auto shaped = layout.xf.toShape({double(cx), double(cy)}); - const double shapeR = std::hypot(shaped.x, shaped.y); - const double shapeTheta = atan2(shaped.y, shaped.x); - if (shapeR < layout.coast.radiusAt(shapeTheta) + islandRadius + 8.0) - continue; // too close to (or inside) the mainland at this specific point - - bool clear = true; - for (unsigned int oc = 0; oc < outlierCenters.size() && clear; ++oc) { - int minSep = islandRadius + outlierRadii[oc] + 8; - if (game.map.warpDistSquare(cx, cy, outlierCenters[oc].x, - outlierCenters[oc].y) < minSep * minSep) - clear = false; - } - if (!clear) - continue; - - int outlierArea = areaNumber++; - createJaggedIsland(game.map, context, grid, outlierArea, cx, cy, - islandRadius, 0.3); - std::vector islandPts; - getAllPoints(game.map, grid, outlierArea, islandPts); - if (islandPts.empty()) - continue; - for (unsigned int p = 0; p < islandPts.size(); ++p) - game.map.setUMatPos(islandPts[p].x, islandPts[p].y, GRASS, 1); - - // Each island leans on one resource theme, so finding one feels like a - // distinct little prize rather than an interchangeable resource dump. - switch (context.bounded("layout", 3)) { - case 0: - placeResourceClump( - game.map, context, - islandPts[context.bounded("resources", islandPts.size())], STONE, - 2); - break; - case 1: - placeResourceClump( - game.map, context, - islandPts[context.bounded("resources", islandPts.size())], - CHERRY + context.bounded("resources", 3), 2); - break; - default: - placeResourceClump( - game.map, context, - islandPts[context.bounded("resources", islandPts.size())], CORN, - 3); - break; - } - - outlierCenters.push_back(MapGeneratorPoint(cx, cy)); - outlierRadii.push_back(islandRadius); - placed = true; - } - } + const FjordContinentOptions &options, std::vector &grid, + int &areaNumber) +{ + // A single global "mainland reach" bound has to stay safe in whichever single direction + // roughness and elongation happen to push the coastline furthest, and that alone can exceed + // half the map (continent-size up to 40%, roughness up to 35%, elongation up to 1.3x) on a + // majority of rolls regardless of map size - a global bound leaves islands unplaced almost + // always, not just at small sizes. Each island's own center is independently randomized, + // though, so it only needs to be safe in the coastline's direction at its own location, not + // everywhere. Checking each candidate directly against coast.radiusAt() there (the same + // per-point technique placeOpenSeaAlgae's placement already uses for its own shoreline check, + // via the same xf.toShape transform) replaces the pessimistic global bound with an exact + // local one, so an island can land close to a narrow stretch of coast even while the + // coastline bulges out far away in some other direction. + double halfMapMargin = std::min(layout.W, layout.H) / 2.0 - 6.0; + // No cap beyond the control's own range here - resource-islands now goes up to 20 for + // players who want an island-heavy map, and each one is still an independent best-effort + // placement (a request that can't all fit in the margin just places as many as do). + int outlierCount = options.resourceIslands + int(context.bounded("layout", 2)); + std::vector outlierCenters; + std::vector outlierRadii; + for (int oi = 0; oi < outlierCount; ++oi) + { + int islandRadius = 5 + context.bounded("layout", 4); + bool placed = false; + for (int attempt = 0; attempt < 60 && !placed; ++attempt) + { + double theta = randomAngle(context); + double r = (context.bounded("layout", 1000)) / 1000.0 * halfMapMargin; + int cx = game.map.normalizeX((int)lround(layout.W / 2.0 + r * cos(theta))); + int cy = game.map.normalizeY((int)lround(layout.H / 2.0 + r * sin(theta))); + + const auto shaped = layout.xf.toShape({double(cx), double(cy)}); + const double shapeR = std::hypot(shaped.x, shaped.y); + const double shapeTheta = atan2(shaped.y, shaped.x); + if (shapeR < layout.coast.radiusAt(shapeTheta) + islandRadius + 8.0) + continue; // too close to (or inside) the mainland at this specific point + + bool clear = true; + for (unsigned int oc = 0; oc < outlierCenters.size() && clear; ++oc) + { + int minSep = islandRadius + outlierRadii[oc] + 8; + if (game.map.warpDistSquare(cx, cy, outlierCenters[oc].x, outlierCenters[oc].y) < + minSep * minSep) + clear = false; + } + if (!clear) + continue; + + int outlierArea = areaNumber++; + createJaggedIsland(game.map, context, grid, outlierArea, cx, cy, islandRadius, 0.3); + std::vector islandPts; + getAllPoints(game.map, grid, outlierArea, islandPts); + if (islandPts.empty()) + continue; + for (unsigned int p = 0; p < islandPts.size(); ++p) + game.map.setUMatPos(islandPts[p].x, islandPts[p].y, GRASS, 1); + + // Each island leans on one resource theme, so finding one feels like a + // distinct little prize rather than an interchangeable resource dump. + switch (context.bounded("layout", 3)) + { + case 0: + placeResourceClump(game.map, context, + islandPts[context.bounded("resources", islandPts.size())], STONE, + 2); + break; + case 1: + placeResourceClump(game.map, context, + islandPts[context.bounded("resources", islandPts.size())], + CHERRY + context.bounded("resources", 3), 2); + break; + default: + placeResourceClump(game.map, context, + islandPts[context.bounded("resources", islandPts.size())], CORN, + 3); + break; + } + + outlierCenters.push_back(MapGeneratorPoint(cx, cy)); + outlierRadii.push_back(islandRadius); + placed = true; + } + } } // 5) Anchor each team out at its own tip: start just inland of the coast at @@ -352,40 +369,45 @@ void placeOutlierIslands(Game &game, GenerationContext &context, const FjordLayo // out to be water (a sharp jaggedness dip, or a fjord belly that swung wider // than expected) -- the same "compute where we want to be, then confirm the // grid agrees" shape as chooseFreeForBuildingSquares. -std::vector anchorTeams(Game &game, const FjordLayout &layout) { - std::vector teamPts; - teamPts.reserve(layout.nbTeams); - for (int i = 0; i < layout.nbTeams; ++i) { - double theta = layout.teamTheta[i]; - double maxR = layout.coast.radiusAt(theta); - double r = std::max(layout.coreR + 6.0, maxR - 8.0); - double stepIn = (maxR - (layout.coreR + 2.0)) / 30.0; - if (stepIn <= 0.0) - stepIn = 1.0; - - int fx = -1, fy = -1; - for (int tries = 0; tries < 30; ++tries) { - const auto mapped = layout.xf.toMap({r * cos(theta), r * sin(theta)}); - double mx = mapped.x, my = mapped.y; - int ix = game.map.normalizeX((int)lround(mx)); - int iy = game.map.normalizeY((int)lround(my)); - if (!game.map.isWater(ix, iy)) { - fx = ix; - fy = iy; - break; - } - r -= stepIn; - } - if (fx < 0) { - const auto mapped = layout.xf.toMap( - {(layout.coreR + 6.0) * cos(theta), (layout.coreR + 6.0) * sin(theta)}); - double mx = mapped.x, my = mapped.y; - fx = game.map.normalizeX((int)lround(mx)); - fy = game.map.normalizeY((int)lround(my)); - } - teamPts.push_back(MapGeneratorPoint(fx, fy)); - } - return teamPts; +std::vector anchorTeams(Game &game, const FjordLayout &layout) +{ + std::vector teamPts; + teamPts.reserve(layout.nbTeams); + for (int i = 0; i < layout.nbTeams; ++i) + { + double theta = layout.teamTheta[i]; + double maxR = layout.coast.radiusAt(theta); + double r = std::max(layout.coreR + 6.0, maxR - 8.0); + double stepIn = (maxR - (layout.coreR + 2.0)) / 30.0; + if (stepIn <= 0.0) + stepIn = 1.0; + + int fx = -1, fy = -1; + for (int tries = 0; tries < 30; ++tries) + { + const auto mapped = layout.xf.toMap({r * cos(theta), r * sin(theta)}); + double mx = mapped.x, my = mapped.y; + int ix = game.map.normalizeX((int)lround(mx)); + int iy = game.map.normalizeY((int)lround(my)); + if (!game.map.isWater(ix, iy)) + { + fx = ix; + fy = iy; + break; + } + r -= stepIn; + } + if (fx < 0) + { + const auto mapped = layout.xf.toMap( + {(layout.coreR + 6.0) * cos(theta), (layout.coreR + 6.0) * sin(theta)}); + double mx = mapped.x, my = mapped.y; + fx = game.map.normalizeX((int)lround(mx)); + fy = game.map.normalizeY((int)lround(my)); + } + teamPts.push_back(MapGeneratorPoint(fx, fy)); + } + return teamPts; } // 6) Connectivity: verify, don't assume. The untouched core should make this @@ -397,34 +419,39 @@ std::vector anchorTeams(Game &game, const FjordLayout &layout // verified locally further down (placeStarterKits fails outright if a team's home area comes // up empty). bool verifyConnectivity(Game &game, const FjordLayout &layout, - const std::vector &teamPts) { - if (layout.lakeConnected) - return true; - const int W = layout.W, H = layout.H; - std::vector visited(W * H, false); - std::vector stack; - stack.push_back(teamPts[0]); - visited[teamPts[0].y * W + teamPts[0].x] = true; - while (!stack.empty()) { - MapGeneratorPoint p = stack.back(); - stack.pop_back(); - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - int nx = game.map.normalizeX(p.x + dx); - int ny = game.map.normalizeY(p.y + dy); - if (!visited[ny * W + nx] && !game.map.isWater(nx, ny)) { - visited[ny * W + nx] = true; - stack.push_back(MapGeneratorPoint(nx, ny)); - } - } - } - } - for (int i = 0; i < layout.nbTeams; ++i) - if (!visited[teamPts[i].y * W + teamPts[i].x]) - return false; - return true; + const std::vector &teamPts) +{ + if (layout.lakeConnected) + return true; + const int W = layout.W, H = layout.H; + std::vector visited(W * H, false); + std::vector stack; + stack.push_back(teamPts[0]); + visited[teamPts[0].y * W + teamPts[0].x] = true; + while (!stack.empty()) + { + MapGeneratorPoint p = stack.back(); + stack.pop_back(); + for (int dy = -1; dy <= 1; ++dy) + { + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = game.map.normalizeX(p.x + dx); + int ny = game.map.normalizeY(p.y + dy); + if (!visited[ny * W + nx] && !game.map.isWater(nx, ny)) + { + visited[ny * W + nx] = true; + stack.push_back(MapGeneratorPoint(nx, ny)); + } + } + } + } + for (int i = 0; i < layout.nbTeams; ++i) + if (!visited[teamPts[i].y * W + teamPts[i].x]) + return false; + return true; } // 7) Stone and fruit in the ring around the new central lake -- the reward for pushing to the @@ -432,323 +459,335 @@ bool verifyConnectivity(Game &game, const FjordLayout &layout, // fruit type instead of a single random pick, so finding this area feels like a genuinely rich // destination and not a single repeated deposit. void placeCoreResources(Game &game, GenerationContext &context, const FjordLayout &layout, - const FjordContinentOptions &options, std::vector &grid, - int &areaNumber) { - int coreArea = areaNumber++; - std::vector corePts; - for (int y = 0; y < layout.H; ++y) { - for (int x = 0; x < layout.W; ++x) { - // Corn/stone/fruit all require grass, so the sand ring the lake just grew (when there - // is one) is deliberately excluded here rather than merely non-water - a clump center - // landing on sand could miss every grass tile within its own radius and place nothing. - if (!game.map.isGrass(x, y) || grid[y * layout.W + x] != 0) - continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= (layout.coreR + 4.0) * (layout.coreR + 4.0)) { - grid[y * layout.W + x] = coreArea; - corePts.push_back(MapGeneratorPoint(x, y)); - } - } - } - if (!corePts.empty()) { - for (int stoneClump = 0; stoneClump < scaledCount(3, options.stone); ++stoneClump) - placeResourceClump(game.map, context, - corePts[context.bounded("resources", corePts.size())], - STONE, 3); - const int fruitClumpsPerType = int(scaledCount(3, options.fruit)); - for (int fruitType = 0; fruitType < 3; ++fruitType) - for (int clump = 0; clump < fruitClumpsPerType; ++clump) - placeResourceClump(game.map, context, - corePts[context.bounded("resources", corePts.size())], - CHERRY + fruitType, 2); - } - - // The lake gets the same algae treatment the open sea gets in placeOpenSeaAlgae below, but - // candidates are drawn from well inside the shoreline (innerLakeR, not lakeR) rather than - // anywhere in the lake - a clump anchored right up against the shore is still entirely valid - // water, but reads as "stuck to one side" rather than "in the lake". The very first clump is - // placed dead center, at the shape transform's own origin - the lake's exact geometric middle - // - so there's always at least one unambiguously centered deposit regardless of how the - // interior sampling below happens to land. - if (layout.hasLake && options.algae > 0) { - const int centerX = game.map.normalizeX((int)std::lround(layout.W / 2.0)); - const int centerY = game.map.normalizeY((int)std::lround(layout.H / 2.0)); - placeResourceClump(game.map, context, MapGeneratorPoint(centerX, centerY), ALGA, 2); - - const double innerLakeR = std::max(0.0, layout.lakeR - 4.0); - std::vector lakeWater; - for (int y = 0; y < layout.H; ++y) - for (int x = 0; x < layout.W; ++x) { - if (!game.map.isWater(x, y)) - continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - if (u * u + v * v <= innerLakeR * innerLakeR) - lakeWater.push_back(MapGeneratorPoint(x, y)); - } - // The center clump above already guarantees at least one, so this is purely bonus - // coverage for a lake big enough to have real interior room left over - no forced minimum. - if (!lakeWater.empty()) - for (int i = 0; i < scaledCount(int(lakeWater.size()) / 40, options.algae); ++i) - placeResourceClump( - game.map, context, - lakeWater[context.bounded("resources", lakeWater.size())], ALGA, 2); - } + const FjordContinentOptions &options, std::vector &grid, + int &areaNumber) +{ + int coreArea = areaNumber++; + std::vector corePts; + for (int y = 0; y < layout.H; ++y) + { + for (int x = 0; x < layout.W; ++x) + { + // Corn/stone/fruit all require grass, so the sand ring the lake just grew (when there + // is one) is deliberately excluded here rather than merely non-water - a clump center + // landing on sand could miss every grass tile within its own radius and place nothing. + if (!game.map.isGrass(x, y) || grid[y * layout.W + x] != 0) + continue; + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= (layout.coreR + 4.0) * (layout.coreR + 4.0)) + { + grid[y * layout.W + x] = coreArea; + corePts.push_back(MapGeneratorPoint(x, y)); + } + } + } + if (!corePts.empty()) + { + for (int stoneClump = 0; stoneClump < scaledCount(3, options.stone); ++stoneClump) + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], STONE, 3); + const int fruitClumpsPerType = int(scaledCount(3, options.fruit)); + for (int fruitType = 0; fruitType < 3; ++fruitType) + for (int clump = 0; clump < fruitClumpsPerType; ++clump) + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], + CHERRY + fruitType, 2); + } + + // The lake gets the same algae treatment the open sea gets in placeOpenSeaAlgae below, but + // candidates are drawn from well inside the shoreline (innerLakeR, not lakeR) rather than + // anywhere in the lake - a clump anchored right up against the shore is still entirely valid + // water, but reads as "stuck to one side" rather than "in the lake". The very first clump is + // placed dead center, at the shape transform's own origin - the lake's exact geometric middle + // - so there's always at least one unambiguously centered deposit regardless of how the + // interior sampling below happens to land. + if (layout.hasLake && options.algae > 0) + { + const int centerX = game.map.normalizeX((int)std::lround(layout.W / 2.0)); + const int centerY = game.map.normalizeY((int)std::lround(layout.H / 2.0)); + placeResourceClump(game.map, context, MapGeneratorPoint(centerX, centerY), ALGA, 2); + + const double innerLakeR = std::max(0.0, layout.lakeR - 4.0); + std::vector lakeWater; + for (int y = 0; y < layout.H; ++y) + for (int x = 0; x < layout.W; ++x) + { + if (!game.map.isWater(x, y)) + continue; + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= innerLakeR * innerLakeR) + lakeWater.push_back(MapGeneratorPoint(x, y)); + } + // The center clump above already guarantees at least one, so this is purely bonus + // coverage for a lake big enough to have real interior room left over - no forced minimum. + if (!lakeWater.empty()) + for (int i = 0; i < scaledCount(int(lakeWater.size()) / 40, options.algae); ++i) + placeResourceClump(game.map, context, + lakeWater[context.bounded("resources", lakeWater.size())], ALGA, + 2); + } } // 8) Algae out in the open sea: any water tile clearly beyond the coastline // (not a fjord, not the moat-ish water right against the shore) gets an // occasional patch. void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout &layout, - int algaePercent) { - std::vector algaeWater; - for (int y = 0; y < layout.H; ++y) { - for (int x = 0; x < layout.W; ++x) { - if (!game.map.isWater(x, y)) - continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); - double u = shaped.x, v = shaped.y; - double theta = atan2(v, u); - double r = sqrt(u * u + v * v); - double shoreR = layout.coast.radiusAt(theta); - if (r > shoreR + 2.0 && r < shoreR + 14.0) - algaeWater.emplace_back(x, y); - } - } - if (!algaeWater.empty()) - for (int i = 0; i < scaledCount(std::max(1, int(algaeWater.size()) / 180), algaePercent); ++i) - placeResourceClump( - game.map, context, - algaeWater[context.bounded("resources", algaeWater.size())], ALGA, 2); + int algaePercent) +{ + std::vector algaeWater; + for (int y = 0; y < layout.H; ++y) + { + for (int x = 0; x < layout.W; ++x) + { + if (!game.map.isWater(x, y)) + continue; + const auto shaped = layout.xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + double theta = atan2(v, u); + double r = sqrt(u * u + v * v); + double shoreR = layout.coast.radiusAt(theta); + if (r > shoreR + 2.0 && r < shoreR + 14.0) + algaeWater.emplace_back(x, y); + } + } + if (!algaeWater.empty()) + for (int i = 0; i < scaledCount(std::max(1, int(algaeWater.size()) / 180), algaePercent); + ++i) + placeResourceClump(game.map, context, + algaeWater[context.bounded("resources", algaeWater.size())], ALGA, + 2); } // 9) A light per-team starter kit so nobody is stuck waiting to reach the // fjord banks before they can build anything; the banks and the core are the // map's real economy. bool placeStarterKits(Game &game, GenerationContext &context, const FjordLayout &layout, - const std::vector &teamPts) { - std::vector allWater; - for (int y = 0; y < layout.H; ++y) - for (int x = 0; x < layout.W; ++x) - if (game.map.isWater(x, y)) - allWater.push_back(MapGeneratorPoint(x, y)); - - for (int i = 0; i < layout.nbTeams; ++i) { - std::vector sources; - sources.push_back(teamPts[i]); - std::vector distances; - computeDistances(game.map, sources, allWater, distances); - - std::vector homePoints; - for (int y = 0; y < layout.H; ++y) - for (int x = 0; x < layout.W; ++x) - if (distances[y * layout.W + x] >= 1 && distances[y * layout.W + x] <= 10) - homePoints.push_back(MapGeneratorPoint(x, y)); - if (homePoints.empty()) - return false; - - placeResourceClump( - game.map, context, - homePoints[context.bounded("resources", homePoints.size())], CORN, 2); - placeResourceClump( - game.map, context, - homePoints[context.bounded("resources", homePoints.size())], WOOD, 2); - - std::vector stonePts = homePoints; - chooseRandomPoints(game.map, context, stonePts, 1); - for (unsigned int j = 0; j < stonePts.size(); ++j) - game.map.setResource(stonePts[j].x, stonePts[j].y, STONE, 1); - - std::vector home(size_t(layout.W) * layout.H, 0); - for (const auto &point : homePoints) - home[point.y * layout.W + point.x] = 1; - if (!placeSettlement(game, context, i, home, teamPts[i], "starts")) - return false; - } - return true; + const std::vector &teamPts) +{ + std::vector allWater; + for (int y = 0; y < layout.H; ++y) + for (int x = 0; x < layout.W; ++x) + if (game.map.isWater(x, y)) + allWater.push_back(MapGeneratorPoint(x, y)); + + for (int i = 0; i < layout.nbTeams; ++i) + { + std::vector sources; + sources.push_back(teamPts[i]); + std::vector distances; + computeDistances(game.map, sources, allWater, distances); + + std::vector homePoints; + for (int y = 0; y < layout.H; ++y) + for (int x = 0; x < layout.W; ++x) + if (distances[y * layout.W + x] >= 1 && distances[y * layout.W + x] <= 10) + homePoints.push_back(MapGeneratorPoint(x, y)); + if (homePoints.empty()) + return false; + + placeResourceClump(game.map, context, + homePoints[context.bounded("resources", homePoints.size())], CORN, 2); + placeResourceClump(game.map, context, + homePoints[context.bounded("resources", homePoints.size())], WOOD, 2); + + std::vector stonePts = homePoints; + chooseRandomPoints(game.map, context, stonePts, 1); + for (unsigned int j = 0; j < stonePts.size(); ++j) + game.map.setResource(stonePts[j].x, stonePts[j].y, STONE, 1); + + std::vector home(size_t(layout.W) * layout.H, 0); + for (const auto &point : homePoints) + home[point.y * layout.W + point.x] = 1; + if (!placeSettlement(game, context, i, home, teamPts[i], "starts")) + return false; + } + return true; } // 11) Place bank resources last so settlement and regional deposits cannot // overwrite the guarantee. Every side of every fjord receives both resources // at distinct points along its length. bool placeBankResources(Game &game, GenerationContext &context, const FjordLayout &layout, - const std::vector> &fjordCenterlines, - const FjordContinentOptions &options) { - if (layout.nbTeams < 2) - return true; - for (int k = 0; k < layout.nbTeams; ++k) { - for (int side : {-1, 1}) - if (!placeBankClump(game.map, context, fjordCenterlines[k], 0.35, side, - CORN) || - !placeBankClump(game.map, context, fjordCenterlines[k], 0.68, side, - WOOD)) - return false; - if (!options.bankDeposits) - continue; - - // A handful more, lighter clumps at other points along the same banks besides - // the two guaranteed spots above, so walking a fjord's edge feels like following - // a shoreline with its own economy rather than passing exactly two fixed - // deposits. Best-effort: unlike the guarantee above, nothing downstream depends - // on any single one of these existing, so a spot that doesn't pan out is simply - // skipped rather than failing generation. A small jitter on each progress value - // keeps the spacing from reading as mechanically regular. - static const double bankScatterProgress[] = {0.10, 0.22, 0.48, 0.58, 0.80, 0.92}; - for (double progress : bankScatterProgress) { - const double jitter = (context.bounded("resources", 41) - 20) / 1000.0; // +/-0.02 - for (int side : {-1, 1}) { - // corn:wood at 2:1 (4:2 of 8) rather than even (3:3), stone's own 2/8 share - // unchanged - the same rebalance as the ambient scatter above, for consistency. - const int roll = context.bounded("resources", 8); - const int resourceType = roll < 4 ? CORN : roll < 6 ? WOOD : STONE; - // The amount controls place each rolled clump that many hundredths of a time: whole - // copies, and one more by chance from a stream of its own, so the rolls stay the same. - const int percent = resourceType == CORN ? options.wheat - : resourceType == WOOD ? options.wood - : options.stone; - int copies = percent / 100; - if (percent % 100 != 0 && int(context.bounded("fjord-bank-amounts", 100)) < percent % 100) - ++copies; - for (int copy = 0; copy < copies; ++copy) - placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, - resourceType); - } - } - } - return true; + const std::vector> &fjordCenterlines, + const FjordContinentOptions &options) +{ + if (layout.nbTeams < 2) + return true; + for (int k = 0; k < layout.nbTeams; ++k) + { + for (int side : {-1, 1}) + if (!placeBankClump(game.map, context, fjordCenterlines[k], 0.35, side, CORN) || + !placeBankClump(game.map, context, fjordCenterlines[k], 0.68, side, WOOD)) + return false; + if (!options.bankDeposits) + continue; + + // A handful more, lighter clumps at other points along the same banks besides + // the two guaranteed spots above, so walking a fjord's edge feels like following + // a shoreline with its own economy rather than passing exactly two fixed + // deposits. Best-effort: unlike the guarantee above, nothing downstream depends + // on any single one of these existing, so a spot that doesn't pan out is simply + // skipped rather than failing generation. A small jitter on each progress value + // keeps the spacing from reading as mechanically regular. + static const double bankScatterProgress[] = {0.10, 0.22, 0.48, 0.58, 0.80, 0.92}; + for (double progress : bankScatterProgress) + { + const double jitter = (context.bounded("resources", 41) - 20) / 1000.0; // +/-0.02 + for (int side : {-1, 1}) + { + // corn:wood at 2:1 (4:2 of 8) rather than even (3:3), stone's own 2/8 share + // unchanged - the same rebalance as the ambient scatter above, for consistency. + const int roll = context.bounded("resources", 8); + const int resourceType = roll < 4 ? CORN : roll < 6 ? WOOD : STONE; + // The amount controls place each rolled clump that many hundredths of a time: whole + // copies, and one more by chance from a stream of its own, so the rolls stay the same. + const int percent = resourceType == CORN ? options.wheat + : resourceType == WOOD ? options.wood + : options.stone; + int copies = percent / 100; + if (percent % 100 != 0 && + int(context.bounded("fjord-bank-amounts", 100)) < percent % 100) + ++copies; + for (int copy = 0; copy < copies; ++copy) + placeBankClump(game.map, context, fjordCenterlines[k], progress + jitter, side, + resourceType); + } + } + } + return true; } -static bool generate(Game &game, GenerationContext &context) { - context.stage = "continent"; - const FjordContinentOptions options(context.request); - game.map.makeHomogenMap(WATER); - for (int i = 0; i < context.request.nbTeams; ++i) - game.addTeam(); - - const int nbTeams = context.request.nbTeams; - if (nbTeams < 1) - return false; - - const FjordLayout layout = computeLayout(game, context, options, nbTeams); - - // grid holds area numbers for resource zoning, kept alongside the real - // terrain (which is written straight onto game.map as we go, the same way - // computeIsles and computeContestedCommons do it). - std::vector grid(layout.W * layout.H, 0); - int areaNumber = 1; - - stampContinent(game, layout); - std::vector> fjordCenterlines = - carveFjords(game, context, layout, options); - carveLake(game, layout, options.sandyLakeShore); - placeOutlierIslands(game, context, layout, options, grid, areaNumber); - - game.map.controlSand(); - - std::vector teamPts = anchorTeams(game, layout); - if (!verifyConnectivity(game, layout, teamPts)) - return false; - - placeCoreResources(game, context, layout, options, grid, areaNumber); - placeOpenSeaAlgae(game, context, layout, options.algae); - - if (!placeStarterKits(game, context, layout, teamPts)) - return false; - - // 10) Every resource so far is a deliberate, counted placement tied to a specific - // purpose: a home starter kit, the core, an outlier island. That leaves the whole - // continent interior in between them bare grass, which reads as empty rather than as a - // place with its own history the way a noise-painted map does. A light map-wide scatter - // fills that gap with - // ordinary, unclaimed deposits. This runs after every swarm and worker is already placed, - // not before: isResourceAllowed refuses any tile with a building or unit on it, so a - // scatter placed earlier can claim the one remaining tile a colony's swarm footprint - // needed, failing generation outright. Running last costs nothing here -- setResource - // always overwrites, so a scatter clump landing on an earlier deposit just loses that one - // tile to it, the same low-stakes trade every other generator's own layering already - // makes. Algae is left at zero: the shoreline band in placeOpenSeaAlgae already places it - // with a shape tuned to the coastline, and scattering more over open water would just - // fight that. - // corn:wood at 2:1 rather than even - wood was reading as overrepresented in practice, and - // total density is held constant (was 18+18=36) rather than just adding more corn on top. - scatterResources(game, context, - {/*corn=*/int(scaledCount(24, options.wheat)), - /*wood=*/int(scaledCount(12, options.wood)), - /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, - /*fruit=*/int(scaledCount(3, options.fruit))}); - - context.stage = "fjord bank resources"; - if (!placeBankResources(game, context, layout, fjordCenterlines, options)) - return false; - - // The starter kit guarantees wheat and wood exist somewhere near each boot tile, but the - // ambient scatter and bank clumps painted afterward don't know that - a wide enough patch of - // one crop can still wall the other off from a swarm that was placed before either existed. - // guaranteeStartingResources re-checks reachability through the actual resource layout (not - // just distance) and, if a wall is responsible for a cramped pocket, clears exactly the tiles - // sealing it before topping up whichever resource is still out of range - the same backstop - // RuggedArchipelago and ShatteredCoast already rely on for the same class of problem. - guaranteeStartingResources(game, context, 24, 32); - - // The scatter and bank clumps above are sized by the resource amounts, and the ambient scatter - // covers up to two thirds of the continent's grass at the top of their range. That can leave a - // colony walled into its own clearing with nowhere to build, which the guarantee above doesn't - // address: a colony buried in wheat has wheat at its feet, so it counts as served. At any - // non-default amount, open such a colony back up and re-run the guarantee in case the clearing - // took its nearest crop with the wall. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100 || options.fruit != 100) { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32); - } - - return true; +static bool generate(Game &game, GenerationContext &context) +{ + context.stage = "continent"; + const FjordContinentOptions options(context.request); + game.map.makeHomogenMap(WATER); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + const int nbTeams = context.request.nbTeams; + if (nbTeams < 1) + return false; + + const FjordLayout layout = computeLayout(game, context, options, nbTeams); + + // grid holds area numbers for resource zoning, kept alongside the real + // terrain (which is written straight onto game.map as we go, the same way + // computeIsles and computeContestedCommons do it). + std::vector grid(layout.W * layout.H, 0); + int areaNumber = 1; + + stampContinent(game, layout); + std::vector> fjordCenterlines = + carveFjords(game, context, layout, options); + carveLake(game, layout, options.sandyLakeShore); + placeOutlierIslands(game, context, layout, options, grid, areaNumber); + + game.map.controlSand(); + + std::vector teamPts = anchorTeams(game, layout); + if (!verifyConnectivity(game, layout, teamPts)) + return false; + + placeCoreResources(game, context, layout, options, grid, areaNumber); + placeOpenSeaAlgae(game, context, layout, options.algae); + + if (!placeStarterKits(game, context, layout, teamPts)) + return false; + + // 10) Every resource so far is a deliberate, counted placement tied to a specific + // purpose: a home starter kit, the core, an outlier island. That leaves the whole + // continent interior in between them bare grass, which reads as empty rather than as a + // place with its own history the way a noise-painted map does. A light map-wide scatter + // fills that gap with + // ordinary, unclaimed deposits. This runs after every swarm and worker is already placed, + // not before: isResourceAllowed refuses any tile with a building or unit on it, so a + // scatter placed earlier can claim the one remaining tile a colony's swarm footprint + // needed, failing generation outright. Running last costs nothing here -- setResource + // always overwrites, so a scatter clump landing on an earlier deposit just loses that one + // tile to it, the same low-stakes trade every other generator's own layering already + // makes. Algae is left at zero: the shoreline band in placeOpenSeaAlgae already places it + // with a shape tuned to the coastline, and scattering more over open water would just + // fight that. + // corn:wood at 2:1 rather than even - wood was reading as overrepresented in practice, and + // total density is held constant (was 18+18=36) rather than just adding more corn on top. + scatterResources(game, context, + {/*corn=*/int(scaledCount(24, options.wheat)), + /*wood=*/int(scaledCount(12, options.wood)), + /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, + /*fruit=*/int(scaledCount(3, options.fruit))}); + + context.stage = "fjord bank resources"; + if (!placeBankResources(game, context, layout, fjordCenterlines, options)) + return false; + + // The starter kit guarantees wheat and wood exist somewhere near each boot tile, but the + // ambient scatter and bank clumps painted afterward don't know that - a wide enough patch of + // one crop can still wall the other off from a swarm that was placed before either existed. + // guaranteeStartingResources re-checks reachability through the actual resource layout (not + // just distance) and, if a wall is responsible for a cramped pocket, clears exactly the tiles + // sealing it before topping up whichever resource is still out of range - the same backstop + // RuggedArchipelago and ShatteredCoast already rely on for the same class of problem. + guaranteeStartingResources(game, context, 24, 32); + + // The scatter and bank clumps above are sized by the resource amounts, and the ambient scatter + // covers up to two thirds of the continent's grass at the top of their range. That can leave a + // colony walled into its own clearing with nowhere to build, which the guarantee above doesn't + // address: a colony buried in wheat has wheat at its feet, so it counts as served. At any + // non-default amount, open such a colony back up and re-run the guarantee in case the clearing + // took its nearest crop with the wall. At the defaults none of this runs. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100 || options.fruit != 100) + { + openCrampedStarts(game, context); + guaranteeStartingResources(game, context, 24, 32); + } + + return true; } } // namespace FjordContinentOptions::FjordContinentOptions(const GenerationRequest &r) - : continentSize(r.option("continent-size")), - roughness(r.option("coast-roughness")), - fjordWidth(r.option("fjord-width")), - resourceIslands(r.option("resource-islands")), - lakeSize(r.option("lake-size")), - lakeConnected(r.option("lake-connected")), - sandyLakeShore(r.option("sandy-lake-shore") != 0), - bankDeposits(r.option("bank-deposits") != 0), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} - -GeneratorDefinition fjordContinentDefinition() { - return {"fjord-continent", - 12, - "Fjord continent", - 10, - false, - {{"continent-size", "Continent size", 28, 40, 2, 34, - ControlGroup::Terrain}, - {"coast-roughness", "Coast roughness", 10, 35, 1, 22, - ControlGroup::Terrain}, - {"fjord-width", "Fjord width", 2, 10, 1, 4, ControlGroup::Terrain}, - {"lake-size", "Lake size", 0, 90, 5, 45, ControlGroup::Terrain}, - GeneratorControl::toggle("lake-connected", "Lake connects to fjords", false, - ControlGroup::Terrain), - {"resource-islands", "Resource islands", 0, 20, 1, 2, - ControlGroup::Resources}, - // Off, the lake has an ordinary beach instead of a wide ring of sand. - GeneratorControl::toggle("sandy-lake-shore", "Sandy lake shore", true, - ControlGroup::Terrain), - // Off, each fjord bank keeps only its guaranteed wheat and wood. - GeneratorControl::toggle("bank-deposits", "Fjord bank deposits", true, - ControlGroup::Resources), - // The ambient scatter, the core's stone and fruit, the lake's and open sea's algae and - // the fjord banks' extra deposits. Starter kits and each bank's guaranteed wheat and - // wood stay as they are. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate}; + : continentSize(r.option("continent-size")), roughness(r.option("coast-roughness")), + fjordWidth(r.option("fjord-width")), resourceIslands(r.option("resource-islands")), + lakeSize(r.option("lake-size")), lakeConnected(r.option("lake-connected")), + sandyLakeShore(r.option("sandy-lake-shore") != 0), + bankDeposits(r.option("bank-deposits") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition fjordContinentDefinition() +{ + return {"fjord-continent", + 12, + "Fjord continent", + 10, + false, + {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, + {"coast-roughness", "Coast roughness", 10, 35, 1, 22, ControlGroup::Terrain}, + {"fjord-width", "Fjord width", 2, 10, 1, 4, ControlGroup::Terrain}, + {"lake-size", "Lake size", 0, 90, 5, 45, ControlGroup::Terrain}, + GeneratorControl::toggle("lake-connected", "Lake connects to fjords", false, + ControlGroup::Terrain), + {"resource-islands", "Resource islands", 0, 20, 1, 2, ControlGroup::Resources}, + // Off, the lake has an ordinary beach instead of a wide ring of sand. + GeneratorControl::toggle("sandy-lake-shore", "Sandy lake shore", true, + ControlGroup::Terrain), + // Off, each fjord bank keeps only its guaranteed wheat and wood. + GeneratorControl::toggle("bank-deposits", "Fjord bank deposits", true, + ControlGroup::Resources), + // The ambient scatter, the core's stone and fruit, the lake's and open sea's algae and + // the fjord banks' extra deposits. Starter kits and each bank's guaranteed wheat and + // wood stay as they are. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate}; } diff --git a/src/map/generator/generators/IslandsGenerator.cpp b/src/map/generator/generators/IslandsGenerator.cpp index 116cd714e..3292ee4e2 100644 --- a/src/map/generator/generators/IslandsGenerator.cpp +++ b/src/map/generator/generators/IslandsGenerator.cpp @@ -9,9 +9,8 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const IslandsOptions options(context.request); - HeightFieldOptions terrain{ - options.water, options.sand, options.grass, options.desert, - options.smoothing, options.fruit, options.repeat, false}; + HeightFieldOptions terrain{options.water, options.sand, options.grass, options.desert, + options.smoothing, options.fruit, options.repeat, false}; readResourceControls(terrain, context.request); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index 8846331f1..1973e8580 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -216,8 +216,7 @@ static bool generate(Game &game, GenerationContext &context) // default size can also wall the colony in with nowhere left to build. At any amount other than // the default, open up such a colony and then make sure each still has both crops within reach, // in case the clearing took the nearest one along with the wall. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100) + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || options.algae != 100) { openCrampedStarts(game, context); guaranteeStartingResources(game, context, 24, 32); @@ -233,21 +232,22 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition islesDefinition() { - return {"isles", - 6, - "Isles", - 2, - false, - {{"island-size", "Island size", 45, 65, 5, 60, ControlGroup::Terrain, false}, - {"bridge-width", "Land bridge width", 3, 6, 1, 4, ControlGroup::Terrain, false}, - // Off, every colony's island stands alone in the sea. - GeneratorControl::toggle("land-bridges", "Land bridges", true, ControlGroup::Terrain), - // Off, islands meet the sea without a band of sand. - GeneratorControl::toggle("sandy-beaches", "Sandy beaches", true, ControlGroup::Terrain), - // Wheat and wood scale each colony's fields, stone its deposits, algae its patch. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount")}, - generate}; + return { + "isles", + 6, + "Isles", + 2, + false, + {{"island-size", "Island size", 45, 65, 5, 60, ControlGroup::Terrain, false}, + {"bridge-width", "Land bridge width", 3, 6, 1, 4, ControlGroup::Terrain, false}, + // Off, every colony's island stands alone in the sea. + GeneratorControl::toggle("land-bridges", "Land bridges", true, ControlGroup::Terrain), + // Off, islands meet the sea without a band of sand. + GeneratorControl::toggle("sandy-beaches", "Sandy beaches", true, ControlGroup::Terrain), + // Wheat and wood scale each colony's fields, stone its deposits, algae its patch. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount")}, + generate}; } diff --git a/src/map/generator/generators/IslesGenerator.h b/src/map/generator/generators/IslesGenerator.h index 658e9863a..7ad6cf2d5 100644 --- a/src/map/generator/generators/IslesGenerator.h +++ b/src/map/generator/generators/IslesGenerator.h @@ -9,9 +9,10 @@ struct IslesOptions int wheat, wood, stone, algae; // percentages of the default amounts explicit IslesOptions(const GenerationRequest &r) : island_size(r.option("island-size")), bridge_width(r.option("bridge-width")), - land_bridges(r.option("land-bridges") != 0), sandy_beaches(r.option("sandy-beaches") != 0), - wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), - stone(r.option("stone-amount")), algae(r.option("algae-amount")) + land_bridges(r.option("land-bridges") != 0), + sandy_beaches(r.option("sandy-beaches") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")) { } }; diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 01cd4d6ea..d56b7d88e 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -22,9 +22,16 @@ using namespace MapGeneration; // shifts shorelines unevenly. Each tile's terrain comes from its four undermap corners // (Map::regenerateMap), so a pure-grass tile needs grass at all four, and grass must never touch // water; every grass area here is therefore stamped inside a one-tile sand ring. -namespace { - -enum Direction { East, South, West, North }; +namespace +{ + +enum Direction +{ + East, + South, + West, + North +}; // Walking out from a cell's centre towards a wall: the passage's grass, then two walkable tiles of // its sand ring, then channel-width all-water tiles, then the wall - two walkable tiles of flank and @@ -50,365 +57,411 @@ constexpr int kShoreBand = 3; // The cell lattice. Boundaries are integer tile coordinates chosen so the cells tile the map // exactly, which lets the maze wrap across the torus seam like any other boundary; when // cell-size doesn't divide the map, neighbouring cells differ in pitch by at most one tile. -struct MazeGrid { - int width, height, columns, rows; - std::vector xs, ys; - - int cells() const { return columns * rows; } - int column(int cell) const { return cell % columns; } - int row(int cell) const { return cell / columns; } - int centerX(int cell) const { return (xs[column(cell)] + xs[column(cell) + 1]) / 2; } - int centerY(int cell) const { return (ys[row(cell)] + ys[row(cell) + 1]) / 2; } - int minimumPitch() const { return std::min(width / columns, height / rows); } - int cellAt(int x, int y) const { - const int c = int(std::upper_bound(xs.begin(), xs.end(), x) - xs.begin()) - 1; - const int r = int(std::upper_bound(ys.begin(), ys.end(), y) - ys.begin()) - 1; - return std::min(r, rows - 1) * columns + std::min(c, columns - 1); - } - - int neighbour(int cell, int direction) const { - const int c = column(cell), r = row(cell); - switch (direction) { - case East: - return r * columns + (c + 1) % columns; - case South: - return ((r + 1) % rows) * columns + c; - case West: - return r * columns + (c + columns - 1) % columns; - default: - return ((r + rows - 1) % rows) * columns + c; - } - } - // Each undirected edge is stored once, owned by its west or north cell. - int edgeId(int cell, int direction) const { - switch (direction) { - case East: - return 2 * cell; - case South: - return 2 * cell + 1; - case West: - return 2 * neighbour(cell, West); - default: - return 2 * neighbour(cell, North) + 1; - } - } +struct MazeGrid +{ + int width, height, columns, rows; + std::vector xs, ys; + + int cells() const { return columns * rows; } + int column(int cell) const { return cell % columns; } + int row(int cell) const { return cell / columns; } + int centerX(int cell) const { return (xs[column(cell)] + xs[column(cell) + 1]) / 2; } + int centerY(int cell) const { return (ys[row(cell)] + ys[row(cell) + 1]) / 2; } + int minimumPitch() const { return std::min(width / columns, height / rows); } + int cellAt(int x, int y) const + { + const int c = int(std::upper_bound(xs.begin(), xs.end(), x) - xs.begin()) - 1; + const int r = int(std::upper_bound(ys.begin(), ys.end(), y) - ys.begin()) - 1; + return std::min(r, rows - 1) * columns + std::min(c, columns - 1); + } + + int neighbour(int cell, int direction) const + { + const int c = column(cell), r = row(cell); + switch (direction) + { + case East: + return r * columns + (c + 1) % columns; + case South: + return ((r + 1) % rows) * columns + c; + case West: + return r * columns + (c + columns - 1) % columns; + default: + return ((r + rows - 1) % rows) * columns + c; + } + } + // Each undirected edge is stored once, owned by its west or north cell. + int edgeId(int cell, int direction) const + { + switch (direction) + { + case East: + return 2 * cell; + case South: + return 2 * cell + 1; + case West: + return 2 * neighbour(cell, West); + default: + return 2 * neighbour(cell, North) + 1; + } + } }; -MazeGrid mazeGrid(int width, int height, int cellSize) { - MazeGrid g{width, height, width / cellSize, height / cellSize, {}, {}}; - for (int i = 0; i <= g.columns; ++i) - g.xs.push_back(i * width / std::max(1, g.columns)); - for (int i = 0; i <= g.rows; ++i) - g.ys.push_back(i * height / std::max(1, g.rows)); - return g; +MazeGrid mazeGrid(int width, int height, int cellSize) +{ + MazeGrid g{width, height, width / cellSize, height / cellSize, {}, {}}; + for (int i = 0; i <= g.columns; ++i) + g.xs.push_back(i * width / std::max(1, g.columns)); + for (int i = 0; i <= g.rows; ++i) + g.ys.push_back(i * height / std::max(1, g.rows)); + return g; } // Passages are as wide as the narrowest cell allows once the wall and its channels are taken // out, and odd so they centre on a tile. -int passageHalf(const MazeGrid &g, int channelWidth) { - return g.minimumPitch() / 2 - kWallFootprint - channelWidth; +int passageHalf(const MazeGrid &g, int channelWidth) +{ + return g.minimumPitch() / 2 - kWallFootprint - channelWidth; } // Whether a set of home cells leaves a valid layout: every home has a non-home neighbour to open // its one passage into, and the non-home cells form a single connected region for the maze to // span. -bool homesFit(const MazeGrid &g, const std::vector &isHome) { - int start = -1, freeCells = 0; - for (int cell = 0; cell < g.cells(); ++cell) { - if (!isHome[cell]) { - ++freeCells; - start = cell; - continue; - } - bool opening = false; - for (int d = 0; d < 4; ++d) - opening |= !isHome[g.neighbour(cell, d)]; - if (!opening) - return false; - } - if (!freeCells) - return false; - std::vector seen(g.cells(), 0); - std::vector stack{start}; - seen[start] = 1; - int reached = 1; - while (!stack.empty()) { - const int cell = stack.back(); - stack.pop_back(); - for (int d = 0; d < 4; ++d) { - const int next = g.neighbour(cell, d); - if (!isHome[next] && !seen[next]) { - seen[next] = 1; - ++reached; - stack.push_back(next); - } - } - } - return reached == freeCells; +bool homesFit(const MazeGrid &g, const std::vector &isHome) +{ + int start = -1, freeCells = 0; + for (int cell = 0; cell < g.cells(); ++cell) + { + if (!isHome[cell]) + { + ++freeCells; + start = cell; + continue; + } + bool opening = false; + for (int d = 0; d < 4; ++d) + opening |= !isHome[g.neighbour(cell, d)]; + if (!opening) + return false; + } + if (!freeCells) + return false; + std::vector seen(g.cells(), 0); + std::vector stack{start}; + seen[start] = 1; + int reached = 1; + while (!stack.empty()) + { + const int cell = stack.back(); + stack.pop_back(); + for (int d = 0; d < 4; ++d) + { + const int next = g.neighbour(cell, d); + if (!isHome[next] && !seen[next]) + { + seen[next] = 1; + ++reached; + stack.push_back(next); + } + } + } + return reached == freeCells; } // The home cells as a pattern fixed by the grid alone, so validation checks exactly what // generation will do: farthest-point spreading on the torus, taking at each step the farthest // cell that still leaves a valid layout. Empty if the grid can't hold every colony. -std::vector homePattern(const MazeGrid &g, int teams) { - auto distance = [&](int a, int b) { - int dx = std::abs(g.column(a) - g.column(b)), dy = std::abs(g.row(a) - g.row(b)); - dx = std::min(dx, g.columns - dx); - dy = std::min(dy, g.rows - dy); - return dx * dx + dy * dy; - }; - std::vector isHome(g.cells(), 0); - std::vector homes, nearest(g.cells(), 0); - for (int team = 0; team < teams; ++team) { - std::vector order; - for (int cell = 0; cell < g.cells(); ++cell) - if (!isHome[cell]) - order.push_back(cell); - std::stable_sort(order.begin(), order.end(), - [&](int a, int b) { return nearest[a] > nearest[b]; }); - int chosen = -1; - for (int cell : order) { - isHome[cell] = 1; - if (homesFit(g, isHome)) { - chosen = cell; - break; - } - isHome[cell] = 0; - } - if (chosen < 0) - return {}; - homes.push_back(chosen); - for (int cell = 0; cell < g.cells(); ++cell) - nearest[cell] = homes.size() == 1 ? distance(cell, chosen) - : std::min(nearest[cell], distance(cell, chosen)); - } - return homes; +std::vector homePattern(const MazeGrid &g, int teams) +{ + auto distance = [&](int a, int b) + { + int dx = std::abs(g.column(a) - g.column(b)), dy = std::abs(g.row(a) - g.row(b)); + dx = std::min(dx, g.columns - dx); + dy = std::min(dy, g.rows - dy); + return dx * dx + dy * dy; + }; + std::vector isHome(g.cells(), 0); + std::vector homes, nearest(g.cells(), 0); + for (int team = 0; team < teams; ++team) + { + std::vector order; + for (int cell = 0; cell < g.cells(); ++cell) + if (!isHome[cell]) + order.push_back(cell); + std::stable_sort(order.begin(), order.end(), + [&](int a, int b) { return nearest[a] > nearest[b]; }); + int chosen = -1; + for (int cell : order) + { + isHome[cell] = 1; + if (homesFit(g, isHome)) + { + chosen = cell; + break; + } + isHome[cell] = 0; + } + if (chosen < 0) + return {}; + homes.push_back(chosen); + for (int cell = 0; cell < g.cells(); ++cell) + nearest[cell] = homes.size() == 1 ? distance(cell, chosen) + : std::min(nearest[cell], distance(cell, chosen)); + } + return homes; } -std::string validate(const GenerationRequest &r) { - const MazeOptions o(r); - const MazeGrid g = mazeGrid(1 << r.wDec, 1 << r.hDec, o.cellSize); - if (g.columns < 2 || g.rows < 2) - return "The maze needs at least two cells across and down; use a bigger map or smaller cells."; - if (passageHalf(g, o.channelWidth) < kMinimumPassageHalf) - return "Channels this wide leave too little grass in each cell; use narrower channels or " - "bigger cells."; - if (int(homePattern(g, r.nbTeams).size()) != r.nbTeams) - return "The maze has too few cul-de-sacs for this many colonies; use a bigger map or smaller " - "cells."; - return ""; +std::string validate(const GenerationRequest &r) +{ + const MazeOptions o(r); + const MazeGrid g = mazeGrid(1 << r.wDec, 1 << r.hDec, o.cellSize); + if (g.columns < 2 || g.rows < 2) + return "The maze needs at least two cells across and down; use a bigger map or smaller " + "cells."; + if (passageHalf(g, o.channelWidth) < kMinimumPassageHalf) + return "Channels this wide leave too little grass in each cell; use narrower channels or " + "bigger cells."; + if (int(homePattern(g, r.nbTeams).size()) != r.nbTeams) + return "The maze has too few cul-de-sacs for this many colonies; use a bigger map or " + "smaller " + "cells."; + return ""; } // The home pattern at a random offset and mirror image on the torus - a translation or reflection // of a valid layout is still valid - dealt to the colonies in random order. -std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int teams) { - std::vector homes = homePattern(g, teams); - const int offsetX = context.bounded("maze", g.columns); - const int offsetY = context.bounded("maze", g.rows); - const bool mirrorX = context.bounded("maze", 2), mirrorY = context.bounded("maze", 2); - for (int &home : homes) { - const int c = mirrorX ? (g.columns - g.column(home)) % g.columns : g.column(home); - const int r = mirrorY ? (g.rows - g.row(home)) % g.rows : g.row(home); - home = ((r + offsetY) % g.rows) * g.columns + (c + offsetX) % g.columns; - } - for (size_t i = homes.size(); i > 1; --i) - std::swap(homes[i - 1], homes[context.bounded("maze", i)]); - return homes; +std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int teams) +{ + std::vector homes = homePattern(g, teams); + const int offsetX = context.bounded("maze", g.columns); + const int offsetY = context.bounded("maze", g.rows); + const bool mirrorX = context.bounded("maze", 2), mirrorY = context.bounded("maze", 2); + for (int &home : homes) + { + const int c = mirrorX ? (g.columns - g.column(home)) % g.columns : g.column(home); + const int r = mirrorY ? (g.rows - g.row(home)) % g.rows : g.row(home); + home = ((r + offsetY) % g.rows) * g.columns + (c + offsetX) % g.columns; + } + for (size_t i = homes.size(); i > 1; --i) + std::swap(homes[i - 1], homes[context.bounded("maze", i)]); + return homes; } // Recursive backtracker (iterative, explicit stack) over the non-home cells: long winding // passages with comparatively few, deep dead ends - the classic hand-drawn maze texture. bool carveSpanningTree(const MazeGrid &g, GenerationContext &context, - const std::vector &isHome, - std::vector &open) { - std::vector free; - for (int cell = 0; cell < g.cells(); ++cell) - if (!isHome[cell]) - free.push_back(cell); - if (free.empty()) - return false; - std::vector visited(isHome); - std::vector stack{free[context.bounded("maze", free.size())]}; - visited[stack.back()] = 1; - while (!stack.empty()) { - const int cell = stack.back(); - int choices[4], count = 0; - for (int d = 0; d < 4; ++d) - if (!visited[g.neighbour(cell, d)]) - choices[count++] = d; - if (!count) { - stack.pop_back(); - continue; - } - const int d = choices[context.bounded("maze", count)]; - const int next = g.neighbour(cell, d); - open[g.edgeId(cell, d)] = 1; - visited[next] = 1; - stack.push_back(next); - } - return std::all_of(visited.begin(), visited.end(), [](unsigned char v) { return v != 0; }); + const std::vector &isHome, std::vector &open) +{ + std::vector free; + for (int cell = 0; cell < g.cells(); ++cell) + if (!isHome[cell]) + free.push_back(cell); + if (free.empty()) + return false; + std::vector visited(isHome); + std::vector stack{free[context.bounded("maze", free.size())]}; + visited[stack.back()] = 1; + while (!stack.empty()) + { + const int cell = stack.back(); + int choices[4], count = 0; + for (int d = 0; d < 4; ++d) + if (!visited[g.neighbour(cell, d)]) + choices[count++] = d; + if (!count) + { + stack.pop_back(); + continue; + } + const int d = choices[context.bounded("maze", count)]; + const int next = g.neighbour(cell, d); + open[g.edgeId(cell, d)] = 1; + visited[next] = 1; + stack.push_back(next); + } + return std::all_of(visited.begin(), visited.end(), [](unsigned char v) { return v != 0; }); } // Loopiness knocks through extra walls between non-home cells, giving routes to flank along; // a home is never touched, so it stays a cul-de-sac. void addLoops(const MazeGrid &g, GenerationContext &context, - const std::vector &isHome, int loopiness, - std::vector &open) { - std::vector closed; - int freeCells = 0; - for (int cell = 0; cell < g.cells(); ++cell) { - if (isHome[cell]) - continue; - ++freeCells; - for (int d : {East, South}) - if (!open[g.edgeId(cell, d)] && !isHome[g.neighbour(cell, d)]) - closed.push_back(g.edgeId(cell, d)); - } - for (size_t i = closed.size(); i > 1; --i) - std::swap(closed[i - 1], closed[context.bounded("maze", i)]); - const size_t extra = std::min(closed.size(), size_t(freeCells * loopiness / 100)); - for (size_t i = 0; i < extra; ++i) - open[closed[i]] = 1; + const std::vector &isHome, int loopiness, + std::vector &open) +{ + std::vector closed; + int freeCells = 0; + for (int cell = 0; cell < g.cells(); ++cell) + { + if (isHome[cell]) + continue; + ++freeCells; + for (int d : {East, South}) + if (!open[g.edgeId(cell, d)] && !isHome[g.neighbour(cell, d)]) + closed.push_back(g.edgeId(cell, d)); + } + for (size_t i = closed.size(); i > 1; --i) + std::swap(closed[i - 1], closed[context.bounded("maze", i)]); + const size_t extra = std::min(closed.size(), size_t(freeCells * loopiness / 100)); + for (size_t i = 0; i < extra; ++i) + open[closed[i]] = 1; } -void fillUndermap(Map &map, int x0, int y0, int w, int h, TerrainType t) { - for (int dy = 0; dy < h; ++dy) - for (int dx = 0; dx < w; ++dx) - map.setUMTerrain(map.normalizeX(x0 + dx), map.normalizeY(y0 + dy), t); +void fillUndermap(Map &map, int x0, int y0, int w, int h, TerrainType t) +{ + for (int dy = 0; dy < h; ++dy) + for (int dx = 0; dx < w; ++dx) + map.setUMTerrain(map.normalizeX(x0 + dx), map.normalizeY(y0 + dy), t); } // An inclusive rectangle of pure-grass tiles. Stamping one takes undermap grass on // [x0, x1 + 1] x [y0, y1 + 1] inside undermap sand one line further out. -struct TileRect { - int x0, y0, x1, y1; +struct TileRect +{ + int x0, y0, x1, y1; }; -void stampRing(Map &map, const TileRect &r) { - fillUndermap(map, r.x0 - 1, r.y0 - 1, r.x1 - r.x0 + 4, r.y1 - r.y0 + 4, SAND); +void stampRing(Map &map, const TileRect &r) +{ + fillUndermap(map, r.x0 - 1, r.y0 - 1, r.x1 - r.x0 + 4, r.y1 - r.y0 + 4, SAND); } -void stampCore(Map &map, const TileRect &r) { - fillUndermap(map, r.x0, r.y0, r.x1 - r.x0 + 2, r.y1 - r.y0 + 2, GRASS); +void stampCore(Map &map, const TileRect &r) +{ + fillUndermap(map, r.x0, r.y0, r.x1 - r.x0 + 2, r.y1 - r.y0 + 2, GRASS); } // One wall per closed boundary: a stone spine covering every tile of the boundary line from // corner to corner inclusive, so perpendicular walls share their corner tile and nothing can slip // between them. As a tile rectangle one tile wide, its pure-grass core is exactly the spine, // which is what lets STONE be placed on every tile of it. -TileRect wallFor(const MazeGrid &g, int cell, int direction) { - const int c = g.column(cell), r = g.row(cell); - if (direction == East) - return {g.xs[c + 1], g.ys[r], g.xs[c + 1], g.ys[r + 1]}; - return {g.xs[c], g.ys[r + 1], g.xs[c + 1], g.ys[r + 1]}; +TileRect wallFor(const MazeGrid &g, int cell, int direction) +{ + const int c = g.column(cell), r = g.row(cell); + if (direction == East) + return {g.xs[c + 1], g.ys[r], g.xs[c + 1], g.ys[r + 1]}; + return {g.xs[c], g.ys[r + 1], g.xs[c + 1], g.ys[r + 1]}; } // Distance in 8-neighbour steps from every tile to the nearest shore - a tile that isn't pure // grass - not counting the sand road, which runs down the middle of passages, not their edges. -std::vector shoreDepth(const Map &map, const std::vector &onRoad) { - const int w = map.getW(), h = map.getH(); - std::vector depth(size_t(w) * h, -1); - std::vector queue; - queue.reserve(size_t(w) * h); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - if (!map.isGrass(x, y) && !onRoad[size_t(y) * w + x]) { - depth[size_t(y) * w + x] = 0; - queue.push_back(y * w + x); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t n = size_t(map.normalizeY(y + dy)) * w + map.normalizeX(x + dx); - if (depth[n] < 0) { - depth[n] = depth[size_t(queue[head])] + 1; - queue.push_back(int(n)); - } - } - } - return depth; +std::vector shoreDepth(const Map &map, const std::vector &onRoad) +{ + const int w = map.getW(), h = map.getH(); + std::vector depth(size_t(w) * h, -1); + std::vector queue; + queue.reserve(size_t(w) * h); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (!map.isGrass(x, y) && !onRoad[size_t(y) * w + x]) + { + depth[size_t(y) * w + x] = 0; + queue.push_back(y * w + x); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t n = size_t(map.normalizeY(y + dy)) * w + map.normalizeX(x + dx); + if (depth[n] < 0) + { + depth[n] = depth[size_t(queue[head])] + 1; + queue.push_back(int(n)); + } + } + } + return depth; } -struct CellTile { - int x, y, u, v; // u: towards the cell's exit; v: to the exit's right +struct CellTile +{ + int x, y, u, v; // u: towards the cell's exit; v: to the exit's right }; // Places the first `count` tiles of a region in the order `before` ranks them. template -void fillInOrder(Map &map, std::vector region, int count, int resourceType, - Order before) { - std::stable_sort(region.begin(), region.end(), before); - for (int i = 0; i < count && i < int(region.size()); ++i) - map.setResource(region[i].x, region[i].y, resourceType, 1); +void fillInOrder(Map &map, std::vector region, int count, int resourceType, Order before) +{ + std::stable_sort(region.begin(), region.end(), before); + for (int i = 0; i < count && i < int(region.size()); ++i) + map.setResource(region[i].x, region[i].y, resourceType, 1); } // A home's own cell, seen from inside looking out through its exit: wheat banks the left shore // and wood the right, both growing forward from the dead end's back wall; stone is a compact // deposit at the back wall's centre (a line along it would read as a second wall). The middle of // the passage and a square around the swarm stay clear, so workers always walk straight out. -void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) { - static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; - const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; - const int cx = g.centerX(cell), cy = g.centerY(cell); - const int lane = std::max(1, half - kShoreBand); - std::vector left, right, back; - const int c = g.column(cell), r = g.row(cell); - for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) - for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) { - if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || - map.getGroundUnit(x, y) != NOGUID) - continue; - const int ox = x - cx, oy = y - cy; - const int u = ox * fx + oy * fy, v = ox * rx + oy * ry; - if (std::abs(u) <= 4 && std::abs(v) <= 4) - continue; - if (v < -lane) - left.push_back({x, y, u, v}); - else if (v > lane) - right.push_back({x, y, u, v}); - else if (u <= -half + 2) - back.push_back({x, y, u, v}); - } - const auto shoreFromBack = [](const CellTile &a, const CellTile &b) { - return a.u != b.u ? a.u < b.u : std::abs(a.v) > std::abs(b.v); - }; - const auto nearBackCentre = [half](const CellTile &a, const CellTile &b) { - const int da = std::max(a.u + half, std::abs(a.v)), db = std::max(b.u + half, std::abs(b.v)); - if (da != db) - return da < db; - if (std::abs(a.v) != std::abs(b.v)) - return std::abs(a.v) < std::abs(b.v); - return a.u != b.u ? a.u < b.u : a.v < b.v; - }; - fillInOrder(map, left, kHomeFarmland, CORN, shoreFromBack); - fillInOrder(map, right, kHomeFarmland, WOOD, shoreFromBack); - fillInOrder(map, back, kHomeStone, STONE, nearBackCentre); +void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) +{ + static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; + const int cx = g.centerX(cell), cy = g.centerY(cell); + const int lane = std::max(1, half - kShoreBand); + std::vector left, right, back; + const int c = g.column(cell), r = g.row(cell); + for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) + for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) + { + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + const int ox = x - cx, oy = y - cy; + const int u = ox * fx + oy * fy, v = ox * rx + oy * ry; + if (std::abs(u) <= 4 && std::abs(v) <= 4) + continue; + if (v < -lane) + left.push_back({x, y, u, v}); + else if (v > lane) + right.push_back({x, y, u, v}); + else if (u <= -half + 2) + back.push_back({x, y, u, v}); + } + const auto shoreFromBack = [](const CellTile &a, const CellTile &b) + { return a.u != b.u ? a.u < b.u : std::abs(a.v) > std::abs(b.v); }; + const auto nearBackCentre = [half](const CellTile &a, const CellTile &b) + { + const int da = std::max(a.u + half, std::abs(a.v)), + db = std::max(b.u + half, std::abs(b.v)); + if (da != db) + return da < db; + if (std::abs(a.v) != std::abs(b.v)) + return std::abs(a.v) < std::abs(b.v); + return a.u != b.u ? a.u < b.u : a.v < b.v; + }; + fillInOrder(map, left, kHomeFarmland, CORN, shoreFromBack); + fillInOrder(map, right, kHomeFarmland, WOOD, shoreFromBack); + fillInOrder(map, back, kHomeStone, STONE, nearBackCentre); } // A treasure: a compact patch of one fruit at the far end of a dead end, so colonies have to // go and take it rather than passing it on the way somewhere else. It sits a few tiles back // from the end wall, so its front always faces the passage's clear lane. void placeTreasure(Map &map, const MazeGrid &g, int cell, int exit, int half, int size, - int fruitType) { - static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; - const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; - const int cx = g.centerX(cell), cy = g.centerY(cell), anchor = -half + 3; - std::vector tiles; - const int c = g.column(cell), r = g.row(cell); - for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) - for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) - if (map.isGrass(x, y) && !map.isResource(x, y)) { - const int ox = x - cx, oy = y - cy; - tiles.push_back({x, y, ox * fx + oy * fy, ox * rx + oy * ry}); - } - fillInOrder(map, tiles, size, fruitType, [anchor](const CellTile &a, const CellTile &b) { - const int da = std::max(std::abs(a.u - anchor), std::abs(a.v)); - const int db = std::max(std::abs(b.u - anchor), std::abs(b.v)); - if (da != db) - return da < db; - if (std::abs(a.v) != std::abs(b.v)) - return std::abs(a.v) < std::abs(b.v); - return a.u != b.u ? a.u < b.u : a.v < b.v; - }); + int fruitType) +{ + static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; + const int cx = g.centerX(cell), cy = g.centerY(cell), anchor = -half + 3; + std::vector tiles; + const int c = g.column(cell), r = g.row(cell); + for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) + for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) + if (map.isGrass(x, y) && !map.isResource(x, y)) + { + const int ox = x - cx, oy = y - cy; + tiles.push_back({x, y, ox * fx + oy * fy, ox * rx + oy * ry}); + } + fillInOrder(map, tiles, size, fruitType, + [anchor](const CellTile &a, const CellTile &b) + { + const int da = std::max(std::abs(a.u - anchor), std::abs(a.v)); + const int db = std::max(std::abs(b.u - anchor), std::abs(b.v)); + if (da != db) + return da < db; + if (std::abs(a.v) != std::abs(b.v)) + return std::abs(a.v) < std::abs(b.v); + return a.u != b.u ? a.u < b.u : a.v < b.v; + }); } // Deposits scattered along the shores of every passage outside the homes, as compact clumps @@ -416,317 +469,375 @@ void placeTreasure(Map &map, const MazeGrid &g, int cell, int exit, int half, in // what keeps each passage's middle clear. Densities are per 256 shore tiles; fruitTiles of fruit, // when the dead ends hold no treasure, follow in small clumps of each kind in turn. void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, - const std::vector &isHome, - const std::vector &onRoad, int half, - const MazeOptions &o, int fruitTiles) { - const int w = map.getW(), h = map.getH(); - const std::vector depth = shoreDepth(map, onRoad); - const int band = std::min(kShoreBand, half - 1); - std::vector free(size_t(w) * h, 0); - std::vector pool; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - if (depth[i] >= 1 && depth[i] <= band && !isHome[g.cellAt(x, y)] && - !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID) { - free[i] = 1; - pool.push_back(int(i)); - } - } - if (pool.empty()) - return; - - auto clump = [&](int resourceType, int size) { - int seed = -1; - for (int attempt = 0; attempt < 32 && seed < 0; ++attempt) { - const int candidate = pool[context.bounded("resources", pool.size())]; - if (free[candidate]) - seed = candidate; - } - if (seed < 0) - return 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - std::vector frontier{seed}; - free[seed] = 0; - int placed = 0; - for (size_t head = 0; head < frontier.size() && placed < size; ++head, ++placed) { - const int x = frontier[head] % w, y = frontier[head] / w; - map.setResource(x, y, resourceType, 1); - for (const auto &s : steps) { - const size_t n = size_t(map.normalizeY(y + s[1])) * w + map.normalizeX(x + s[0]); - if (free[n]) { - free[n] = 0; - frontier.push_back(int(n)); - } - } - } - for (size_t k = placed; k < frontier.size(); ++k) - free[frontier[k]] = 1; - return placed; - }; - - const int shore = int(pool.size()); - for (const auto &layer : {std::pair{CORN, o.corn}, std::pair{WOOD, o.wood}, - std::pair{STONE, o.stone}}) { - int remaining = shore * layer.second / 256; - for (int attempt = 0; remaining > 0 && attempt < shore; ++attempt) - remaining -= - clump(layer.first, std::min(remaining, 4 + int(context.bounded("resources", 9)))); - } - for (int attempt = 0, kind = 0; fruitTiles > 0 && attempt < shore; ++attempt, kind = (kind + 1) % 3) - fruitTiles -= clump(CHERRY + kind, std::min(fruitTiles, 3 + int(context.bounded("resources", 3)))); + const std::vector &isHome, + const std::vector &onRoad, int half, const MazeOptions &o, + int fruitTiles) +{ + const int w = map.getW(), h = map.getH(); + const std::vector depth = shoreDepth(map, onRoad); + const int band = std::min(kShoreBand, half - 1); + std::vector free(size_t(w) * h, 0); + std::vector pool; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + if (depth[i] >= 1 && depth[i] <= band && !isHome[g.cellAt(x, y)] && + !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID) + { + free[i] = 1; + pool.push_back(int(i)); + } + } + if (pool.empty()) + return; + + auto clump = [&](int resourceType, int size) + { + int seed = -1; + for (int attempt = 0; attempt < 32 && seed < 0; ++attempt) + { + const int candidate = pool[context.bounded("resources", pool.size())]; + if (free[candidate]) + seed = candidate; + } + if (seed < 0) + return 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + std::vector frontier{seed}; + free[seed] = 0; + int placed = 0; + for (size_t head = 0; head < frontier.size() && placed < size; ++head, ++placed) + { + const int x = frontier[head] % w, y = frontier[head] / w; + map.setResource(x, y, resourceType, 1); + for (const auto &s : steps) + { + const size_t n = size_t(map.normalizeY(y + s[1])) * w + map.normalizeX(x + s[0]); + if (free[n]) + { + free[n] = 0; + frontier.push_back(int(n)); + } + } + } + for (size_t k = placed; k < frontier.size(); ++k) + free[frontier[k]] = 1; + return placed; + }; + + const int shore = int(pool.size()); + for (const auto &layer : {std::pair{CORN, o.corn}, std::pair{WOOD, o.wood}, + std::pair{STONE, o.stone}}) + { + int remaining = shore * layer.second / 256; + for (int attempt = 0; remaining > 0 && attempt < shore; ++attempt) + remaining -= + clump(layer.first, std::min(remaining, 4 + int(context.bounded("resources", 9)))); + } + for (int attempt = 0, kind = 0; fruitTiles > 0 && attempt < shore; + ++attempt, kind = (kind + 1) % 3) + fruitTiles -= + clump(CHERRY + kind, std::min(fruitTiles, 3 + int(context.bounded("resources", 3)))); } // Algae is seeded along the channels. They are only a tile or two wide, so any water tile can // seed a clump: channel water borders a passage's shore, where the algae stays harvestable, and // algae on water never blocks anyone on foot. -void seedAlgae(Map &map, GenerationContext &context, int algae) { - const int w = map.getW(), h = map.getH(); - std::vector water; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - if (map.isWater(x, y)) - water.emplace_back(x, y); - if (water.empty()) - return; - int remaining = int(water.size()) * algae / 400; - for (int attempt = 0; remaining > 0 && attempt < int(water.size()) / 4 + 16; ++attempt) - remaining -= placeResourceClump(map, context, water[context.bounded("resources", water.size())], - ALGA, 1); +void seedAlgae(Map &map, GenerationContext &context, int algae) +{ + const int w = map.getW(), h = map.getH(); + std::vector water; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (map.isWater(x, y)) + water.emplace_back(x, y); + if (water.empty()) + return; + int remaining = int(water.size()) * algae / 400; + for (int attempt = 0; remaining > 0 && attempt < int(water.size()) / 4 + 16; ++attempt) + remaining -= placeResourceClump(map, context, + water[context.bounded("resources", water.size())], ALGA, 1); } -bool generate(Game &game, GenerationContext &context) { - context.stage = "maze layout"; - const MazeOptions o(context.request); - Map &map = game.map; - const MazeGrid g = mazeGrid(map.getW(), map.getH(), o.cellSize); - const int teams = context.request.nbTeams; - const int half = passageHalf(g, o.channelWidth); - map.makeHomogenMap(WATER); - for (int i = 0; i < teams; ++i) - game.addTeam(); - if (g.columns < 2 || g.rows < 2 || half < kMinimumPassageHalf) { - context.detail = "the maze grid is too small for these settings"; - return false; - } - - const std::vector homes = chooseHomes(g, context, teams); - if (int(homes.size()) != teams) { - context.detail = "the maze grid has too few cul-de-sacs for every colony"; - return false; - } - std::vector isHome(g.cells(), 0); - for (int home : homes) - isHome[home] = 1; - std::vector open(2 * g.cells(), 0); - if (!carveSpanningTree(g, context, isHome, open)) { - context.detail = "the non-home cells did not form one connected maze"; - return false; - } - std::vector exitOf(g.cells(), -1); - for (int home : homes) { - int choices[4], count = 0; - for (int d = 0; d < 4; ++d) - if (!isHome[g.neighbour(home, d)]) - choices[count++] = d; - const int d = choices[context.bounded("maze", count)]; - open[g.edgeId(home, d)] = 1; - exitOf[home] = d; - } - addLoops(g, context, isHome, o.loopiness, open); - std::vector deadEnds; - for (int cell = 0; cell < g.cells(); ++cell) { - if (isHome[cell]) - continue; - int degree = 0, exit = -1; - for (int d = 0; d < 4; ++d) - if (open[g.edgeId(cell, d)]) { - ++degree; - exit = d; - } - if (degree == 1) { - deadEnds.push_back(cell); - exitOf[cell] = exit; - } - } - - // Every cell is a chamber as wide as a passage, and every open boundary a band of that width - // joining two chambers, so a run of passage reads as one continuous strip of grass. - context.stage = "maze terrain"; - std::vector passages, walls, roads; - for (int cell = 0; cell < g.cells(); ++cell) { - const int cx = g.centerX(cell), cy = g.centerY(cell); - passages.push_back({cx - half, cy - half, cx + half, cy + half}); - for (int d : {East, South}) { - if (!open[g.edgeId(cell, d)]) { - walls.push_back(wallFor(g, cell, d)); - continue; - } - // A road runs centre to centre, three tiles wide and one tile past each centre so roads - // meeting at a cell overlap. At a home it stops against the swarm's 4x4 footprint (the - // settlement is anchored on the centre), so the swarm still stands on grass. - const int next = g.neighbour(cell, d); - if (d == East) { - const int farX = g.centerX(next) < cx ? g.centerX(next) + g.width : g.centerX(next); - passages.push_back({cx - half, cy - half, farX + half, cy + half}); - roads.push_back({isHome[cell] ? cx + 2 : cx - 1, cy - 1, - isHome[next] ? farX - 3 : farX + 1, cy + 1}); - } else { - const int farY = g.centerY(next) < cy ? g.centerY(next) + g.height : g.centerY(next); - passages.push_back({cx - half, cy - half, cx + half, farY + half}); - roads.push_back({cx - 1, isHome[cell] ? cy + 2 : cy - 1, cx + 1, - isHome[next] ? farY - 3 : farY + 1}); - } - } - } - // All sand before any grass, so a grass core is never overwritten by a neighbouring ring. - for (const TileRect &r : passages) - stampRing(map, r); - for (const TileRect &r : walls) - stampRing(map, r); - for (const TileRect &r : passages) - stampCore(map, r); - for (const TileRect &r : walls) - stampCore(map, r); - // Then the roads, cut through the passages' grass: a sand road links every cell to the maze - // and can never be closed. Map::incResource only seeds a resource on its own terrain and - // buildings need pure grass, so nothing can grow over a road tile or be built on one. A road's - // tile rectangle takes undermap sand on (x0, x1] x (y0, y1], giving each of its tiles at least - // one sand corner and leaving every tile around it untouched. - // Without roads, passages are grass from shore to shore. - std::vector onRoad(size_t(map.getW()) * map.getH(), 0); - for (const TileRect &r : roads) { - if (!o.sandRoads) - break; - fillUndermap(map, r.x0 + 1, r.y0 + 1, r.x1 - r.x0, r.y1 - r.y0, SAND); - for (int y = r.y0; y <= r.y1; ++y) - for (int x = r.x0; x <= r.x1; ++x) - onRoad[size_t(map.normalizeY(y)) * map.getW() + map.normalizeX(x)] = 1; - } - map.rebuildTerrain(); - for (const TileRect &r : walls) - for (int y = r.y0; y <= r.y1; ++y) - for (int x = r.x0; x <= r.x1; ++x) { - const int nx = map.normalizeX(x), ny = map.normalizeY(y); - if (map.getTerrainType(nx, ny) != GRASS) { - context.detail = "a wall spine tile is not solid grass"; - return false; - } - map.setResource(nx, ny, STONE, 1); - } - - for (int team = 0; team < teams; ++team) { - const int cell = homes[team], c = g.column(cell), r = g.row(cell); - std::vector home(size_t(map.getW()) * map.getH(), 0); - for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) - for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) - if (!map.isWater(x, y)) - home[size_t(y) * map.getW() + x] = 1; - // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. - if (!placeSettlement(game, context, team, home, {g.centerX(cell) - 2, g.centerY(cell) - 2}, - "starts")) - return false; - } - - context.stage = "maze resources"; - for (int home : homes) - furnishHome(map, g, home, exitOf[home], half); - // Fruit is treasure: one patch at the far end of every dead end that isn't a home, with fruit - // types dealt round-robin so every kind is somewhere in the maze and a colony has to go and - // fight for the ones it lacks. Placed before the shore scatter, which works around them. - // Without treasure the same fruit is scattered along the passages' shores instead. - if (o.fruit > 0 && o.treasure) { - for (size_t i = deadEnds.size(); i > 1; --i) - std::swap(deadEnds[i - 1], deadEnds[context.bounded("resources", i)]); - const int firstType = context.bounded("resources", 3); - for (size_t i = 0; i < deadEnds.size(); ++i) - placeTreasure(map, g, deadEnds[i], exitOf[deadEnds[i]], half, o.fruit, - CHERRY + int((firstType + i) % 3)); - } - scatterThroughMaze(map, context, g, isHome, onRoad, half, o, - o.treasure ? 0 : int(deadEnds.size()) * o.fruit); - seedAlgae(map, context, o.algae); - return true; +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "maze layout"; + const MazeOptions o(context.request); + Map &map = game.map; + const MazeGrid g = mazeGrid(map.getW(), map.getH(), o.cellSize); + const int teams = context.request.nbTeams; + const int half = passageHalf(g, o.channelWidth); + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + if (g.columns < 2 || g.rows < 2 || half < kMinimumPassageHalf) + { + context.detail = "the maze grid is too small for these settings"; + return false; + } + + const std::vector homes = chooseHomes(g, context, teams); + if (int(homes.size()) != teams) + { + context.detail = "the maze grid has too few cul-de-sacs for every colony"; + return false; + } + std::vector isHome(g.cells(), 0); + for (int home : homes) + isHome[home] = 1; + std::vector open(2 * g.cells(), 0); + if (!carveSpanningTree(g, context, isHome, open)) + { + context.detail = "the non-home cells did not form one connected maze"; + return false; + } + std::vector exitOf(g.cells(), -1); + for (int home : homes) + { + int choices[4], count = 0; + for (int d = 0; d < 4; ++d) + if (!isHome[g.neighbour(home, d)]) + choices[count++] = d; + const int d = choices[context.bounded("maze", count)]; + open[g.edgeId(home, d)] = 1; + exitOf[home] = d; + } + addLoops(g, context, isHome, o.loopiness, open); + std::vector deadEnds; + for (int cell = 0; cell < g.cells(); ++cell) + { + if (isHome[cell]) + continue; + int degree = 0, exit = -1; + for (int d = 0; d < 4; ++d) + if (open[g.edgeId(cell, d)]) + { + ++degree; + exit = d; + } + if (degree == 1) + { + deadEnds.push_back(cell); + exitOf[cell] = exit; + } + } + + // Every cell is a chamber as wide as a passage, and every open boundary a band of that width + // joining two chambers, so a run of passage reads as one continuous strip of grass. + context.stage = "maze terrain"; + std::vector passages, walls, roads; + for (int cell = 0; cell < g.cells(); ++cell) + { + const int cx = g.centerX(cell), cy = g.centerY(cell); + passages.push_back({cx - half, cy - half, cx + half, cy + half}); + for (int d : {East, South}) + { + if (!open[g.edgeId(cell, d)]) + { + walls.push_back(wallFor(g, cell, d)); + continue; + } + // A road runs centre to centre, three tiles wide and one tile past each centre so roads + // meeting at a cell overlap. At a home it stops against the swarm's 4x4 footprint (the + // settlement is anchored on the centre), so the swarm still stands on grass. + const int next = g.neighbour(cell, d); + if (d == East) + { + const int farX = g.centerX(next) < cx ? g.centerX(next) + g.width : g.centerX(next); + passages.push_back({cx - half, cy - half, farX + half, cy + half}); + roads.push_back({isHome[cell] ? cx + 2 : cx - 1, cy - 1, + isHome[next] ? farX - 3 : farX + 1, cy + 1}); + } + else + { + const int farY = + g.centerY(next) < cy ? g.centerY(next) + g.height : g.centerY(next); + passages.push_back({cx - half, cy - half, cx + half, farY + half}); + roads.push_back({cx - 1, isHome[cell] ? cy + 2 : cy - 1, cx + 1, + isHome[next] ? farY - 3 : farY + 1}); + } + } + } + // All sand before any grass, so a grass core is never overwritten by a neighbouring ring. + for (const TileRect &r : passages) + stampRing(map, r); + for (const TileRect &r : walls) + stampRing(map, r); + for (const TileRect &r : passages) + stampCore(map, r); + for (const TileRect &r : walls) + stampCore(map, r); + // Then the roads, cut through the passages' grass: a sand road links every cell to the maze + // and can never be closed. Map::incResource only seeds a resource on its own terrain and + // buildings need pure grass, so nothing can grow over a road tile or be built on one. A road's + // tile rectangle takes undermap sand on (x0, x1] x (y0, y1], giving each of its tiles at least + // one sand corner and leaving every tile around it untouched. + // Without roads, passages are grass from shore to shore. + std::vector onRoad(size_t(map.getW()) * map.getH(), 0); + for (const TileRect &r : roads) + { + if (!o.sandRoads) + break; + fillUndermap(map, r.x0 + 1, r.y0 + 1, r.x1 - r.x0, r.y1 - r.y0, SAND); + for (int y = r.y0; y <= r.y1; ++y) + for (int x = r.x0; x <= r.x1; ++x) + onRoad[size_t(map.normalizeY(y)) * map.getW() + map.normalizeX(x)] = 1; + } + map.rebuildTerrain(); + for (const TileRect &r : walls) + for (int y = r.y0; y <= r.y1; ++y) + for (int x = r.x0; x <= r.x1; ++x) + { + const int nx = map.normalizeX(x), ny = map.normalizeY(y); + if (map.getTerrainType(nx, ny) != GRASS) + { + context.detail = "a wall spine tile is not solid grass"; + return false; + } + map.setResource(nx, ny, STONE, 1); + } + + for (int team = 0; team < teams; ++team) + { + const int cell = homes[team], c = g.column(cell), r = g.row(cell); + std::vector home(size_t(map.getW()) * map.getH(), 0); + for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) + for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) + if (!map.isWater(x, y)) + home[size_t(y) * map.getW() + x] = 1; + // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. + if (!placeSettlement(game, context, team, home, {g.centerX(cell) - 2, g.centerY(cell) - 2}, + "starts")) + return false; + } + + context.stage = "maze resources"; + for (int home : homes) + furnishHome(map, g, home, exitOf[home], half); + // Fruit is treasure: one patch at the far end of every dead end that isn't a home, with fruit + // types dealt round-robin so every kind is somewhere in the maze and a colony has to go and + // fight for the ones it lacks. Placed before the shore scatter, which works around them. + // Without treasure the same fruit is scattered along the passages' shores instead. + if (o.fruit > 0 && o.treasure) + { + for (size_t i = deadEnds.size(); i > 1; --i) + std::swap(deadEnds[i - 1], deadEnds[context.bounded("resources", i)]); + const int firstType = context.bounded("resources", 3); + for (size_t i = 0; i < deadEnds.size(); ++i) + placeTreasure(map, g, deadEnds[i], exitOf[deadEnds[i]], half, o.fruit, + CHERRY + int((firstType + i) % 3)); + } + scatterThroughMaze(map, context, g, isHome, onRoad, half, o, + o.treasure ? 0 : int(deadEnds.size()) * o.fruit); + seedAlgae(map, context, o.algae); + return true; } // The maze's defining guarantee, checked on the finished world rather than trusted: every // colony can walk to every other one. Water, buildings and every resource (including the wall // spines) block the flood; units don't, since they move. -std::string validateWorld(const Game &game, const GenerationContext &context) { - const Map &map = game.map; - const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; - std::vector reached(size_t(w) * h, 0); - std::vector queue; - queue.reserve(size_t(w) * h); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) { - reached[size_t(y) * w + x] = 1; - queue.push_back(y * w + x); - } - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const size_t n = size_t(ny) * w + nx; - if (!reached[n] && !map.isWater(nx, ny) && !map.isResource(nx, ny) && - map.getBuilding(nx, ny) == NOGBID) { - reached[n] = 1; - queue.push_back(int(n)); - } - } - } - std::vector teamReached(teams, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && reached[size_t(y) * w + x] && Unit::GIDtoTeam(gid) < teams) - teamReached[Unit::GIDtoTeam(gid)] = 1; - } - for (int team = 0; team < teams; ++team) - if (!teamReached[team]) - return "Colony " + std::to_string(team) + " cannot walk to colony 0 through the maze."; - return ""; +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + const Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + std::vector reached(size_t(w) * h, 0); + std::vector queue; + queue.reserve(size_t(w) * h); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) + { + reached[size_t(y) * w + x] = 1; + queue.push_back(y * w + x); + } + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const size_t n = size_t(ny) * w + nx; + if (!reached[n] && !map.isWater(nx, ny) && !map.isResource(nx, ny) && + map.getBuilding(nx, ny) == NOGBID) + { + reached[n] = 1; + queue.push_back(int(n)); + } + } + } + std::vector teamReached(teams, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && reached[size_t(y) * w + x] && Unit::GIDtoTeam(gid) < teams) + teamReached[Unit::GIDtoTeam(gid)] = 1; + } + for (int team = 0; team < teams; ++team) + if (!teamReached[team]) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 through the maze."; + return ""; } } // namespace MazeOptions::MazeOptions(const GenerationRequest &r) - : cellSize(r.option("cell-size")), channelWidth(r.option("channel-width")), - loopiness(r.option("loopiness")), corn(r.option("wheat")), wood(r.option("wood")), - stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")), - sandRoads(r.option("sand-roads") != 0), treasure(r.option("dead-end-treasure") != 0) {} - -GeneratorDefinition mazeDefinition() { - return {"maze", - 11, - "Maze", - 11, - false, - {{"cell-size", "Cell size", 24, 48, 1, 32, ControlGroup::Layout, false, false, - {24, 32, 40, 48}}, - // Open water on each side of a wall's stone line; passages widen to fill the rest. - {"channel-width", "Channel width", 1, 6, 1, 2, ControlGroup::Layout}, - {"loopiness", "Loopiness", 0, 50, 1, 5, ControlGroup::Layout, false, false, - {0, 5, 10, 20, 35, 50}}, - // Densities for the deposits scattered along the passages (per 256 shore tiles); - // homes always get the same fixed amounts. Fruit is the size, in tiles, of the - // treasure at every dead end that isn't a home. - {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, - {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, - {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, - {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}, - // Off, passages are grass from shore to shore, with no sand road down the middle. - GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), - // Off, the treasure's fruit is scattered along the passages' shores instead. - GeneratorControl::toggle("dead-end-treasure", "Treasure in dead ends", true, - ControlGroup::Resources)}, - generate, - true, - validate, - validateWorld}; + : cellSize(r.option("cell-size")), channelWidth(r.option("channel-width")), + loopiness(r.option("loopiness")), corn(r.option("wheat")), wood(r.option("wood")), + stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")), + sandRoads(r.option("sand-roads") != 0), treasure(r.option("dead-end-treasure") != 0) +{ +} + +GeneratorDefinition mazeDefinition() +{ + return {"maze", + 11, + "Maze", + 11, + false, + {{"cell-size", + "Cell size", + 24, + 48, + 1, + 32, + ControlGroup::Layout, + false, + false, + {24, 32, 40, 48}}, + // Open water on each side of a wall's stone line; passages widen to fill the rest. + {"channel-width", "Channel width", 1, 6, 1, 2, ControlGroup::Layout}, + {"loopiness", + "Loopiness", + 0, + 50, + 1, + 5, + ControlGroup::Layout, + false, + false, + {0, 5, 10, 20, 35, 50}}, + // Densities for the deposits scattered along the passages (per 256 shore tiles); + // homes always get the same fixed amounts. Fruit is the size, in tiles, of the + // treasure at every dead end that isn't a home. + {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, + {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, + {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, + {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, + {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}, + // Off, passages are grass from shore to shore, with no sand road down the middle. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // Off, the treasure's fruit is scattered along the passages' shores instead. + GeneratorControl::toggle("dead-end-treasure", "Treasure in dead ends", true, + ControlGroup::Resources)}, + generate, + true, + validate, + validateWorld}; } diff --git a/src/map/generator/generators/MazeGenerator.h b/src/map/generator/generators/MazeGenerator.h index 60fc19767..cc1bb83b8 100644 --- a/src/map/generator/generators/MazeGenerator.h +++ b/src/map/generator/generators/MazeGenerator.h @@ -2,10 +2,11 @@ #pragma once #include "GenerationRequest.h" #include "GeneratorDefinition.h" -struct MazeOptions { - int cellSize, channelWidth, loopiness; - int corn, wood, stone, algae, fruit; - bool sandRoads, treasure; - explicit MazeOptions(const GenerationRequest &r); +struct MazeOptions +{ + int cellSize, channelWidth, loopiness; + int corn, wood, stone, algae, fruit; + bool sandRoads, treasure; + explicit MazeOptions(const GenerationRequest &r); }; GeneratorDefinition mazeDefinition(); diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index 06335fca3..d5d3145e6 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -26,7 +26,8 @@ using namespace MapGeneration; // seam can't be seen. Terrain is written straight to the undermap with an order-independent beach // pass rather than Map::controlSand(), whose in-place raster scan makes shorelines depend on scan // order. -namespace { +namespace +{ constexpr double pi = 3.14159265358979323846; @@ -57,546 +58,620 @@ constexpr int kSwarmClearance = 2; constexpr int kUnreached = INT_MIN; -double unitDraw(std::mt19937 &rng) { return rng() / 4294967296.0; } +double unitDraw(std::mt19937 &rng) +{ + return rng() / 4294967296.0; +} // Belt coordinates: u runs along the belt and v across it. The belt follows the map's longer // axis, so a tall map gets one that wraps top to bottom. -struct Axes { - int width, height; - bool alongX; - int length() const { return alongX ? width : height; } - int breadth() const { return alongX ? height : width; } - int u(int x, int y) const { return alongX ? x : y; } - int v(int x, int y) const { return alongX ? y : x; } - int stepU(int dx, int dy) const { return alongX ? dx : dy; } +struct Axes +{ + int width, height; + bool alongX; + int length() const { return alongX ? width : height; } + int breadth() const { return alongX ? height : width; } + int u(int x, int y) const { return alongX ? x : y; } + int v(int x, int y) const { return alongX ? y : x; } + int stepU(int dx, int dy) const { return alongX ? dx : dy; } }; -Axes axesFor(int width, int height) { return {width, height, width >= height}; } +Axes axesFor(int width, int height) +{ + return {width, height, width >= height}; +} -int wrapSquare(int width, int height, int ax, int ay, int bx, int by) { - int dx = std::abs(ax - bx), dy = std::abs(ay - by); - dx = std::min(dx, width - dx); - dy = std::min(dy, height - dy); - return dx * dx + dy * dy; +int wrapSquare(int width, int height, int ax, int ay, int bx, int by) +{ + int dx = std::abs(ax - bx), dy = std::abs(ay - by); + dx = std::min(dx, width - dx); + dy = std::min(dy, height - dy); + return dx * dx + dy * dy; } // Breadth-first 8-neighbour steps on the torus from every source tile through open tiles; -1 // where the flood never arrives. std::vector stepsFrom(int width, int height, const std::vector &source, - const std::vector &open) { - std::vector dist(size_t(width) * height, -1); - std::vector queue; - queue.reserve(dist.size()); - for (size_t i = 0; i < dist.size(); ++i) - if (source[i]) { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % width, y = queue[head] / width; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t n = size_t((y + dy + height) % height) * width + (x + dx + width) % width; - if (dist[n] < 0 && open[n]) { - dist[n] = dist[queue[head]] + 1; - queue.push_back(int(n)); - } - } - } - return dist; + const std::vector &open) +{ + std::vector dist(size_t(width) * height, -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) + { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % width, y = queue[head] / width; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t n = + size_t((y + dy + height) % height) * width + (x + dx + width) % width; + if (dist[n] < 0 && open[n]) + { + dist[n] = dist[queue[head]] + 1; + queue.push_back(int(n)); + } + } + } + return dist; } // A smooth closed curve along the belt, one sample per tile. Its harmonics are whole numbers of // cycles per map length, so it meets itself exactly at the seam. Normalised to [-1, 1]; `slope` // receives its steepest step between neighbouring samples. -std::vector closedCurve(int length, double falloff, std::mt19937 &rng, double &slope) { - const int harmonics = std::clamp(length / 40, 2, 10); - std::vector amplitude(harmonics), phase(harmonics); - for (int k = 0; k < harmonics; ++k) { - amplitude[k] = (0.3 + 0.7 * unitDraw(rng)) / std::pow(k + 1.0, falloff); - phase[k] = 2 * pi * unitDraw(rng); - } - std::vector curve(length, 0.0); - double peak = 0; - for (int u = 0; u < length; ++u) { - for (int k = 0; k < harmonics; ++k) - curve[u] += amplitude[k] * std::sin(2 * pi * (k + 1) * u / length + phase[k]); - peak = std::max(peak, std::abs(curve[u])); - } - slope = 0; - if (peak <= 0) - return curve; - for (double &sample : curve) - sample /= peak; - for (int u = 0; u < length; ++u) - slope = std::max(slope, std::abs(curve[(u + 1) % length] - curve[u])); - return curve; +std::vector closedCurve(int length, double falloff, std::mt19937 &rng, double &slope) +{ + const int harmonics = std::clamp(length / 40, 2, 10); + std::vector amplitude(harmonics), phase(harmonics); + for (int k = 0; k < harmonics; ++k) + { + amplitude[k] = (0.3 + 0.7 * unitDraw(rng)) / std::pow(k + 1.0, falloff); + phase[k] = 2 * pi * unitDraw(rng); + } + std::vector curve(length, 0.0); + double peak = 0; + for (int u = 0; u < length; ++u) + { + for (int k = 0; k < harmonics; ++k) + curve[u] += amplitude[k] * std::sin(2 * pi * (k + 1) * u / length + phase[k]); + peak = std::max(peak, std::abs(curve[u])); + } + slope = 0; + if (peak <= 0) + return curve; + for (double &sample : curve) + sample /= peak; + for (int u = 0; u < length; ++u) + slope = std::max(slope, std::abs(curve[(u + 1) % length] - curve[u])); + return curve; } // Value noise whose lattice cells divide the map exactly (both sides are powers of two), so it // tiles the torus with no seam in either direction. Normalised to [-1, 1]. -std::vector torusNoise(int width, int height, std::mt19937 &rng) { - static const std::pair octaves[] = {{32, 0.4}, {16, 0.3}, {8, 0.2}, {4, 0.1}}; - std::vector field(size_t(width) * height, 0.0); - const auto ease = [](double t) { return t * t * (3 - 2 * t); }; - for (const auto &octave : octaves) { - const int cell = std::min({octave.first, width, height}); - const int columns = width / cell, rows = height / cell; - std::vector lattice(size_t(columns) * rows); - for (double &value : lattice) - value = 2 * unitDraw(rng) - 1; - for (int y = 0; y < height; ++y) { - const int r0 = y / cell, r1 = (r0 + 1) % rows; - const double ty = ease(double(y % cell) / cell); - for (int x = 0; x < width; ++x) { - const int c0 = x / cell, c1 = (c0 + 1) % columns; - const double tx = ease(double(x % cell) / cell); - const double top = lattice[r0 * columns + c0] * (1 - tx) + lattice[r0 * columns + c1] * tx; - const double bottom = - lattice[r1 * columns + c0] * (1 - tx) + lattice[r1 * columns + c1] * tx; - field[size_t(y) * width + x] += octave.second * (top * (1 - ty) + bottom * ty); - } - } - } - double peak = 0; - for (double value : field) - peak = std::max(peak, std::abs(value)); - std::vector result(field.size(), 0.0f); - if (peak > 0) - for (size_t i = 0; i < field.size(); ++i) - result[i] = float(field[i] / peak); - return result; +std::vector torusNoise(int width, int height, std::mt19937 &rng) +{ + static const std::pair octaves[] = {{32, 0.4}, {16, 0.3}, {8, 0.2}, {4, 0.1}}; + std::vector field(size_t(width) * height, 0.0); + const auto ease = [](double t) { return t * t * (3 - 2 * t); }; + for (const auto &octave : octaves) + { + const int cell = std::min({octave.first, width, height}); + const int columns = width / cell, rows = height / cell; + std::vector lattice(size_t(columns) * rows); + for (double &value : lattice) + value = 2 * unitDraw(rng) - 1; + for (int y = 0; y < height; ++y) + { + const int r0 = y / cell, r1 = (r0 + 1) % rows; + const double ty = ease(double(y % cell) / cell); + for (int x = 0; x < width; ++x) + { + const int c0 = x / cell, c1 = (c0 + 1) % columns; + const double tx = ease(double(x % cell) / cell); + const double top = + lattice[r0 * columns + c0] * (1 - tx) + lattice[r0 * columns + c1] * tx; + const double bottom = + lattice[r1 * columns + c0] * (1 - tx) + lattice[r1 * columns + c1] * tx; + field[size_t(y) * width + x] += octave.second * (top * (1 - ty) + bottom * ty); + } + } + } + double peak = 0; + for (double value : field) + peak = std::max(peak, std::abs(value)); + std::vector result(field.size(), 0.0f); + if (peak > 0) + for (size_t i = 0; i < field.size(); ++i) + result[i] = float(field[i] / peak); + return result; } -struct Belt { - Axes axes; - std::vector centre, halfWidth; // per u - std::vector coast; // per tile, in [-1, 1] - double roughness; // coast displacement, in tiles, where |coast| is 1 - - // Signed distance across the belt from its centre line, wrapped into [-breadth/2, breadth/2]. - double across(int x, int y) const { - return std::remainder(axes.v(x, y) - centre[axes.u(x, y)], double(axes.breadth())); - } - bool land(int x, int y) const { - return std::abs(across(x, y)) < - halfWidth[axes.u(x, y)] + roughness * coast[size_t(y) * axes.width + x]; - } +struct Belt +{ + Axes axes; + std::vector centre, halfWidth; // per u + std::vector coast; // per tile, in [-1, 1] + double roughness; // coast displacement, in tiles, where |coast| is 1 + + // Signed distance across the belt from its centre line, wrapped into [-breadth/2, breadth/2]. + double across(int x, int y) const + { + return std::remainder(axes.v(x, y) - centre[axes.u(x, y)], double(axes.breadth())); + } + bool land(int x, int y) const + { + return std::abs(across(x, y)) < + halfWidth[axes.u(x, y)] + roughness * coast[size_t(y) * axes.width + x]; + } }; // The belt's centre line bends and its half-width swings along its length; the coast is roughened // on top of that. Swing and roughness share one budget: the coast may bulge out until the ocean is // at its narrowest and pinch in until it reaches the spine, never further, so the belt can neither // break nor swallow the ocean whatever the controls say. -Belt shapeBelt(const Axes &axes, GenerationContext &context, const RingWorldOptions &options) { - std::mt19937 &rng = context.stream("belt"); - const int length = axes.length(); - const double breadth = axes.breadth(); - const double meanHalf = options.beltWidth / 100.0 * breadth / 2; - const double oceanHalf = std::max(kOceanHalf, 0.06 * breadth); - const double budget = - std::max(0.0, std::min(breadth / 2 - oceanHalf - meanHalf, meanHalf - kSpineHalf - 1)); - double swing = 0.22 * meanHalf; - double roughness = options.coastRoughness / 100.0 * 0.9 * meanHalf; - if (swing + roughness > budget) { - const double shrink = budget / (swing + roughness); - swing *= shrink; - roughness *= shrink; - } - double bendSlope = 0, swingSlope = 0; - const std::vector bend = closedCurve(length, 1.6, rng, bendSlope); - const std::vector widthCurve = closedCurve(length, 1.2, rng, swingSlope); - // A belt that doesn't wind still draws its curve, so the rest of the belt is unchanged. - double bendAmplitude = options.windingBelt ? 0.11 * breadth : 0.0; - if (bendSlope > 0) - bendAmplitude = std::min(bendAmplitude, kMaxBendSlope / bendSlope); - if (swingSlope > 0) - swing = std::min(swing, kMaxSwingSlope / swingSlope); - - Belt belt{axes, std::vector(length), std::vector(length), - torusNoise(axes.width, axes.height, rng), roughness}; - for (int u = 0; u < length; ++u) { - belt.centre[u] = breadth / 2 + bendAmplitude * bend[u]; - belt.halfWidth[u] = meanHalf + swing * widthCurve[u]; - } - return belt; +Belt shapeBelt(const Axes &axes, GenerationContext &context, const RingWorldOptions &options) +{ + std::mt19937 &rng = context.stream("belt"); + const int length = axes.length(); + const double breadth = axes.breadth(); + const double meanHalf = options.beltWidth / 100.0 * breadth / 2; + const double oceanHalf = std::max(kOceanHalf, 0.06 * breadth); + const double budget = + std::max(0.0, std::min(breadth / 2 - oceanHalf - meanHalf, meanHalf - kSpineHalf - 1)); + double swing = 0.22 * meanHalf; + double roughness = options.coastRoughness / 100.0 * 0.9 * meanHalf; + if (swing + roughness > budget) + { + const double shrink = budget / (swing + roughness); + swing *= shrink; + roughness *= shrink; + } + double bendSlope = 0, swingSlope = 0; + const std::vector bend = closedCurve(length, 1.6, rng, bendSlope); + const std::vector widthCurve = closedCurve(length, 1.2, rng, swingSlope); + // A belt that doesn't wind still draws its curve, so the rest of the belt is unchanged. + double bendAmplitude = options.windingBelt ? 0.11 * breadth : 0.0; + if (bendSlope > 0) + bendAmplitude = std::min(bendAmplitude, kMaxBendSlope / bendSlope); + if (swingSlope > 0) + swing = std::min(swing, kMaxSwingSlope / swingSlope); + + Belt belt{axes, std::vector(length), std::vector(length), + torusNoise(axes.width, axes.height, rng), roughness}; + for (int u = 0; u < length; ++u) + { + belt.centre[u] = breadth / 2 + bendAmplitude * bend[u]; + belt.halfWidth[u] = meanHalf + swing * widthCurve[u]; + } + return belt; } // Inland lakes for fertility. Each keeps kLakeShore tiles of land to the ocean, kLakeGap to other // lakes, and stays clear of the spine, so no lake or chain of lakes can cut the belt. Density is // lakes per 4096 tiles of belt. void carveLakes(std::vector &terrain, const Belt &belt, GenerationContext &context, - int density) { - const int width = belt.axes.width, height = belt.axes.height; - const size_t area = size_t(width) * height; - std::vector water(area), land(area); - std::vector dry; - for (size_t i = 0; i < area; ++i) { - water[i] = terrain[i] == WATER; - land[i] = !water[i]; - if (land[i]) - dry.push_back(int(i)); - } - if (density <= 0 || dry.empty()) - return; - const std::vector shore = stepsFrom(width, height, water, land); - const int wanted = int(std::lround(density * double(dry.size()) / 4096.0)); - const double scale = std::clamp(std::sqrt(belt.axes.breadth() / 128.0), 0.8, 1.6); - struct Lake { - int x, y; - double reach; - }; - std::vector lakes; - for (int attempt = 0; int(lakes.size()) < wanted && attempt < wanted * 40; ++attempt) { - const int at = dry[context.bounded("lakes", dry.size())]; - const int x = at % width, y = at / width; - const RadialShape shape((4 + context.bounded("lakes", 5)) * scale, 0.35, context, "lakes"); - const double reach = shape.maximumRadius(); - // The centre line leans at most kMaxBendSlope per tile, so a lake's far side can sit that much - // nearer the spine than its centre. - if (shore[at] < reach + kLakeShore || - std::abs(belt.across(x, y)) < kSpineHalf + (1 + kMaxBendSlope) * reach + 1) - continue; - bool clear = true; - for (const Lake &other : lakes) { - const double gap = reach + other.reach + kLakeGap; - clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; - } - if (!clear) - continue; - const int r = int(std::ceil(reach)); - for (int dy = -r; dy <= r; ++dy) - for (int dx = -r; dx <= r; ++dx) - if (std::hypot(double(dx), double(dy)) < shape.radiusAt(std::atan2(double(dy), double(dx)))) - terrain[size_t((y + dy + height) % height) * width + (x + dx + width) % width] = WATER; - lakes.push_back({x, y, reach}); - } + int density) +{ + const int width = belt.axes.width, height = belt.axes.height; + const size_t area = size_t(width) * height; + std::vector water(area), land(area); + std::vector dry; + for (size_t i = 0; i < area; ++i) + { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + if (land[i]) + dry.push_back(int(i)); + } + if (density <= 0 || dry.empty()) + return; + const std::vector shore = stepsFrom(width, height, water, land); + const int wanted = int(std::lround(density * double(dry.size()) / 4096.0)); + const double scale = std::clamp(std::sqrt(belt.axes.breadth() / 128.0), 0.8, 1.6); + struct Lake + { + int x, y; + double reach; + }; + std::vector lakes; + for (int attempt = 0; int(lakes.size()) < wanted && attempt < wanted * 40; ++attempt) + { + const int at = dry[context.bounded("lakes", dry.size())]; + const int x = at % width, y = at / width; + const RadialShape shape((4 + context.bounded("lakes", 5)) * scale, 0.35, context, "lakes"); + const double reach = shape.maximumRadius(); + // The centre line leans at most kMaxBendSlope per tile, so a lake's far side can sit that much + // nearer the spine than its centre. + if (shore[at] < reach + kLakeShore || + std::abs(belt.across(x, y)) < kSpineHalf + (1 + kMaxBendSlope) * reach + 1) + continue; + bool clear = true; + for (const Lake &other : lakes) + { + const double gap = reach + other.reach + kLakeGap; + clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; + } + if (!clear) + continue; + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (std::hypot(double(dx), double(dy)) < + shape.radiusAt(std::atan2(double(dy), double(dx)))) + terrain[size_t((y + dy + height) % height) * width + (x + dx + width) % width] = + WATER; + lakes.push_back({x, y, reach}); + } } -struct Island { - int x, y; - double reach; - std::vector tiles; +struct Island +{ + int x, y; + double reach; + std::vector tiles; }; // Small islands out in the open ocean, each well clear of the belt and of each other so they are // only ever reached by swimming. The control counts islands per 128x128 of map. std::vector raiseIslands(std::vector &terrain, int width, int height, - GenerationContext &context, int perStandardMap) { - std::vector islands; - if (perStandardMap <= 0) - return islands; - const size_t area = size_t(width) * height; - std::vector water(area), land(area); - std::vector sea; - for (size_t i = 0; i < area; ++i) { - water[i] = terrain[i] == WATER; - land[i] = !water[i]; - if (water[i]) - sea.push_back(int(i)); - } - if (sea.empty()) - return islands; - const std::vector offshore = stepsFrom(width, height, land, water); - const int wanted = std::max(1, int(std::lround(perStandardMap * double(area) / 16384.0))); - // Never smaller than on a 128-tile map: any smaller and the beach leaves no grass for a prize. - const double scale = std::clamp(std::sqrt(std::min(width, height) / 128.0), 1.0, 1.6); - for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 40; ++attempt) { - const int at = sea[context.bounded("islands", sea.size())]; - const int x = at % width, y = at / width; - const RadialShape shape((4 + context.bounded("islands", 3)) * scale, 0.3, context, "islands"); - const double reach = shape.maximumRadius(); - if (offshore[at] < reach + kIslandMoat) - continue; - bool clear = true; - for (const Island &other : islands) { - const double gap = reach + other.reach + kIslandMoat; - clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; - } - if (!clear) - continue; - Island island{x, y, reach, {}}; - const int r = int(std::ceil(reach)); - for (int dy = -r; dy <= r; ++dy) - for (int dx = -r; dx <= r; ++dx) - if (std::hypot(double(dx), double(dy)) < - shape.radiusAt(std::atan2(double(dy), double(dx)))) { - const int i = (y + dy + height) % height * width + (x + dx + width) % width; - terrain[i] = GRASS; - island.tiles.push_back(i); - } - islands.push_back(std::move(island)); - } - return islands; + GenerationContext &context, int perStandardMap) +{ + std::vector islands; + if (perStandardMap <= 0) + return islands; + const size_t area = size_t(width) * height; + std::vector water(area), land(area); + std::vector sea; + for (size_t i = 0; i < area; ++i) + { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + if (water[i]) + sea.push_back(int(i)); + } + if (sea.empty()) + return islands; + const std::vector offshore = stepsFrom(width, height, land, water); + const int wanted = std::max(1, int(std::lround(perStandardMap * double(area) / 16384.0))); + // Never smaller than on a 128-tile map: any smaller and the beach leaves no grass for a prize. + const double scale = std::clamp(std::sqrt(std::min(width, height) / 128.0), 1.0, 1.6); + for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 40; ++attempt) + { + const int at = sea[context.bounded("islands", sea.size())]; + const int x = at % width, y = at / width; + const RadialShape shape((4 + context.bounded("islands", 3)) * scale, 0.3, context, + "islands"); + const double reach = shape.maximumRadius(); + if (offshore[at] < reach + kIslandMoat) + continue; + bool clear = true; + for (const Island &other : islands) + { + const double gap = reach + other.reach + kIslandMoat; + clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; + } + if (!clear) + continue; + Island island{x, y, reach, {}}; + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (std::hypot(double(dx), double(dy)) < + shape.radiusAt(std::atan2(double(dy), double(dx)))) + { + const int i = (y + dy + height) % height * width + (x + dx + width) % width; + terrain[i] = GRASS; + island.tiles.push_back(i); + } + islands.push_back(std::move(island)); + } + return islands; } // Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), // so every land corner beside water becomes sand. Unlike Map::controlSand() this reads only the // original terrain, so the result doesn't depend on scan order and water is never eaten away. -void layBeaches(std::vector &terrain, int width, int height) { - const std::vector original(terrain); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - const size_t i = size_t(y) * width + x; - if (original[i] == WATER) - continue; - bool shore = false; - for (int dy = -1; dy <= 1 && !shore; ++dy) - for (int dx = -1; dx <= 1 && !shore; ++dx) - shore = original[size_t((y + dy + height) % height) * width + (x + dx + width) % width] == - WATER; - if (shore) - terrain[i] = SAND; - } +void layBeaches(std::vector &terrain, int width, int height) +{ + const std::vector original(terrain); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + const size_t i = size_t(y) * width + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[size_t((y + dy + height) % height) * width + + (x + dx + width) % width] == WATER; + if (shore) + terrain[i] = SAND; + } } // A swarm anchored at (x, y) needs pure grass under its 4x4 footprint and dry, unoccupied ground in // the ring around it where its workers will stand. -bool siteFits(const Map &map, int x, int y) { - for (int dy = -1; dy <= 4; ++dy) - for (int dx = -1; dx <= 4; ++dx) { - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const bool footprint = dx >= 0 && dx < 4 && dy >= 0 && dy < 4; - if (footprint ? !map.isGrass(nx, ny) : map.isWater(nx, ny)) - return false; - if (map.getBuilding(nx, ny) != NOGBID || map.getGroundUnit(nx, ny) != NOGUID) - return false; - } - return true; +bool siteFits(const Map &map, int x, int y) +{ + for (int dy = -1; dy <= 4; ++dy) + for (int dx = -1; dx <= 4; ++dx) + { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const bool footprint = dx >= 0 && dx < 4 && dy >= 0 && dy < 4; + if (footprint ? !map.isGrass(nx, ny) : map.isWater(nx, ny)) + return false; + if (map.getBuilding(nx, ny) != NOGBID || map.getGroundUnit(nx, ny) != NOGUID) + return false; + } + return true; } // Colonies are dealt evenly spaced slots around the ring from a random starting point, with a // little jitter, and alternate between the belt's two coasts (or all take one). Within its slot a // colony takes the site whose nearest water is closest to kHomeShore tiles away, so every start // has the same shore. -bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, bool bothCoasts) { - Map &map = game.map; - const Axes &axes = belt.axes; - const int width = axes.width, height = axes.height, length = axes.length(); - const int teams = context.request.nbTeams; - const size_t area = size_t(width) * height; - std::vector water(area), dry(area); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - water[size_t(y) * width + x] = map.isWater(x, y); - dry[size_t(y) * width + x] = !map.isWater(x, y); - } - const std::vector shore = stepsFrom(width, height, water, dry); - // Only the belt itself: an island or a stretch of rough coast cut off at sea can offer the same - // shore, but a colony there would have no land neighbours at all. The spine is always dry, so - // the belt is whatever land is reached from it. - std::vector spine(area, 0); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - spine[size_t(y) * width + x] = - !map.isWater(x, y) && std::abs(belt.across(x, y)) < kSpineHalf - 1; - const std::vector onBelt = stepsFrom(width, height, spine, dry); - const double slot = double(length) / teams; - const double first = context.bounded("colonies", 3600) / 3600.0 * length; - const int firstSide = int(context.bounded("colonies", 2)); - std::vector placed; - for (int team = 0; team < teams; ++team) { - const double jitter = (int(context.bounded("colonies", 2001)) - 1000) / 1000.0 * 0.08 * slot; - const double target = first + team * slot + jitter; - const double side = (firstSide + (bothCoasts ? team : 0)) % 2 ? 1.0 : -1.0; - const double spacing = 0.5 * slot; - int bestX = -1, bestY = -1; - double bestScore = std::numeric_limits::max(); - for (double reach : {0.12, 0.25, 0.5}) { - const double window = std::max(3.0, reach * slot); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - const double offset = - std::abs(std::remainder(axes.u(x, y) + 1.5 - target, double(length))); - if (offset > window || onBelt[size_t(y) * width + x] < 0 || !siteFits(map, x, y)) - continue; - bool crowded = false; - for (const MapGeneratorPoint &other : placed) - crowded = crowded || wrapSquare(width, height, x, y, other.x, other.y) < - spacing * spacing; - if (crowded) - continue; - int gap = INT_MAX; - for (int dy = 0; dy < 4; ++dy) - for (int dx = 0; dx < 4; ++dx) - gap = std::min(gap, shore[size_t(map.normalizeY(y + dy)) * width + - map.normalizeX(x + dx)]); - const double across = belt.across(map.normalizeX(x + 2), map.normalizeY(y + 2)); - const double score = - 3.0 * std::abs(gap - kHomeShore) + offset / window + (across * side < 0 ? 6.0 : 0.0); - if (score < bestScore) { - bestScore = score; - bestX = x; - bestY = y; - } - } - if (bestX >= 0) - break; - } - if (bestX < 0) { - context.detail = - "Colony " + std::to_string(team) + ": no room for a swarm on the belt near its slot"; - return false; - } - std::vector home(area, 0); - for (int dy = -3; dy <= 6; ++dy) - for (int dx = -3; dx <= 6; ++dx) { - const int nx = map.normalizeX(bestX + dx), ny = map.normalizeY(bestY + dy); - if (!map.isWater(nx, ny)) - home[size_t(ny) * width + nx] = 1; - } - if (!placeSettlement(game, context, team, home, MapGeneratorPoint(bestX, bestY), "starts")) - return false; - placed.emplace_back(bestX, bestY); - } - return true; +bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, bool bothCoasts) +{ + Map &map = game.map; + const Axes &axes = belt.axes; + const int width = axes.width, height = axes.height, length = axes.length(); + const int teams = context.request.nbTeams; + const size_t area = size_t(width) * height; + std::vector water(area), dry(area); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + water[size_t(y) * width + x] = map.isWater(x, y); + dry[size_t(y) * width + x] = !map.isWater(x, y); + } + const std::vector shore = stepsFrom(width, height, water, dry); + // Only the belt itself: an island or a stretch of rough coast cut off at sea can offer the same + // shore, but a colony there would have no land neighbours at all. The spine is always dry, so + // the belt is whatever land is reached from it. + std::vector spine(area, 0); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + spine[size_t(y) * width + x] = + !map.isWater(x, y) && std::abs(belt.across(x, y)) < kSpineHalf - 1; + const std::vector onBelt = stepsFrom(width, height, spine, dry); + const double slot = double(length) / teams; + const double first = context.bounded("colonies", 3600) / 3600.0 * length; + const int firstSide = int(context.bounded("colonies", 2)); + std::vector placed; + for (int team = 0; team < teams; ++team) + { + const double jitter = + (int(context.bounded("colonies", 2001)) - 1000) / 1000.0 * 0.08 * slot; + const double target = first + team * slot + jitter; + const double side = (firstSide + (bothCoasts ? team : 0)) % 2 ? 1.0 : -1.0; + const double spacing = 0.5 * slot; + int bestX = -1, bestY = -1; + double bestScore = std::numeric_limits::max(); + for (double reach : {0.12, 0.25, 0.5}) + { + const double window = std::max(3.0, reach * slot); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + const double offset = + std::abs(std::remainder(axes.u(x, y) + 1.5 - target, double(length))); + if (offset > window || onBelt[size_t(y) * width + x] < 0 || + !siteFits(map, x, y)) + continue; + bool crowded = false; + for (const MapGeneratorPoint &other : placed) + crowded = crowded || wrapSquare(width, height, x, y, other.x, other.y) < + spacing * spacing; + if (crowded) + continue; + int gap = INT_MAX; + for (int dy = 0; dy < 4; ++dy) + for (int dx = 0; dx < 4; ++dx) + gap = std::min(gap, shore[size_t(map.normalizeY(y + dy)) * width + + map.normalizeX(x + dx)]); + const double across = belt.across(map.normalizeX(x + 2), map.normalizeY(y + 2)); + const double score = 3.0 * std::abs(gap - kHomeShore) + offset / window + + (across * side < 0 ? 6.0 : 0.0); + if (score < bestScore) + { + bestScore = score; + bestX = x; + bestY = y; + } + } + if (bestX >= 0) + break; + } + if (bestX < 0) + { + context.detail = "Colony " + std::to_string(team) + + ": no room for a swarm on the belt near its slot"; + return false; + } + std::vector home(area, 0); + for (int dy = -3; dy <= 6; ++dy) + for (int dx = -3; dx <= 6; ++dx) + { + const int nx = map.normalizeX(bestX + dx), ny = map.normalizeY(bestY + dy); + if (!map.isWater(nx, ny)) + home[size_t(ny) * width + nx] = 1; + } + if (!placeSettlement(game, context, team, home, MapGeneratorPoint(bestX, bestY), "starts")) + return false; + placed.emplace_back(bestX, bestY); + } + return true; } // Grows a compact patch of one resource outward from a seed tile, breadth-first over the four // cardinal neighbours, onto tiles the predicate allows. Returns how many tiles it placed. template -int growPatch(Map &map, int seed, int type, int count, Eligible eligible) { - const int width = map.getW(), height = map.getH(); - std::vector queued(size_t(width) * height, 0); - std::vector frontier{seed}; - queued[seed] = 1; - int placed = 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - for (size_t head = 0; head < frontier.size() && placed < count; ++head) { - const int i = frontier[head], x = i % width, y = i / width; - if (!eligible(i) || !map.isResourceAllowed(x, y, type)) - continue; - map.setResource(x, y, type, 1); - ++placed; - for (const auto &step : steps) { - const int n = map.normalizeY(y + step[1]) * width + map.normalizeX(x + step[0]); - if (!queued[n]) { - queued[n] = 1; - frontier.push_back(n); - } - } - } - return placed; +int growPatch(Map &map, int seed, int type, int count, Eligible eligible) +{ + const int width = map.getW(), height = map.getH(); + std::vector queued(size_t(width) * height, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) + { + const int i = frontier[head], x = i % width, y = i / width; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) + { + const int n = map.normalizeY(y + step[1]) * width + map.normalizeX(x + step[0]); + if (!queued[n]) + { + queued[n] = 1; + frontier.push_back(n); + } + } + } + return placed; } // Every home's starter kit: a wheat and a wood patch on the most fertile ground a short walk from // the swarm, and a little stone further inland. -void furnishHomes(Game &game, GenerationContext &context) { - Map &map = game.map; - const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; - const size_t area = size_t(width) * height; - std::vector water(area), dry(area), reserved(area, 0); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - water[size_t(y) * width + x] = map.isWater(x, y); - dry[size_t(y) * width + x] = !map.isWater(x, y); - } - for (int team = 0; team < teams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - reserved[size_t(map.normalizeY(context.bootY[team] + dy)) * width + - map.normalizeX(context.bootX[team] + dx)] = 1; - const std::vector shore = stepsFrom(width, height, water, dry); - const auto eligible = [&](int i) { - const int x = i % width, y = i / width; - return !reserved[i] && map.isGrass(x, y) && !map.isResource(x, y) && - map.getBuilding(x, y) == NOGBID && map.getGroundUnit(x, y) == NOGUID; - }; - for (int team = 0; team < teams; ++team) { - std::vector footprint(area, 0); - for (int dy = 0; dy < 4; ++dy) - for (int dx = 0; dx < 4; ++dx) - footprint[size_t(map.normalizeY(context.bootY[team] + dy)) * width + - map.normalizeX(context.bootX[team] + dx)] = 1; - const std::vector walk = stepsFrom(width, height, footprint, dry); - // Wettest (or, for stone, driest) eligible tile in a walking band, nearest first on ties. - const auto pick = [&](int nearest, int farthest, bool wet, int avoid) { - int best = -1; - for (int i = 0; i < int(area); ++i) { - if (walk[i] < nearest || walk[i] > farthest || !eligible(i)) - continue; - if (avoid >= 0 && - wrapSquare(width, height, i % width, i / width, avoid % width, avoid / width) < 36) - continue; - const bool better = - best < 0 || (wet ? shore[i] < shore[best] : shore[i] > shore[best]) || - (shore[i] == shore[best] && walk[i] < walk[best]); - if (better) - best = i; - } - return best; - }; - const int wheat = pick(5, 9, true, -1); - if (wheat >= 0) - growPatch(map, wheat, CORN, kHomeWheat, eligible); - const int wood = pick(5, 9, true, wheat); - if (wood >= 0) - growPatch(map, wood, WOOD, kHomeWood, eligible); - const int stone = pick(7, 12, false, -1); - if (stone >= 0) - growPatch(map, stone, STONE, kHomeStone, eligible); - } +void furnishHomes(Game &game, GenerationContext &context) +{ + Map &map = game.map; + const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; + const size_t area = size_t(width) * height; + std::vector water(area), dry(area), reserved(area, 0); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + water[size_t(y) * width + x] = map.isWater(x, y); + dry[size_t(y) * width + x] = !map.isWater(x, y); + } + for (int team = 0; team < teams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + reserved[size_t(map.normalizeY(context.bootY[team] + dy)) * width + + map.normalizeX(context.bootX[team] + dx)] = 1; + const std::vector shore = stepsFrom(width, height, water, dry); + const auto eligible = [&](int i) + { + const int x = i % width, y = i / width; + return !reserved[i] && map.isGrass(x, y) && !map.isResource(x, y) && + map.getBuilding(x, y) == NOGBID && map.getGroundUnit(x, y) == NOGUID; + }; + for (int team = 0; team < teams; ++team) + { + std::vector footprint(area, 0); + for (int dy = 0; dy < 4; ++dy) + for (int dx = 0; dx < 4; ++dx) + footprint[size_t(map.normalizeY(context.bootY[team] + dy)) * width + + map.normalizeX(context.bootX[team] + dx)] = 1; + const std::vector walk = stepsFrom(width, height, footprint, dry); + // Wettest (or, for stone, driest) eligible tile in a walking band, nearest first on ties. + const auto pick = [&](int nearest, int farthest, bool wet, int avoid) + { + int best = -1; + for (int i = 0; i < int(area); ++i) + { + if (walk[i] < nearest || walk[i] > farthest || !eligible(i)) + continue; + if (avoid >= 0 && wrapSquare(width, height, i % width, i / width, avoid % width, + avoid / width) < 36) + continue; + const bool better = best < 0 || + (wet ? shore[i] < shore[best] : shore[i] > shore[best]) || + (shore[i] == shore[best] && walk[i] < walk[best]); + if (better) + best = i; + } + return best; + }; + const int wheat = pick(5, 9, true, -1); + if (wheat >= 0) + growPatch(map, wheat, CORN, kHomeWheat, eligible); + const int wood = pick(5, 9, true, wheat); + if (wood >= 0) + growPatch(map, wood, WOOD, kHomeWood, eligible); + const int stone = pick(7, 12, false, -1); + if (stone >= 0) + growPatch(map, stone, STONE, kHomeStone, eligible); + } } // Each island carries one themed prize, as on Fjord continent's outliers. -void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) { - const int width = map.getW(); - for (const Island &island : islands) { - std::vector grass; - for (int i : island.tiles) - if (map.isGrass(i % width, i / width)) - grass.emplace_back(i % width, i / width); - if (grass.empty()) - continue; - MapGeneratorPoint centre(island.x, island.y); - if (!map.isGrass(centre.x, centre.y)) - centre = grass[context.bounded("islands", grass.size())]; - switch (context.bounded("islands", 3)) { - case 0: - placeResourceClump(map, context, centre, STONE, 2); - break; - case 1: - placeResourceClump(map, context, centre, CHERRY + int(context.bounded("islands", 3)), 2); - break; - default: - placeResourceClump(map, context, centre, CORN, 2); - break; - } - } +void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) +{ + const int width = map.getW(); + for (const Island &island : islands) + { + std::vector grass; + for (int i : island.tiles) + if (map.isGrass(i % width, i / width)) + grass.emplace_back(i % width, i / width); + if (grass.empty()) + continue; + MapGeneratorPoint centre(island.x, island.y); + if (!map.isGrass(centre.x, centre.y)) + centre = grass[context.bounded("islands", grass.size())]; + switch (context.bounded("islands", 3)) + { + case 0: + placeResourceClump(map, context, centre, STONE, 2); + break; + case 1: + placeResourceClump(map, context, centre, CHERRY + int(context.bounded("islands", 3)), + 2); + break; + default: + placeResourceClump(map, context, centre, CORN, 2); + break; + } + } } // Algae in the shallows along every coast, the ocean's and the lakes' alike: close enough to shore // to regrow and to be harvested. -void seedAlgae(Map &map, GenerationContext &context, int algaePercent) { - const int width = map.getW(), height = map.getH(); - const size_t area = size_t(width) * height; - std::vector water(area), dry(area); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - water[size_t(y) * width + x] = map.isWater(x, y); - dry[size_t(y) * width + x] = !map.isWater(x, y); - } - const std::vector offshore = stepsFrom(width, height, dry, water); - std::vector shallows; - for (int i = 0; i < int(area); ++i) - if (offshore[i] >= 2 && offshore[i] <= 5) - shallows.emplace_back(i % width, i / width); - if (shallows.empty()) - return; - for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) - placeResourceClump(map, context, shallows[context.bounded("resources", shallows.size())], - ALGA, 1); +void seedAlgae(Map &map, GenerationContext &context, int algaePercent) +{ + const int width = map.getW(), height = map.getH(); + const size_t area = size_t(width) * height; + std::vector water(area), dry(area); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + water[size_t(y) * width + x] = map.isWater(x, y); + dry[size_t(y) * width + x] = !map.isWater(x, y); + } + const std::vector offshore = stepsFrom(width, height, dry, water); + std::vector shallows; + for (int i = 0; i < int(area); ++i) + if (offshore[i] >= 2 && offshore[i] <= 5) + shallows.emplace_back(i % width, i / width); + if (shallows.empty()) + return; + for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) + placeResourceClump(map, context, shallows[context.bounded("resources", shallows.size())], + ALGA, 1); } -void clearAroundSwarms(Map &map, const GenerationContext &context) { - for (int team = 0; team < context.request.nbTeams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) { - const int x = map.normalizeX(context.bootX[team] + dx); - const int y = map.normalizeY(context.bootY[team] + dy); - if (map.isResource(x, y)) - map.setNoResource(x, y, 1); - } +void clearAroundSwarms(Map &map, const GenerationContext &context) +{ + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + { + const int x = map.normalizeX(context.bootX[team] + dx); + const int y = map.normalizeY(context.bootY[team] + dy); + if (map.isResource(x, y)) + map.setNoResource(x, y, 1); + } } // Deposits may land anywhere, and stone is never cleared in play, so a band of them could wall @@ -605,168 +680,183 @@ void clearAroundSwarms(Map &map, const GenerationContext &context) { // impassable) is found in the belt's unrolled cover, and only the deposits on it are cleared. The // last walk crosses the seam back to the first colony, so together they close one loop all the way // around the map. Almost always nothing is in the way and nothing is cleared. -bool openBeltRoad(Game &game, GenerationContext &context, const Axes &axes) { - Map &map = game.map; - const int width = map.getW(), height = map.getH(), length = axes.length(); - const int teams = context.request.nbTeams; - const int area = width * height; - constexpr int kLayers = 4; // windings -1 to 2 along the belt - std::vector> workers(teams); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); - } - std::vector> order; - for (int team = 0; team < teams; ++team) - order.emplace_back( - std::fmod(axes.u(context.bootX[team], context.bootY[team]) + 2.0, double(length)), team); - std::sort(order.begin(), order.end()); - - std::vector cost(size_t(kLayers) * area), parent(size_t(kLayers) * area); - std::vector done(size_t(kLayers) * area); - std::vector goal(area); - // The winding that puts tile i nearest to a position along the unrolled belt. - const auto windingNear = [&](int i, double position) { - return int(std::lround((position - axes.u(i % width, i / width)) / length)); - }; - for (int k = 0; k < teams; ++k) { - const double fromU = order[k].first; - const double toU = order[(k + 1) % teams].first + (k + 1 == teams ? length : 0); - const int from = order[k].second, to = order[(k + 1) % teams].second; - std::fill(cost.begin(), cost.end(), INT_MAX); - std::fill(parent.begin(), parent.end(), -1); - std::fill(done.begin(), done.end(), 0); - std::fill(goal.begin(), goal.end(), kUnreached); - std::deque queue; - for (int i : workers[from]) { - const int w = windingNear(i, fromU); - if (w >= -1 && w < kLayers - 1) { - cost[(w + 1) * area + i] = 0; - queue.push_back((w + 1) * area + i); - } - } - for (int i : workers[to]) - goal[i] = windingNear(i, toU); - int reached = -1; - while (!queue.empty() && reached < 0) { - const int node = queue.front(); - queue.pop_front(); - if (done[node]) - continue; - done[node] = 1; - const int w = node / area - 1, i = node % area, x = i % width, y = i / width; - if (goal[i] == w) { - reached = node; - break; - } - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (!dx && !dy) - continue; - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID) - continue; - const int step = axes.u(x, y) + axes.stepU(dx, dy); - const int nw = w + (step < 0 ? -1 : step >= length ? 1 : 0); - if (nw < -1 || nw >= kLayers - 1) - continue; - const int next = (nw + 1) * area + ny * width + nx; - const int nextCost = cost[node] + (map.isResource(nx, ny) ? 1 : 0); - if (nextCost < cost[next]) { - cost[next] = nextCost; - parent[next] = node; - if (nextCost == cost[node]) - queue.push_front(next); - else - queue.push_back(next); - } - } - } - if (reached < 0) { - context.detail = "Colony " + std::to_string(from) + " has no way along the belt to colony " + - std::to_string(to); - return false; - } - for (int node = reached; node >= 0; node = parent[node]) { - const int i = node % area; - if (map.isResource(i % width, i / width)) - map.setNoResource(i % width, i / width, 1); - } - } - return true; +bool openBeltRoad(Game &game, GenerationContext &context, const Axes &axes) +{ + Map &map = game.map; + const int width = map.getW(), height = map.getH(), length = axes.length(); + const int teams = context.request.nbTeams; + const int area = width * height; + constexpr int kLayers = 4; // windings -1 to 2 along the belt + std::vector> workers(teams); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); + } + std::vector> order; + for (int team = 0; team < teams; ++team) + order.emplace_back( + std::fmod(axes.u(context.bootX[team], context.bootY[team]) + 2.0, double(length)), + team); + std::sort(order.begin(), order.end()); + + std::vector cost(size_t(kLayers) * area), parent(size_t(kLayers) * area); + std::vector done(size_t(kLayers) * area); + std::vector goal(area); + // The winding that puts tile i nearest to a position along the unrolled belt. + const auto windingNear = [&](int i, double position) + { return int(std::lround((position - axes.u(i % width, i / width)) / length)); }; + for (int k = 0; k < teams; ++k) + { + const double fromU = order[k].first; + const double toU = order[(k + 1) % teams].first + (k + 1 == teams ? length : 0); + const int from = order[k].second, to = order[(k + 1) % teams].second; + std::fill(cost.begin(), cost.end(), INT_MAX); + std::fill(parent.begin(), parent.end(), -1); + std::fill(done.begin(), done.end(), 0); + std::fill(goal.begin(), goal.end(), kUnreached); + std::deque queue; + for (int i : workers[from]) + { + const int w = windingNear(i, fromU); + if (w >= -1 && w < kLayers - 1) + { + cost[(w + 1) * area + i] = 0; + queue.push_back((w + 1) * area + i); + } + } + for (int i : workers[to]) + goal[i] = windingNear(i, toU); + int reached = -1; + while (!queue.empty() && reached < 0) + { + const int node = queue.front(); + queue.pop_front(); + if (done[node]) + continue; + done[node] = 1; + const int w = node / area - 1, i = node % area, x = i % width, y = i / width; + if (goal[i] == w) + { + reached = node; + break; + } + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID) + continue; + const int step = axes.u(x, y) + axes.stepU(dx, dy); + const int nw = w + (step < 0 ? -1 : step >= length ? 1 : 0); + if (nw < -1 || nw >= kLayers - 1) + continue; + const int next = (nw + 1) * area + ny * width + nx; + const int nextCost = cost[node] + (map.isResource(nx, ny) ? 1 : 0); + if (nextCost < cost[next]) + { + cost[next] = nextCost; + parent[next] = node; + if (nextCost == cost[node]) + queue.push_front(next); + else + queue.push_back(next); + } + } + } + if (reached < 0) + { + context.detail = "Colony " + std::to_string(from) + + " has no way along the belt to colony " + std::to_string(to); + return false; + } + for (int node = reached; node >= 0; node = parent[node]) + { + const int i = node % area; + if (map.isResource(i % width, i / width)) + map.setNoResource(i % width, i / width, 1); + } + } + return true; } -bool generate(Game &game, GenerationContext &context) { - context.stage = "ring layout"; - const RingWorldOptions options(context.request); - Map &map = game.map; - const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; - map.makeHomogenMap(WATER); - for (int i = 0; i < teams; ++i) - game.addTeam(); - const Axes axes = axesFor(width, height); - if (teams < 1 || axes.length() < kBeltPerColony * teams) { - context.detail = "the belt is too short for this many colonies"; - return false; - } - - context.stage = "ring terrain"; - const Belt belt = shapeBelt(axes, context, options); - std::vector terrain(size_t(width) * height, WATER); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - if (belt.land(x, y)) - terrain[size_t(y) * width + x] = GRASS; - carveLakes(terrain, belt, context, options.lakeDensity); - const std::vector islands = - raiseIslands(terrain, width, height, context, options.resourceIslands); - layBeaches(terrain, width, height); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * width + x])); - map.rebuildTerrain(); - - context.stage = "ring colonies"; - if (!placeColonies(game, context, belt, options.bothCoasts)) - return false; - - context.stage = "ring resources"; - furnishHomes(game, context); - stockIslands(map, context, islands); - // The same ambient layer as Fjord continent: corn:wood 2:1, some stone, rare fruit. Algae is - // seeded along the shallows below instead, since the shared band only scatters over land. - scatterResources(game, context, - {/*corn=*/int(scaledCount(24, options.wheat)), - /*wood=*/int(scaledCount(12, options.wood)), - /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, - /*fruit=*/int(scaledCount(3, options.fruit))}); - seedAlgae(map, context, options.algae); - clearAroundSwarms(map, context); - guaranteeStartingResources(game, context, 24, 32, 6); - clearAroundSwarms(map, context); - - // The scatter above is sized by the resource amounts, and at the top of their range it can wall - // a colony into its own clearing with nowhere to build — which the guarantee doesn't address, - // since a colony buried in wheat has wheat at its feet. At any non-default amount, open such a - // colony back up, then guarantee and clear again as above. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100 || options.fruit != 100) { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32, 6); - clearAroundSwarms(map, context); - } - - context.stage = "ring road"; - return openBeltRoad(game, context, axes); +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "ring layout"; + const RingWorldOptions options(context.request); + Map &map = game.map; + const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Axes axes = axesFor(width, height); + if (teams < 1 || axes.length() < kBeltPerColony * teams) + { + context.detail = "the belt is too short for this many colonies"; + return false; + } + + context.stage = "ring terrain"; + const Belt belt = shapeBelt(axes, context, options); + std::vector terrain(size_t(width) * height, WATER); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + if (belt.land(x, y)) + terrain[size_t(y) * width + x] = GRASS; + carveLakes(terrain, belt, context, options.lakeDensity); + const std::vector islands = + raiseIslands(terrain, width, height, context, options.resourceIslands); + layBeaches(terrain, width, height); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * width + x])); + map.rebuildTerrain(); + + context.stage = "ring colonies"; + if (!placeColonies(game, context, belt, options.bothCoasts)) + return false; + + context.stage = "ring resources"; + furnishHomes(game, context); + stockIslands(map, context, islands); + // The same ambient layer as Fjord continent: corn:wood 2:1, some stone, rare fruit. Algae is + // seeded along the shallows below instead, since the shared band only scatters over land. + scatterResources(game, context, + {/*corn=*/int(scaledCount(24, options.wheat)), + /*wood=*/int(scaledCount(12, options.wood)), + /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, + /*fruit=*/int(scaledCount(3, options.fruit))}); + seedAlgae(map, context, options.algae); + clearAroundSwarms(map, context); + guaranteeStartingResources(game, context, 24, 32, 6); + clearAroundSwarms(map, context); + + // The scatter above is sized by the resource amounts, and at the top of their range it can wall + // a colony into its own clearing with nowhere to build — which the guarantee doesn't address, + // since a colony buried in wheat has wheat at its feet. At any non-default amount, open such a + // colony back up, then guarantee and clear again as above. At the defaults none of this runs. + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || + options.algae != 100 || options.fruit != 100) + { + openCrampedStarts(game, context); + guaranteeStartingResources(game, context, 24, 32, 6); + clearAroundSwarms(map, context); + } + + context.stage = "ring road"; + return openBeltRoad(game, context, axes); } -std::string validate(const GenerationRequest &r) { - const int length = std::max(1 << r.wDec, 1 << r.hDec); - if (r.nbTeams < 1 || length < kBeltPerColony * r.nbTeams) - return "The belt is too short for this many colonies; use a longer map or fewer colonies."; - return ""; +std::string validate(const GenerationRequest &r) +{ + const int length = std::max(1 << r.wDec, 1 << r.hDec); + if (r.nbTeams < 1 || length < kBeltPerColony * r.nbTeams) + return "The belt is too short for this many colonies; use a longer map or fewer colonies."; + return ""; } // Floods open tiles from the sources while counting how often each step crosses the seam along the @@ -775,41 +865,48 @@ std::string validate(const GenerationRequest &r) { // disjoint regions so each tile is labelled once; `reached` receives this flood's tiles. template bool floodAround(const Map &map, const Axes &axes, const std::vector &sources, Open open, - std::vector &winding, std::vector &reached) { - const int width = map.getW(), length = axes.length(); - reached.clear(); - if (sources.empty()) - return false; - const int anchor = axes.u(sources[0] % width, sources[0] / width); - for (int i : sources) { - if (winding[i] != kUnreached) - continue; - // Each source starts at its image nearest the first, so a colony astride the seam is consistent. - winding[i] = int(std::lround(double(anchor - axes.u(i % width, i / width)) / length)); - reached.push_back(i); - } - bool wraps = false; - for (size_t head = 0; head < reached.size(); ++head) { - const int i = reached[head], x = i % width, y = i / width, w = winding[i]; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (!dx && !dy) - continue; - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - if (!open(nx, ny)) - continue; - const int step = axes.u(x, y) + axes.stepU(dx, dy); - const int nw = w + (step < 0 ? -1 : step >= length ? 1 : 0); - const int n = ny * width + nx; - if (winding[n] == kUnreached) { - winding[n] = nw; - reached.push_back(n); - } else if (winding[n] != nw) { - wraps = true; - } - } - } - return wraps; + std::vector &winding, std::vector &reached) +{ + const int width = map.getW(), length = axes.length(); + reached.clear(); + if (sources.empty()) + return false; + const int anchor = axes.u(sources[0] % width, sources[0] / width); + for (int i : sources) + { + if (winding[i] != kUnreached) + continue; + // Each source starts at its image nearest the first, so a colony astride the seam is consistent. + winding[i] = int(std::lround(double(anchor - axes.u(i % width, i / width)) / length)); + reached.push_back(i); + } + bool wraps = false; + for (size_t head = 0; head < reached.size(); ++head) + { + const int i = reached[head], x = i % width, y = i / width, w = winding[i]; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + if (!open(nx, ny)) + continue; + const int step = axes.u(x, y) + axes.stepU(dx, dy); + const int nw = w + (step < 0 ? -1 : step >= length ? 1 : 0); + const int n = ny * width + nx; + if (winding[n] == kUnreached) + { + winding[n] = nw; + reached.push_back(n); + } + else if (winding[n] != nw) + { + wraps = true; + } + } + } + return wraps; } // The ring's defining guarantees, checked on the finished world rather than trusted. Walking from @@ -817,83 +914,88 @@ bool floodAround(const Map &map, const Axes &axes, const std::vector &sourc // move) must reach every other colony and must close a loop around the map along the belt. And the // seas on either side of the belt must be one ocean: some body of water wraps around the map // beside it, so the belt can't have swallowed the ocean anywhere. -std::string validateWorld(const Game &game, const GenerationContext &context) { - const Map &map = game.map; - const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; - const Axes axes = axesFor(width, height); - const size_t area = size_t(width) * height; - std::vector> workers(std::max(teams, 1)); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); - } - if (teams < 1 || workers[0].empty()) - return "Colony 0 has no workers to walk the belt."; - std::vector winding(area, kUnreached), reached; - const bool beltWraps = floodAround( - map, axes, workers[0], - [&map](int x, int y) { - return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - }, - winding, reached); - for (int team = 1; team < teams; ++team) { - bool arrived = false; - for (int i : workers[team]) - arrived = arrived || winding[i] != kUnreached; - if (!arrived) - return "Colony " + std::to_string(team) + " cannot walk to colony 0 along the belt."; - } - if (!beltWraps) - return "The walkable belt does not reach all the way around the map."; - - std::fill(winding.begin(), winding.end(), kUnreached); - bool oceanWraps = false; - for (int i = 0; i < int(area) && !oceanWraps; ++i) - if (winding[i] == kUnreached && map.isWater(i % width, i / width)) - oceanWraps = floodAround( - map, axes, {i}, [&map](int x, int y) { return map.isWater(x, y); }, winding, reached); - if (!oceanWraps) - return "The seas beside the belt do not meet around the map."; - return ""; +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + const Map &map = game.map; + const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; + const Axes axes = axesFor(width, height); + const size_t area = size_t(width) * height; + std::vector> workers(std::max(teams, 1)); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); + } + if (teams < 1 || workers[0].empty()) + return "Colony 0 has no workers to walk the belt."; + std::vector winding(area, kUnreached), reached; + const bool beltWraps = floodAround( + map, axes, workers[0], [&map](int x, int y) + { return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; }, + winding, reached); + for (int team = 1; team < teams; ++team) + { + bool arrived = false; + for (int i : workers[team]) + arrived = arrived || winding[i] != kUnreached; + if (!arrived) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 along the belt."; + } + if (!beltWraps) + return "The walkable belt does not reach all the way around the map."; + + std::fill(winding.begin(), winding.end(), kUnreached); + bool oceanWraps = false; + for (int i = 0; i < int(area) && !oceanWraps; ++i) + if (winding[i] == kUnreached && map.isWater(i % width, i / width)) + oceanWraps = floodAround( + map, axes, {i}, [&map](int x, int y) { return map.isWater(x, y); }, winding, + reached); + if (!oceanWraps) + return "The seas beside the belt do not meet around the map."; + return ""; } } // namespace RingWorldOptions::RingWorldOptions(const GenerationRequest &r) - : beltWidth(r.option("belt-width")), coastRoughness(r.option("coast-roughness")), - lakeDensity(r.option("lake-density")), resourceIslands(r.option("resource-islands")), - windingBelt(r.option("winding-belt") != 0), bothCoasts(r.option("both-coasts") != 0), - wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), - stone(r.option("stone-amount")), algae(r.option("algae-amount")), - fruit(r.option("fruit-amount")) {} - -GeneratorDefinition ringWorldDefinition() { - return {"ring-world", - 16, - "Ring world", - 1, - false, - // Belt width is the share of the map's breadth the belt covers on average; lake density - // is lakes per 4096 tiles of belt; resource islands are counted per 128x128 of map. - {{"belt-width", "Belt width", 30, 70, 5, 45, ControlGroup::Terrain}, - {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, - {"lake-density", "Lake density", 0, 8, 1, 2, ControlGroup::Terrain}, - {"resource-islands", "Resource islands", 0, 10, 1, 2, ControlGroup::Resources}, - // Off, the belt's centre line runs straight round the map. - GeneratorControl::toggle("winding-belt", "Winding belt", true, ControlGroup::Terrain), - // Off, every colony starts on the same coast of the belt. - GeneratorControl::toggle("both-coasts", "Colonies on both coasts", true, - ControlGroup::Layout), - // The ambient scatter's wheat, wood, stone and fruit and the shallows' algae. Every - // home's starter patches and each island's prize stay as they are. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - validate, - validateWorld}; + : beltWidth(r.option("belt-width")), coastRoughness(r.option("coast-roughness")), + lakeDensity(r.option("lake-density")), resourceIslands(r.option("resource-islands")), + windingBelt(r.option("winding-belt") != 0), bothCoasts(r.option("both-coasts") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition ringWorldDefinition() +{ + return {"ring-world", + 16, + "Ring world", + 1, + false, + // Belt width is the share of the map's breadth the belt covers on average; lake density + // is lakes per 4096 tiles of belt; resource islands are counted per 128x128 of map. + {{"belt-width", "Belt width", 30, 70, 5, 45, ControlGroup::Terrain}, + {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, + {"lake-density", "Lake density", 0, 8, 1, 2, ControlGroup::Terrain}, + {"resource-islands", "Resource islands", 0, 10, 1, 2, ControlGroup::Resources}, + // Off, the belt's centre line runs straight round the map. + GeneratorControl::toggle("winding-belt", "Winding belt", true, ControlGroup::Terrain), + // Off, every colony starts on the same coast of the belt. + GeneratorControl::toggle("both-coasts", "Colonies on both coasts", true, + ControlGroup::Layout), + // The ambient scatter's wheat, wood, stone and fruit and the shallows' algae. Every + // home's starter patches and each island's prize stay as they are. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validate, + validateWorld}; } diff --git a/src/map/generator/generators/RiverGenerator.cpp b/src/map/generator/generators/RiverGenerator.cpp index 6a1d70f95..80dda134f 100644 --- a/src/map/generator/generators/RiverGenerator.cpp +++ b/src/map/generator/generators/RiverGenerator.cpp @@ -9,16 +9,12 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const RiverOptions options(context.request); - HeightFieldOptions terrain{ - options.water, options.sand, options.grass, options.desert, - options.smoothing, options.fruit, options.repeat, false}; + HeightFieldOptions terrain{options.water, options.sand, options.grass, options.desert, + options.smoothing, options.fruit, options.repeat, false}; readResourceControls(terrain, context.request); - if (!generateHeightField(game, context, terrain, - [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) - { - hm.makeRiver(options.river_width * (w + h) / 2 / 100, smoothing, - options.winding); - })) + if (!generateHeightField( + game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) + { hm.makeRiver(options.river_width * (w + h) / 2 / 100, smoothing, options.winding); })) return false; context.stage = "starts"; if (!placeStarts(game, context)) diff --git a/src/map/generator/generators/RiverGenerator.h b/src/map/generator/generators/RiverGenerator.h index 9a1955b0e..3e2c39388 100644 --- a/src/map/generator/generators/RiverGenerator.h +++ b/src/map/generator/generators/RiverGenerator.h @@ -9,8 +9,8 @@ struct RiverOptions explicit RiverOptions(const GenerationRequest &r) : water(r.option("water")), sand(r.option("sand")), grass(r.option("grass")), desert(r.option("desert")), smoothing(r.option("smoothing")), - river_width(r.option("river-width")), fruit(r.option("fruit")), repeat(r.option("repeat")), - winding(r.option("winding-river") != 0) + river_width(r.option("river-width")), fruit(r.option("fruit")), + repeat(r.option("repeat")), winding(r.option("winding-river") != 0) { } }; diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index 4bb530763..4bf0778e2 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -56,7 +56,6 @@ static bool terrain(Game &game, GenerationContext &context, const RuggedArchipel } if (failed) { - i--; if (c++ > 65536) { @@ -399,8 +398,7 @@ static bool generate(Game &game, GenerationContext &context) // nowhere left to build on an island this small, which the guarantee above doesn't address: a // colony buried in wood has wood at its feet. At any amount other than the default, open such a // colony back up and guarantee the crops once more. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100) + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || options.algae != 100) { openCrampedStarts(game, context); guaranteeStartingResources(game, context, 24, 32); @@ -410,20 +408,21 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition ruggedArchipelagoDefinition() { - return {"rugged-archipelago", - 8, - "Old islands", - 1, - false, - {{"island-size", "Island size", 50, 70, 1, 65, ControlGroup::Terrain, false}, - {"beach-size", "Beach size", 0, 4, 1, 1, ControlGroup::Terrain, false}, - // Off, an island gets no fourth deposit of whichever of wheat or wood came out smaller. - GeneratorControl::toggle("extra-deposit", "Extra starting deposit", true, - ControlGroup::Resources), - // The area of each island's own wheat, wood, stone and algae deposits. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount")}, - generate}; + return { + "rugged-archipelago", + 8, + "Old islands", + 1, + false, + {{"island-size", "Island size", 50, 70, 1, 65, ControlGroup::Terrain, false}, + {"beach-size", "Beach size", 0, 4, 1, 1, ControlGroup::Terrain, false}, + // Off, an island gets no fourth deposit of whichever of wheat or wood came out smaller. + GeneratorControl::toggle("extra-deposit", "Extra starting deposit", true, + ControlGroup::Resources), + // The area of each island's own wheat, wood, stone and algae deposits. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount")}, + generate}; } diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index c8800ccf0..3b99e6d31 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -852,8 +852,7 @@ static bool generate(Game &game, GenerationContext &context) // nowhere left to build, which the guarantee above doesn't address: a colony buried in wheat // has wheat at its feet. At any amount other than the default, open such a colony back up and // guarantee the crops once more. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100) + if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || options.algae != 100) { openCrampedStarts(game, context); guaranteeStartingResources(game, context, 24, 32); @@ -863,21 +862,22 @@ static bool generate(Game &game, GenerationContext &context) GeneratorDefinition shatteredCoastDefinition() { - return {"shattered-coast", - 7, - "Old random", - 2, - false, - {{"water", "Water weight", 0, 100, 1, 40, ControlGroup::Terrain, false, true}, - {"sand", "Sand weight", 0, 100, 1, 4, ControlGroup::Terrain, false, true}, - {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, - {"smoothing", "Smoothing", 1, 8, 1, 3, ControlGroup::Terrain, false}, - // Off, colonies start in the shattered terrain rather than in a cleared meadow. - GeneratorControl::toggle("colony-meadows", "Colony meadows", true, ControlGroup::Terrain), - // The area of each colony's own wheat, wood, stone and algae deposits. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount")}, - generate}; + return { + "shattered-coast", + 7, + "Old random", + 2, + false, + {{"water", "Water weight", 0, 100, 1, 40, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 4, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 3, ControlGroup::Terrain, false}, + // Off, colonies start in the shattered terrain rather than in a cleared meadow. + GeneratorControl::toggle("colony-meadows", "Colony meadows", true, ControlGroup::Terrain), + // The area of each colony's own wheat, wood, stone and algae deposits. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount")}, + generate}; } diff --git a/src/map/generator/generators/ShatteredCoastGenerator.h b/src/map/generator/generators/ShatteredCoastGenerator.h index c629fe7a1..17639142b 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.h +++ b/src/map/generator/generators/ShatteredCoastGenerator.h @@ -16,7 +16,10 @@ struct ShatteredCoastOptions } int percent(int resourceType) const { - return resourceType == CORN ? wheat : resourceType == WOOD ? wood : resourceType == STONE ? stone : algae; + return resourceType == CORN ? wheat + : resourceType == WOOD ? wood + : resourceType == STONE ? stone + : algae; } }; GeneratorDefinition shatteredCoastDefinition(); diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 3c4d98923..8b8b6b6da 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -30,7 +30,8 @@ using namespace MapGeneration; // all, and loopiness opens extra passes for routes to flank along. Colonies take the roomiest // valleys, one each while they last, and ponds fill the basins around them. The whole design is a // pure function of the request, so validateWorld rebuilds it and checks the finished world. -namespace { +namespace +{ // Enclosed pockets smaller than this become part of the ridge: too small to live in, and a pass // into one would only lead to a dead end a few tiles deep. @@ -74,1207 +75,1371 @@ constexpr int kFruitGrove = 6; constexpr int kAreaPerColony = 1024; constexpr int kFar = INT_MAX / 4; -struct Torus { - int w, h; - int x(int v) const { return ((v % w) + w) % w; } - int y(int v) const { return ((v % h) + h) % h; } - int at(int px, int py) const { return y(py) * w + x(px); } - // Signed shortest offset from a to b. - int offsetX(int a, int b) const { - const int d = x(b - a); - return d > w / 2 ? d - w : d; - } - int offsetY(int a, int b) const { - const int d = y(b - a); - return d > h / 2 ? d - h : d; - } - int dist2(int ax, int ay, int bx, int by) const { - const int dx = offsetX(ax, bx), dy = offsetY(ay, by); - return dx * dx + dy * dy; - } - int chebyshev(int ax, int ay, int bx, int by) const { - return std::max(std::abs(offsetX(ax, bx)), std::abs(offsetY(ay, by))); - } +struct Torus +{ + int w, h; + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + // Signed shortest offset from a to b. + int offsetX(int a, int b) const + { + const int d = x(b - a); + return d > w / 2 ? d - w : d; + } + int offsetY(int a, int b) const + { + const int d = y(b - a); + return d > h / 2 ? d - h : d; + } + int dist2(int ax, int ay, int bx, int by) const + { + const int dx = offsetX(ax, bx), dy = offsetY(ay, by); + return dx * dx + dy * dy; + } + int chebyshev(int ax, int ay, int bx, int by) const + { + return std::max(std::abs(offsetX(ax, bx)), std::abs(offsetY(ay, by))); + } }; template -void shuffle(std::vector &values, GenerationContext &context, const char *stream) { - for (size_t i = values.size(); i > 1; --i) - std::swap(values[i - 1], values[context.bounded(stream, std::uint32_t(i))]); +void shuffle(std::vector &values, GenerationContext &context, const char *stream) +{ + for (size_t i = values.size(); i > 1; --i) + std::swap(values[i - 1], values[context.bounded(stream, std::uint32_t(i))]); } // Smooth value noise that tiles the map's torus exactly: a lattice of random values about // `period` tiles apart (a whole number of lattice cells spans the map), blended with a // smoothstep. Integer arithmetic throughout. Values are 0..65535. -std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng) { - const int gw = std::max(1, (w + period / 2) / std::max(1, period)); - const int gh = std::max(1, (h + period / 2) / std::max(1, period)); - std::vector lattice(size_t(gw) * gh); - for (int &v : lattice) - v = int(rng() >> 16); - const auto smooth = [](int t) { - return int(std::int64_t(t) * t * (3 * 1024 - 2 * t) / (1024 * 1024)); - }; - std::vector field(size_t(w) * h); - for (int y = 0; y < h; ++y) { - const std::int64_t fy = std::int64_t(y) * gh * 1024 / h; - const int y0 = int(fy >> 10), y1 = (y0 + 1) % gh, ty = smooth(int(fy & 1023)); - for (int x = 0; x < w; ++x) { - const std::int64_t fx = std::int64_t(x) * gw * 1024 / w; - const int x0 = int(fx >> 10), x1 = (x0 + 1) % gw, tx = smooth(int(fx & 1023)); - const int a = lattice[size_t(y0) * gw + x0], b = lattice[size_t(y0) * gw + x1]; - const int c = lattice[size_t(y1) * gw + x0], d = lattice[size_t(y1) * gw + x1]; - const int top = a + (b - a) * tx / 1024, bottom = c + (d - c) * tx / 1024; - field[size_t(y) * w + x] = top + (bottom - top) * ty / 1024; - } - } - return field; +std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng) +{ + const int gw = std::max(1, (w + period / 2) / std::max(1, period)); + const int gh = std::max(1, (h + period / 2) / std::max(1, period)); + std::vector lattice(size_t(gw) * gh); + for (int &v : lattice) + v = int(rng() >> 16); + const auto smooth = [](int t) + { return int(std::int64_t(t) * t * (3 * 1024 - 2 * t) / (1024 * 1024)); }; + std::vector field(size_t(w) * h); + for (int y = 0; y < h; ++y) + { + const std::int64_t fy = std::int64_t(y) * gh * 1024 / h; + const int y0 = int(fy >> 10), y1 = (y0 + 1) % gh, ty = smooth(int(fy & 1023)); + for (int x = 0; x < w; ++x) + { + const std::int64_t fx = std::int64_t(x) * gw * 1024 / w; + const int x0 = int(fx >> 10), x1 = (x0 + 1) % gw, tx = smooth(int(fx & 1023)); + const int a = lattice[size_t(y0) * gw + x0], b = lattice[size_t(y0) * gw + x1]; + const int c = lattice[size_t(y1) * gw + x0], d = lattice[size_t(y1) * gw + x1]; + const int top = a + (b - a) * tx / 1024, bottom = c + (d - c) * tx / 1024; + field[size_t(y) * w + x] = top + (bottom - top) * ty / 1024; + } + } + return field; } // Octaves at period, period/2, ... weighted 2:1 per step. -std::vector fractalNoise(int w, int h, int period, int octaves, std::mt19937 &rng) { - std::vector sum(size_t(w) * h, 0); - int total = 0; - for (int octave = 0; octave < octaves; ++octave) { - const int weight = 1 << (octaves - 1 - octave); - const std::vector layer = periodicNoise(w, h, std::max(2, period >> octave), rng); - for (size_t i = 0; i < sum.size(); ++i) - sum[i] += layer[i] * weight; - total += weight; - } - for (int &v : sum) - v /= total; - return sum; +std::vector fractalNoise(int w, int h, int period, int octaves, std::mt19937 &rng) +{ + std::vector sum(size_t(w) * h, 0); + int total = 0; + for (int octave = 0; octave < octaves; ++octave) + { + const int weight = 1 << (octaves - 1 - octave); + const std::vector layer = periodicNoise(w, h, std::max(2, period >> octave), rng); + for (size_t i = 0; i < sum.size(); ++i) + sum[i] += layer[i] * weight; + total += weight; + } + for (int &v : sum) + v /= total; + return sum; } // The value below which `percent` of the given samples fall. -int percentile(std::vector samples, int percent) { - if (samples.empty()) - return 0; - const size_t k = std::min(samples.size() - 1, samples.size() * size_t(percent) / 100); - std::nth_element(samples.begin(), samples.begin() + k, samples.end()); - return samples[k]; +int percentile(std::vector samples, int percent) +{ + if (samples.empty()) + return 0; + const size_t k = std::min(samples.size() - 1, samples.size() * size_t(percent) / 100); + std::nth_element(samples.begin(), samples.begin() + k, samples.end()); + return samples[k]; } // Chebyshev steps from every tile to the nearest source tile; kFar with no sources. -std::vector chebyshevDistance(const Torus &t, const std::vector &source) { - std::vector distance(source.size(), kFar); - std::vector queue; - queue.reserve(source.size()); - for (size_t i = 0; i < source.size(); ++i) - if (source[i]) { - distance[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int q = t.at(px + dx, py + dy); - if (distance[q] == kFar) { - distance[q] = distance[p] + 1; - queue.push_back(q); - } - } - } - return distance; +std::vector chebyshevDistance(const Torus &t, const std::vector &source) +{ + std::vector distance(source.size(), kFar); + std::vector queue; + queue.reserve(source.size()); + for (size_t i = 0; i < source.size(); ++i) + if (source[i]) + { + distance[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int q = t.at(px + dx, py + dy); + if (distance[q] == kFar) + { + distance[q] = distance[p] + 1; + queue.push_back(q); + } + } + } + return distance; } -struct Site { - int x, y; +struct Site +{ + int x, y; }; // Bucket rows or columns to search around bucket c: all of them when a window would wrap onto // itself. -std::vector bucketWindow(int c, int count, int radius) { - std::vector result; - if (count <= 2 * radius + 1) { - for (int i = 0; i < count; ++i) - result.push_back(i); - } else { - for (int d = -radius; d <= radius; ++d) - result.push_back(((c + d) % count + count) % count); - } - return result; +std::vector bucketWindow(int c, int count, int radius) +{ + std::vector result; + if (count <= 2 * radius + 1) + { + for (int i = 0; i < count; ++i) + result.push_back(i); + } + else + { + for (int d = -radius; d <= radius; ++d) + result.push_back(((c + d) % count + count) % count); + } + return result; } // Basin sites by dart throwing on the torus. Sites keep 0.83 of the requested spacing apart, // which, once the map is saturated, leaves one site per spacing-squared tiles on average. -std::vector scatterSites(const Torus &t, int spacing, GenerationContext &context) { - const int minimum = std::max(4, spacing * 83 / 100); - const std::int64_t expected = - std::max(2, std::int64_t(t.w) * t.h / (std::int64_t(spacing) * spacing)); - const int gx = std::max(1, t.w / minimum), gy = std::max(1, t.h / minimum); - std::vector> columns(gx), rows(gy); - for (int c = 0; c < gx; ++c) - columns[c] = bucketWindow(c, gx, 1); - for (int r = 0; r < gy; ++r) - rows[r] = bucketWindow(r, gy, 1); - std::vector> buckets(size_t(gx) * gy); - std::vector sites; - for (std::int64_t attempt = 0; attempt < expected * 60; ++attempt) { - const int x = int(context.bounded("highlands-sites", t.w)); - const int y = int(context.bounded("highlands-sites", t.h)); - const int bx = x * gx / t.w, by = y * gy / t.h; - bool clear = true; - for (int row : rows[by]) { - for (int column : columns[bx]) { - for (int s : buckets[size_t(row) * gx + column]) - if (t.dist2(x, y, sites[s].x, sites[s].y) < minimum * minimum) { - clear = false; - break; - } - if (!clear) - break; - } - if (!clear) - break; - } - if (!clear) - continue; - buckets[size_t(by) * gx + bx].push_back(int(sites.size())); - sites.push_back({x, y}); - } - return sites; +std::vector scatterSites(const Torus &t, int spacing, GenerationContext &context) +{ + const int minimum = std::max(4, spacing * 83 / 100); + const std::int64_t expected = + std::max(2, std::int64_t(t.w) * t.h / (std::int64_t(spacing) * spacing)); + const int gx = std::max(1, t.w / minimum), gy = std::max(1, t.h / minimum); + std::vector> columns(gx), rows(gy); + for (int c = 0; c < gx; ++c) + columns[c] = bucketWindow(c, gx, 1); + for (int r = 0; r < gy; ++r) + rows[r] = bucketWindow(r, gy, 1); + std::vector> buckets(size_t(gx) * gy); + std::vector sites; + for (std::int64_t attempt = 0; attempt < expected * 60; ++attempt) + { + const int x = int(context.bounded("highlands-sites", t.w)); + const int y = int(context.bounded("highlands-sites", t.h)); + const int bx = x * gx / t.w, by = y * gy / t.h; + bool clear = true; + for (int row : rows[by]) + { + for (int column : columns[bx]) + { + for (int s : buckets[size_t(row) * gx + column]) + if (t.dist2(x, y, sites[s].x, sites[s].y) < minimum * minimum) + { + clear = false; + break; + } + if (!clear) + break; + } + if (!clear) + break; + } + if (!clear) + continue; + buckets[size_t(by) * gx + bx].push_back(int(sites.size())); + sites.push_back({x, y}); + } + return sites; } // Every tile's nearest site, measured from a warped copy of the tile so cell borders wander // instead of running straight. Positions are in sixteenths of a tile. std::vector labelCells(const Torus &t, const std::vector &sites, int spacing, - GenerationContext &context) { - const int period = spacing * 3 / 2; - const std::vector warpX = fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); - const std::vector warpY = fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); - const std::int64_t amplitude = std::int64_t(spacing) * 16 * 30 / 100; - const int W = t.w * 16, H = t.h * 16; - const int gx = std::max(1, t.w / spacing), gy = std::max(1, t.h / spacing); - std::vector> buckets(size_t(gx) * gy), columns(gx), rows(gy); - for (size_t s = 0; s < sites.size(); ++s) - buckets[size_t(sites[s].y * gy / t.h) * gx + sites[s].x * gx / t.w].push_back(int(s)); - for (int c = 0; c < gx; ++c) - columns[c] = bucketWindow(c, gx, 2); - for (int r = 0; r < gy; ++r) - rows[r] = bucketWindow(r, gy, 2); - std::vector label(size_t(t.w) * t.h, 0); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const size_t i = size_t(y) * t.w + x; - int px = x * 16 + 8 + int((warpX[i] - 32768) * amplitude / 32768); - int py = y * 16 + 8 + int((warpY[i] - 32768) * amplitude / 32768); - px = ((px % W) + W) % W; - py = ((py % H) + H) % H; - const int bx = int(std::int64_t(px) * gx / W), by = int(std::int64_t(py) * gy / H); - std::int64_t best = INT64_MAX; - int nearest = 0; - for (int row : rows[by]) - for (int column : columns[bx]) - for (int s : buckets[size_t(row) * gx + column]) { - int dx = std::abs(px - (sites[s].x * 16 + 8)), dy = std::abs(py - (sites[s].y * 16 + 8)); - dx = std::min(dx, W - dx); - dy = std::min(dy, H - dy); - const std::int64_t d = std::int64_t(dx) * dx + std::int64_t(dy) * dy; - if (d < best || (d == best && s < nearest)) { - best = d; - nearest = s; - } - } - label[i] = nearest; - } - return label; + GenerationContext &context) +{ + const int period = spacing * 3 / 2; + const std::vector warpX = + fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); + const std::vector warpY = + fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); + const std::int64_t amplitude = std::int64_t(spacing) * 16 * 30 / 100; + const int W = t.w * 16, H = t.h * 16; + const int gx = std::max(1, t.w / spacing), gy = std::max(1, t.h / spacing); + std::vector> buckets(size_t(gx) * gy), columns(gx), rows(gy); + for (size_t s = 0; s < sites.size(); ++s) + buckets[size_t(sites[s].y * gy / t.h) * gx + sites[s].x * gx / t.w].push_back(int(s)); + for (int c = 0; c < gx; ++c) + columns[c] = bucketWindow(c, gx, 2); + for (int r = 0; r < gy; ++r) + rows[r] = bucketWindow(r, gy, 2); + std::vector label(size_t(t.w) * t.h, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const size_t i = size_t(y) * t.w + x; + int px = x * 16 + 8 + int((warpX[i] - 32768) * amplitude / 32768); + int py = y * 16 + 8 + int((warpY[i] - 32768) * amplitude / 32768); + px = ((px % W) + W) % W; + py = ((py % H) + H) % H; + const int bx = int(std::int64_t(px) * gx / W), by = int(std::int64_t(py) * gy / H); + std::int64_t best = INT64_MAX; + int nearest = 0; + for (int row : rows[by]) + for (int column : columns[bx]) + for (int s : buckets[size_t(row) * gx + column]) + { + int dx = std::abs(px - (sites[s].x * 16 + 8)), + dy = std::abs(py - (sites[s].y * 16 + 8)); + dx = std::min(dx, W - dx); + dy = std::min(dy, H - dy); + const std::int64_t d = std::int64_t(dx) * dx + std::int64_t(dy) * dy; + if (d < best || (d == best && s < nearest)) + { + best = d; + nearest = s; + } + } + label[i] = nearest; + } + return label; } -struct Pass { - int x, y; // centre tile - int lo, hi; // the cleared box is [x + lo, x + hi] by [y + lo, y + hi] - int a, b; // the valleys it joins +struct Pass +{ + int x, y; // centre tile + int lo, hi; // the cleared box is [x + lo, x + hi] by [y + lo, y + hi] + int a, b; // the valleys it joins }; -struct Home { - int valley, x, y; // swarm centre +struct Home +{ + int valley, x, y; // swarm centre }; // The terrain design: a pure function of the request, rebuilt by validateWorld. -struct Layout { - Torus t{1, 1}; - std::vector ridge; // designed stone - std::vector valley; // valley id; -1 on ridge tiles - int valleys = 0; - std::vector passes; - std::vector passTile; // inside a pass's cleared box - std::vector ridgeDistance; // Chebyshev steps to the nearest ridge or pass tile - std::vector> tilesOf; // each valley's tiles outside pass boxes - std::vector pole; // each valley's tile deepest inside it - std::vector homes; - std::vector pond; // undermap water - std::string failure; +struct Layout +{ + Torus t{1, 1}; + std::vector ridge; // designed stone + std::vector valley; // valley id; -1 on ridge tiles + int valleys = 0; + std::vector passes; + std::vector passTile; // inside a pass's cleared box + std::vector ridgeDistance; // Chebyshev steps to the nearest ridge or pass tile + std::vector> tilesOf; // each valley's tiles outside pass boxes + std::vector pole; // each valley's tile deepest inside it + std::vector homes; + std::vector pond; // undermap water + std::string failure; }; -struct Disjoint { - std::vector parent; - explicit Disjoint(int count) : parent(count) { - for (int i = 0; i < count; ++i) - parent[i] = i; - } - int find(int v) { - while (parent[v] != v) { - parent[v] = parent[parent[v]]; - v = parent[v]; - } - return v; - } - bool join(int a, int b) { - a = find(a); - b = find(b); - if (a == b) - return false; - parent[b] = a; - return true; - } +struct Disjoint +{ + std::vector parent; + explicit Disjoint(int count) : parent(count) + { + for (int i = 0; i < count; ++i) + parent[i] = i; + } + int find(int v) + { + while (parent[v] != v) + { + parent[v] = parent[parent[v]]; + v = parent[v]; + } + return v; + } + bool join(int a, int b) + { + a = find(a); + b = find(b); + if (a == b) + return false; + parent[b] = a; + return true; + } }; // Distinct valley ids within Chebyshev radius r of a tile, sorted, at most `limit` of them. -int nearbyValleys(const Layout &L, int x, int y, int r, int ids[], int limit) { - int count = 0; - for (int dy = -r; dy <= r; ++dy) - for (int dx = -r; dx <= r; ++dx) { - const int v = L.valley[L.t.at(x + dx, y + dy)]; - if (v < 0 || std::find(ids, ids + count, v) != ids + count) - continue; - if (count == limit) - return count; - ids[count++] = v; - } - std::sort(ids, ids + count); - return count; +int nearbyValleys(const Layout &L, int x, int y, int r, int ids[], int limit) +{ + int count = 0; + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + { + const int v = L.valley[L.t.at(x + dx, y + dy)]; + if (v < 0 || std::find(ids, ids + count, v) != ids + count) + continue; + if (count == limit) + return count; + ids[count++] = v; + } + std::sort(ids, ids + count); + return count; } // Whether valleys a and b meet inside the window around (cx, cy), walking through non-ridge // tiles. -bool joins(const Layout &L, int cx, int cy, int a, int b, int radius) { - const int side = 2 * radius + 1; - std::vector seen(size_t(side) * side, 0); - std::vector queue; - for (int dy = -radius; dy <= radius; ++dy) - for (int dx = -radius; dx <= radius; ++dx) - if (L.valley[L.t.at(cx + dx, cy + dy)] == a) { - const int local = (dy + radius) * side + dx + radius; - seen[local] = 1; - queue.push_back(local); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int lx = queue[head] % side, ly = queue[head] / side; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int nx = lx + dx, ny = ly + dy; - if (nx < 0 || ny < 0 || nx >= side || ny >= side || seen[size_t(ny) * side + nx]) - continue; - const int tile = L.t.at(cx + nx - radius, cy + ny - radius); - if (L.ridge[tile]) - continue; - if (L.valley[tile] == b) - return true; - seen[size_t(ny) * side + nx] = 1; - queue.push_back(ny * side + nx); - } - } - return false; +bool joins(const Layout &L, int cx, int cy, int a, int b, int radius) +{ + const int side = 2 * radius + 1; + std::vector seen(size_t(side) * side, 0); + std::vector queue; + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) + if (L.valley[L.t.at(cx + dx, cy + dy)] == a) + { + const int local = (dy + radius) * side + dx + radius; + seen[local] = 1; + queue.push_back(local); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int lx = queue[head] % side, ly = queue[head] / side; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int nx = lx + dx, ny = ly + dy; + if (nx < 0 || ny < 0 || nx >= side || ny >= side || seen[size_t(ny) * side + nx]) + continue; + const int tile = L.t.at(cx + nx - radius, cy + ny - radius); + if (L.ridge[tile]) + continue; + if (L.valley[tile] == b) + return true; + seen[size_t(ny) * side + nx] = 1; + queue.push_back(ny * side + nx); + } + } + return false; } // Cuts a pass of the given width through the ridge between valleys a and b. Sites far along the // ridge from any junction come first, so a pass sits in the middle of its ridgeline; each try is // kept only if it really joins the two valleys. bool carvePass(Layout &L, GenerationContext &context, int a, int b, std::vector candidates, - const std::vector &alongRidge, int width) { - const Torus &t = L.t; - candidates.erase(std::remove_if(candidates.begin(), candidates.end(), - [&](int c) { - if (!L.ridge[c]) - return true; - for (const Pass &p : L.passes) - if (t.chebyshev(c % t.w, c / t.w, p.x, p.y) < kPassSpacing) - return true; - return false; - }), - candidates.end()); - if (candidates.empty()) - return false; - const auto score = [&](int c) { return alongRidge[c] < 0 ? kFar : alongRidge[c]; }; - std::stable_sort(candidates.begin(), candidates.end(), - [&](int p, int q) { return score(p) > score(q); }); - const std::int64_t best = score(candidates.front()); - size_t band = 0; - while (band < candidates.size() && std::int64_t(score(candidates[band])) * 4 >= best * 3) - ++band; - band = std::max(1, band); - const size_t first = context.bounded("highlands-passes", std::uint32_t(band)); - const int lo = -(width - 1) / 2, hi = width / 2; - for (size_t k = 0; k < candidates.size() && k < 12; ++k) { - const int c = candidates[k < band ? (first + k) % band : k]; - const int cx = c % t.w, cy = c / t.w; - std::vector cleared; - for (int dy = lo; dy <= hi; ++dy) - for (int dx = lo; dx <= hi; ++dx) { - const int i = t.at(cx + dx, cy + dy); - if (L.ridge[i]) { - L.ridge[i] = 0; - cleared.push_back(i); - } - } - if (joins(L, cx, cy, a, b, width + 4)) { - L.passes.push_back({cx, cy, lo, hi, a, b}); - for (int dy = lo; dy <= hi; ++dy) - for (int dx = lo; dx <= hi; ++dx) - L.passTile[t.at(cx + dx, cy + dy)] = 1; - return true; - } - for (int i : cleared) - L.ridge[i] = 1; - } - return false; + const std::vector &alongRidge, int width) +{ + const Torus &t = L.t; + candidates.erase(std::remove_if(candidates.begin(), candidates.end(), + [&](int c) + { + if (!L.ridge[c]) + return true; + for (const Pass &p : L.passes) + if (t.chebyshev(c % t.w, c / t.w, p.x, p.y) < + kPassSpacing) + return true; + return false; + }), + candidates.end()); + if (candidates.empty()) + return false; + const auto score = [&](int c) { return alongRidge[c] < 0 ? kFar : alongRidge[c]; }; + std::stable_sort(candidates.begin(), candidates.end(), + [&](int p, int q) { return score(p) > score(q); }); + const std::int64_t best = score(candidates.front()); + size_t band = 0; + while (band < candidates.size() && std::int64_t(score(candidates[band])) * 4 >= best * 3) + ++band; + band = std::max(1, band); + const size_t first = context.bounded("highlands-passes", std::uint32_t(band)); + const int lo = -(width - 1) / 2, hi = width / 2; + for (size_t k = 0; k < candidates.size() && k < 12; ++k) + { + const int c = candidates[k < band ? (first + k) % band : k]; + const int cx = c % t.w, cy = c / t.w; + std::vector cleared; + for (int dy = lo; dy <= hi; ++dy) + for (int dx = lo; dx <= hi; ++dx) + { + const int i = t.at(cx + dx, cy + dy); + if (L.ridge[i]) + { + L.ridge[i] = 0; + cleared.push_back(i); + } + } + if (joins(L, cx, cy, a, b, width + 4)) + { + L.passes.push_back({cx, cy, lo, hi, a, b}); + for (int dy = lo; dy <= hi; ++dy) + for (int dx = lo; dx <= hi; ++dx) + L.passTile[t.at(cx + dx, cy + dy)] = 1; + return true; + } + for (int i : cleared) + L.ridge[i] = 1; + } + return false; } // Homes by farthest-point spreading over the roomier valleys, one colony per valley while they // last, then anywhere deep enough and well away from every other home. A valley's home site sits // out from its deepest point, leaving that interior for the pond beside it. -bool chooseHomes(Layout &L, GenerationContext &context, int teams) { - const Torus &t = L.t; - std::vector site(L.valleys, -1), room(L.valleys, 0), rooms; - for (int v = 0; v < L.valleys; ++v) { - for (int i : L.tilesOf[v]) - if (L.ridgeDistance[i] >= kRidgeRoad) - ++room[v]; - const int pole = L.pole[v]; - if (pole < 0 || L.ridgeDistance[pole] < kHomeRidge) - continue; - // About halfway out from the pole towards the ridge: room to build on the ridge side, and - // the valley's interior left for the pond on the other. - const int depth = std::max(kHomeRidge, L.ridgeDistance[pole] / 2); - const int offset = std::min(kHomeOffset, L.ridgeDistance[pole] - depth); - int bestKey = INT_MAX; - for (int i : L.tilesOf[v]) { - if (L.ridgeDistance[i] < depth) - continue; - const double d = std::sqrt(double(t.dist2(i % t.w, i / t.w, pole % t.w, pole / t.w))); - const int key = std::abs(int(std::lround(d)) - offset) * 64 - L.ridgeDistance[i]; - if (key < bestKey) { - bestKey = key; - site[v] = i; - } - } - rooms.push_back(room[v]); - } - if (rooms.empty()) { - L.failure = "no valley is deep enough for a colony"; - return false; - } - // Colonies take the roomiest valleys, the 2 x colonies largest and none under half the median, - // so no colony starts cramped while a roomier valley goes spare. - std::vector ranked = rooms; - std::sort(ranked.begin(), ranked.end(), std::greater()); - const int roomFloor = - std::max(percentile(rooms, 50) / 2, - ranked[std::min(ranked.size(), size_t(2 * std::max(1, teams))) - 1]); - std::vector primary; - for (int v = 0; v < L.valleys; ++v) - if (site[v] >= 0 && room[v] >= roomFloor) - primary.push_back(v); - std::vector used(L.valleys, 0); - const auto spread = [&](int x, int y) { - int nearest = INT_MAX; - for (const Home &h : L.homes) - nearest = std::min(nearest, t.dist2(x, y, h.x, h.y)); - return nearest; - }; - const auto take = [&](int tile) { - used[L.valley[tile]] = 1; - L.homes.push_back({L.valley[tile], tile % t.w, tile / t.w}); - }; - if (teams > 0 && !primary.empty()) - take(site[primary[context.bounded("highlands-homes", std::uint32_t(primary.size()))]]); - while (int(L.homes.size()) < teams) { - int chosen = -1, chosenSpread = -1; - for (int v : primary) { - if (used[v]) - continue; - const int s = spread(site[v] % t.w, site[v] / t.w); - if (s > chosenSpread || (s == chosenSpread && room[v] > room[L.valley[chosen]])) { - chosen = site[v]; - chosenSpread = s; - } - } - if (chosen < 0) { - for (int v = 0; v < L.valleys; ++v) - for (int i : L.tilesOf[v]) { - const int x = i % t.w, y = i / t.w; - if (((x | y) & 1) || L.ridgeDistance[i] < kHomeRidge) - continue; - const int s = spread(x, y); - if (s > chosenSpread) { - chosen = i; - chosenSpread = s; - } - } - if (chosenSpread < kHomeSeparation * kHomeSeparation) - chosen = -1; - } - if (chosen < 0) { - L.failure = "no room for colony " + std::to_string(L.homes.size()) + " in any valley"; - return false; - } - take(chosen); - } - return true; +bool chooseHomes(Layout &L, GenerationContext &context, int teams) +{ + const Torus &t = L.t; + std::vector site(L.valleys, -1), room(L.valleys, 0), rooms; + for (int v = 0; v < L.valleys; ++v) + { + for (int i : L.tilesOf[v]) + if (L.ridgeDistance[i] >= kRidgeRoad) + ++room[v]; + const int pole = L.pole[v]; + if (pole < 0 || L.ridgeDistance[pole] < kHomeRidge) + continue; + // About halfway out from the pole towards the ridge: room to build on the ridge side, and + // the valley's interior left for the pond on the other. + const int depth = std::max(kHomeRidge, L.ridgeDistance[pole] / 2); + const int offset = std::min(kHomeOffset, L.ridgeDistance[pole] - depth); + int bestKey = INT_MAX; + for (int i : L.tilesOf[v]) + { + if (L.ridgeDistance[i] < depth) + continue; + const double d = std::sqrt(double(t.dist2(i % t.w, i / t.w, pole % t.w, pole / t.w))); + const int key = std::abs(int(std::lround(d)) - offset) * 64 - L.ridgeDistance[i]; + if (key < bestKey) + { + bestKey = key; + site[v] = i; + } + } + rooms.push_back(room[v]); + } + if (rooms.empty()) + { + L.failure = "no valley is deep enough for a colony"; + return false; + } + // Colonies take the roomiest valleys, the 2 x colonies largest and none under half the median, + // so no colony starts cramped while a roomier valley goes spare. + std::vector ranked = rooms; + std::sort(ranked.begin(), ranked.end(), std::greater()); + const int roomFloor = + std::max(percentile(rooms, 50) / 2, + ranked[std::min(ranked.size(), size_t(2 * std::max(1, teams))) - 1]); + std::vector primary; + for (int v = 0; v < L.valleys; ++v) + if (site[v] >= 0 && room[v] >= roomFloor) + primary.push_back(v); + std::vector used(L.valleys, 0); + const auto spread = [&](int x, int y) + { + int nearest = INT_MAX; + for (const Home &h : L.homes) + nearest = std::min(nearest, t.dist2(x, y, h.x, h.y)); + return nearest; + }; + const auto take = [&](int tile) + { + used[L.valley[tile]] = 1; + L.homes.push_back({L.valley[tile], tile % t.w, tile / t.w}); + }; + if (teams > 0 && !primary.empty()) + take(site[primary[context.bounded("highlands-homes", std::uint32_t(primary.size()))]]); + while (int(L.homes.size()) < teams) + { + int chosen = -1, chosenSpread = -1; + for (int v : primary) + { + if (used[v]) + continue; + const int s = spread(site[v] % t.w, site[v] / t.w); + if (s > chosenSpread || (s == chosenSpread && room[v] > room[L.valley[chosen]])) + { + chosen = site[v]; + chosenSpread = s; + } + } + if (chosen < 0) + { + for (int v = 0; v < L.valleys; ++v) + for (int i : L.tilesOf[v]) + { + const int x = i % t.w, y = i / t.w; + if (((x | y) & 1) || L.ridgeDistance[i] < kHomeRidge) + continue; + const int s = spread(x, y); + if (s > chosenSpread) + { + chosen = i; + chosenSpread = s; + } + } + if (chosenSpread < kHomeSeparation * kHomeSeparation) + chosen = -1; + } + if (chosen < 0) + { + L.failure = "no room for colony " + std::to_string(L.homes.size()) + " in any valley"; + return false; + } + take(chosen); + } + return true; } // Grows one pond from its seed, always taking the lowest point of the depth field next, so it // fills its hollow the way water would. Distance from the seed counts as depth too, which keeps // the outline round rather than following the noise lattice into points. void growPond(Layout &L, const std::vector &depth, const std::vector &eligible, - int eligibleStamp, std::vector &queued, int queuedStamp, int seed, int target) { - using Entry = std::pair; - const int sx = seed % L.t.w, sy = seed / L.t.w; - const auto key = [&](int tile) { - const double d = std::sqrt(double(L.t.dist2(sx, sy, tile % L.t.w, tile / L.t.w))); - return depth[tile] + int(std::lround(d * 1500)); - }; - std::priority_queue, std::greater> frontier; - frontier.push({key(seed), seed}); - queued[seed] = queuedStamp; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - int grown = 0; - while (!frontier.empty() && grown < target) { - const int tile = frontier.top().second; - frontier.pop(); - if (L.pond[tile]) - continue; - L.pond[tile] = 1; - ++grown; - for (const auto &s : steps) { - const int next = L.t.at(tile % L.t.w + s[0], tile / L.t.w + s[1]); - if (eligible[next] == eligibleStamp && !L.pond[next] && queued[next] != queuedStamp) { - queued[next] = queuedStamp; - frontier.push({key(next), next}); - } - } - } + int eligibleStamp, std::vector &queued, int queuedStamp, int seed, int target) +{ + using Entry = std::pair; + const int sx = seed % L.t.w, sy = seed / L.t.w; + const auto key = [&](int tile) + { + const double d = std::sqrt(double(L.t.dist2(sx, sy, tile % L.t.w, tile / L.t.w))); + return depth[tile] + int(std::lround(d * 1500)); + }; + std::priority_queue, std::greater> frontier; + frontier.push({key(seed), seed}); + queued[seed] = queuedStamp; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + int grown = 0; + while (!frontier.empty() && grown < target) + { + const int tile = frontier.top().second; + frontier.pop(); + if (L.pond[tile]) + continue; + L.pond[tile] = 1; + ++grown; + for (const auto &s : steps) + { + const int next = L.t.at(tile % L.t.w + s[0], tile / L.t.w + s[1]); + if (eligible[next] == eligibleStamp && !L.pond[next] && queued[next] != queuedStamp) + { + queued[next] = queuedStamp; + frontier.push({key(next), next}); + } + } + } } -void placePonds(Layout &L, GenerationContext &context, int spacing, int pondSize) { - const Torus &t = L.t; - const int n = t.w * t.h; - const std::vector depth = - fractalNoise(t.w, t.h, std::max(8, spacing / 2), 2, context.stream("highlands-ponds")); - L.pond.assign(n, 0); - // Ground kept for every colony, and the home each valley's first pond is placed beside. - std::vector reserved(n, 0); - std::vector homeOf(L.valleys, -1); - for (size_t k = 0; k < L.homes.size(); ++k) { - const Home &h = L.homes[k]; - if (homeOf[h.valley] < 0) - homeOf[h.valley] = int(k); - for (int dy = -kHomePondGap; dy <= kHomePondGap; ++dy) - for (int dx = -kHomePondGap; dx <= kHomePondGap; ++dx) - if (dx * dx + dy * dy < kHomePondGap * kHomePondGap) - reserved[t.at(h.x + dx, h.y + dy)] = 1; - // And a five-wide way out, straight down to the band along the ridges, so however the ponds - // grow around a home they can never close it in. - int x = h.x, y = h.y; - while (L.ridgeDistance[t.at(x, y)] > 2) { - int nextX = x, nextY = y; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (L.ridgeDistance[t.at(x + dx, y + dy)] < L.ridgeDistance[t.at(nextX, nextY)]) { - nextX = x + dx; - nextY = y + dy; - } - x = nextX; - y = nextY; - for (int dy = -2; dy <= 2; ++dy) - for (int dx = -2; dx <= 2; ++dx) - reserved[t.at(x + dx, y + dy)] = 1; - } - } - std::vector eligible(n, -1), queued(n, -1); - int queuedStamp = 0; - for (int v = 0; v < L.valleys; ++v) { - const std::vector &tiles = L.tilesOf[v]; - if (int(tiles.size()) < kPondMinimumValley) - continue; - std::vector room; - for (int clearance = kPondClearance; - clearance >= kPondMinimumClearance && int(room.size()) < kMinimumPond; --clearance) { - room.clear(); - for (int i : tiles) - if (L.ridgeDistance[i] >= clearance && !reserved[i]) - room.push_back(i); - } - if (int(room.size()) < kMinimumPond) - continue; - for (int i : room) - eligible[i] = v; - // A colony's pond lies beside it, towards the middle of its valley. - int targetX = -1, targetY = -1; - if (homeOf[v] >= 0) { - const Home &h = L.homes[homeOf[v]]; - const int ox = t.offsetX(h.x, L.pole[v] % t.w), oy = t.offsetY(h.y, L.pole[v] / t.w); - const double length = std::sqrt(double(ox * ox + oy * oy)); - const double ux = length < 1 ? 0.0 : ox / length, uy = length < 1 ? 1.0 : oy / length; - targetX = t.x(h.x + int(std::lround(ux * kHomePondReach))); - targetY = t.y(h.y + int(std::lround(uy * kHomePondReach))); - } - const int area = int(tiles.size()); - const int target = std::max(kMinimumPond, area * pondSize / 100); - const int count = std::max(1, std::min(1 + area / kPondArea, int(room.size()) / (2 * kMinimumPond))); - const int each = std::max(kMinimumPond, target / count); - std::vector seeds; - for (int k = 0; k < count; ++k) { - const bool besideHome = k == 0 && targetX >= 0; - const auto better = [&](int i, int current) { - if (besideHome) { - const int di = t.dist2(i % t.w, i / t.w, targetX, targetY); - const int dc = t.dist2(current % t.w, current / t.w, targetX, targetY); - if (di != dc) - return di < dc; - } - return depth[i] < depth[current]; - }; - // A seed with room all round it, where there is one, so the pond has somewhere to grow. - const auto interior = [&](int i) { - for (int dy = -2; dy <= 2; ++dy) - for (int dx = -2; dx <= 2; ++dx) - if (eligible[t.at(i % t.w + dx, i / t.w + dy)] != v) - return false; - return true; - }; - int seed = -1; - for (int pass = 0; pass < 2 && seed < 0; ++pass) - for (int i : room) { - if (L.pond[i] || (pass == 0 && !interior(i))) - continue; - bool apart = true; - for (int s : seeds) - if (t.dist2(i % t.w, i / t.w, s % t.w, s / t.w) < kPondSeparation * kPondSeparation) { - apart = false; - break; - } - if (apart && (seed < 0 || better(i, seed))) - seed = i; - } - if (seed < 0) - break; - seeds.push_back(seed); - growPond(L, depth, eligible, v, queued, queuedStamp++, seed, each); - } - } - // Fill any ground a pond has closed in: everything still walkable is reachable from the band - // along the ridges, which no pond comes near. - std::vector reached(n, 0); - std::vector queue; - for (int i = 0; i < n; ++i) - if (!L.ridge[i] && !L.pond[i] && L.ridgeDistance[i] <= 2) { - reached[i] = 1; - queue.push_back(i); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int q = t.at(px + dx, py + dy); - if (!reached[q] && !L.ridge[q] && !L.pond[q]) { - reached[q] = 1; - queue.push_back(q); - } - } - } - for (int i = 0; i < n; ++i) - if (!L.ridge[i] && !L.pond[i] && !reached[i]) - L.pond[i] = 1; +void placePonds(Layout &L, GenerationContext &context, int spacing, int pondSize) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + const std::vector depth = + fractalNoise(t.w, t.h, std::max(8, spacing / 2), 2, context.stream("highlands-ponds")); + L.pond.assign(n, 0); + // Ground kept for every colony, and the home each valley's first pond is placed beside. + std::vector reserved(n, 0); + std::vector homeOf(L.valleys, -1); + for (size_t k = 0; k < L.homes.size(); ++k) + { + const Home &h = L.homes[k]; + if (homeOf[h.valley] < 0) + homeOf[h.valley] = int(k); + for (int dy = -kHomePondGap; dy <= kHomePondGap; ++dy) + for (int dx = -kHomePondGap; dx <= kHomePondGap; ++dx) + if (dx * dx + dy * dy < kHomePondGap * kHomePondGap) + reserved[t.at(h.x + dx, h.y + dy)] = 1; + // And a five-wide way out, straight down to the band along the ridges, so however the ponds + // grow around a home they can never close it in. + int x = h.x, y = h.y; + while (L.ridgeDistance[t.at(x, y)] > 2) + { + int nextX = x, nextY = y; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (L.ridgeDistance[t.at(x + dx, y + dy)] < L.ridgeDistance[t.at(nextX, nextY)]) + { + nextX = x + dx; + nextY = y + dy; + } + x = nextX; + y = nextY; + for (int dy = -2; dy <= 2; ++dy) + for (int dx = -2; dx <= 2; ++dx) + reserved[t.at(x + dx, y + dy)] = 1; + } + } + std::vector eligible(n, -1), queued(n, -1); + int queuedStamp = 0; + for (int v = 0; v < L.valleys; ++v) + { + const std::vector &tiles = L.tilesOf[v]; + if (int(tiles.size()) < kPondMinimumValley) + continue; + std::vector room; + for (int clearance = kPondClearance; + clearance >= kPondMinimumClearance && int(room.size()) < kMinimumPond; --clearance) + { + room.clear(); + for (int i : tiles) + if (L.ridgeDistance[i] >= clearance && !reserved[i]) + room.push_back(i); + } + if (int(room.size()) < kMinimumPond) + continue; + for (int i : room) + eligible[i] = v; + // A colony's pond lies beside it, towards the middle of its valley. + int targetX = -1, targetY = -1; + if (homeOf[v] >= 0) + { + const Home &h = L.homes[homeOf[v]]; + const int ox = t.offsetX(h.x, L.pole[v] % t.w), oy = t.offsetY(h.y, L.pole[v] / t.w); + const double length = std::sqrt(double(ox * ox + oy * oy)); + const double ux = length < 1 ? 0.0 : ox / length, uy = length < 1 ? 1.0 : oy / length; + targetX = t.x(h.x + int(std::lround(ux * kHomePondReach))); + targetY = t.y(h.y + int(std::lround(uy * kHomePondReach))); + } + const int area = int(tiles.size()); + const int target = std::max(kMinimumPond, area * pondSize / 100); + const int count = + std::max(1, std::min(1 + area / kPondArea, int(room.size()) / (2 * kMinimumPond))); + const int each = std::max(kMinimumPond, target / count); + std::vector seeds; + for (int k = 0; k < count; ++k) + { + const bool besideHome = k == 0 && targetX >= 0; + const auto better = [&](int i, int current) + { + if (besideHome) + { + const int di = t.dist2(i % t.w, i / t.w, targetX, targetY); + const int dc = t.dist2(current % t.w, current / t.w, targetX, targetY); + if (di != dc) + return di < dc; + } + return depth[i] < depth[current]; + }; + // A seed with room all round it, where there is one, so the pond has somewhere to grow. + const auto interior = [&](int i) + { + for (int dy = -2; dy <= 2; ++dy) + for (int dx = -2; dx <= 2; ++dx) + if (eligible[t.at(i % t.w + dx, i / t.w + dy)] != v) + return false; + return true; + }; + int seed = -1; + for (int pass = 0; pass < 2 && seed < 0; ++pass) + for (int i : room) + { + if (L.pond[i] || (pass == 0 && !interior(i))) + continue; + bool apart = true; + for (int s : seeds) + if (t.dist2(i % t.w, i / t.w, s % t.w, s / t.w) < + kPondSeparation * kPondSeparation) + { + apart = false; + break; + } + if (apart && (seed < 0 || better(i, seed))) + seed = i; + } + if (seed < 0) + break; + seeds.push_back(seed); + growPond(L, depth, eligible, v, queued, queuedStamp++, seed, each); + } + } + // Fill any ground a pond has closed in: everything still walkable is reachable from the band + // along the ridges, which no pond comes near. + std::vector reached(n, 0); + std::vector queue; + for (int i = 0; i < n; ++i) + if (!L.ridge[i] && !L.pond[i] && L.ridgeDistance[i] <= 2) + { + reached[i] = 1; + queue.push_back(i); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int q = t.at(px + dx, py + dy); + if (!reached[q] && !L.ridge[q] && !L.pond[q]) + { + reached[q] = 1; + queue.push_back(q); + } + } + } + for (int i = 0; i < n; ++i) + if (!L.ridge[i] && !L.pond[i] && !reached[i]) + L.pond[i] = 1; } -Layout design(const GenerationRequest &request, GenerationContext &context) { - const StoneHighlandsOptions o(request); - Layout L; - L.t = {1 << request.wDec, 1 << request.hDec}; - const Torus &t = L.t; - const int n = t.w * t.h; +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const StoneHighlandsOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.w * t.h; - const std::vector sites = scatterSites(t, o.valleySize, context); - const std::vector label = labelCells(t, sites, o.valleySize, context); + const std::vector sites = scatterSites(t, o.valleySize, context); + const std::vector label = labelCells(t, sites, o.valleySize, context); - // A tile is ridge when any of its eight neighbours has a lower label. A tile no ridge was - // painted on then touches only its own label or ridge, so no unit can step, even diagonally, - // from one cell into another. - L.ridge.assign(n, 0); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - for (int dy = -1; dy <= 1 && !L.ridge[i]; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (label[t.at(x + dx, y + dy)] < label[i]) { - L.ridge[i] = 1; - break; - } - } - // Stretches of ridgeline two tiles thick, where a noise field says so: 45% of the ridge at the - // default stone amount, which scales that share. More stone never opens a gap. - const std::vector thickness = - periodicNoise(t.w, t.h, std::max(4, o.valleySize / 2), context.stream("highlands-ridge")); - const int thickShare = int(std::min(100, scaledCount(45, o.stone))); - const int thickLevel = percentile(thickness, 100 - thickShare); - std::vector thick = L.ridge; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - for (int i = 0; i < n; ++i) { - if (!L.ridge[i] || thickShare <= 0 || thickness[i] < thickLevel) - continue; - for (const auto &s : steps) { - const int j = t.at(i % t.w + s[0], i / t.w + s[1]); - if (label[j] < label[i]) - thick[j] = 1; - } - } - L.ridge.swap(thick); + // A tile is ridge when any of its eight neighbours has a lower label. A tile no ridge was + // painted on then touches only its own label or ridge, so no unit can step, even diagonally, + // from one cell into another. + L.ridge.assign(n, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + for (int dy = -1; dy <= 1 && !L.ridge[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (label[t.at(x + dx, y + dy)] < label[i]) + { + L.ridge[i] = 1; + break; + } + } + // Stretches of ridgeline two tiles thick, where a noise field says so: 45% of the ridge at the + // default stone amount, which scales that share. More stone never opens a gap. + const std::vector thickness = + periodicNoise(t.w, t.h, std::max(4, o.valleySize / 2), context.stream("highlands-ridge")); + const int thickShare = int(std::min(100, scaledCount(45, o.stone))); + const int thickLevel = percentile(thickness, 100 - thickShare); + std::vector thick = L.ridge; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (int i = 0; i < n; ++i) + { + if (!L.ridge[i] || thickShare <= 0 || thickness[i] < thickLevel) + continue; + for (const auto &s : steps) + { + const int j = t.at(i % t.w + s[0], i / t.w + s[1]); + if (label[j] < label[i]) + thick[j] = 1; + } + } + L.ridge.swap(thick); - // Valleys are the walkable components left between the ridges; pockets too small to live in - // are filled with stone. - std::vector open(n); - for (int i = 0; i < n; ++i) - open[i] = !L.ridge[i]; - const std::vector region = connectedRegions(open, t.w, t.h, true, GridNeighbors::Eight); - int regions = 0; - for (int r : region) - regions = std::max(regions, r + 1); - std::vector size(regions, 0), id(regions, -1); - for (int r : region) - if (r >= 0) - ++size[r]; - for (int r = 0; r < regions; ++r) - if (size[r] >= kPocketTiles) - id[r] = L.valleys++; - L.valley.assign(n, -1); - for (int i = 0; i < n; ++i) - if (region[i] >= 0) { - if (id[region[i]] < 0) - L.ridge[i] = 1; - else - L.valley[i] = id[region[i]]; - } - if (L.valleys < 1) { - L.failure = "the ridges left no valley to live in"; - return L; - } + // Valleys are the walkable components left between the ridges; pockets too small to live in + // are filled with stone. + std::vector open(n); + for (int i = 0; i < n; ++i) + open[i] = !L.ridge[i]; + const std::vector region = connectedRegions(open, t.w, t.h, true, GridNeighbors::Eight); + int regions = 0; + for (int r : region) + regions = std::max(regions, r + 1); + std::vector size(regions, 0), id(regions, -1); + for (int r : region) + if (r >= 0) + ++size[r]; + for (int r = 0; r < regions; ++r) + if (size[r] >= kPocketTiles) + id[r] = L.valleys++; + L.valley.assign(n, -1); + for (int i = 0; i < n; ++i) + if (region[i] >= 0) + { + if (id[region[i]] < 0) + L.ridge[i] = 1; + else + L.valley[i] = id[region[i]]; + } + if (L.valleys < 1) + { + L.failure = "the ridges left no valley to live in"; + return L; + } - // Pass sites: ridge tiles with exactly two valleys close by, and no third one within reach of - // the cleared box. Junction tiles touch three or more valleys; distance along the ridge from - // the nearest junction ranks how central a site is on its ridgeline. - const int reach = o.passWidth / 2 + 2; - std::map, std::vector> exclusive, anyPair; - std::vector alongRidge(n, -1), queue; - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - if (!L.ridge[i]) - continue; - // Not near/far: both are legacy macros in Windows' windef.h that expand to nothing, which - // turns `int near[3]` into a structured binding declaration and fails to compile on mingw. - int nearIds[3], farIds[3]; - const int count = nearbyValleys(L, x, y, 2, nearIds, 3); - if (count >= 3) { - alongRidge[i] = 0; - queue.push_back(i); - } - for (int p = 0; p < count; ++p) - for (int q = p + 1; q < count; ++q) - anyPair[{nearIds[p], nearIds[q]}].push_back(i); - if (count == 2 && nearbyValleys(L, x, y, reach, farIds, 3) == 2) - exclusive[{nearIds[0], nearIds[1]}].push_back(i); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int q = t.at(px + dx, py + dy); - if (L.ridge[q] && alongRidge[q] < 0) { - alongRidge[q] = alongRidge[p] + 1; - queue.push_back(q); - } - } - } + // Pass sites: ridge tiles with exactly two valleys close by, and no third one within reach of + // the cleared box. Junction tiles touch three or more valleys; distance along the ridge from + // the nearest junction ranks how central a site is on its ridgeline. + const int reach = o.passWidth / 2 + 2; + std::map, std::vector> exclusive, anyPair; + std::vector alongRidge(n, -1), queue; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (!L.ridge[i]) + continue; + // Not near/far: both are legacy macros in Windows' windef.h that expand to nothing, which + // turns `int near[3]` into a structured binding declaration and fails to compile on mingw. + int nearIds[3], farIds[3]; + const int count = nearbyValleys(L, x, y, 2, nearIds, 3); + if (count >= 3) + { + alongRidge[i] = 0; + queue.push_back(i); + } + for (int p = 0; p < count; ++p) + for (int q = p + 1; q < count; ++q) + anyPair[{nearIds[p], nearIds[q]}].push_back(i); + if (count == 2 && nearbyValleys(L, x, y, reach, farIds, 3) == 2) + exclusive[{nearIds[0], nearIds[1]}].push_back(i); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int q = t.at(px + dx, py + dy); + if (L.ridge[q] && alongRidge[q] < 0) + { + alongRidge[q] = alongRidge[p] + 1; + queue.push_back(q); + } + } + } - // A random spanning tree of passes joins every valley; loopiness then opens that share of the - // remaining shared ridgelines as well. - L.passTile.assign(n, 0); - Disjoint sets(L.valleys); - int groups = L.valleys; - std::vector> edges, spare; - for (const auto &e : exclusive) - if (int(e.second.size()) >= kMinimumBoundary) - edges.push_back(e.first); - shuffle(edges, context, "highlands-passes"); - for (const auto &e : edges) { - if (sets.find(e.first) == sets.find(e.second)) { - spare.push_back(e); - continue; - } - if (carvePass(L, context, e.first, e.second, exclusive[e], alongRidge, o.passWidth)) { - sets.join(e.first, e.second); - --groups; - } - } - if (groups > 1) { - // Some valley only meets its neighbours near a junction; allow a pass there. - std::vector> fallback; - for (const auto &e : anyPair) - fallback.push_back(e.first); - shuffle(fallback, context, "highlands-passes"); - for (const auto &e : fallback) - if (sets.find(e.first) != sets.find(e.second) && - carvePass(L, context, e.first, e.second, anyPair[e], alongRidge, o.passWidth)) { - sets.join(e.first, e.second); - --groups; - } - } - if (groups > 1) { - L.failure = "passes could not join every valley (" + std::to_string(groups) + " groups remain)"; - return L; - } - const int extra = int((std::int64_t(spare.size()) * o.loopiness + 50) / 100); - for (size_t k = 0, added = 0; k < spare.size() && int(added) < extra; ++k) - if (carvePass(L, context, spare[k].first, spare[k].second, exclusive[spare[k]], alongRidge, - o.passWidth)) - ++added; + // A random spanning tree of passes joins every valley; loopiness then opens that share of the + // remaining shared ridgelines as well. + L.passTile.assign(n, 0); + Disjoint sets(L.valleys); + int groups = L.valleys; + std::vector> edges, spare; + for (const auto &e : exclusive) + if (int(e.second.size()) >= kMinimumBoundary) + edges.push_back(e.first); + shuffle(edges, context, "highlands-passes"); + for (const auto &e : edges) + { + if (sets.find(e.first) == sets.find(e.second)) + { + spare.push_back(e); + continue; + } + if (carvePass(L, context, e.first, e.second, exclusive[e], alongRidge, o.passWidth)) + { + sets.join(e.first, e.second); + --groups; + } + } + if (groups > 1) + { + // Some valley only meets its neighbours near a junction; allow a pass there. + std::vector> fallback; + for (const auto &e : anyPair) + fallback.push_back(e.first); + shuffle(fallback, context, "highlands-passes"); + for (const auto &e : fallback) + if (sets.find(e.first) != sets.find(e.second) && + carvePass(L, context, e.first, e.second, anyPair[e], alongRidge, o.passWidth)) + { + sets.join(e.first, e.second); + --groups; + } + } + if (groups > 1) + { + L.failure = + "passes could not join every valley (" + std::to_string(groups) + " groups remain)"; + return L; + } + const int extra = int((std::int64_t(spare.size()) * o.loopiness + 50) / 100); + for (size_t k = 0, added = 0; k < spare.size() && int(added) < extra; ++k) + if (carvePass(L, context, spare[k].first, spare[k].second, exclusive[spare[k]], alongRidge, + o.passWidth)) + ++added; - std::vector wall(n); - for (int i = 0; i < n; ++i) - wall[i] = L.ridge[i] || L.passTile[i]; - L.ridgeDistance = chebyshevDistance(t, wall); - L.tilesOf.assign(L.valleys, {}); - L.pole.assign(L.valleys, -1); - for (int i = 0; i < n; ++i) { - const int v = L.valley[i]; - if (v < 0 || L.passTile[i]) - continue; - L.tilesOf[v].push_back(i); - if (L.pole[v] < 0 || L.ridgeDistance[i] > L.ridgeDistance[L.pole[v]]) - L.pole[v] = i; - } - if (!chooseHomes(L, context, request.nbTeams)) - return L; - placePonds(L, context, o.valleySize, o.pondSize); - for (size_t k = 0; k < L.homes.size(); ++k) - if (L.pond[t.at(L.homes[k].x, L.homes[k].y)]) - L.failure = "a pond covered the home of colony " + std::to_string(k); - return L; + std::vector wall(n); + for (int i = 0; i < n; ++i) + wall[i] = L.ridge[i] || L.passTile[i]; + L.ridgeDistance = chebyshevDistance(t, wall); + L.tilesOf.assign(L.valleys, {}); + L.pole.assign(L.valleys, -1); + for (int i = 0; i < n; ++i) + { + const int v = L.valley[i]; + if (v < 0 || L.passTile[i]) + continue; + L.tilesOf[v].push_back(i); + if (L.pole[v] < 0 || L.ridgeDistance[i] > L.ridgeDistance[L.pole[v]]) + L.pole[v] = i; + } + if (!chooseHomes(L, context, request.nbTeams)) + return L; + placePonds(L, context, o.valleySize, o.pondSize); + for (size_t k = 0; k < L.homes.size(); ++k) + if (L.pond[t.at(L.homes[k].x, L.homes[k].y)]) + L.failure = "a pond covered the home of colony " + std::to_string(k); + return L; } // Terrain straight from the design: pond water inside a two-tile sand beach, grass everywhere // else, and stone on every ridge tile. Algae only regrows beside a tile of pure sand, which a // one-tile beach never makes: every tile takes its terrain from four undermap corners. -bool stampTerrain(Map &map, GenerationContext &context, const Layout &L) { - const Torus &t = L.t; - constexpr int kBeach = 2; - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - if (L.pond[i]) { - map.setUMTerrain(x, y, WATER); - continue; - } - bool shore = false; - for (int dy = -kBeach; dy <= kBeach && !shore; ++dy) - for (int dx = -kBeach; dx <= kBeach; ++dx) - if (L.pond[t.at(x + dx, y + dy)]) { - shore = true; - break; - } - map.setUMTerrain(x, y, shore ? SAND : GRASS); - } - map.rebuildTerrain(); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - if (!L.ridge[size_t(y) * t.w + x]) - continue; - if (map.getTerrainType(x, y) != GRASS) { - context.detail = "ridge tile (" + std::to_string(x) + ", " + std::to_string(y) + - ") is not solid grass"; - return false; - } - map.setResource(x, y, STONE, 1); - } - return true; +bool stampTerrain(Map &map, GenerationContext &context, const Layout &L) +{ + const Torus &t = L.t; + constexpr int kBeach = 2; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (L.pond[i]) + { + map.setUMTerrain(x, y, WATER); + continue; + } + bool shore = false; + for (int dy = -kBeach; dy <= kBeach && !shore; ++dy) + for (int dx = -kBeach; dx <= kBeach; ++dx) + if (L.pond[t.at(x + dx, y + dy)]) + { + shore = true; + break; + } + map.setUMTerrain(x, y, shore ? SAND : GRASS); + } + map.rebuildTerrain(); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + if (!L.ridge[size_t(y) * t.w + x]) + continue; + if (map.getTerrainType(x, y) != GRASS) + { + context.detail = "ridge tile (" + std::to_string(x) + ", " + std::to_string(y) + + ") is not solid grass"; + return false; + } + map.setResource(x, y, STONE, 1); + } + return true; } // Every home gets the same kit: a wheat patch and a wood patch of kHomeKit tiles each, a few // steps from the swarm on either side of the way down to its pond. -void furnishHome(Map &map, const Layout &L, int bootX, int bootY, int homeValley) { - const Torus &t = L.t; - const int cx = bootX + 2, cy = bootY + 2; - int ox = 0, oy = 1, nearest = INT_MAX; - for (int dy = -24; dy <= 24; ++dy) - for (int dx = -24; dx <= 24; ++dx) - if (L.pond[t.at(cx + dx, cy + dy)] && dx * dx + dy * dy < nearest) { - nearest = dx * dx + dy * dy; - ox = dx; - oy = dy; - } - const double length = std::max(1.0, std::sqrt(double(ox * ox + oy * oy))); - const double ux = ox / length, uy = oy / length; - const double cosine = std::cos(50.0 * 3.14159265358979 / 180.0); - const double sine = std::sin(50.0 * 3.14159265358979 / 180.0); - for (int side : {1, -1}) { - const double rx = ux * cosine - side * uy * sine, ry = side * ux * sine + uy * cosine; - const int ax = cx + int(std::lround(rx * kKitReach)), ay = cy + int(std::lround(ry * kKitReach)); - std::vector> tiles; - for (int dy = -kKitSearch; dy <= kKitSearch; ++dy) - for (int dx = -kKitSearch; dx <= kKitSearch; ++dx) { - const int x = t.x(ax + dx), y = t.y(ay + dy), i = y * t.w + x; - if (L.valley[i] != homeValley || L.passTile[i] || L.ridgeDistance[i] < kRidgeRoad) - continue; - if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || - map.getGroundUnit(x, y) != NOGUID) - continue; - // Two clear tiles all round the swarm, where its workers stand and walk out. - const int fx = t.offsetX(bootX, x), fy = t.offsetY(bootY, y); - const int gapX = fx < 0 ? -fx : std::max(0, fx - 3), gapY = fy < 0 ? -fy : std::max(0, fy - 3); - if (std::max(gapX, gapY) <= 2) - continue; - tiles.push_back({dx * dx + dy * dy, i}); - } - std::sort(tiles.begin(), tiles.end()); - for (int k = 0; k < kHomeKit && k < int(tiles.size()); ++k) - map.setResource(tiles[k].second % t.w, tiles[k].second / t.w, side > 0 ? CORN : WOOD, 1); - } +void furnishHome(Map &map, const Layout &L, int bootX, int bootY, int homeValley) +{ + const Torus &t = L.t; + const int cx = bootX + 2, cy = bootY + 2; + int ox = 0, oy = 1, nearest = INT_MAX; + for (int dy = -24; dy <= 24; ++dy) + for (int dx = -24; dx <= 24; ++dx) + if (L.pond[t.at(cx + dx, cy + dy)] && dx * dx + dy * dy < nearest) + { + nearest = dx * dx + dy * dy; + ox = dx; + oy = dy; + } + const double length = std::max(1.0, std::sqrt(double(ox * ox + oy * oy))); + const double ux = ox / length, uy = oy / length; + const double cosine = std::cos(50.0 * 3.14159265358979 / 180.0); + const double sine = std::sin(50.0 * 3.14159265358979 / 180.0); + for (int side : {1, -1}) + { + const double rx = ux * cosine - side * uy * sine, ry = side * ux * sine + uy * cosine; + const int ax = cx + int(std::lround(rx * kKitReach)), + ay = cy + int(std::lround(ry * kKitReach)); + std::vector> tiles; + for (int dy = -kKitSearch; dy <= kKitSearch; ++dy) + for (int dx = -kKitSearch; dx <= kKitSearch; ++dx) + { + const int x = t.x(ax + dx), y = t.y(ay + dy), i = y * t.w + x; + if (L.valley[i] != homeValley || L.passTile[i] || L.ridgeDistance[i] < kRidgeRoad) + continue; + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + // Two clear tiles all round the swarm, where its workers stand and walk out. + const int fx = t.offsetX(bootX, x), fy = t.offsetY(bootY, y); + const int gapX = fx < 0 ? -fx : std::max(0, fx - 3), + gapY = fy < 0 ? -fy : std::max(0, fy - 3); + if (std::max(gapX, gapY) <= 2) + continue; + tiles.push_back({dx * dx + dy * dy, i}); + } + std::sort(tiles.begin(), tiles.end()); + for (int k = 0; k < kHomeKit && k < int(tiles.size()); ++k) + map.setResource(tiles[k].second % t.w, tiles[k].second / t.w, side > 0 ? CORN : WOOD, + 1); + } } -void stampBox(const Torus &t, std::vector &mask, int cx, int cy, int radius) { - for (int dy = -radius; dy <= radius; ++dy) - for (int dx = -radius; dx <= radius; ++dx) - mask[t.at(cx + dx, cy + dy)] = 1; +void stampBox(const Torus &t, std::vector &mask, int cx, int cy, int radius) +{ + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) + mask[t.at(cx + dx, cy + dy)] = 1; } // Ambient farmland: the most fertile ground around each pond, in patches, split 2:1 between // wheat and wood by an unrelated noise field so the two crops alternate around the shore. void scatterFarmland(Map &map, GenerationContext &context, const Layout &L, - const std::vector &pondDistance, const std::vector &keepClear, - int wheatPercent, int woodPercent) { - const Torus &t = L.t; - const int n = t.w * t.h; - const Fertility::Field fertility = Fertility::forMap(map, false); - const std::vector split = periodicNoise(t.w, t.h, 8, context.stream("highlands-farmland")); - const std::vector patch = fractalNoise(t.w, t.h, 16, 2, context.stream("highlands-farmland")); - const int patchLevel = percentile(patch, 45); - std::vector> pool(L.valleys); - std::vector pondTiles(L.valleys, 0); - for (int i = 0; i < n; ++i) { - const int v = L.valley[i]; - if (v < 0) - continue; - if (L.pond[i]) { - ++pondTiles[v]; - continue; - } - const int x = i % t.w, y = i / t.w; - if (keepClear[i] || L.ridgeDistance[i] < kRidgeRoad || pondDistance[i] < 2 || - pondDistance[i] > 9 || patch[i] < patchLevel || fertility.at(x, y) == 0) - continue; - if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || - map.getGroundUnit(x, y) != NOGUID) - continue; - pool[v].push_back(i); - } - for (int v = 0; v < L.valleys; ++v) { - std::vector &tiles = pool[v]; - const int wanted = std::min(int(std::int64_t(pondTiles[v]) * kFarmlandPercent / 100), - int(tiles.size()) * 2 / 3); - // Two thirds wheat and the rest wood, each scaled by its amount; more than the default may - // take every eligible tile, keeping the two crops' proportions. - int wheat = int(scaledCount(wanted * 2 / 3, wheatPercent)); - int total = wheat + int(scaledCount(wanted - wanted * 2 / 3, woodPercent)); - if (total > int(tiles.size())) { - wheat = int(std::int64_t(wheat) * tiles.size() / total); - total = int(tiles.size()); - } - if (total <= 0) - continue; - std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { - return fertility.at(a % t.w, a / t.w) > fertility.at(b % t.w, b / t.w); - }); - tiles.resize(total); - std::stable_sort(tiles.begin(), tiles.end(), [&](int a, int b) { return split[a] < split[b]; }); - for (int k = 0; k < total; ++k) - map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheat ? CORN : WOOD, 1); - } + const std::vector &pondDistance, + const std::vector &keepClear, int wheatPercent, int woodPercent) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector split = periodicNoise(t.w, t.h, 8, context.stream("highlands-farmland")); + const std::vector patch = + fractalNoise(t.w, t.h, 16, 2, context.stream("highlands-farmland")); + const int patchLevel = percentile(patch, 45); + std::vector> pool(L.valleys); + std::vector pondTiles(L.valleys, 0); + for (int i = 0; i < n; ++i) + { + const int v = L.valley[i]; + if (v < 0) + continue; + if (L.pond[i]) + { + ++pondTiles[v]; + continue; + } + const int x = i % t.w, y = i / t.w; + if (keepClear[i] || L.ridgeDistance[i] < kRidgeRoad || pondDistance[i] < 2 || + pondDistance[i] > 9 || patch[i] < patchLevel || fertility.at(x, y) == 0) + continue; + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + pool[v].push_back(i); + } + for (int v = 0; v < L.valleys; ++v) + { + std::vector &tiles = pool[v]; + const int wanted = std::min(int(std::int64_t(pondTiles[v]) * kFarmlandPercent / 100), + int(tiles.size()) * 2 / 3); + // Two thirds wheat and the rest wood, each scaled by its amount; more than the default may + // take every eligible tile, keeping the two crops' proportions. + int wheat = int(scaledCount(wanted * 2 / 3, wheatPercent)); + int total = wheat + int(scaledCount(wanted - wanted * 2 / 3, woodPercent)); + if (total > int(tiles.size())) + { + wheat = int(std::int64_t(wheat) * tiles.size() / total); + total = int(tiles.size()); + } + if (total <= 0) + continue; + std::stable_sort( + tiles.begin(), tiles.end(), [&](int a, int b) + { return fertility.at(a % t.w, a / t.w) > fertility.at(b % t.w, b / t.w); }); + tiles.resize(total); + std::stable_sort(tiles.begin(), tiles.end(), + [&](int a, int b) { return split[a] < split[b]; }); + for (int k = 0; k < total; ++k) + map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheat ? CORN : WOOD, 1); + } } // Algae in the open water of every pond. -void seedAlgae(Map &map, GenerationContext &context, const Layout &L, int algaePercent) { - const Torus &t = L.t; - const std::vector noise = periodicNoise(t.w, t.h, 6, context.stream("highlands-algae")); - std::vector water, levels; - for (int i = 0; i < t.w * t.h; ++i) - if (L.pond[i] && map.isWater(i % t.w, i / t.w)) { - water.push_back(i); - levels.push_back(noise[i]); - } - if (water.empty() || algaePercent <= 0) - return; - const int level = percentile( - levels, int(std::min(100, scaledCount(kAlgaePercent, algaePercent)))); - for (int i : water) - if (noise[i] <= level) - map.setResource(i % t.w, i / t.w, ALGA, 1); +void seedAlgae(Map &map, GenerationContext &context, const Layout &L, int algaePercent) +{ + const Torus &t = L.t; + const std::vector noise = periodicNoise(t.w, t.h, 6, context.stream("highlands-algae")); + std::vector water, levels; + for (int i = 0; i < t.w * t.h; ++i) + if (L.pond[i] && map.isWater(i % t.w, i / t.w)) + { + water.push_back(i); + levels.push_back(noise[i]); + } + if (water.empty() || algaePercent <= 0) + return; + const int level = percentile( + levels, int(std::min(100, scaledCount(kAlgaePercent, algaePercent)))); + for (int i : water) + if (noise[i] <= level) + map.setResource(i % t.w, i / t.w, ALGA, 1); } // Fruit is rare: small groves beside the ponds of valleys no colony starts in, a prize for // whoever holds the passes to them - or in any valley, colonies' own included, if asked. void plantFruit(Map &map, GenerationContext &context, const Layout &L, - const std::vector &pondDistance, const std::vector &keepClear, - const std::vector &homeValley, int fruit, bool inHomeValleys) { - if (fruit <= 0) - return; - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector> pool(L.valleys); - for (int i = 0; i < n; ++i) { - const int v = L.valley[i], x = i % t.w, y = i / t.w; - if (v < 0 || keepClear[i] || L.pond[i] || L.ridgeDistance[i] < kRidgeRoad + 1 || - pondDistance[i] < 3 || pondDistance[i] > 8) - continue; - if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || - map.getGroundUnit(x, y) != NOGUID) - continue; - pool[v].push_back(i); - } - std::vector valleys; - for (int pass = 0; pass < 2 && valleys.empty(); ++pass) - for (int v = 0; v < L.valleys; ++v) - if (!pool[v].empty() && (pass == 1 || inHomeValleys || !homeValley[v])) - valleys.push_back(v); - if (valleys.empty()) - return; - shuffle(valleys, context, "highlands-fruit"); - const int groves = std::max(1, int(std::int64_t(fruit) * n / 16384)); - const int firstType = int(context.bounded("highlands-fruit", 3)); - for (int g = 0; g < groves; ++g) { - const std::vector &tiles = pool[valleys[g % valleys.size()]]; - const int anchor = tiles[context.bounded("highlands-fruit", std::uint32_t(tiles.size()))]; - const int ax = anchor % t.w, ay = anchor / t.w; - std::vector> grove; - for (int tile : tiles) { - const int d = t.dist2(ax, ay, tile % t.w, tile / t.w); - if (d <= 8 && !map.isResource(tile % t.w, tile / t.w)) - grove.push_back({d, tile}); - } - std::sort(grove.begin(), grove.end()); - for (int k = 0; k < kFruitGrove && k < int(grove.size()); ++k) - map.setResource(grove[k].second % t.w, grove[k].second / t.w, CHERRY + (firstType + g) % 3, 1); - } + const std::vector &pondDistance, const std::vector &keepClear, + const std::vector &homeValley, int fruit, bool inHomeValleys) +{ + if (fruit <= 0) + return; + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector> pool(L.valleys); + for (int i = 0; i < n; ++i) + { + const int v = L.valley[i], x = i % t.w, y = i / t.w; + if (v < 0 || keepClear[i] || L.pond[i] || L.ridgeDistance[i] < kRidgeRoad + 1 || + pondDistance[i] < 3 || pondDistance[i] > 8) + continue; + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || + map.getGroundUnit(x, y) != NOGUID) + continue; + pool[v].push_back(i); + } + std::vector valleys; + for (int pass = 0; pass < 2 && valleys.empty(); ++pass) + for (int v = 0; v < L.valleys; ++v) + if (!pool[v].empty() && (pass == 1 || inHomeValleys || !homeValley[v])) + valleys.push_back(v); + if (valleys.empty()) + return; + shuffle(valleys, context, "highlands-fruit"); + const int groves = std::max(1, int(std::int64_t(fruit) * n / 16384)); + const int firstType = int(context.bounded("highlands-fruit", 3)); + for (int g = 0; g < groves; ++g) + { + const std::vector &tiles = pool[valleys[g % valleys.size()]]; + const int anchor = tiles[context.bounded("highlands-fruit", std::uint32_t(tiles.size()))]; + const int ax = anchor % t.w, ay = anchor / t.w; + std::vector> grove; + for (int tile : tiles) + { + const int d = t.dist2(ax, ay, tile % t.w, tile / t.w); + if (d <= 8 && !map.isResource(tile % t.w, tile / t.w)) + grove.push_back({d, tile}); + } + std::sort(grove.begin(), grove.end()); + for (int k = 0; k < kFruitGrove && k < int(grove.size()); ++k) + map.setResource(grove[k].second % t.w, grove[k].second / t.w, + CHERRY + (firstType + g) % 3, 1); + } } -bool generate(Game &game, GenerationContext &context) { - context.stage = "highland layout"; - const StoneHighlandsOptions o(context.request); - Map &map = game.map; - const int teams = context.request.nbTeams; - map.makeHomogenMap(GRASS); - for (int i = 0; i < teams; ++i) - game.addTeam(); - const Layout L = design(context.request, context); - if (!L.failure.empty()) { - context.detail = L.failure; - return false; - } - const Torus &t = L.t; - const int n = t.w * t.h; +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "highland layout"; + const StoneHighlandsOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(GRASS); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.w * t.h; - context.stage = "highland terrain"; - if (!stampTerrain(map, context, L)) - return false; + context.stage = "highland terrain"; + if (!stampTerrain(map, context, L)) + return false; - context.stage = "highland colonies"; - const std::vector pondDistance = chebyshevDistance(t, L.pond); - for (int team = 0; team < teams; ++team) { - const Home &h = L.homes[team]; - std::vector home(n, 0); - for (int dy = -10; dy <= 10; ++dy) - for (int dx = -10; dx <= 10; ++dx) { - const int i = t.at(h.x + dx, h.y + dy); - if (L.valley[i] == h.valley && !L.passTile[i] && L.ridgeDistance[i] >= kRidgeRoad && - pondDistance[i] >= 3) - home[i] = 1; - } - // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. - if (!placeSettlement(game, context, team, home, {h.x - 2, h.y - 2}, "highlands-starts")) - return false; - } - context.stage = "highland resources"; - std::vector keepClear(n, 0), homeValley(L.valleys, 0); - for (const Pass &p : L.passes) - stampBox(t, keepClear, p.x, p.y, p.hi + kRidgeRoad + 1); - for (int team = 0; team < teams; ++team) { - stampBox(t, keepClear, context.bootX[team] + 2, context.bootY[team] + 2, kHomeClear); - homeValley[L.homes[team].valley] = 1; - furnishHome(map, L, context.bootX[team], context.bootY[team], L.homes[team].valley); - } - scatterFarmland(map, context, L, pondDistance, keepClear, o.wheat, o.wood); - seedAlgae(map, context, L, o.algae); - plantFruit(map, context, L, pondDistance, keepClear, homeValley, o.fruit, o.homeValleyFruit); - // The kits above already put wheat and wood a few steps from every swarm; this is only the - // backstop, and the ridges are designed walls it must never clear. - guaranteeStartingResources(game, context, 24, 32, 0, &L.ridge); - // The backstop places what it has to wherever a colony can walk, and that includes passes. - // Passes and their mouths hold nothing but their own ridge stone. - for (const Pass &p : L.passes) - for (int dy = p.lo - 2; dy <= p.hi + 2; ++dy) - for (int dx = p.lo - 2; dx <= p.hi + 2; ++dx) { - const int x = t.x(p.x + dx), y = t.y(p.y + dy); - if (map.isResource(x, y) && map.getResource(x, y).type != STONE) - map.setNoResource(x, y, 1); - } - return true; + context.stage = "highland colonies"; + const std::vector pondDistance = chebyshevDistance(t, L.pond); + for (int team = 0; team < teams; ++team) + { + const Home &h = L.homes[team]; + std::vector home(n, 0); + for (int dy = -10; dy <= 10; ++dy) + for (int dx = -10; dx <= 10; ++dx) + { + const int i = t.at(h.x + dx, h.y + dy); + if (L.valley[i] == h.valley && !L.passTile[i] && L.ridgeDistance[i] >= kRidgeRoad && + pondDistance[i] >= 3) + home[i] = 1; + } + // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. + if (!placeSettlement(game, context, team, home, {h.x - 2, h.y - 2}, "highlands-starts")) + return false; + } + context.stage = "highland resources"; + std::vector keepClear(n, 0), homeValley(L.valleys, 0); + for (const Pass &p : L.passes) + stampBox(t, keepClear, p.x, p.y, p.hi + kRidgeRoad + 1); + for (int team = 0; team < teams; ++team) + { + stampBox(t, keepClear, context.bootX[team] + 2, context.bootY[team] + 2, kHomeClear); + homeValley[L.homes[team].valley] = 1; + furnishHome(map, L, context.bootX[team], context.bootY[team], L.homes[team].valley); + } + scatterFarmland(map, context, L, pondDistance, keepClear, o.wheat, o.wood); + seedAlgae(map, context, L, o.algae); + plantFruit(map, context, L, pondDistance, keepClear, homeValley, o.fruit, o.homeValleyFruit); + // The kits above already put wheat and wood a few steps from every swarm; this is only the + // backstop, and the ridges are designed walls it must never clear. + guaranteeStartingResources(game, context, 24, 32, 0, &L.ridge); + // The backstop places what it has to wherever a colony can walk, and that includes passes. + // Passes and their mouths hold nothing but their own ridge stone. + for (const Pass &p : L.passes) + for (int dy = p.lo - 2; dy <= p.hi + 2; ++dy) + for (int dx = p.lo - 2; dx <= p.hi + 2; ++dx) + { + const int x = t.x(p.x + dx), y = t.y(p.y + dy); + if (map.isResource(x, y) && map.getResource(x, y).type != STONE) + map.setNoResource(x, y, 1); + } + return true; } -std::string validateRequest(const GenerationRequest &r) { - const StoneHighlandsOptions o(r); - const std::int64_t area = std::int64_t(1) << (r.wDec + r.hDec); - if (area < 4 * std::int64_t(o.valleySize) * o.valleySize) - return "These valleys are too big for the map; use a bigger map or smaller valleys."; - if (area < std::int64_t(kAreaPerColony) * r.nbTeams) - return "The highlands have too little room for this many colonies; use a bigger map or fewer " - "colonies."; - return ""; +std::string validateRequest(const GenerationRequest &r) +{ + const StoneHighlandsOptions o(r); + const std::int64_t area = std::int64_t(1) << (r.wDec + r.hDec); + if (area < 4 * std::int64_t(o.valleySize) * o.valleySize) + return "These valleys are too big for the map; use a bigger map or smaller valleys."; + if (area < std::int64_t(kAreaPerColony) * r.nbTeams) + return "The highlands have too little room for this many colonies; use a bigger map or " + "fewer " + "colonies."; + return ""; } // Checked on the finished world against the rebuilt design: every ridge still carries its stone, // every pass is open and reachable on foot, and every colony can walk to every other. Water, // buildings and every resource tile (farmland included) block the flood; units don't. -std::string validateWorld(const Game &game, const GenerationContext &context) { - GenerationContext replay(context.request); - const Layout L = design(context.request, replay); - if (!L.failure.empty()) - return "The highland design could not be rebuilt: " + L.failure; - const Map &map = game.map; - const Torus &t = L.t; - const int n = t.w * t.h, teams = context.request.nbTeams; - if (map.getW() != t.w || map.getH() != t.h) - return "The highland design does not match the map size."; - for (int i = 0; i < n; ++i) - if (L.ridge[i] && map.getResource(i % t.w, i / t.w).type != STONE) - return "The ridge at (" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + - ") has lost its stone."; - const auto walkable = [&](int x, int y) { - return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - }; - for (size_t k = 0; k < L.passes.size(); ++k) { - const Pass &p = L.passes[k]; - for (int dy = p.lo; dy <= p.hi; ++dy) - for (int dx = p.lo; dx <= p.hi; ++dx) - if (!walkable(t.x(p.x + dx), t.y(p.y + dy))) - return "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + ", " + - std::to_string(p.y) + ") is blocked."; - } - std::vector reached(n, 0); - std::vector queue; - for (int i = 0; i < n; ++i) { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) { - reached[i] = 1; - queue.push_back(i); - } - } - for (size_t head = 0; head < queue.size(); ++head) { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int x = t.x(px + dx), y = t.y(py + dy), q = y * t.w + x; - if (!reached[q] && walkable(x, y)) { - reached[q] = 1; - queue.push_back(q); - } - } - } - std::vector teamReached(teams, 0); - for (int i = 0; i < n; ++i) { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && reached[i] && Unit::GIDtoTeam(gid) < teams) - teamReached[Unit::GIDtoTeam(gid)] = 1; - } - for (int team = 0; team < teams; ++team) - if (!teamReached[team]) - return "Colony " + std::to_string(team) + " cannot walk to colony 0 over the highlands."; - // Each pass must be on the colonies' walkable network and lead straight through: walking out - // of its box, without going far, reaches both valleys it joins. - for (size_t k = 0; k < L.passes.size(); ++k) { - const Pass &p = L.passes[k]; - const std::string where = "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + ", " + - std::to_string(p.y) + ")"; - if (!reached[t.at(p.x, p.y)]) - return where + " cannot be reached on foot."; - const int radius = p.hi + 4, side = 2 * radius + 1; - std::vector seen(size_t(side) * side, 0); - std::vector local; - for (int dy = p.lo; dy <= p.hi; ++dy) - for (int dx = p.lo; dx <= p.hi; ++dx) { - seen[size_t(dy + radius) * side + dx + radius] = 1; - local.push_back((dy + radius) * side + dx + radius); - } - bool sideA = false, sideB = false; - for (size_t head = 0; head < local.size() && !(sideA && sideB); ++head) { - const int lx = local[head] % side, ly = local[head] / side; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int nx = lx + dx, ny = ly + dy; - if (nx < 0 || ny < 0 || nx >= side || ny >= side || seen[size_t(ny) * side + nx]) - continue; - const int x = t.x(p.x + nx - radius), y = t.y(p.y + ny - radius); - if (!walkable(x, y)) - continue; - seen[size_t(ny) * side + nx] = 1; - local.push_back(ny * side + nx); - sideA |= L.valley[y * t.w + x] == p.a; - sideB |= L.valley[y * t.w + x] == p.b; - } - } - if (!sideA || !sideB) - return where + " does not lead through to both of its valleys."; - } - return ""; +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (!L.failure.empty()) + return "The highland design could not be rebuilt: " + L.failure; + const Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (map.getW() != t.w || map.getH() != t.h) + return "The highland design does not match the map size."; + for (int i = 0; i < n; ++i) + if (L.ridge[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "The ridge at (" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + + ") has lost its stone."; + const auto walkable = [&](int x, int y) + { return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; }; + for (size_t k = 0; k < L.passes.size(); ++k) + { + const Pass &p = L.passes[k]; + for (int dy = p.lo; dy <= p.hi; ++dy) + for (int dx = p.lo; dx <= p.hi; ++dx) + if (!walkable(t.x(p.x + dx), t.y(p.y + dy))) + return "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + ", " + + std::to_string(p.y) + ") is blocked."; + } + std::vector reached(n, 0); + std::vector queue; + for (int i = 0; i < n; ++i) + { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) + { + reached[i] = 1; + queue.push_back(i); + } + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int p = queue[head], px = p % t.w, py = p / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int x = t.x(px + dx), y = t.y(py + dy), q = y * t.w + x; + if (!reached[q] && walkable(x, y)) + { + reached[q] = 1; + queue.push_back(q); + } + } + } + std::vector teamReached(teams, 0); + for (int i = 0; i < n; ++i) + { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && reached[i] && Unit::GIDtoTeam(gid) < teams) + teamReached[Unit::GIDtoTeam(gid)] = 1; + } + for (int team = 0; team < teams; ++team) + if (!teamReached[team]) + return "Colony " + std::to_string(team) + + " cannot walk to colony 0 over the highlands."; + // Each pass must be on the colonies' walkable network and lead straight through: walking out + // of its box, without going far, reaches both valleys it joins. + for (size_t k = 0; k < L.passes.size(); ++k) + { + const Pass &p = L.passes[k]; + const std::string where = "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + + ", " + std::to_string(p.y) + ")"; + if (!reached[t.at(p.x, p.y)]) + return where + " cannot be reached on foot."; + const int radius = p.hi + 4, side = 2 * radius + 1; + std::vector seen(size_t(side) * side, 0); + std::vector local; + for (int dy = p.lo; dy <= p.hi; ++dy) + for (int dx = p.lo; dx <= p.hi; ++dx) + { + seen[size_t(dy + radius) * side + dx + radius] = 1; + local.push_back((dy + radius) * side + dx + radius); + } + bool sideA = false, sideB = false; + for (size_t head = 0; head < local.size() && !(sideA && sideB); ++head) + { + const int lx = local[head] % side, ly = local[head] / side; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int nx = lx + dx, ny = ly + dy; + if (nx < 0 || ny < 0 || nx >= side || ny >= side || + seen[size_t(ny) * side + nx]) + continue; + const int x = t.x(p.x + nx - radius), y = t.y(p.y + ny - radius); + if (!walkable(x, y)) + continue; + seen[size_t(ny) * side + nx] = 1; + local.push_back(ny * side + nx); + sideA |= L.valley[y * t.w + x] == p.a; + sideB |= L.valley[y * t.w + x] == p.b; + } + } + if (!sideA || !sideB) + return where + " does not lead through to both of its valleys."; + } + return ""; } } // namespace StoneHighlandsOptions::StoneHighlandsOptions(const GenerationRequest &r) - : valleySize(r.option("valley-size")), passWidth(r.option("pass-width")), - loopiness(r.option("loopiness")), pondSize(r.option("pond-size")), fruit(r.option("fruit")), - homeValleyFruit(r.option("home-valley-fruit") != 0), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")) {} + : valleySize(r.option("valley-size")), passWidth(r.option("pass-width")), + loopiness(r.option("loopiness")), pondSize(r.option("pond-size")), fruit(r.option("fruit")), + homeValleyFruit(r.option("home-valley-fruit") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")) +{ +} -GeneratorDefinition stoneHighlandsDefinition() { - return {"stone-highlands", - 14, - "Stone highlands", - 1, - false, - {// Average spacing between valley centres, in tiles. - {"valley-size", "Valley size", 20, 44, 4, 32, ControlGroup::Layout}, - {"pass-width", "Pass width", 3, 5, 1, 4, ControlGroup::Layout}, - // Share of the ridgelines left over after the spanning tree that get a pass as well. - {"loopiness", "Loopiness", 0, 100, 5, 30, ControlGroup::Layout}, - // Pond area as a percentage of each valley's area. - {"pond-size", "Pond size", 4, 16, 2, 8, ControlGroup::Terrain}, - // Fruit groves per 128x128 tiles of map. - {"fruit", "Fruit", 0, 12, 1, 4, ControlGroup::Resources}, - // On, fruit groves may grow in the valleys colonies start in too. - GeneratorControl::toggle("home-valley-fruit", "Fruit in home valleys", false, - ControlGroup::Resources), - // Wheat and wood scale the farmland around the ponds and algae the ponds' own; stone - // scales how much of the ridgeline is two tiles thick (45% at 100). Every colony's - // starting kit stays as it is. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount", 200), - GeneratorControl::percentage("algae-amount", "Algae amount")}, - generate, - true, - validateRequest, - validateWorld}; +GeneratorDefinition stoneHighlandsDefinition() +{ + return {"stone-highlands", + 14, + "Stone highlands", + 1, + false, + {// Average spacing between valley centres, in tiles. + {"valley-size", "Valley size", 20, 44, 4, 32, ControlGroup::Layout}, + {"pass-width", "Pass width", 3, 5, 1, 4, ControlGroup::Layout}, + // Share of the ridgelines left over after the spanning tree that get a pass as well. + {"loopiness", "Loopiness", 0, 100, 5, 30, ControlGroup::Layout}, + // Pond area as a percentage of each valley's area. + {"pond-size", "Pond size", 4, 16, 2, 8, ControlGroup::Terrain}, + // Fruit groves per 128x128 tiles of map. + {"fruit", "Fruit", 0, 12, 1, 4, ControlGroup::Resources}, + // On, fruit groves may grow in the valleys colonies start in too. + GeneratorControl::toggle("home-valley-fruit", "Fruit in home valleys", false, + ControlGroup::Resources), + // Wheat and wood scale the farmland around the ponds and algae the ponds' own; stone + // scales how much of the ridgeline is two tiles thick (45% at 100). Every colony's + // starting kit stays as it is. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount", 200), + GeneratorControl::percentage("algae-amount", "Algae amount")}, + generate, + true, + validateRequest, + validateWorld}; } diff --git a/src/map/generator/generators/StoneHighlandsGenerator.h b/src/map/generator/generators/StoneHighlandsGenerator.h index 6ff251b9e..668f7c35e 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.h +++ b/src/map/generator/generators/StoneHighlandsGenerator.h @@ -2,10 +2,11 @@ #pragma once #include "GenerationRequest.h" #include "GeneratorDefinition.h" -struct StoneHighlandsOptions { - int valleySize, passWidth, loopiness, pondSize, fruit; - bool homeValleyFruit; - int wheat, wood, stone, algae; // percentages of the default amounts - explicit StoneHighlandsOptions(const GenerationRequest &r); +struct StoneHighlandsOptions +{ + int valleySize, passWidth, loopiness, pondSize, fruit; + bool homeValleyFruit; + int wheat, wood, stone, algae; // percentages of the default amounts + explicit StoneHighlandsOptions(const GenerationRequest &r); }; GeneratorDefinition stoneHighlandsDefinition(); diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index ae9750dbc..d8c069065 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -32,7 +32,8 @@ using MapGeneration::scaledShare; // row-order pass is replaced by the same rule applied to every corner at once, per-tile random // clumps by deterministic growth, and the amounts the engine RNG gives each resource tile are // equalised over its orbit. validateWorld then checks the invariance on the finished world. -namespace { +namespace +{ constexpr double kPi = 3.14159265358979323846; @@ -50,9 +51,10 @@ constexpr double kMinimumSpacing = 28.0; // Every home's starting kit, the same whatever the richness control says. constexpr int kKitFarmland = 16, kKitStone = 4; -int wrap(int v, int n) { - v %= n; - return v < 0 ? v + n : v; +int wrap(int v, int n) +{ + v %= n; + return v < 0 ? v + n : v; } // A symmetry of the map about its centre, as a signed permutation matrix acting on doubled @@ -60,38 +62,43 @@ int wrap(int v, int n) { // (2u - W, 2v - H). A tile's terrain comes from its four corners (Map::regenerateMap), and the // same matrix maps a tile's corners onto its image's corners, so the tile rotation // (x, y) -> (W-1-y, x) is the corner rotation (u, v) -> (W-v, u). -struct Isometry { - int a, b, c, d; +struct Isometry +{ + int a, b, c, d; }; -struct Symmetry { - int width = 0, height = 0; - // elements[0] is the identity; colony k starts on elements[k]'s image of colony 0's home. - std::vector elements; +struct Symmetry +{ + int width = 0, height = 0; + // elements[0] is the identity; colony k starts on elements[k]'s image of colony 0's home. + std::vector elements; - int order() const { return int(elements.size()); } - int tile(int e, int x, int y) const { - const Isometry &g = elements[e]; - const int X = 2 * x + 1 - width, Y = 2 * y + 1 - height; - return wrap((g.c * X + g.d * Y + height - 1) / 2, height) * width + - wrap((g.a * X + g.b * Y + width - 1) / 2, width); - } - int corner(int e, int u, int v) const { - const Isometry &g = elements[e]; - const int U = 2 * u - width, V = 2 * v - height; - return wrap((g.c * U + g.d * V + height) / 2, height) * width + - wrap((g.a * U + g.b * V + width) / 2, width); - } - // The top-left tile of the image of a w x h footprint anchored at (x, y): the image of a - // rectangle is a rectangle, so it is the image's lowest corner on each axis. - std::pair anchor(int e, int x, int y, int w, int h) const { - const Isometry &g = elements[e]; - const int U0 = 2 * x - width, V0 = 2 * y - height; - const int U1 = 2 * (x + w) - width, V1 = 2 * (y + h) - height; - const int X = std::min(g.a * U0 + g.b * V0, g.a * U1 + g.b * V1); - const int Y = std::min(g.c * U0 + g.d * V0, g.c * U1 + g.d * V1); - return {wrap((X + width) / 2, width), wrap((Y + height) / 2, height)}; - } + int order() const { return int(elements.size()); } + int tile(int e, int x, int y) const + { + const Isometry &g = elements[e]; + const int X = 2 * x + 1 - width, Y = 2 * y + 1 - height; + return wrap((g.c * X + g.d * Y + height - 1) / 2, height) * width + + wrap((g.a * X + g.b * Y + width - 1) / 2, width); + } + int corner(int e, int u, int v) const + { + const Isometry &g = elements[e]; + const int U = 2 * u - width, V = 2 * v - height; + return wrap((g.c * U + g.d * V + height) / 2, height) * width + + wrap((g.a * U + g.b * V + width) / 2, width); + } + // The top-left tile of the image of a w x h footprint anchored at (x, y): the image of a + // rectangle is a rectangle, so it is the image's lowest corner on each axis. + std::pair anchor(int e, int x, int y, int w, int h) const + { + const Isometry &g = elements[e]; + const int U0 = 2 * x - width, V0 = 2 * y - height; + const int U1 = 2 * (x + w) - width, V1 = 2 * (y + h) - height; + const int X = std::min(g.a * U0 + g.b * V0, g.a * U1 + g.b * V1); + const int Y = std::min(g.c * U0 + g.d * V0, g.c * U1 + g.d * V1); + return {wrap((X + width) / 2, width), wrap((Y + height) / 2, height)}; + } }; // The symmetry that gives every colony identical ground: a half turn for two colonies, a @@ -99,849 +106,948 @@ struct Symmetry { // quarter turn doesn't map the map onto itself), and all eight symmetries of a square for // eight. Elements are listed in order round the centre, so neighbouring colonies are // neighbouring team numbers. Empty for colony counts no symmetry serves. -Symmetry symmetryFor(int width, int height, int teams) { - Symmetry s; - s.width = width; - s.height = height; - const bool square = width == height; - if (teams == 2) - s.elements = {{1, 0, 0, 1}, {-1, 0, 0, -1}}; - else if (teams == 4 && square) - s.elements = {{1, 0, 0, 1}, {0, -1, 1, 0}, {-1, 0, 0, -1}, {0, 1, -1, 0}}; - else if (teams == 4) - s.elements = {{1, 0, 0, 1}, {-1, 0, 0, 1}, {-1, 0, 0, -1}, {1, 0, 0, -1}}; - else if (teams == 8 && square) - s.elements = {{1, 0, 0, 1}, {0, 1, 1, 0}, {0, -1, 1, 0}, {-1, 0, 0, 1}, - {-1, 0, 0, -1}, {0, -1, -1, 0}, {0, 1, -1, 0}, {1, 0, 0, -1}}; - return s; +Symmetry symmetryFor(int width, int height, int teams) +{ + Symmetry s; + s.width = width; + s.height = height; + const bool square = width == height; + if (teams == 2) + s.elements = {{1, 0, 0, 1}, {-1, 0, 0, -1}}; + else if (teams == 4 && square) + s.elements = {{1, 0, 0, 1}, {0, -1, 1, 0}, {-1, 0, 0, -1}, {0, 1, -1, 0}}; + else if (teams == 4) + s.elements = {{1, 0, 0, 1}, {-1, 0, 0, 1}, {-1, 0, 0, -1}, {1, 0, 0, -1}}; + else if (teams == 8 && square) + s.elements = {{1, 0, 0, 1}, {0, 1, 1, 0}, {0, -1, 1, 0}, {-1, 0, 0, 1}, + {-1, 0, 0, -1}, {0, -1, -1, 0}, {0, 1, -1, 0}, {1, 0, 0, -1}}; + return s; } // Continuous positions relative to the map centre, in tiles. The centre is an undermap corner, // so corners sit on integers and tile centres on half-integers. -struct Point { - double x, y; +struct Point +{ + double x, y; }; -Point cornerPoint(int width, int height, int u, int v) { - return {u - width / 2.0, v - height / 2.0}; +Point cornerPoint(int width, int height, int u, int v) +{ + return {u - width / 2.0, v - height / 2.0}; } -Point tilePoint(int width, int height, int x, int y) { - return {x + 0.5 - width / 2.0, y + 0.5 - height / 2.0}; +Point tilePoint(int width, int height, int x, int y) +{ + return {x + 0.5 - width / 2.0, y + 0.5 - height / 2.0}; } -double wrapDelta(double d, double period) { - d = std::fmod(d, period); - if (d >= period / 2) - d -= period; - else if (d < -period / 2) - d += period; - return d; +double wrapDelta(double d, double period) +{ + d = std::fmod(d, period); + if (d >= period / 2) + d -= period; + else if (d < -period / 2) + d += period; + return d; } -double torusDistance(int width, int height, Point p, Point q) { - return std::hypot(wrapDelta(p.x - q.x, width), wrapDelta(p.y - q.y, height)); +double torusDistance(int width, int height, Point p, Point q) +{ + return std::hypot(wrapDelta(p.x - q.x, width), wrapDelta(p.y - q.y, height)); } // Squared distance from the centre in doubled units: an integer, so exactly equal across an // orbit, and comparing it against a radius gives every member of the orbit the same answer. -std::int64_t cornerRadius2(int width, int height, int u, int v) { - const std::int64_t U = 2 * u - width, V = 2 * v - height; - return U * U + V * V; +std::int64_t cornerRadius2(int width, int height, int u, int v) +{ + const std::int64_t U = 2 * u - width, V = 2 * v - height; + return U * U + V * V; } -std::int64_t tileRadius2(int width, int height, int x, int y) { - const std::int64_t X = 2 * x + 1 - width, Y = 2 * y + 1 - height; - return X * X + Y * Y; +std::int64_t tileRadius2(int width, int height, int x, int y) +{ + const std::int64_t X = 2 * x + 1 - width, Y = 2 * y + 1 - height; + return X * X + Y * Y; } -bool within(std::int64_t radius2, double radius) { - return double(radius2) < 4.0 * radius * radius; +bool within(std::int64_t radius2, double radius) +{ + return double(radius2) < 4.0 * radius * radius; } // The request's geometry. -struct Arena { - int width, height, teams, causeways; - double centre, moat, causewayWidth; - Symmetry symmetry; +struct Arena +{ + int width, height, teams, causeways; + double centre, moat, causewayWidth; + Symmetry symmetry; }; -Arena arenaFor(const GenerationRequest &r) { - const SymmetricArenaOptions o(r); - Arena a; - a.width = 1 << r.wDec; - a.height = 1 << r.hDec; - a.teams = r.nbTeams; - a.causeways = o.causeways; - // centre-size is a share of the shorter side, over a floor that keeps room for all three - // fruits on the smallest maps. - a.centre = 4.0 + o.centreSize / 100.0 * 0.75 * std::min(a.width, a.height); - a.moat = o.moatWidth; - a.causewayWidth = o.causewayWidth; - a.symmetry = symmetryFor(a.width, a.height, a.teams); - return a; +Arena arenaFor(const GenerationRequest &r) +{ + const SymmetricArenaOptions o(r); + Arena a; + a.width = 1 << r.wDec; + a.height = 1 << r.hDec; + a.teams = r.nbTeams; + a.causeways = o.causeways; + // centre-size is a share of the shorter side, over a floor that keeps room for all three + // fruits on the smallest maps. + a.centre = 4.0 + o.centreSize / 100.0 * 0.75 * std::min(a.width, a.height); + a.moat = o.moatWidth; + a.causewayWidth = o.causewayWidth; + a.symmetry = symmetryFor(a.width, a.height, a.teams); + return a; } -double imageAngle(const Isometry &g, double angle) { - const double x = std::cos(angle), y = std::sin(angle); - return std::atan2(g.c * x + g.d * y, g.a * x + g.b * y); +double imageAngle(const Isometry &g, double angle) +{ + const double x = std::cos(angle), y = std::sin(angle); + return std::atan2(g.c * x + g.d * y, g.a * x + g.b * y); } // Colony 0's causeways, as directions from the centre: one straight towards its home, or two // flanking it, each a quarter of the way to the nearest neighbour's direction. -std::vector gateAngles(const Arena &a, Point home) { - const double angle = std::atan2(home.y, home.x); - if (a.causeways < 2) - return {angle}; - double gap = 2 * kPi; - for (int e = 1; e < a.symmetry.order(); ++e) { - double d = std::fmod(std::fabs(imageAngle(a.symmetry.elements[e], angle) - angle), 2 * kPi); - gap = std::min(gap, std::min(d, 2 * kPi - d)); - } - return {angle - gap / 4, angle + gap / 4}; +std::vector gateAngles(const Arena &a, Point home) +{ + const double angle = std::atan2(home.y, home.x); + if (a.causeways < 2) + return {angle}; + double gap = 2 * kPi; + for (int e = 1; e < a.symmetry.order(); ++e) + { + double d = std::fmod(std::fabs(imageAngle(a.symmetry.elements[e], angle) - angle), 2 * kPi); + gap = std::min(gap, std::min(d, 2 * kPi - d)); + } + return {angle - gap / 4, angle + gap / 4}; } // The smallest angle between two causeways once every image of colony 0's is drawn. -double gateSeparation(const Arena &a, const std::vector &gates) { - std::vector all; - for (const Isometry &g : a.symmetry.elements) - for (double angle : gates) { - double image = std::fmod(imageAngle(g, angle), 2 * kPi); - all.push_back(image < 0 ? image + 2 * kPi : image); - } - std::sort(all.begin(), all.end()); - double gap = all.front() + 2 * kPi - all.back(); - for (size_t i = 1; i < all.size(); ++i) - gap = std::min(gap, all[i] - all[i - 1]); - return gap; +double gateSeparation(const Arena &a, const std::vector &gates) +{ + std::vector all; + for (const Isometry &g : a.symmetry.elements) + for (double angle : gates) + { + double image = std::fmod(imageAngle(g, angle), 2 * kPi); + all.push_back(image < 0 ? image + 2 * kPi : image); + } + std::sort(all.begin(), all.end()); + double gap = all.front() + 2 * kPi - all.back(); + for (size_t i = 1; i < all.size(); ++i) + gap = std::min(gap, all[i] - all[i - 1]); + return gap; } -struct Home { - int u, v; - double spacing, radius; +struct Home +{ + int u, v; + double spacing, radius; }; -enum class Fit { Ok, Crowded, Causeways }; +enum class Fit +{ + Ok, + Crowded, + Causeways +}; // Every corner that could be colony 0's home. This depends only on the request, so validation // and generation agree: the home's clear ground misses the moat, it is well inside half the // shorter side (so the direction to the centre is unambiguous on the torus), it is far enough // from every rival, and its causeways leave shore between them. -std::vector homeCandidates(const Arena &a, Fit &fit) { - const int w = a.width, h = a.height, shortSide = std::min(w, h); - const int step = std::max(1, shortSide / 128); - const double inner = a.centre + a.moat + kHomeRadius + 4, outer = 0.45 * shortSide; - std::vector homes; - bool spaced = false; - for (int v = 0; v < h && a.symmetry.order() > 1; v += step) - for (int u = 0; u < w; u += step) { - const Point p = cornerPoint(w, h, u, v); - const double radius = std::hypot(p.x, p.y); - if (radius < inner || radius > outer) - continue; - double spacing = 1e9; - for (int e = 1; e < a.symmetry.order(); ++e) { - const int c = a.symmetry.corner(e, u, v); - spacing = std::min(spacing, torusDistance(w, h, p, cornerPoint(w, h, c % w, c / w))); - } - if (spacing < kMinimumSpacing) - continue; - spaced = true; - if (gateSeparation(a, gateAngles(a, p)) * a.centre < a.causewayWidth + 5) - continue; - homes.push_back({u, v, spacing, radius}); - } - fit = !homes.empty() ? Fit::Ok : spaced ? Fit::Causeways : Fit::Crowded; - return homes; +std::vector homeCandidates(const Arena &a, Fit &fit) +{ + const int w = a.width, h = a.height, shortSide = std::min(w, h); + const int step = std::max(1, shortSide / 128); + const double inner = a.centre + a.moat + kHomeRadius + 4, outer = 0.45 * shortSide; + std::vector homes; + bool spaced = false; + for (int v = 0; v < h && a.symmetry.order() > 1; v += step) + for (int u = 0; u < w; u += step) + { + const Point p = cornerPoint(w, h, u, v); + const double radius = std::hypot(p.x, p.y); + if (radius < inner || radius > outer) + continue; + double spacing = 1e9; + for (int e = 1; e < a.symmetry.order(); ++e) + { + const int c = a.symmetry.corner(e, u, v); + spacing = + std::min(spacing, torusDistance(w, h, p, cornerPoint(w, h, c % w, c / w))); + } + if (spacing < kMinimumSpacing) + continue; + spaced = true; + if (gateSeparation(a, gateAngles(a, p)) * a.centre < a.causewayWidth + 5) + continue; + homes.push_back({u, v, spacing, radius}); + } + fit = !homes.empty() ? Fit::Ok : spaced ? Fit::Causeways : Fit::Crowded; + return homes; } // The best-spaced homes, and of those the ones nearest the centre, picked from at random. -Home chooseHome(const std::vector &homes, GenerationContext &context, int shortSide) { - double best = 0; - for (const Home &home : homes) - best = std::max(best, home.spacing); - const double floor = best - std::max(2.0, 0.1 * best); - double nearest = 1e9; - for (const Home &home : homes) - if (home.spacing >= floor) - nearest = std::min(nearest, home.radius); - std::vector pool; - for (const Home &home : homes) - if (home.spacing >= floor && home.radius <= nearest + 0.15 * shortSide) - pool.push_back(home); - return pool[context.bounded("layout", pool.size())]; +Home chooseHome(const std::vector &homes, GenerationContext &context, int shortSide) +{ + double best = 0; + for (const Home &home : homes) + best = std::max(best, home.spacing); + const double floor = best - std::max(2.0, 0.1 * best); + double nearest = 1e9; + for (const Home &home : homes) + if (home.spacing >= floor) + nearest = std::min(nearest, home.radius); + std::vector pool; + for (const Home &home : homes) + if (home.spacing >= floor && home.radius <= nearest + 0.15 * shortSide) + pool.push_back(home); + return pool[context.bounded("layout", pool.size())]; } -std::string validate(const GenerationRequest &r) { - if (r.nbTeams != 2 && r.nbTeams != 4 && r.nbTeams != 8) - return "Symmetric arena needs 2, 4 or 8 colonies."; - if (r.nbTeams == 8 && r.wDec != r.hDec) - return "Eight colonies need a square map in the symmetric arena."; - const Arena a = arenaFor(r); - if (a.centre < kMinimumCentre) - return "The centre is too small for an orchard on this map; use a bigger centre or a bigger " - "map."; - Fit fit; - homeCandidates(a, fit); - if (fit == Fit::Crowded) - return "The arena leaves too little room for every colony; use a bigger map, a smaller " - "centre or a narrower moat."; - if (fit == Fit::Causeways) - return "The causeways are too close together; use fewer or narrower causeways, or a bigger " - "centre."; - return ""; +std::string validate(const GenerationRequest &r) +{ + if (r.nbTeams != 2 && r.nbTeams != 4 && r.nbTeams != 8) + return "Symmetric arena needs 2, 4 or 8 colonies."; + if (r.nbTeams == 8 && r.wDec != r.hDec) + return "Eight colonies need a square map in the symmetric arena."; + const Arena a = arenaFor(r); + if (a.centre < kMinimumCentre) + return "The centre is too small for an orchard on this map; use a bigger centre or a " + "bigger " + "map."; + Fit fit; + homeCandidates(a, fit); + if (fit == Fit::Crowded) + return "The arena leaves too little room for every colony; use a bigger map, a smaller " + "centre or a narrower moat."; + if (fit == Fit::Causeways) + return "The causeways are too close together; use fewer or narrower causeways, or a bigger " + "centre."; + return ""; } // Colony 0's choices; everything else is its images. -struct Layout { - int homeU, homeV; - Point home, pond; - double pondAngle; - std::vector gates; - int orchardPhase, orchardPattern; +struct Layout +{ + int homeU, homeV; + Point home, pond; + double pondAngle; + std::vector gates; + int orchardPhase, orchardPattern; }; // Colony 0's feature stamped onto every image of it: an entry is set when any image of its // tile (or corner) lies in the feature. A union doesn't depend on the order an orbit is // visited in, so the result is exactly symmetric. std::vector stamp(const Symmetry &s, const std::vector &feature, - bool corners) { - std::vector result(feature.size(), 0); - for (int y = 0; y < s.height; ++y) - for (int x = 0; x < s.width; ++x) - for (int e = 0; e < s.order(); ++e) - if (feature[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]) { - result[size_t(y) * s.width + x] = 1; - break; - } - return result; + bool corners) +{ + std::vector result(feature.size(), 0); + for (int y = 0; y < s.height; ++y) + for (int x = 0; x < s.width; ++x) + for (int e = 0; e < s.order(); ++e) + if (feature[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]) + { + result[size_t(y) * s.width + x] = 1; + break; + } + return result; } // Plain noise summed over every orbit, as integers so the sum is exact in any order. std::vector orbitNoise(GenerationContext &context, const Symmetry &s, - const std::string &stream, float smoothing, bool corners) { - const int w = s.width, h = s.height; - HeightMap noise(w, h, context.stream(stream)); - noise.makePlain(smoothing); - std::vector raw(size_t(w) * h), summed(size_t(w) * h, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - raw[size_t(y) * w + x] = int(noise(x, y) * 4096); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - for (int e = 0; e < s.order(); ++e) - summed[size_t(y) * w + x] += raw[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]; - return summed; + const std::string &stream, float smoothing, bool corners) +{ + const int w = s.width, h = s.height; + HeightMap noise(w, h, context.stream(stream)); + noise.makePlain(smoothing); + std::vector raw(size_t(w) * h), summed(size_t(w) * h, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + raw[size_t(y) * w + x] = int(noise(x, y) * 4096); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + for (int e = 0; e < s.order(); ++e) + summed[size_t(y) * w + x] += + raw[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]; + return summed; } // The share (0 to 1) of eligible entries with the highest (or lowest) values, as a mask. Ties // at the cut-off are all kept, so the members of an orbit, which have equal values, are never // split. std::vector topShare(const std::vector &value, - const std::vector &eligible, double share, - bool highest) { - std::vector pool; - for (size_t i = 0; i < value.size(); ++i) - if (eligible[i]) - pool.push_back(value[i]); - std::vector mask(value.size(), 0); - const size_t count = - std::min(pool.size(), size_t(std::lround(std::max(0.0, share) * pool.size()))); - if (!count) - return mask; - std::sort(pool.begin(), pool.end()); - const int threshold = highest ? pool[pool.size() - count] : pool[count - 1]; - for (size_t i = 0; i < value.size(); ++i) - mask[i] = eligible[i] && (highest ? value[i] >= threshold : value[i] <= threshold); - return mask; + const std::vector &eligible, double share, + bool highest) +{ + std::vector pool; + for (size_t i = 0; i < value.size(); ++i) + if (eligible[i]) + pool.push_back(value[i]); + std::vector mask(value.size(), 0); + const size_t count = + std::min(pool.size(), size_t(std::lround(std::max(0.0, share) * pool.size()))); + if (!count) + return mask; + std::sort(pool.begin(), pool.end()); + const int threshold = highest ? pool[pool.size() - count] : pool[count - 1]; + for (size_t i = 0; i < value.size(); ++i) + mask[i] = eligible[i] && (highest ? value[i] >= threshold : value[i] <= threshold); + return mask; } // Whether a point lies on one of colony 0's causeways, extended past the island's shore by // `inner` and past the outer shore by `outer`. bool onCauseway(const Arena &a, const Layout &l, Point p, double inner, double outer, - double halfWidth) { - for (double angle : l.gates) { - const double dx = std::cos(angle), dy = std::sin(angle); - const double along = p.x * dx + p.y * dy, across = std::fabs(p.x * dy - p.y * dx); - if (along >= a.centre - inner && along <= a.centre + a.moat + outer && across <= halfWidth) - return true; - } - return false; + double halfWidth) +{ + for (double angle : l.gates) + { + const double dx = std::cos(angle), dy = std::sin(angle); + const double along = p.x * dx + p.y * dy, across = std::fabs(p.x * dy - p.y * dx); + if (along >= a.centre - inner && along <= a.centre + a.moat + outer && across <= halfWidth) + return true; + } + return false; } // The finished undermap, and the corner masks later steps need. -struct Terrain { - std::vector undermap, moat, ponds, causeways, paths; +struct Terrain +{ + std::vector undermap, moat, ponds, causeways, paths; }; // A walking route for colony 0 from its home to the outer end of each of its causeways: the // shortest one, where every lake corner crossed costs as much as a long detour, so it keeps to // land wherever land will do; the moat and the ponds are never crossed. The route's images give // every colony the same one. Lake water on and beside a route becomes land. -bool carvePaths(const Arena &a, const Layout &l, Terrain &t) { - constexpr int kStraight = 2, kDiagonal = 3, kFord = 60; - const int w = a.width, h = a.height; - const size_t n = size_t(w) * h; - std::vector route(n, 0); - for (double angle : l.gates) { - const double reach = a.centre + a.moat + 3.5; - const int target = wrap(int(std::lround(h / 2.0 + reach * std::sin(angle))), h) * w + - wrap(int(std::lround(w / 2.0 + reach * std::cos(angle))), w); - const int start = l.homeV * w + l.homeU; - std::vector cost(n, INT_MAX), parent(n, -1); - using Entry = std::pair; - std::priority_queue, std::greater> queue; - queue.push({0, start}); - cost[size_t(start)] = 0; - while (!queue.empty()) { - const auto [c, i] = queue.top(); - queue.pop(); - if (c != cost[size_t(i)]) - continue; - if (i == target) - break; - const int u = i % w, v = i / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (!dx && !dy) - continue; - const int j = wrap(v + dy, h) * w + wrap(u + dx, w); - const bool water = t.undermap[size_t(j)] == WATER; - if (water && (t.moat[size_t(j)] || t.ponds[size_t(j)])) - continue; - const int next = c + (dx && dy ? kDiagonal : kStraight) + (water ? kFord : 0); - if (next < cost[size_t(j)]) { - cost[size_t(j)] = next; - parent[size_t(j)] = i; - queue.push({next, j}); - } - } - } - if (cost[size_t(target)] == INT_MAX) - return false; - for (int i = target; i != -1; i = parent[size_t(i)]) - route[size_t(i)] = 1; - } - t.paths = stamp(a.symmetry, route, true); - for (int v = 0; v < h; ++v) - for (int u = 0; u < w; ++u) { - if (!t.paths[size_t(v) * w + u]) - continue; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t j = size_t(wrap(v + dy, h)) * w + wrap(u + dx, w); - if (t.undermap[j] == WATER && !t.moat[j] && !t.ponds[j]) - t.undermap[j] = GRASS; - } - } - return true; +bool carvePaths(const Arena &a, const Layout &l, Terrain &t) +{ + constexpr int kStraight = 2, kDiagonal = 3, kFord = 60; + const int w = a.width, h = a.height; + const size_t n = size_t(w) * h; + std::vector route(n, 0); + for (double angle : l.gates) + { + const double reach = a.centre + a.moat + 3.5; + const int target = wrap(int(std::lround(h / 2.0 + reach * std::sin(angle))), h) * w + + wrap(int(std::lround(w / 2.0 + reach * std::cos(angle))), w); + const int start = l.homeV * w + l.homeU; + std::vector cost(n, INT_MAX), parent(n, -1); + using Entry = std::pair; + std::priority_queue, std::greater> queue; + queue.push({0, start}); + cost[size_t(start)] = 0; + while (!queue.empty()) + { + const auto [c, i] = queue.top(); + queue.pop(); + if (c != cost[size_t(i)]) + continue; + if (i == target) + break; + const int u = i % w, v = i / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int j = wrap(v + dy, h) * w + wrap(u + dx, w); + const bool water = t.undermap[size_t(j)] == WATER; + if (water && (t.moat[size_t(j)] || t.ponds[size_t(j)])) + continue; + const int next = c + (dx && dy ? kDiagonal : kStraight) + (water ? kFord : 0); + if (next < cost[size_t(j)]) + { + cost[size_t(j)] = next; + parent[size_t(j)] = i; + queue.push({next, j}); + } + } + } + if (cost[size_t(target)] == INT_MAX) + return false; + for (int i = target; i != -1; i = parent[size_t(i)]) + route[size_t(i)] = 1; + } + t.paths = stamp(a.symmetry, route, true); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + { + if (!t.paths[size_t(v) * w + u]) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t j = size_t(wrap(v + dy, h)) * w + wrap(u + dx, w); + if (t.undermap[j] == WATER && !t.moat[j] && !t.ponds[j]) + t.undermap[j] = GRASS; + } + } + return true; } // The 8-connected groups of set entries in a mask, on the torus. Which entries share a group is // a matter of geometry alone, so every symmetry maps groups onto groups of the same size. -std::vector> groupsOf(const std::vector &mask, int w, int h) { - std::vector> groups; - std::vector seen(mask.size(), 0); - for (size_t first = 0; first < mask.size(); ++first) { - if (!mask[first] || seen[first]) - continue; - std::vector group{int(first)}; - seen[first] = 1; - for (size_t head = 0; head < group.size(); ++head) { - const int x = group[head] % w, y = group[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); - if (mask[j] && !seen[j]) { - seen[j] = 1; - group.push_back(int(j)); - } - } - } - groups.push_back(std::move(group)); - } - return groups; +std::vector> groupsOf(const std::vector &mask, int w, int h) +{ + std::vector> groups; + std::vector seen(mask.size(), 0); + for (size_t first = 0; first < mask.size(); ++first) + { + if (!mask[first] || seen[first]) + continue; + std::vector group{int(first)}; + seen[first] = 1; + for (size_t head = 0; head < group.size(); ++head) + { + const int x = group[head] % w, y = group[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); + if (mask[j] && !seen[j]) + { + seen[j] = 1; + group.push_back(int(j)); + } + } + } + groups.push_back(std::move(group)); + } + return groups; } bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const Layout &l, - const SymmetricArenaOptions &o, Terrain &t) { - // A lake of fewer corners than this is a patch of shoreline with no open water in it. - constexpr size_t kSmallestLake = 16; - context.stage = "arena terrain"; - const int w = a.width, h = a.height; - const size_t n = size_t(w) * h; - const Symmetry &s = a.symmetry; - // Lakes keep clear of every home and of its pond's banks, where the starting kit grows. - std::vector homeDisc(n, 0), homeZone(n, 0), pond(n, 0), gate(n, 0); - for (int v = 0; v < h; ++v) - for (int u = 0; u < w; ++u) { - const size_t i = size_t(v) * w + u; - const Point p = cornerPoint(w, h, u, v); - const double toHome = torusDistance(w, h, p, l.home); - const double toPond = torusDistance(w, h, p, l.pond); - homeDisc[i] = toHome <= kHomeRadius; - homeZone[i] = toHome <= kHomeRadius + 4 || toPond <= kPondRadius + 7; - pond[i] = toPond <= kPondRadius; - gate[i] = onCauseway(a, l, p, 1.5, 1.5, a.causewayWidth / 2); - } - homeDisc = stamp(s, homeDisc, true); - homeZone = stamp(s, homeZone, true); - t.ponds = stamp(s, pond, true); - // Without a moat the orchard island joins the land round it, and no causeway is needed; the - // moat's width still spaces the homes. - t.causeways = o.moat ? stamp(s, gate, true) : std::vector(n, 0); + const SymmetricArenaOptions &o, Terrain &t) +{ + // A lake of fewer corners than this is a patch of shoreline with no open water in it. + constexpr size_t kSmallestLake = 16; + context.stage = "arena terrain"; + const int w = a.width, h = a.height; + const size_t n = size_t(w) * h; + const Symmetry &s = a.symmetry; + // Lakes keep clear of every home and of its pond's banks, where the starting kit grows. + std::vector homeDisc(n, 0), homeZone(n, 0), pond(n, 0), gate(n, 0); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + { + const size_t i = size_t(v) * w + u; + const Point p = cornerPoint(w, h, u, v); + const double toHome = torusDistance(w, h, p, l.home); + const double toPond = torusDistance(w, h, p, l.pond); + homeDisc[i] = toHome <= kHomeRadius; + homeZone[i] = toHome <= kHomeRadius + 4 || toPond <= kPondRadius + 7; + pond[i] = toPond <= kPondRadius; + gate[i] = onCauseway(a, l, p, 1.5, 1.5, a.causewayWidth / 2); + } + homeDisc = stamp(s, homeDisc, true); + homeZone = stamp(s, homeZone, true); + t.ponds = stamp(s, pond, true); + // Without a moat the orchard island joins the land round it, and no causeway is needed; the + // moat's width still spaces the homes. + t.causeways = o.moat ? stamp(s, gate, true) : std::vector(n, 0); - t.moat.assign(n, 0); - std::vector lakeEligible(n, 0); - for (int v = 0; v < h; ++v) - for (int u = 0; u < w; ++u) { - const size_t i = size_t(v) * w + u; - const std::int64_t r2 = cornerRadius2(w, h, u, v); - t.moat[i] = o.moat && !within(r2, a.centre) && within(r2, a.centre + a.moat); - lakeEligible[i] = !within(r2, a.centre + a.moat + kApron) && !homeZone[i]; - } - // Capped so big maps get more lakes rather than bigger ones: more shore, more farmland. - const float smoothing = std::min(32.0f, std::max(12.0f, std::min(w, h) / 10.0f)); - std::vector lakes = - topShare(orbitNoise(context, s, "arena-lakes", smoothing, true), lakeEligible, - o.lakes / 100.0, false); - for (const auto &group : groupsOf(lakes, w, h)) - if (group.size() < kSmallestLake) - for (int i : group) - lakes[size_t(i)] = 0; + t.moat.assign(n, 0); + std::vector lakeEligible(n, 0); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + { + const size_t i = size_t(v) * w + u; + const std::int64_t r2 = cornerRadius2(w, h, u, v); + t.moat[i] = o.moat && !within(r2, a.centre) && within(r2, a.centre + a.moat); + lakeEligible[i] = !within(r2, a.centre + a.moat + kApron) && !homeZone[i]; + } + // Capped so big maps get more lakes rather than bigger ones: more shore, more farmland. + const float smoothing = std::min(32.0f, std::max(12.0f, std::min(w, h) / 10.0f)); + std::vector lakes = + topShare(orbitNoise(context, s, "arena-lakes", smoothing, true), lakeEligible, + o.lakes / 100.0, false); + for (const auto &group : groupsOf(lakes, w, h)) + if (group.size() < kSmallestLake) + for (int i : group) + lakes[size_t(i)] = 0; - // Later layers win: lakes and ponds, then clear home ground, the moat, and causeways of sand. - t.undermap.assign(n, GRASS); - for (size_t i = 0; i < n; ++i) { - if (lakes[i] || t.ponds[i]) - t.undermap[i] = WATER; - if (homeDisc[i]) - t.undermap[i] = GRASS; - if (t.moat[i]) - t.undermap[i] = WATER; - if (t.causeways[i]) - t.undermap[i] = SAND; - } - if (!carvePaths(a, l, t)) { - context.detail = "no route from colony 0's home to its causeway"; - return false; - } + // Later layers win: lakes and ponds, then clear home ground, the moat, and causeways of sand. + t.undermap.assign(n, GRASS); + for (size_t i = 0; i < n; ++i) + { + if (lakes[i] || t.ponds[i]) + t.undermap[i] = WATER; + if (homeDisc[i]) + t.undermap[i] = GRASS; + if (t.moat[i]) + t.undermap[i] = WATER; + if (t.causeways[i]) + t.undermap[i] = SAND; + } + if (!carvePaths(a, l, t)) + { + context.detail = "no route from colony 0's home to its causeway"; + return false; + } - // Map::controlSand()'s rule - grass touching water becomes sand - applied to every corner at - // once rather than in place in row order, which is what keeps it symmetric. Water is left - // alone, so no moat, pond or channel silts up. - std::vector sanded(t.undermap); - for (int v = 0; v < h; ++v) - for (int u = 0; u < w; ++u) { - const size_t i = size_t(v) * w + u; - if (t.undermap[i] != GRASS) - continue; - for (int dy = -1; dy <= 1 && sanded[i] == GRASS; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (t.undermap[size_t(wrap(v + dy, h)) * w + wrap(u + dx, w)] == WATER) { - sanded[i] = SAND; - break; - } - } - t.undermap.swap(sanded); - for (int v = 0; v < h; ++v) - for (int u = 0; u < w; ++u) - map.setUMTerrain(u, v, TerrainType(t.undermap[size_t(v) * w + u])); - map.rebuildTerrain(); - return true; + // Map::controlSand()'s rule - grass touching water becomes sand - applied to every corner at + // once rather than in place in row order, which is what keeps it symmetric. Water is left + // alone, so no moat, pond or channel silts up. + std::vector sanded(t.undermap); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + { + const size_t i = size_t(v) * w + u; + if (t.undermap[i] != GRASS) + continue; + for (int dy = -1; dy <= 1 && sanded[i] == GRASS; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (t.undermap[size_t(wrap(v + dy, h)) * w + wrap(u + dx, w)] == WATER) + { + sanded[i] = SAND; + break; + } + } + t.undermap.swap(sanded); + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + map.setUMTerrain(u, v, TerrainType(t.undermap[size_t(v) * w + u])); + map.rebuildTerrain(); + return true; } // Walking steps (8-neighbour) from every tile to the nearest source tile: exact integers, so // symmetric terrain gives symmetric distances. -1 where no source is reachable. -std::vector stepsFrom(int w, int h, const std::vector &source) { - std::vector dist(source.size(), -1), queue; - queue.reserve(source.size()); - for (size_t i = 0; i < source.size(); ++i) - if (source[i]) { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); - if (dist[j] < 0) { - dist[j] = dist[size_t(queue[head])] + 1; - queue.push_back(int(j)); - } - } - } - return dist; +std::vector stepsFrom(int w, int h, const std::vector &source) +{ + std::vector dist(source.size(), -1), queue; + queue.reserve(source.size()); + for (size_t i = 0; i < source.size(); ++i) + if (source[i]) + { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); + if (dist[j] < 0) + { + dist[j] = dist[size_t(queue[head])] + 1; + queue.push_back(int(j)); + } + } + } + return dist; } // Every swarm, then every worker, from colony 0's choices mapped by each colony's symmetry: // its footprint centred on its home corner (the image of a footprint needs its own top-left // anchor, since a turn or mirror moves which corner is top-left), and a random choice of the // free tiles touching it. -bool placeColonies(Game &game, GenerationContext &context, const Arena &a, const Layout &l) { - context.stage = "settlement"; - Map &map = game.map; - const Symmetry &s = a.symmetry; - const int w = a.width, h = a.height; - const int type = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); - const BuildingType *swarm = globalContainer->buildingsTypes.get(type); - if (!swarm) { - context.detail = "missing swarm type"; - return false; - } - const int x0 = l.homeU - swarm->width / 2, y0 = l.homeV - swarm->height / 2; - std::vector buildings; - for (int team = 0; team < a.teams; ++team) { - const auto [x, y] = s.anchor(team, x0, y0, swarm->width, swarm->height); - Building *building = game.checkRoomForBuilding(x, y, swarm, team, false) - ? game.addBuilding(x, y, type, team, 1, 0) - : nullptr; - if (!building) { - context.detail = "Colony " + std::to_string(team) + ": the swarm does not fit its home"; - return false; - } - buildings.push_back(building); - } - std::vector> ring; - for (int dy = -1; dy <= swarm->height; ++dy) - for (int dx = -1; dx <= swarm->width; ++dx) { - if (dx >= 0 && dx < swarm->width && dy >= 0 && dy < swarm->height) - continue; - const int x = wrap(x0 + dx, w), y = wrap(y0 + dy, h); - if (map.isFreeForGroundUnit(x, y, false, Team::teamNumberToMask(0))) - ring.emplace_back(x, y); - } - const int workers = context.request.nbWorkers; - if (int(ring.size()) < workers) { - context.detail = "need " + std::to_string(workers) + " worker tiles; found " + - std::to_string(ring.size()); - return false; - } - for (int i = 0; i < workers; ++i) - std::swap(ring[size_t(i)], ring[size_t(i) + context.bounded("starts", ring.size() - i)]); - for (int team = 0; team < a.teams; ++team) { - for (int i = 0; i < workers; ++i) { - const int tile = s.tile(team, ring[size_t(i)].first, ring[size_t(i)].second); - if (!game.addUnit(tile % w, tile / w, team, WORKER, 0, 0, 0, 0)) { - context.detail = "Colony " + std::to_string(team) + ": worker placement failed"; - return false; - } - } - Team *colony = game.teams[team]; - colony->startPosX = buildings[size_t(team)]->posX; - colony->startPosY = buildings[size_t(team)]->posY; - colony->startPosSet = Team::START_POS_FROM_SWARM; - context.bootX[team] = colony->startPosX; - context.bootY[team] = colony->startPosY; - colony->createLists(); - } - return true; +bool placeColonies(Game &game, GenerationContext &context, const Arena &a, const Layout &l) +{ + context.stage = "settlement"; + Map &map = game.map; + const Symmetry &s = a.symmetry; + const int w = a.width, h = a.height; + const int type = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + const BuildingType *swarm = globalContainer->buildingsTypes.get(type); + if (!swarm) + { + context.detail = "missing swarm type"; + return false; + } + const int x0 = l.homeU - swarm->width / 2, y0 = l.homeV - swarm->height / 2; + std::vector buildings; + for (int team = 0; team < a.teams; ++team) + { + const auto [x, y] = s.anchor(team, x0, y0, swarm->width, swarm->height); + Building *building = game.checkRoomForBuilding(x, y, swarm, team, false) + ? game.addBuilding(x, y, type, team, 1, 0) + : nullptr; + if (!building) + { + context.detail = "Colony " + std::to_string(team) + ": the swarm does not fit its home"; + return false; + } + buildings.push_back(building); + } + std::vector> ring; + for (int dy = -1; dy <= swarm->height; ++dy) + for (int dx = -1; dx <= swarm->width; ++dx) + { + if (dx >= 0 && dx < swarm->width && dy >= 0 && dy < swarm->height) + continue; + const int x = wrap(x0 + dx, w), y = wrap(y0 + dy, h); + if (map.isFreeForGroundUnit(x, y, false, Team::teamNumberToMask(0))) + ring.emplace_back(x, y); + } + const int workers = context.request.nbWorkers; + if (int(ring.size()) < workers) + { + context.detail = "need " + std::to_string(workers) + " worker tiles; found " + + std::to_string(ring.size()); + return false; + } + for (int i = 0; i < workers; ++i) + std::swap(ring[size_t(i)], ring[size_t(i) + context.bounded("starts", ring.size() - i)]); + for (int team = 0; team < a.teams; ++team) + { + for (int i = 0; i < workers; ++i) + { + const int tile = s.tile(team, ring[size_t(i)].first, ring[size_t(i)].second); + if (!game.addUnit(tile % w, tile / w, team, WORKER, 0, 0, 0, 0)) + { + context.detail = "Colony " + std::to_string(team) + ": worker placement failed"; + return false; + } + } + Team *colony = game.teams[team]; + colony->startPosX = buildings[size_t(team)]->posX; + colony->startPosY = buildings[size_t(team)]->posY; + colony->startPosSet = Team::START_POS_FROM_SWARM; + context.bootX[team] = colony->startPosX; + context.bootY[team] = colony->startPosY; + colony->createLists(); + } + return true; } bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layout &l, - const SymmetricArenaOptions &o, const Terrain &t) { - context.stage = "arena resources"; - Map &map = game.map; - const Symmetry &s = a.symmetry; - const int w = a.width, h = a.height; - const size_t n = size_t(w) * h; - const double richness = o.richness / 100.0; + const SymmetricArenaOptions &o, const Terrain &t) +{ + context.stage = "arena resources"; + Map &map = game.map; + const Symmetry &s = a.symmetry; + const int w = a.width, h = a.height; + const size_t n = size_t(w) * h; + const double richness = o.richness / 100.0; - // Ground kept free of deposits: every home's clear ground, both approaches to every causeway, - // and a lane along every walking route. - std::vector grass(n), water(n), land(n), homeClear(n), landing(n), blocked(n); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - grass[i] = map.isGrass(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID; - water[i] = map.isWater(x, y); - land[i] = !water[i]; - const Point p = tilePoint(w, h, x, y); - homeClear[i] = torusDistance(w, h, p, l.home) <= kHomeRadius + 1.5; - landing[i] = onCauseway(a, l, p, 3.0, 4.0, a.causewayWidth / 2 + 2); - } - homeClear = stamp(s, homeClear, false); - landing = stamp(s, landing, false); - const auto onPath = [&](int u, int v) { - return t.paths[size_t(wrap(v, h)) * w + wrap(u, w)] != 0; - }; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - blocked[i] = homeClear[i] || landing[i] || onPath(x, y) || onPath(x + 1, y) || - onPath(x, y + 1) || onPath(x + 1, y + 1); - } - const std::vector waterSteps = stepsFrom(w, h, water), landSteps = stepsFrom(w, h, land); - const float coarse = std::min(24.0f, std::max(8.0f, std::min(w, h) / 10.0f)); - std::vector plan(n, -1); + // Ground kept free of deposits: every home's clear ground, both approaches to every causeway, + // and a lane along every walking route. + std::vector grass(n), water(n), land(n), homeClear(n), landing(n), blocked(n); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + grass[i] = map.isGrass(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; + water[i] = map.isWater(x, y); + land[i] = !water[i]; + const Point p = tilePoint(w, h, x, y); + homeClear[i] = torusDistance(w, h, p, l.home) <= kHomeRadius + 1.5; + landing[i] = onCauseway(a, l, p, 3.0, 4.0, a.causewayWidth / 2 + 2); + } + homeClear = stamp(s, homeClear, false); + landing = stamp(s, landing, false); + const auto onPath = [&](int u, int v) + { return t.paths[size_t(wrap(v, h)) * w + wrap(u, w)] != 0; }; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + blocked[i] = homeClear[i] || landing[i] || onPath(x, y) || onPath(x + 1, y) || + onPath(x, y + 1) || onPath(x + 1, y + 1); + } + const std::vector waterSteps = stepsFrom(w, h, water), landSteps = stepsFrom(w, h, land); + const float coarse = std::min(24.0f, std::max(8.0f, std::min(w, h) / 10.0f)); + std::vector plan(n, -1); - // The ambient layer beyond the open apron round the moat: farmland on lake and pond shores - // (wheat to wood 2:1), stone outcrops away from the water, and algae off every shore. - std::vector farm(n, 0), rock(n, 0), alga(n, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - const bool open = grass[i] && !blocked[i] && - !within(tileRadius2(w, h, x, y), a.centre + a.moat + kApron + 2); - farm[i] = open && waterSteps[i] >= 2 && waterSteps[i] <= 7; - rock[i] = open && waterSteps[i] >= 3; - alga[i] = water[i] && landSteps[i] >= 2; - } - // The amount controls scale each layer on top of richness: wheat and wood their shares of the - // farmland (2:1 at the defaults, which use the shares exactly as they were). - const bool farmDefault = o.wheat == 100 && o.wood == 100; - const double wheatShare = scaledShare(0.3 * richness * 2 / 3, o.wheat); - const double woodShare = scaledShare(0.3 * richness / 3, o.wood); - const double farmShare = - farmDefault ? std::min(0.9, 0.3 * richness) : std::min(0.9, wheatShare + woodShare); - const double woodFraction = farmDefault ? 1.0 / 3 - : wheatShare + woodShare > 0.0 ? woodShare / (wheatShare + woodShare) - : 0.0; - const auto farmland = topShare(orbitNoise(context, s, "arena-farmland", coarse, false), farm, - farmShare, true); - const auto wood = topShare(orbitNoise(context, s, "arena-split", 6.0f, false), farmland, - woodFraction, true); - const auto outcrops = topShare(orbitNoise(context, s, "arena-stone", coarse, false), rock, - scaledShare(0.012 * richness, o.stone), true); - const auto algae = topShare(orbitNoise(context, s, "arena-algae", coarse, false), alga, - scaledShare(0.1 * richness, o.algae), true); - for (size_t i = 0; i < n; ++i) { - if (farmland[i]) - plan[i] = wood[i] ? WOOD : CORN; - if (outcrops[i]) - plan[i] = STONE; - if (algae[i]) - plan[i] = ALGA; - } + // The ambient layer beyond the open apron round the moat: farmland on lake and pond shores + // (wheat to wood 2:1), stone outcrops away from the water, and algae off every shore. + std::vector farm(n, 0), rock(n, 0), alga(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + const bool open = grass[i] && !blocked[i] && + !within(tileRadius2(w, h, x, y), a.centre + a.moat + kApron + 2); + farm[i] = open && waterSteps[i] >= 2 && waterSteps[i] <= 7; + rock[i] = open && waterSteps[i] >= 3; + alga[i] = water[i] && landSteps[i] >= 2; + } + // The amount controls scale each layer on top of richness: wheat and wood their shares of the + // farmland (2:1 at the defaults, which use the shares exactly as they were). + const bool farmDefault = o.wheat == 100 && o.wood == 100; + const double wheatShare = scaledShare(0.3 * richness * 2 / 3, o.wheat); + const double woodShare = scaledShare(0.3 * richness / 3, o.wood); + const double farmShare = + farmDefault ? std::min(0.9, 0.3 * richness) : std::min(0.9, wheatShare + woodShare); + const double woodFraction = farmDefault ? 1.0 / 3 + : wheatShare + woodShare > 0.0 + ? woodShare / (wheatShare + woodShare) + : 0.0; + const auto farmland = + topShare(orbitNoise(context, s, "arena-farmland", coarse, false), farm, farmShare, true); + const auto wood = + topShare(orbitNoise(context, s, "arena-split", 6.0f, false), farmland, woodFraction, true); + const auto outcrops = topShare(orbitNoise(context, s, "arena-stone", coarse, false), rock, + scaledShare(0.012 * richness, o.stone), true); + const auto algae = topShare(orbitNoise(context, s, "arena-algae", coarse, false), alga, + scaledShare(0.1 * richness, o.algae), true); + for (size_t i = 0; i < n; ++i) + { + if (farmland[i]) + plan[i] = wood[i] ? WOOD : CORN; + if (outcrops[i]) + plan[i] = STONE; + if (algae[i]) + plan[i] = ALGA; + } - // The orchard: two-by-two groves on a four-tile lattice squared up to the centre, so every - // grove has walkway at least two tiles wide on all four sides and every tree can be picked. - // Groves are dealt out an orbit at a time - the groves the symmetries carry onto each other - - // from the centre outwards: cherries, oranges and prunes in turn from a seeded start, with - // about a fifth of the orbits turned to stone but never fewer than three left for fruit. The - // lattice phase comes from the seed; if it leaves a small centre fewer than three orbits, the - // other phases are tried. - struct Orbit { - std::int64_t radius2 = INT64_MAX; - int key = 0, value = INT_MIN; - bool stone = false; - std::vector tiles; - }; - const std::vector orchardNoise = orbitNoise(context, s, "arena-orchard", 4.0f, false); - bool planted = false; - for (int attempt = 0; attempt < 4 && !planted; ++attempt) { - // Phase 1 gives even walkways, 2 an avenue along each axis, 3 a grove on the centre; 0 - // would join the four groves round the centre into one block with unpickable middle trees. - const int phase = attempt == 0 ? l.orchardPhase : attempt; - // Whether a tile's doubled centred coordinate falls in a grove row. It depends only on the - // distance from the axis, so turns and mirrors keep it. - const auto inGrove = [phase](int doubled) { - return ((std::abs(doubled) - 1) / 2 + 4 - phase) % 4 < 2; - }; - std::vector trees(n, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - trees[i] = grass[i] && !blocked[i] && within(tileRadius2(w, h, x, y), a.centre - 2) && - inGrove(2 * x + 1 - w) && inGrove(2 * y + 1 - h); - } - // An orbit is named by the lowest tile index any symmetry carries one of its tiles to, and - // ranked by its nearest tile to the centre and its highest noise value; all three are the - // same whichever of its groves they are read from. - std::map orbits; - for (const auto &group : groupsOf(trees, w, h)) { - int key = INT_MAX, value = INT_MIN; - std::int64_t radius2 = INT64_MAX; - for (int i : group) { - value = std::max(value, orchardNoise[size_t(i)]); - radius2 = std::min(radius2, tileRadius2(w, h, i % w, i / w)); - for (int e = 0; e < s.order(); ++e) - key = std::min(key, s.tile(e, i % w, i / w)); - } - Orbit &orbit = orbits[key]; - orbit.key = key; - orbit.radius2 = radius2; - orbit.value = value; - orbit.tiles.insert(orbit.tiles.end(), group.begin(), group.end()); - } - if (orbits.size() < 3) - continue; - std::vector ranked; - for (auto &entry : orbits) - ranked.push_back(&entry.second); - std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) { - return p->value != q->value ? p->value > q->value : p->key < q->key; - }); - const size_t stones = - o.orchardStone ? std::min(ranked.size() - 3, size_t(std::lround(0.2 * double(ranked.size())))) - : 0; - for (size_t k = 0; k < stones; ++k) - ranked[k]->stone = true; - std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) { - return p->radius2 != q->radius2 ? p->radius2 < q->radius2 : p->key < q->key; - }); - // The fruit amount keeps that share of the fruit orbits nearest the centre, never fewer than - // three so every fruit stays; the rest are left as open grass. - const size_t fruitOrbits = ranked.size() - stones; - const size_t keptFruit = - std::clamp(size_t(scaledCount(std::int64_t(fruitOrbits), o.fruit)), - std::min(3, fruitOrbits), fruitOrbits); - int fruit = l.orchardPattern; - size_t fruitPlanted = 0; - for (const Orbit *orbit : ranked) { - if (!orbit->stone && fruitPlanted++ >= keptFruit) - continue; - const int type = orbit->stone ? STONE : CHERRY + fruit++ % 3; - for (int i : orbit->tiles) - plan[size_t(i)] = type; - } - planted = true; - } - if (!planted) { - context.detail = "the centre is too small to hold all three fruits"; - return false; - } + // The orchard: two-by-two groves on a four-tile lattice squared up to the centre, so every + // grove has walkway at least two tiles wide on all four sides and every tree can be picked. + // Groves are dealt out an orbit at a time - the groves the symmetries carry onto each other - + // from the centre outwards: cherries, oranges and prunes in turn from a seeded start, with + // about a fifth of the orbits turned to stone but never fewer than three left for fruit. The + // lattice phase comes from the seed; if it leaves a small centre fewer than three orbits, the + // other phases are tried. + struct Orbit + { + std::int64_t radius2 = INT64_MAX; + int key = 0, value = INT_MIN; + bool stone = false; + std::vector tiles; + }; + const std::vector orchardNoise = orbitNoise(context, s, "arena-orchard", 4.0f, false); + bool planted = false; + for (int attempt = 0; attempt < 4 && !planted; ++attempt) + { + // Phase 1 gives even walkways, 2 an avenue along each axis, 3 a grove on the centre; 0 + // would join the four groves round the centre into one block with unpickable middle trees. + const int phase = attempt == 0 ? l.orchardPhase : attempt; + // Whether a tile's doubled centred coordinate falls in a grove row. It depends only on the + // distance from the axis, so turns and mirrors keep it. + const auto inGrove = [phase](int doubled) + { return ((std::abs(doubled) - 1) / 2 + 4 - phase) % 4 < 2; }; + std::vector trees(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + trees[i] = grass[i] && !blocked[i] && + within(tileRadius2(w, h, x, y), a.centre - 2) && + inGrove(2 * x + 1 - w) && inGrove(2 * y + 1 - h); + } + // An orbit is named by the lowest tile index any symmetry carries one of its tiles to, and + // ranked by its nearest tile to the centre and its highest noise value; all three are the + // same whichever of its groves they are read from. + std::map orbits; + for (const auto &group : groupsOf(trees, w, h)) + { + int key = INT_MAX, value = INT_MIN; + std::int64_t radius2 = INT64_MAX; + for (int i : group) + { + value = std::max(value, orchardNoise[size_t(i)]); + radius2 = std::min(radius2, tileRadius2(w, h, i % w, i / w)); + for (int e = 0; e < s.order(); ++e) + key = std::min(key, s.tile(e, i % w, i / w)); + } + Orbit &orbit = orbits[key]; + orbit.key = key; + orbit.radius2 = radius2; + orbit.value = value; + orbit.tiles.insert(orbit.tiles.end(), group.begin(), group.end()); + } + if (orbits.size() < 3) + continue; + std::vector ranked; + for (auto &entry : orbits) + ranked.push_back(&entry.second); + std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) + { return p->value != q->value ? p->value > q->value : p->key < q->key; }); + const size_t stones = + o.orchardStone + ? std::min(ranked.size() - 3, size_t(std::lround(0.2 * double(ranked.size())))) + : 0; + for (size_t k = 0; k < stones; ++k) + ranked[k]->stone = true; + std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) + { return p->radius2 != q->radius2 ? p->radius2 < q->radius2 : p->key < q->key; }); + // The fruit amount keeps that share of the fruit orbits nearest the centre, never fewer than + // three so every fruit stays; the rest are left as open grass. + const size_t fruitOrbits = ranked.size() - stones; + const size_t keptFruit = + std::clamp(size_t(scaledCount(std::int64_t(fruitOrbits), o.fruit)), + std::min(3, fruitOrbits), fruitOrbits); + int fruit = l.orchardPattern; + size_t fruitPlanted = 0; + for (const Orbit *orbit : ranked) + { + if (!orbit->stone && fruitPlanted++ >= keptFruit) + continue; + const int type = orbit->stone ? STONE : CHERRY + fruit++ % 3; + for (int i : orbit->tiles) + plan[size_t(i)] = type; + } + planted = true; + } + if (!planted) + { + context.detail = "the centre is too small to hold all three fruits"; + return false; + } - // Colony 0's starting kit, grown tile by tile from seeds on its pond's banks - wheat on one - // side, wood on the other - with a little stone round the side of its home. Stamped over the - // ambient layer, with a fixed priority wherever two colonies' kits overlap. - std::vector kit(n, -1); - std::vector kitGround(n, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - kitGround[i] = grass[i] && !blocked[i] && waterSteps[i] >= 2 && - torusDistance(w, h, tilePoint(w, h, x, y), l.home) <= kHomeRadius + 12; - } - // A clump grows outward from the free kit ground nearest its target; if that pocket fills - // first, it carries on from the next nearest free tile, so a cramped bank still gets its kit. - const auto grow = [&](Point target, int type, int size) { - std::vector> nearest; - for (size_t i = 0; i < n; ++i) - if (kitGround[i] && kit[i] < 0) - nearest.emplace_back( - torusDistance(w, h, tilePoint(w, h, int(i % w), int(i / w)), target), int(i)); - std::sort(nearest.begin(), nearest.end()); - static const int steps[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; - std::vector queued(n, 0); - int placed = 0; - for (const auto &candidate : nearest) { - if (placed >= size) - break; - if (queued[size_t(candidate.second)]) - continue; - std::vector queue{candidate.second}; - queued[size_t(candidate.second)] = 1; - for (size_t head = 0; head < queue.size() && placed < size; ++head, ++placed) { - const int i = queue[head], x = i % w, y = i / w; - kit[size_t(i)] = type; - for (const auto &step : steps) { - const size_t j = size_t(wrap(y + step[1], h)) * w + wrap(x + step[0], w); - if (!queued[j] && kitGround[j] && kit[j] < 0) { - queued[j] = 1; - queue.push_back(int(j)); - } - } - } - } - return placed; - }; - const Point side{-std::sin(l.pondAngle), std::cos(l.pondAngle)}; - const double stoneAngle = l.pondAngle + 2 * kPi / 3; - const int wheatKit = - grow({l.pond.x + 4.5 * side.x, l.pond.y + 4.5 * side.y}, CORN, kKitFarmland); - const int woodKit = - grow({l.pond.x - 4.5 * side.x, l.pond.y - 4.5 * side.y}, WOOD, kKitFarmland); - const int stoneKit = grow({l.home.x + (kHomeRadius + 3) * std::cos(stoneAngle), - l.home.y + (kHomeRadius + 3) * std::sin(stoneAngle)}, - STONE, kKitStone); - if (wheatKit < kKitFarmland / 2 || woodKit < kKitFarmland / 2 || stoneKit < 1) { - context.detail = "colony 0's starting kit has no room beside its pond"; - return false; - } - const auto rank = [](int type) { - return type == STONE ? 3 : type == WOOD ? 2 : type == CORN ? 1 : 0; - }; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - int best = -1; - for (int e = 0; e < s.order(); ++e) { - const int type = kit[size_t(s.tile(e, x, y))]; - if (rank(type) > rank(best)) - best = type; - } - if (best >= 0) - plan[size_t(y) * w + x] = best; - } + // Colony 0's starting kit, grown tile by tile from seeds on its pond's banks - wheat on one + // side, wood on the other - with a little stone round the side of its home. Stamped over the + // ambient layer, with a fixed priority wherever two colonies' kits overlap. + std::vector kit(n, -1); + std::vector kitGround(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + kitGround[i] = grass[i] && !blocked[i] && waterSteps[i] >= 2 && + torusDistance(w, h, tilePoint(w, h, x, y), l.home) <= kHomeRadius + 12; + } + // A clump grows outward from the free kit ground nearest its target; if that pocket fills + // first, it carries on from the next nearest free tile, so a cramped bank still gets its kit. + const auto grow = [&](Point target, int type, int size) + { + std::vector> nearest; + for (size_t i = 0; i < n; ++i) + if (kitGround[i] && kit[i] < 0) + nearest.emplace_back( + torusDistance(w, h, tilePoint(w, h, int(i % w), int(i / w)), target), int(i)); + std::sort(nearest.begin(), nearest.end()); + static const int steps[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + std::vector queued(n, 0); + int placed = 0; + for (const auto &candidate : nearest) + { + if (placed >= size) + break; + if (queued[size_t(candidate.second)]) + continue; + std::vector queue{candidate.second}; + queued[size_t(candidate.second)] = 1; + for (size_t head = 0; head < queue.size() && placed < size; ++head, ++placed) + { + const int i = queue[head], x = i % w, y = i / w; + kit[size_t(i)] = type; + for (const auto &step : steps) + { + const size_t j = size_t(wrap(y + step[1], h)) * w + wrap(x + step[0], w); + if (!queued[j] && kitGround[j] && kit[j] < 0) + { + queued[j] = 1; + queue.push_back(int(j)); + } + } + } + } + return placed; + }; + const Point side{-std::sin(l.pondAngle), std::cos(l.pondAngle)}; + const double stoneAngle = l.pondAngle + 2 * kPi / 3; + const int wheatKit = + grow({l.pond.x + 4.5 * side.x, l.pond.y + 4.5 * side.y}, CORN, kKitFarmland); + const int woodKit = + grow({l.pond.x - 4.5 * side.x, l.pond.y - 4.5 * side.y}, WOOD, kKitFarmland); + const int stoneKit = grow({l.home.x + (kHomeRadius + 3) * std::cos(stoneAngle), + l.home.y + (kHomeRadius + 3) * std::sin(stoneAngle)}, + STONE, kKitStone); + if (wheatKit < kKitFarmland / 2 || woodKit < kKitFarmland / 2 || stoneKit < 1) + { + context.detail = "colony 0's starting kit has no room beside its pond"; + return false; + } + const auto rank = [](int type) + { + return type == STONE ? 3 : type == WOOD ? 2 : type == CORN ? 1 : 0; + }; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + int best = -1; + for (int e = 0; e < s.order(); ++e) + { + const int type = kit[size_t(s.tile(e, x, y))]; + if (rank(type) > rank(best)) + best = type; + } + if (best >= 0) + plan[size_t(y) * w + x] = best; + } - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - if (plan[size_t(y) * w + x] >= 0) - map.setResource(x, y, plan[size_t(y) * w + x], 1); - // The engine draws each deposit's amount and look from the gameplay RNG. Every orbit takes - // its lowest-indexed tile's, so deposits are symmetric in size as well as type. - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const int i = y * w + x; - int first = i; - for (int e = 1; e < s.order(); ++e) - first = std::min(first, s.tile(e, x, y)); - if (first == i) - continue; - const Resource source = map.getResource(size_t(first)); - Resource &target = map.getResource(size_t(i)); - if (source.type != target.type) { - context.detail = "deposits differ across the orbit of (" + std::to_string(x) + ", " + - std::to_string(y) + ")"; - return false; - } - target = source; - } - return true; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (plan[size_t(y) * w + x] >= 0) + map.setResource(x, y, plan[size_t(y) * w + x], 1); + // The engine draws each deposit's amount and look from the gameplay RNG. Every orbit takes + // its lowest-indexed tile's, so deposits are symmetric in size as well as type. + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const int i = y * w + x; + int first = i; + for (int e = 1; e < s.order(); ++e) + first = std::min(first, s.tile(e, x, y)); + if (first == i) + continue; + const Resource source = map.getResource(size_t(first)); + Resource &target = map.getResource(size_t(i)); + if (source.type != target.type) + { + context.detail = "deposits differ across the orbit of (" + std::to_string(x) + + ", " + std::to_string(y) + ")"; + return false; + } + target = source; + } + return true; } -bool generate(Game &game, GenerationContext &context) { - context.stage = "arena layout"; - const SymmetricArenaOptions o(context.request); - const Arena a = arenaFor(context.request); - Map &map = game.map; - map.makeHomogenMap(WATER); - for (int i = 0; i < a.teams; ++i) - game.addTeam(); - if (a.teams < 2 || a.symmetry.order() != a.teams) { - context.detail = "no symmetry gives this many colonies the same ground"; - return false; - } - Fit fit; - const std::vector homes = homeCandidates(a, fit); - if (homes.empty()) { - context.detail = "no home position fits these settings"; - return false; - } - const Home home = chooseHome(homes, context, std::min(a.width, a.height)); - Layout l; - l.homeU = home.u; - l.homeV = home.v; - l.home = cornerPoint(a.width, a.height, home.u, home.v); - l.gates = gateAngles(a, l.home); - // The pond lies away from the centre, turned up to 60 degrees either way. - l.pondAngle = - std::atan2(l.home.y, l.home.x) + (int(context.bounded("layout", 121)) - 60) * kPi / 180; - l.pond = {l.home.x + kPondDistance * std::cos(l.pondAngle), - l.home.y + kPondDistance * std::sin(l.pondAngle)}; - l.orchardPhase = 1 + int(context.bounded("layout", 3)); - l.orchardPattern = int(context.bounded("layout", 3)); - Terrain t; - return buildTerrain(map, context, a, l, o, t) && placeColonies(game, context, a, l) && - furnish(game, context, a, l, o, t); +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "arena layout"; + const SymmetricArenaOptions o(context.request); + const Arena a = arenaFor(context.request); + Map &map = game.map; + map.makeHomogenMap(WATER); + for (int i = 0; i < a.teams; ++i) + game.addTeam(); + if (a.teams < 2 || a.symmetry.order() != a.teams) + { + context.detail = "no symmetry gives this many colonies the same ground"; + return false; + } + Fit fit; + const std::vector homes = homeCandidates(a, fit); + if (homes.empty()) + { + context.detail = "no home position fits these settings"; + return false; + } + const Home home = chooseHome(homes, context, std::min(a.width, a.height)); + Layout l; + l.homeU = home.u; + l.homeV = home.v; + l.home = cornerPoint(a.width, a.height, home.u, home.v); + l.gates = gateAngles(a, l.home); + // The pond lies away from the centre, turned up to 60 degrees either way. + l.pondAngle = + std::atan2(l.home.y, l.home.x) + (int(context.bounded("layout", 121)) - 60) * kPi / 180; + l.pond = {l.home.x + kPondDistance * std::cos(l.pondAngle), + l.home.y + kPondDistance * std::sin(l.pondAngle)}; + l.orchardPhase = 1 + int(context.bounded("layout", 3)); + l.orchardPattern = int(context.bounded("layout", 3)); + Terrain t; + return buildTerrain(map, context, a, l, o, t) && placeColonies(game, context, a, l) && + furnish(game, context, a, l, o, t); } // The class of a tile's graphic (Map::lookup): grass, grass and sand, sand, sand and water, // water. It depends only on which terrains a tile's corners hold, not on which corner holds // which, so it is unchanged by any symmetry. -int terrainClass(Uint16 terrain) { - return terrain < 16 ? 0 : terrain < 128 ? 1 : terrain < 144 ? 2 : terrain < 256 ? 3 : 4; +int terrainClass(Uint16 terrain) +{ + return terrain < 16 ? 0 : terrain < 128 ? 1 : terrain < 144 ? 2 : terrain < 256 ? 3 : 4; } -std::string at(int x, int y) { - return " at (" + std::to_string(x) + ", " + std::to_string(y) + ")"; +std::string at(int x, int y) +{ + return " at (" + std::to_string(x) + ", " + std::to_string(y) + ")"; } // The arena's guarantees, checked on the finished world rather than trusted: every symmetry of @@ -950,183 +1056,206 @@ std::string at(int x, int y) { // colony onto any other; and every colony walks to wheat, wood, each fruit and open ground in // the orchard, all in the same number of steps. Water, buildings and every resource block the // walk; units don't, since they move. -std::string validateWorld(const Game &game, const GenerationContext &context) { - const Map &map = game.map; - const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; - const size_t n = size_t(w) * h; - const Arena a = arenaFor(context.request); - const Symmetry &s = a.symmetry; - if (s.order() != teams) - return "No symmetry gives " + std::to_string(teams) + " colonies the same ground."; - std::vector> permutations; - for (int e = 1; e < s.order(); ++e) { - const std::string under = " is not symmetric under symmetry " + std::to_string(e) + "."; - std::vector image(size_t(teams), -1); - const auto relate = [&](int from, int to) { - if (from < 0 || from >= teams || to < 0 || to >= teams) - return false; - if (image[size_t(from)] < 0) - image[size_t(from)] = to; - return image[size_t(from)] == to; - }; - for (int v = 0; v < h; ++v) - for (int u = 0; u < w; ++u) { - const int c = s.corner(e, u, v); - if (map.getUMTerrain(u, v) != map.getUMTerrain(c % w, c / w)) - return "Undermap corner" + at(u, v) + under; - } - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const int q = s.tile(e, x, y), qx = q % w, qy = q / w; - if (terrainClass(map.getTerrain(x, y)) != terrainClass(map.getTerrain(qx, qy))) - return "Terrain" + at(x, y) + under; - const Resource &ra = map.getResource(x, y), &rb = map.getResource(qx, qy); - if (ra.type != rb.type || ra.amount != rb.amount) - return "Deposit" + at(x, y) + under; - const Uint16 ba = map.getBuilding(x, y), bb = map.getBuilding(qx, qy); - if ((ba == NOGBID) != (bb == NOGBID)) - return "Building" + at(x, y) + under; - if (ba != NOGBID && - (!relate(Building::GIDtoTeam(ba), Building::GIDtoTeam(bb)) || - game.teams[Building::GIDtoTeam(ba)]->myBuildings[Building::GIDtoID(ba)]->type != - game.teams[Building::GIDtoTeam(bb)]->myBuildings[Building::GIDtoID(bb)]->type)) - return "Building" + at(x, y) + under; - const Uint16 ua = map.getGroundUnit(x, y), ub = map.getGroundUnit(qx, qy); - if ((ua == NOGUID) != (ub == NOGUID)) - return "Unit" + at(x, y) + under; - if (ua != NOGUID && - (!relate(Unit::GIDtoTeam(ua), Unit::GIDtoTeam(ub)) || - game.teams[Unit::GIDtoTeam(ua)]->myUnits[Unit::GIDtoID(ua)]->typeNum != - game.teams[Unit::GIDtoTeam(ub)]->myUnits[Unit::GIDtoID(ub)]->typeNum)) - return "Unit" + at(x, y) + under; - } - std::vector hit(size_t(teams), 0); - for (int team = 0; team < teams; ++team) { - const int to = image[size_t(team)]; - if (to < 0 || hit[size_t(to)]) - return "Symmetry " + std::to_string(e) + " does not map colonies one to one."; - hit[size_t(to)] = 1; - } - permutations.push_back(image); - } - std::vector carried{0}; - std::vector seen(size_t(teams), 0); - seen[0] = 1; - for (size_t head = 0; head < carried.size(); ++head) - for (const auto &image : permutations) - if (!seen[size_t(image[size_t(carried[head])])]) { - seen[size_t(image[size_t(carried[head])])] = 1; - carried.push_back(image[size_t(carried[head])]); - } - if (int(carried.size()) != teams) - return "The symmetries do not carry colony 0 onto every other colony."; +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + const Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + const size_t n = size_t(w) * h; + const Arena a = arenaFor(context.request); + const Symmetry &s = a.symmetry; + if (s.order() != teams) + return "No symmetry gives " + std::to_string(teams) + " colonies the same ground."; + std::vector> permutations; + for (int e = 1; e < s.order(); ++e) + { + const std::string under = " is not symmetric under symmetry " + std::to_string(e) + "."; + std::vector image(size_t(teams), -1); + const auto relate = [&](int from, int to) + { + if (from < 0 || from >= teams || to < 0 || to >= teams) + return false; + if (image[size_t(from)] < 0) + image[size_t(from)] = to; + return image[size_t(from)] == to; + }; + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + { + const int c = s.corner(e, u, v); + if (map.getUMTerrain(u, v) != map.getUMTerrain(c % w, c / w)) + return "Undermap corner" + at(u, v) + under; + } + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const int q = s.tile(e, x, y), qx = q % w, qy = q / w; + if (terrainClass(map.getTerrain(x, y)) != terrainClass(map.getTerrain(qx, qy))) + return "Terrain" + at(x, y) + under; + const Resource &ra = map.getResource(x, y), &rb = map.getResource(qx, qy); + if (ra.type != rb.type || ra.amount != rb.amount) + return "Deposit" + at(x, y) + under; + const Uint16 ba = map.getBuilding(x, y), bb = map.getBuilding(qx, qy); + if ((ba == NOGBID) != (bb == NOGBID)) + return "Building" + at(x, y) + under; + if (ba != NOGBID && (!relate(Building::GIDtoTeam(ba), Building::GIDtoTeam(bb)) || + game.teams[Building::GIDtoTeam(ba)] + ->myBuildings[Building::GIDtoID(ba)] + ->type != game.teams[Building::GIDtoTeam(bb)] + ->myBuildings[Building::GIDtoID(bb)] + ->type)) + return "Building" + at(x, y) + under; + const Uint16 ua = map.getGroundUnit(x, y), ub = map.getGroundUnit(qx, qy); + if ((ua == NOGUID) != (ub == NOGUID)) + return "Unit" + at(x, y) + under; + if (ua != NOGUID && + (!relate(Unit::GIDtoTeam(ua), Unit::GIDtoTeam(ub)) || + game.teams[Unit::GIDtoTeam(ua)]->myUnits[Unit::GIDtoID(ua)]->typeNum != + game.teams[Unit::GIDtoTeam(ub)]->myUnits[Unit::GIDtoID(ub)]->typeNum)) + return "Unit" + at(x, y) + under; + } + std::vector hit(size_t(teams), 0); + for (int team = 0; team < teams; ++team) + { + const int to = image[size_t(team)]; + if (to < 0 || hit[size_t(to)]) + return "Symmetry " + std::to_string(e) + " does not map colonies one to one."; + hit[size_t(to)] = 1; + } + permutations.push_back(image); + } + std::vector carried{0}; + std::vector seen(size_t(teams), 0); + seen[0] = 1; + for (size_t head = 0; head < carried.size(); ++head) + for (const auto &image : permutations) + if (!seen[size_t(image[size_t(carried[head])])]) + { + seen[size_t(image[size_t(carried[head])])] = 1; + carried.push_back(image[size_t(carried[head])]); + } + if (int(carried.size()) != teams) + return "The symmetries do not carry colony 0 onto every other colony."; - static const char *const targets[] = {"wheat", "wood", "cherries", "oranges", "prunes", - "the orchard"}; - std::vector walkable(n); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - walkable[size_t(y) * w + x] = !map.isWater(x, y) && !map.isResource(x, y) && - map.getBuilding(x, y) == NOGBID; - std::array first{}; - for (int team = 0; team < teams; ++team) { - std::vector dist(n, -1), queue; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) { - dist[size_t(y) * w + x] = 0; - queue.push_back(y * w + x); - } - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); - if (dist[j] < 0 && walkable[j]) { - dist[j] = dist[size_t(queue[head])] + 1; - queue.push_back(int(j)); - } - } - } - std::array steps; - steps.fill(-1); - const auto reach = [&](int slot, int d) { - if (steps[size_t(slot)] < 0 || d < steps[size_t(slot)]) - steps[size_t(slot)] = d; - }; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const int d = dist[size_t(y) * w + x]; - if (d < 0) - continue; - if (within(tileRadius2(w, h, x, y), a.centre - 2)) - reach(5, d); - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int type = map.getResource(wrap(x + dx, w), wrap(y + dy, h)).type; - if (type == CORN) - reach(0, d + 1); - else if (type == WOOD) - reach(1, d + 1); - else if (type >= CHERRY && type <= CHERRY + 2) - reach(2 + type - CHERRY, d + 1); - } - } - for (size_t k = 0; k < steps.size(); ++k) { - if (steps[k] < 0) - return "Colony " + std::to_string(team) + " cannot walk to " + targets[k] + "."; - if (team > 0 && steps[k] != first[k]) - return "Colony " + std::to_string(team) + " walks " + std::to_string(steps[k]) + - " steps to " + targets[k] + ", colony 0 walks " + std::to_string(first[k]) + "."; - } - if (team == 0) - first = steps; - } - return ""; + static const char *const targets[] = {"wheat", "wood", "cherries", + "oranges", "prunes", "the orchard"}; + std::vector walkable(n); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + walkable[size_t(y) * w + x] = + !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + std::array first{}; + for (int team = 0; team < teams; ++team) + { + std::vector dist(n, -1), queue; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) + { + dist[size_t(y) * w + x] = 0; + queue.push_back(y * w + x); + } + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); + if (dist[j] < 0 && walkable[j]) + { + dist[j] = dist[size_t(queue[head])] + 1; + queue.push_back(int(j)); + } + } + } + std::array steps; + steps.fill(-1); + const auto reach = [&](int slot, int d) + { + if (steps[size_t(slot)] < 0 || d < steps[size_t(slot)]) + steps[size_t(slot)] = d; + }; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const int d = dist[size_t(y) * w + x]; + if (d < 0) + continue; + if (within(tileRadius2(w, h, x, y), a.centre - 2)) + reach(5, d); + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int type = map.getResource(wrap(x + dx, w), wrap(y + dy, h)).type; + if (type == CORN) + reach(0, d + 1); + else if (type == WOOD) + reach(1, d + 1); + else if (type >= CHERRY && type <= CHERRY + 2) + reach(2 + type - CHERRY, d + 1); + } + } + for (size_t k = 0; k < steps.size(); ++k) + { + if (steps[k] < 0) + return "Colony " + std::to_string(team) + " cannot walk to " + targets[k] + "."; + if (team > 0 && steps[k] != first[k]) + return "Colony " + std::to_string(team) + " walks " + std::to_string(steps[k]) + + " steps to " + targets[k] + ", colony 0 walks " + std::to_string(first[k]) + + "."; + } + if (team == 0) + first = steps; + } + return ""; } } // namespace SymmetricArenaOptions::SymmetricArenaOptions(const GenerationRequest &r) - : centreSize(r.option("centre-size")), moatWidth(r.option("moat-width")), - causewayWidth(r.option("causeway-width")), causeways(r.option("causeways")), - lakes(r.option("lakes")), richness(r.option("richness")), moat(r.option("moat") != 0), - orchardStone(r.option("orchard-stone") != 0), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} + : centreSize(r.option("centre-size")), moatWidth(r.option("moat-width")), + causewayWidth(r.option("causeway-width")), causeways(r.option("causeways")), + lakes(r.option("lakes")), richness(r.option("richness")), moat(r.option("moat") != 0), + orchardStone(r.option("orchard-stone") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} -GeneratorDefinition symmetricArenaDefinition() { - return {"symmetric-arena", - 15, - "Symmetric arena", - 1, - false, - // centre-size: the orchard island's radius as a share of the shorter side (over a - // small floor); moat and causeway widths are in tiles; causeways per colony, one - // straight towards it or two flanking it; lakes as a share of the open land outside - // the arena; richness scales the ambient deposits, never a home's own kit. - {{"centre-size", "Centre size", 6, 16, 1, 10, ControlGroup::Terrain}, - {"moat-width", "Moat width", 3, 10, 1, 5, ControlGroup::Terrain}, - {"causeway-width", "Causeway width", 2, 8, 1, 4, ControlGroup::Layout}, - {"causeways", "Causeways", 1, 2, 1, 1, ControlGroup::Layout}, - {"lakes", "Lakes", 0, 40, 5, 20, ControlGroup::Terrain}, - {"richness", "Resource richness", 0, 200, 25, 100, ControlGroup::Resources}, - // Off, no water rings the orchard island; it joins the land around it. - GeneratorControl::toggle("moat", "Moat", true, ControlGroup::Terrain), - // Off, every grove in the orchard is fruit. - GeneratorControl::toggle("orchard-stone", "Stone in the orchard", true, - ControlGroup::Resources), - // Each ambient layer on top of richness: the farmland's wheat and wood, stone outcrops - // and algae. Fruit keeps that share of the orchard's groves nearest the centre, never - // fewer than three. Every home's kit stays as it is. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount", 100)}, - generate, - true, - validate, - validateWorld}; +GeneratorDefinition symmetricArenaDefinition() +{ + return {"symmetric-arena", + 15, + "Symmetric arena", + 1, + false, + // centre-size: the orchard island's radius as a share of the shorter side (over a + // small floor); moat and causeway widths are in tiles; causeways per colony, one + // straight towards it or two flanking it; lakes as a share of the open land outside + // the arena; richness scales the ambient deposits, never a home's own kit. + {{"centre-size", "Centre size", 6, 16, 1, 10, ControlGroup::Terrain}, + {"moat-width", "Moat width", 3, 10, 1, 5, ControlGroup::Terrain}, + {"causeway-width", "Causeway width", 2, 8, 1, 4, ControlGroup::Layout}, + {"causeways", "Causeways", 1, 2, 1, 1, ControlGroup::Layout}, + {"lakes", "Lakes", 0, 40, 5, 20, ControlGroup::Terrain}, + {"richness", "Resource richness", 0, 200, 25, 100, ControlGroup::Resources}, + // Off, no water rings the orchard island; it joins the land around it. + GeneratorControl::toggle("moat", "Moat", true, ControlGroup::Terrain), + // Off, every grove in the orchard is fruit. + GeneratorControl::toggle("orchard-stone", "Stone in the orchard", true, + ControlGroup::Resources), + // Each ambient layer on top of richness: the farmland's wheat and wood, stone outcrops + // and algae. Fruit keeps that share of the orchard's groves nearest the centre, never + // fewer than three. Every home's kit stays as it is. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount", 100)}, + generate, + true, + validate, + validateWorld}; } diff --git a/src/map/generator/generators/SymmetricArenaGenerator.h b/src/map/generator/generators/SymmetricArenaGenerator.h index a03a83837..641cc035b 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.h +++ b/src/map/generator/generators/SymmetricArenaGenerator.h @@ -2,10 +2,11 @@ #pragma once #include "GenerationRequest.h" #include "GeneratorDefinition.h" -struct SymmetricArenaOptions { - int centreSize, moatWidth, causewayWidth, causeways, lakes, richness; - bool moat, orchardStone; - int wheat, wood, stone, algae, fruit; // percentages of the default amounts - explicit SymmetricArenaOptions(const GenerationRequest &r); +struct SymmetricArenaOptions +{ + int centreSize, moatWidth, causewayWidth, causeways, lakes, richness; + bool moat, orchardStone; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit SymmetricArenaOptions(const GenerationRequest &r); }; GeneratorDefinition symmetricArenaDefinition(); diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index ae22f3169..459f8ec03 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -32,7 +32,8 @@ using namespace MapGeneration; // colony gets the same neighbourhood turned round the centre, and the layout is fair for any // colony count. The whole design is a pure function of the request, so validateWorld rebuilds // it and checks the finished world. -namespace { +namespace +{ constexpr double pi = 3.14159265358979323846; @@ -54,599 +55,696 @@ constexpr double kCentralShare = 0.13; // Sandbars are just big enough to hold a tower or an inn. constexpr double kSandbarLow = 3.0, kSandbarHigh = 4.5; -struct Torus { - int w, h; - int x(int v) const { return ((v % w) + w) % w; } - int y(int v) const { return ((v % h) + h) % h; } - int at(int px, int py) const { return y(py) * w + x(px); } - int offsetX(int from, int to) const { - const int d = x(to - from); - return d > w / 2 ? d - w : d; - } - int offsetY(int from, int to) const { - const int d = y(to - from); - return d > h / 2 ? d - h : d; - } +struct Torus +{ + int w, h; + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + int offsetX(int from, int to) const + { + const int d = x(to - from); + return d > w / 2 ? d - w : d; + } + int offsetY(int from, int to) const + { + const int d = y(to - from); + return d > h / 2 ? d - h : d; + } }; std::vector stepsFrom(const Torus &t, const std::vector &source, - const std::vector &open) { - std::vector dist(size_t(t.w) * t.h, -1); - std::vector queue; - queue.reserve(dist.size()); - for (size_t i = 0; i < dist.size(); ++i) - if (source[i]) { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % t.w, y = queue[head] / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int n = t.at(x + dx, y + dy); - if (dist[n] < 0 && open[n]) { - dist[n] = dist[queue[head]] + 1; - queue.push_back(n); - } - } - } - return dist; + const std::vector &open) +{ + std::vector dist(size_t(t.w) * t.h, -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) + { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % t.w, y = queue[head] / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int n = t.at(x + dx, y + dy); + if (dist[n] < 0 && open[n]) + { + dist[n] = dist[queue[head]] + 1; + queue.push_back(n); + } + } + } + return dist; } -struct Geometry { - int teams, half; - double homeRing, homeRadius, centralRadius, amplitude; +struct Geometry +{ + int teams, half; + double homeRing, homeRadius, centralRadius, amplitude; }; -Geometry geometryFor(const GenerationRequest &r) { - const TidalFlatsOptions o(r); - Geometry g; - g.teams = std::max(1, r.nbTeams); - g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; - g.homeRing = kHomeRing * g.half; - g.centralRadius = o.centralIsland ? std::max(8.0, kCentralShare * g.half) : 0.0; - g.amplitude = o.coastRoughness / 100.0 * 0.35; - // The control is the islands' radius as a share of the half side; with many colonies on the - // ring, or a small map, the islands shrink to what the ring, the wrap and the central island - // leave room for, and validateRequest refuses a map where that is too small for a base. - double radius = o.homeIslandSize / 100.0 * g.half; - const double reach = 1 + g.amplitude; - if (g.teams >= 2) - radius = std::min(radius, (2 * g.homeRing * std::sin(pi / g.teams) - kIslandGap) / (2 * reach)); - radius = std::min(radius, (g.half - kIslandGap / 2.0 - g.homeRing) / reach); - if (g.centralRadius > 0) - radius = std::min(radius, (g.homeRing - g.centralRadius * 1.3 - kIslandGap) / reach); - g.homeRadius = std::floor(radius); - return g; +Geometry geometryFor(const GenerationRequest &r) +{ + const TidalFlatsOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; + g.homeRing = kHomeRing * g.half; + g.centralRadius = o.centralIsland ? std::max(8.0, kCentralShare * g.half) : 0.0; + g.amplitude = o.coastRoughness / 100.0 * 0.35; + // The control is the islands' radius as a share of the half side; with many colonies on the + // ring, or a small map, the islands shrink to what the ring, the wrap and the central island + // leave room for, and validateRequest refuses a map where that is too small for a base. + double radius = o.homeIslandSize / 100.0 * g.half; + const double reach = 1 + g.amplitude; + if (g.teams >= 2) + radius = + std::min(radius, (2 * g.homeRing * std::sin(pi / g.teams) - kIslandGap) / (2 * reach)); + radius = std::min(radius, (g.half - kIslandGap / 2.0 - g.homeRing) / reach); + if (g.centralRadius > 0) + radius = std::min(radius, (g.homeRing - g.centralRadius * 1.3 - kIslandGap) / reach); + g.homeRadius = std::floor(radius); + return g; } // A round feature in a wedge's frame: arc offset from the wedge's middle, radius from the centre, // and what it is. Shapes are evaluated in the wedge's frame too, so every wedge gets the same. -enum Kind { HomeIsland = 0, Neutral, Central, Sandbar, Pool, Lagoon }; -struct Blob { - Kind kind; - double s, r, reach, stretch, turn; - int prize; // neutral islands: 0 fruit, 1 stone, 2 wheat - RadialShape shape; - bool holds(double ds, double dr) const { - // Rotate into the shape's own frame and undo its stretch. - const double c = std::cos(turn), sn = std::sin(turn); - const double px = (ds * c + dr * sn) / stretch, py = (-ds * sn + dr * c) * stretch; - return std::hypot(px, py) < shape.radiusAt(std::atan2(py, px)); - } +enum Kind +{ + HomeIsland = 0, + Neutral, + Central, + Sandbar, + Pool, + Lagoon +}; +struct Blob +{ + Kind kind; + double s, r, reach, stretch, turn; + int prize; // neutral islands: 0 fruit, 1 stone, 2 wheat + RadialShape shape; + bool holds(double ds, double dr) const + { + // Rotate into the shape's own frame and undo its stretch. + const double c = std::cos(turn), sn = std::sin(turn); + const double px = (ds * c + dr * sn) / stretch, py = (-ds * sn + dr * c) * stretch; + return std::hypot(px, py) < shape.radiusAt(std::atan2(py, px)); + } }; -struct Layout { - Torus t{1, 1}; - Geometry g{}; - int cx = 0, cy = 0; - double phase = 0; - std::vector blobs; // in the wedge's frame - std::vector homeX, homeY; // every home island's centre - std::vector pondX, pondY; // and its pond's - std::vector grass, water, island; // per tile; island: which home, else -1 below - std::vector islandOf; // -1 flats, -2 central, -3 neutral or sandbar, k a home - std::vector prizeOf; // neutral islands: the prize, else -1 - std::vector radius, angle; - std::string failure; +struct Layout +{ + Torus t{1, 1}; + Geometry g{}; + int cx = 0, cy = 0; + double phase = 0; + std::vector blobs; // in the wedge's frame + std::vector homeX, homeY; // every home island's centre + std::vector pondX, pondY; // and its pond's + std::vector grass, water, island; // per tile; island: which home, else -1 below + std::vector islandOf; // -1 flats, -2 central, -3 neutral or sandbar, k a home + std::vector prizeOf; // neutral islands: the prize, else -1 + std::vector radius, angle; + std::string failure; }; -Layout design(const GenerationRequest &request, GenerationContext &context) { - Layout L; - L.t = {1 << request.wDec, 1 << request.hDec}; - L.g = geometryFor(request); - const Torus &t = L.t; - const Geometry &g = L.g; - const int n = t.w * t.h, teams = g.teams; - const TidalFlatsOptions o(request); - L.cx = t.w / 2; - L.cy = t.h / 2; - L.phase = context.bounded("flats-layout", 3600) / 3600.0 * 2 * pi; - const double wedge = 2 * pi / teams; - const double arcHalfAt = [&](double d) { return teams < 2 ? pi * d : pi * d / teams; }(1.0); +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.w * t.h, teams = g.teams; + const TidalFlatsOptions o(request); + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("flats-layout", 3600) / 3600.0 * 2 * pi; + const double wedge = 2 * pi / teams; + const double arcHalfAt = [&](double d) { return teams < 2 ? pi * d : pi * d / teams; }(1.0); - // Everything in one wedge, then stamped into every wedge: the home island in the middle of the - // ring, then neutral islands, sandbars, lagoons and pools that keep clear of each other, of the - // central island and of the map's wrap. - std::vector &blobs = L.blobs; - blobs.push_back({HomeIsland, 0.0, g.homeRing, 0, 1.0, 0.0, -1, - RadialShape(g.homeRadius, g.amplitude, context, "flats-coast")}); - blobs.back().reach = blobs.back().shape.maximumRadius(); - const auto fits = [&](double s0, double r0, double reach, double gap) { - if (r0 - reach < 6 + g.centralRadius * 1.3 + (g.centralRadius > 0 ? gap : 0)) - return false; - if (r0 + reach > g.half - 4) - return false; - if (teams >= 2 && std::abs(s0) + reach + gap / 2 > arcHalfAt * r0) - return false; - for (const Blob &b : blobs) { - const double need = reach + b.reach + std::max(gap, b.kind == HomeIsland ? double(kIslandGap) : gap); - if (std::hypot(s0 - b.s, r0 - b.r) < need) - return false; - } - return true; - }; - const auto scatter = [&](Kind kind, int wanted, double low, double high, double roughness, - double gap, const char *stream, bool stretched) { - for (int attempt = 0, placed = 0; attempt < wanted * 80 && placed < wanted; ++attempt) { - const double r0 = 8 + context.bounded(stream, 1000) / 1000.0 * (g.half - 16); - const double s0 = (context.bounded(stream, 2001) / 1000.0 - 1) * arcHalfAt * r0; - const double radius = low + context.bounded(stream, 1000) / 1000.0 * (high - low); - const double stretch = stretched ? 1.2 + context.bounded(stream, 81) / 100.0 : 1.0; - const double turn = stretched ? context.bounded(stream, 3600) / 3600.0 * pi : 0.0; - RadialShape shape(radius, roughness, context, stream); - const double reach = shape.maximumRadius() * stretch; - if (!fits(s0, r0, reach, gap)) - continue; - blobs.push_back({kind, s0, r0, reach, stretch, turn, placed % 2, shape}); - ++placed; - } - }; - scatter(Neutral, o.extraIslands, std::max(kNeutralMinimum, kNeutralLow * g.homeRadius), - std::max(kNeutralMinimum + 2, kNeutralHigh * g.homeRadius), 0.35, kIslandGap, - "flats-islands", false); - scatter(Sandbar, o.sandbars, kSandbarLow, kSandbarHigh, 0.3, 6, "flats-sandbars", false); - scatter(Lagoon, o.lagoons, 8.0, 14.0, 0.35, 6, "flats-lagoons", true); - const double wedgeArea = wedge * g.half * g.half / 2; - scatter(Pool, int(std::lround(o.tidePools * wedgeArea / 16384.0)), 1.5, 4.0, 0.4, 4, - "flats-pools", false); - const RadialShape central(std::max(1.0, g.centralRadius), g.amplitude * 0.6, context, "flats-central"); + // Everything in one wedge, then stamped into every wedge: the home island in the middle of the + // ring, then neutral islands, sandbars, lagoons and pools that keep clear of each other, of the + // central island and of the map's wrap. + std::vector &blobs = L.blobs; + blobs.push_back({HomeIsland, 0.0, g.homeRing, 0, 1.0, 0.0, -1, + RadialShape(g.homeRadius, g.amplitude, context, "flats-coast")}); + blobs.back().reach = blobs.back().shape.maximumRadius(); + const auto fits = [&](double s0, double r0, double reach, double gap) + { + if (r0 - reach < 6 + g.centralRadius * 1.3 + (g.centralRadius > 0 ? gap : 0)) + return false; + if (r0 + reach > g.half - 4) + return false; + if (teams >= 2 && std::abs(s0) + reach + gap / 2 > arcHalfAt * r0) + return false; + for (const Blob &b : blobs) + { + const double need = + reach + b.reach + std::max(gap, b.kind == HomeIsland ? double(kIslandGap) : gap); + if (std::hypot(s0 - b.s, r0 - b.r) < need) + return false; + } + return true; + }; + const auto scatter = [&](Kind kind, int wanted, double low, double high, double roughness, + double gap, const char *stream, bool stretched) + { + for (int attempt = 0, placed = 0; attempt < wanted * 80 && placed < wanted; ++attempt) + { + const double r0 = 8 + context.bounded(stream, 1000) / 1000.0 * (g.half - 16); + const double s0 = (context.bounded(stream, 2001) / 1000.0 - 1) * arcHalfAt * r0; + const double radius = low + context.bounded(stream, 1000) / 1000.0 * (high - low); + const double stretch = stretched ? 1.2 + context.bounded(stream, 81) / 100.0 : 1.0; + const double turn = stretched ? context.bounded(stream, 3600) / 3600.0 * pi : 0.0; + RadialShape shape(radius, roughness, context, stream); + const double reach = shape.maximumRadius() * stretch; + if (!fits(s0, r0, reach, gap)) + continue; + blobs.push_back({kind, s0, r0, reach, stretch, turn, placed % 2, shape}); + ++placed; + } + }; + scatter(Neutral, o.extraIslands, std::max(kNeutralMinimum, kNeutralLow * g.homeRadius), + std::max(kNeutralMinimum + 2, kNeutralHigh * g.homeRadius), 0.35, kIslandGap, + "flats-islands", false); + scatter(Sandbar, o.sandbars, kSandbarLow, kSandbarHigh, 0.3, 6, "flats-sandbars", false); + scatter(Lagoon, o.lagoons, 8.0, 14.0, 0.35, 6, "flats-lagoons", true); + const double wedgeArea = wedge * g.half * g.half / 2; + scatter(Pool, int(std::lround(o.tidePools * wedgeArea / 16384.0)), 1.5, 4.0, 0.4, 4, + "flats-pools", false); + const RadialShape central(std::max(1.0, g.centralRadius), g.amplitude * 0.6, context, + "flats-central"); - L.grass.assign(n, 0); - L.water.assign(n, 0); - L.islandOf.assign(n, -1); - L.prizeOf.assign(n, -1); - L.radius.assign(n, 0.0); - L.angle.assign(n, 0.0); - L.homeX.assign(teams, 0); - L.homeY.assign(teams, 0); - L.pondX.assign(teams, 0); - L.pondY.assign(teams, 0); - for (int k = 0; k < teams; ++k) { - const double a = L.phase + wedge * (k + 0.5); - L.homeX[k] = t.x(L.cx + int(std::lround(g.homeRing * std::cos(a)))); - L.homeY[k] = t.y(L.cy + int(std::lround(g.homeRing * std::sin(a)))); - L.pondX[k] = L.homeX[k]; - L.pondY[k] = L.homeY[k]; - } - const double pondRadius = std::clamp(0.2 * g.homeRadius, 3.0, 6.0); - const RadialShape pond(pondRadius, 0.3, context, "flats-ponds"); - const RadialShape oasis(2.5, 0.3, context, "flats-ponds"); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); - const double d = std::hypot(double(dx), double(dy)); - const double theta = std::atan2(double(dy), double(dx)); - L.radius[i] = d; - L.angle[i] = theta; - if (g.centralRadius > 0 && d < central.radiusAt(theta)) { - L.grass[i] = 1; - L.islandOf[i] = -2; - if (d < std::max(1.5, 0.22 * g.centralRadius)) - L.water[i] = 1; - continue; - } - const double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); - const int k = std::min(teams - 1, int(turn / wedge)); - const double u = turn / wedge - k; - const double s = d * (u - 0.5) * wedge; - for (const Blob &b : blobs) { - if (!b.holds(s - b.s, d - b.r)) - continue; - if (b.kind == Pool || b.kind == Lagoon) { - L.water[i] = 1; - L.grass[i] = 0; - L.islandOf[i] = -1; - } else { - L.grass[i] = 1; - L.islandOf[i] = b.kind == HomeIsland ? k : -3; - if (b.kind == Neutral) - L.prizeOf[i] = b.prize; - if (b.kind == HomeIsland && - std::hypot(s - b.s, d - b.r) < pond.radiusAt(std::atan2(d - b.r, s - b.s))) - L.water[i] = 1; - if (b.kind == Neutral && - std::hypot(s - b.s, d - b.r) < oasis.radiusAt(std::atan2(d - b.r, s - b.s))) - L.water[i] = 1; - } - } - } - return L; + L.grass.assign(n, 0); + L.water.assign(n, 0); + L.islandOf.assign(n, -1); + L.prizeOf.assign(n, -1); + L.radius.assign(n, 0.0); + L.angle.assign(n, 0.0); + L.homeX.assign(teams, 0); + L.homeY.assign(teams, 0); + L.pondX.assign(teams, 0); + L.pondY.assign(teams, 0); + for (int k = 0; k < teams; ++k) + { + const double a = L.phase + wedge * (k + 0.5); + L.homeX[k] = t.x(L.cx + int(std::lround(g.homeRing * std::cos(a)))); + L.homeY[k] = t.y(L.cy + int(std::lround(g.homeRing * std::sin(a)))); + L.pondX[k] = L.homeX[k]; + L.pondY[k] = L.homeY[k]; + } + const double pondRadius = std::clamp(0.2 * g.homeRadius, 3.0, 6.0); + const RadialShape pond(pondRadius, 0.3, context, "flats-ponds"); + const RadialShape oasis(2.5, 0.3, context, "flats-ponds"); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); + const double d = std::hypot(double(dx), double(dy)); + const double theta = std::atan2(double(dy), double(dx)); + L.radius[i] = d; + L.angle[i] = theta; + if (g.centralRadius > 0 && d < central.radiusAt(theta)) + { + L.grass[i] = 1; + L.islandOf[i] = -2; + if (d < std::max(1.5, 0.22 * g.centralRadius)) + L.water[i] = 1; + continue; + } + const double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); + const int k = std::min(teams - 1, int(turn / wedge)); + const double u = turn / wedge - k; + const double s = d * (u - 0.5) * wedge; + for (const Blob &b : blobs) + { + if (!b.holds(s - b.s, d - b.r)) + continue; + if (b.kind == Pool || b.kind == Lagoon) + { + L.water[i] = 1; + L.grass[i] = 0; + L.islandOf[i] = -1; + } + else + { + L.grass[i] = 1; + L.islandOf[i] = b.kind == HomeIsland ? k : -3; + if (b.kind == Neutral) + L.prizeOf[i] = b.prize; + if (b.kind == HomeIsland && + std::hypot(s - b.s, d - b.r) < pond.radiusAt(std::atan2(d - b.r, s - b.s))) + L.water[i] = 1; + if (b.kind == Neutral && + std::hypot(s - b.s, d - b.r) < oasis.radiusAt(std::atan2(d - b.r, s - b.s))) + L.water[i] = 1; + } + } + } + return L; } // Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), // so every land corner beside water becomes sand. -void layBeaches(std::vector &terrain, const Torus &t) { - const std::vector original(terrain); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - if (original[i] == WATER) - continue; - bool shore = false; - for (int dy = -1; dy <= 1 && !shore; ++dy) - for (int dx = -1; dx <= 1 && !shore; ++dx) - shore = original[t.at(x + dx, y + dy)] == WATER; - if (shore) - terrain[i] = SAND; - } +void layBeaches(std::vector &terrain, const Torus &t) +{ + const std::vector original(terrain); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[t.at(x + dx, y + dy)] == WATER; + if (shore) + terrain[i] = SAND; + } } template -int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) { - std::vector queued(size_t(t.w) * t.h, 0); - std::vector frontier{seed}; - queued[seed] = 1; - int placed = 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - for (size_t head = 0; head < frontier.size() && placed < count; ++head) { - const int i = frontier[head], x = i % t.w, y = i / t.w; - if (!eligible(i) || !map.isResourceAllowed(x, y, type)) - continue; - map.setResource(x, y, type, 1); - ++placed; - for (const auto &step : steps) { - const int m = t.at(x + step[0], y + step[1]); - if (!queued[m]) { - queued[m] = 1; - frontier.push_back(m); - } - } - } - return placed; +int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) +{ + std::vector queued(size_t(t.w) * t.h, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) + { + const int i = frontier[head], x = i % t.w, y = i / t.w; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) + { + const int m = t.at(x + step[0], y + step[1]); + if (!queued[m]) + { + queued[m] = 1; + frontier.push_back(m); + } + } + } + return placed; } -bool clearGround(const Map &map, int x, int y) { - return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID; +bool clearGround(const Map &map, int x, int y) +{ + return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; } // The nearest eligible tile to a point, within a box. template -int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) { - int seed = -1, nearest = INT_MAX; - for (int dy = -within; dy <= within; ++dy) - for (int dx = -within; dx <= within; ++dx) { - const int i = t.at(ax + dx, ay + dy); - if (eligible(i) && dx * dx + dy * dy < nearest) { - nearest = dx * dx + dy * dy; - seed = i; - } - } - return seed; +int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) +{ + int seed = -1, nearest = INT_MAX; + for (int dy = -within; dy <= within; ++dy) + for (int dx = -within; dx <= within; ++dx) + { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) + { + nearest = dx * dx + dy * dy; + seed = i; + } + } + return seed; } // Every home's kit, identical and unscaled: wheat and wood beside the pond on the side away from // the swarm, a quarry on the island's rim towards the centre of the map; then its own scaled // ambient farmland and outcrops. -void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const TidalFlatsOptions &o) { - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector reserved(n, 0); - for (int team = 0; team < L.g.teams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; - const Fertility::Field fertility = Fertility::forMap(map, false); - HeightMap split(t.w, t.h, context.stream("flats-home-split")); - split.makePlain(6); - const double pondRadius = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 3; - for (int team = 0; team < L.g.teams; ++team) { - const auto eligible = [&](int i) { - return L.islandOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); - }; - const double a = std::atan2(double(t.offsetY(L.cy, L.pondY[team])), double(t.offsetX(L.cx, L.pondX[team]))); - const auto at = [&](double along, double across) { - return std::make_pair(L.pondX[team] + int(std::lround(along * std::cos(a) - across * std::sin(a))), - L.pondY[team] + int(std::lround(along * std::sin(a) + across * std::cos(a)))); - }; - // Wheat and wood on the two sides of the pond, the quarry towards the map's centre. - if (const auto [x, y] = at(0, -pondRadius); true) - if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) - growPatch(map, t, seed, CORN, kHomeWheat, eligible); - if (const auto [x, y] = at(0, pondRadius); true) - if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) - growPatch(map, t, seed, WOOD, kHomeWood, eligible); - if (const auto [x, y] = at(-(pondRadius + 4), 0); true) - if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); - // Ambient farmland on the island's fertile ground, split two to one. - std::vector ground; - std::vector> farm; - for (int i = 0; i < n; ++i) - if (eligible(i)) { - ground.push_back(i); - if (const std::uint32_t f = fertility.at(i % t.w, i / t.w); f > 0) - farm.push_back({-double(f), i}); - } - std::stable_sort(farm.begin(), farm.end()); - const int area = int(ground.size()); - const int wheat = int(scaledCount(area * 4 / 100, o.wheat)), wood = int(scaledCount(area * 2 / 100, o.wood)); - const int total = std::min(int(farm.size()), wheat + wood); - std::vector chosen; - for (int k = 0; k < total; ++k) - chosen.push_back(farm[k].second); - std::stable_sort(chosen.begin(), chosen.end(), [&](int p, int q) { - return split.uiLevel(p % t.w, p / t.w, 2048) < split.uiLevel(q % t.w, q / t.w, 2048); - }); - const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); - for (int k = 0; k < total; ++k) - map.setResource(chosen[k] % t.w, chosen[k] / t.w, k < wheatShare ? CORN : WOOD, 1); - const int outcrops = int(scaledCount(1, o.stone)); - for (int k = 0; k < outcrops && !ground.empty(); ++k) - for (int attempt = 0; attempt < 100; ++attempt) { - const int at2 = ground[context.bounded("flats-home-stone", ground.size())]; - if (eligible(at2)) { - placeResourceClump(map, context, {at2 % t.w, at2 / t.w}, STONE, 1); - break; - } - } - } +void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const TidalFlatsOptions &o) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector reserved(n, 0); + for (int team = 0; team < L.g.teams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap split(t.w, t.h, context.stream("flats-home-split")); + split.makePlain(6); + const double pondRadius = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 3; + for (int team = 0; team < L.g.teams; ++team) + { + const auto eligible = [&](int i) + { return L.islandOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + const double a = std::atan2(double(t.offsetY(L.cy, L.pondY[team])), + double(t.offsetX(L.cx, L.pondX[team]))); + const auto at = [&](double along, double across) + { + return std::make_pair( + L.pondX[team] + int(std::lround(along * std::cos(a) - across * std::sin(a))), + L.pondY[team] + int(std::lround(along * std::sin(a) + across * std::cos(a)))); + }; + // Wheat and wood on the two sides of the pond, the quarry towards the map's centre. + if (const auto [x, y] = at(0, -pondRadius); true) + if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheat, eligible); + if (const auto [x, y] = at(0, pondRadius); true) + if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) + growPatch(map, t, seed, WOOD, kHomeWood, eligible); + if (const auto [x, y] = at(-(pondRadius + 4), 0); true) + if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + // Ambient farmland on the island's fertile ground, split two to one. + std::vector ground; + std::vector> farm; + for (int i = 0; i < n; ++i) + if (eligible(i)) + { + ground.push_back(i); + if (const std::uint32_t f = fertility.at(i % t.w, i / t.w); f > 0) + farm.push_back({-double(f), i}); + } + std::stable_sort(farm.begin(), farm.end()); + const int area = int(ground.size()); + const int wheat = int(scaledCount(area * 4 / 100, o.wheat)), + wood = int(scaledCount(area * 2 / 100, o.wood)); + const int total = std::min(int(farm.size()), wheat + wood); + std::vector chosen; + for (int k = 0; k < total; ++k) + chosen.push_back(farm[k].second); + std::stable_sort(chosen.begin(), chosen.end(), + [&](int p, int q) + { + return split.uiLevel(p % t.w, p / t.w, 2048) < + split.uiLevel(q % t.w, q / t.w, 2048); + }); + const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); + for (int k = 0; k < total; ++k) + map.setResource(chosen[k] % t.w, chosen[k] / t.w, k < wheatShare ? CORN : WOOD, 1); + const int outcrops = int(scaledCount(1, o.stone)); + for (int k = 0; k < outcrops && !ground.empty(); ++k) + for (int attempt = 0; attempt < 100; ++attempt) + { + const int at2 = ground[context.bounded("flats-home-stone", ground.size())]; + if (eligible(at2)) + { + placeResourceClump(map, context, {at2 % t.w, at2 / t.w}, STONE, 1); + break; + } + } + } } // Every neutral island is an oasis, its pond ringed by wheat to its shores, with one prize inside, // a fruit grove or a stone deposit in turn; the central island carries the orchard of all three // fruits round its pond, with a quarry. -void stockIslands(Map &map, const Layout &L, GenerationContext &context, const TidalFlatsOptions &o) { - const Torus &t = L.t; - const int n = t.w * t.h; - // Every neutral island is its own component of grass; each gets its prize at its middle. - std::vector seen(n, 0); - for (int start = 0; start < n; ++start) { - if (seen[start] || L.islandOf[start] != -3 || L.prizeOf[start] < 0) - continue; - std::vector tiles{start}; - seen[start] = 1; - for (size_t head = 0; head < tiles.size(); ++head) { - const int i = tiles[head]; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int m = t.at(i % t.w + dx, i / t.w + dy); - if (!seen[m] && L.islandOf[m] == -3 && L.prizeOf[m] >= 0) { - seen[m] = 1; - tiles.push_back(m); - } - } - } - double sx = 0, sy = 0; - for (int i : tiles) { - sx += t.offsetX(start, i % t.w); - sy += t.offsetY(start / t.w, i / t.w); - } - const int mx = t.x(start % t.w + int(std::lround(sx / tiles.size()))); - const int my = t.y(start / t.w + int(std::lround(sy / tiles.size()))); - const auto eligible = [&](int i) { return L.islandOf[i] == -3 && clearGround(map, i % t.w, i / t.w); }; - // The prize, on the far side of the pond from the map's centre. - const double a = std::atan2(double(t.offsetY(L.cy, my)), double(t.offsetX(L.cx, mx))); - if (const int seed = seedNear(t, mx + int(std::lround(6 * std::cos(a))), my + int(std::lround(6 * std::sin(a))), 8, eligible); - seed >= 0) { - const MapGeneratorPoint centre(seed % t.w, seed / t.w); - if (L.prizeOf[start] == 0) { - if (scaledCount(1, o.fruit) > 0) - placeResourceClump(map, context, centre, CHERRY + int(context.bounded("flats-prizes", 3)), 2); - } else if (scaledCount(1, o.stone) > 0) { - placeResourceClump(map, context, centre, STONE, 2); - } - } - // The oasis: every other tile of the island under wheat, unscaled, so it has to be cleared. - for (int i : tiles) - if (eligible(i) && map.isResourceAllowed(i % t.w, i / t.w, CORN)) - map.setResource(i % t.w, i / t.w, CORN, 1); - } - if (L.g.centralRadius > 0) { - const auto eligible = [&](int i) { return L.islandOf[i] == -2 && clearGround(map, i % t.w, i / t.w); }; - const double rho = std::max(1.5, 0.22 * L.g.centralRadius) + 4; - const double spin = context.bounded("flats-prizes", 3600) / 3600.0 * 2 * pi; - if (scaledCount(1, o.fruit) > 0) - for (int f = 0; f < 3; ++f) { - const double a = spin + 2 * pi * f / 3; - const int seed = seedNear(t, L.cx + int(std::lround(rho * std::cos(a))), - L.cy + int(std::lround(rho * std::sin(a))), 8, eligible); - if (seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); - } - if (scaledCount(1, o.stone) > 0) - if (const int seed = seedNear(t, L.cx + int(std::lround((L.g.centralRadius - 4) * std::cos(spin + pi / 3))), - L.cy + int(std::lround((L.g.centralRadius - 4) * std::sin(spin + pi / 3))), 8, eligible); - seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); - } +void stockIslands(Map &map, const Layout &L, GenerationContext &context, const TidalFlatsOptions &o) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + // Every neutral island is its own component of grass; each gets its prize at its middle. + std::vector seen(n, 0); + for (int start = 0; start < n; ++start) + { + if (seen[start] || L.islandOf[start] != -3 || L.prizeOf[start] < 0) + continue; + std::vector tiles{start}; + seen[start] = 1; + for (size_t head = 0; head < tiles.size(); ++head) + { + const int i = tiles[head]; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int m = t.at(i % t.w + dx, i / t.w + dy); + if (!seen[m] && L.islandOf[m] == -3 && L.prizeOf[m] >= 0) + { + seen[m] = 1; + tiles.push_back(m); + } + } + } + double sx = 0, sy = 0; + for (int i : tiles) + { + sx += t.offsetX(start, i % t.w); + sy += t.offsetY(start / t.w, i / t.w); + } + const int mx = t.x(start % t.w + int(std::lround(sx / tiles.size()))); + const int my = t.y(start / t.w + int(std::lround(sy / tiles.size()))); + const auto eligible = [&](int i) + { return L.islandOf[i] == -3 && clearGround(map, i % t.w, i / t.w); }; + // The prize, on the far side of the pond from the map's centre. + const double a = std::atan2(double(t.offsetY(L.cy, my)), double(t.offsetX(L.cx, mx))); + if (const int seed = seedNear(t, mx + int(std::lround(6 * std::cos(a))), + my + int(std::lround(6 * std::sin(a))), 8, eligible); + seed >= 0) + { + const MapGeneratorPoint centre(seed % t.w, seed / t.w); + if (L.prizeOf[start] == 0) + { + if (scaledCount(1, o.fruit) > 0) + placeResourceClump(map, context, centre, + CHERRY + int(context.bounded("flats-prizes", 3)), 2); + } + else if (scaledCount(1, o.stone) > 0) + { + placeResourceClump(map, context, centre, STONE, 2); + } + } + // The oasis: every other tile of the island under wheat, unscaled, so it has to be cleared. + for (int i : tiles) + if (eligible(i) && map.isResourceAllowed(i % t.w, i / t.w, CORN)) + map.setResource(i % t.w, i / t.w, CORN, 1); + } + if (L.g.centralRadius > 0) + { + const auto eligible = [&](int i) + { return L.islandOf[i] == -2 && clearGround(map, i % t.w, i / t.w); }; + const double rho = std::max(1.5, 0.22 * L.g.centralRadius) + 4; + const double spin = context.bounded("flats-prizes", 3600) / 3600.0 * 2 * pi; + if (scaledCount(1, o.fruit) > 0) + for (int f = 0; f < 3; ++f) + { + const double a = spin + 2 * pi * f / 3; + const int seed = seedNear(t, L.cx + int(std::lround(rho * std::cos(a))), + L.cy + int(std::lround(rho * std::sin(a))), 8, eligible); + if (seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); + } + if (scaledCount(1, o.stone) > 0) + if (const int seed = seedNear( + t, L.cx + int(std::lround((L.g.centralRadius - 4) * std::cos(spin + pi / 3))), + L.cy + int(std::lround((L.g.centralRadius - 4) * std::sin(spin + pi / 3))), 8, + eligible); + seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + } } // Algae in every pool and lagoon: water beside sand is exactly where it regrows. -void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) { - const int n = t.w * t.h; - std::vector water; - for (int i = 0; i < n; ++i) - if (map.isWater(i % t.w, i / t.w)) - water.emplace_back(i % t.w, i / t.w); - if (water.empty()) - return; - for (int clump = 0; clump < scaledCount(int(water.size()) / 40, algaePercent); ++clump) - placeResourceClump(map, context, water[context.bounded("flats-algae", water.size())], ALGA, 1); +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) +{ + const int n = t.w * t.h; + std::vector water; + for (int i = 0; i < n; ++i) + if (map.isWater(i % t.w, i / t.w)) + water.emplace_back(i % t.w, i / t.w); + if (water.empty()) + return; + for (int clump = 0; clump < scaledCount(int(water.size()) / 40, algaePercent); ++clump) + placeResourceClump(map, context, water[context.bounded("flats-algae", water.size())], ALGA, + 1); } -void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t) { - for (int team = 0; team < context.request.nbTeams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) { - const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); - if (map.isResource(x, y)) - map.setNoResource(x, y, 1); - } +void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t) +{ + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + { + const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); + if (map.isResource(x, y)) + map.setNoResource(x, y, 1); + } } -bool generate(Game &game, GenerationContext &context) { - context.stage = "flats layout"; - const TidalFlatsOptions o(context.request); - Map &map = game.map; - const int teams = context.request.nbTeams; - map.makeHomogenMap(SAND); - for (int i = 0; i < teams; ++i) - game.addTeam(); - const Layout L = design(context.request, context); - if (!L.failure.empty()) { - context.detail = L.failure; - return false; - } - const Torus &t = L.t; - const int n = t.w * t.h; +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "flats layout"; + const TidalFlatsOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(SAND); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.w * t.h; - context.stage = "flats terrain"; - std::vector terrain(n, SAND); - for (int i = 0; i < n; ++i) { - if (L.grass[i]) - terrain[i] = GRASS; - if (L.water[i]) - terrain[i] = WATER; - } - layBeaches(terrain, t); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); - map.rebuildTerrain(); + context.stage = "flats terrain"; + std::vector terrain(n, SAND); + for (int i = 0; i < n; ++i) + { + if (L.grass[i]) + terrain[i] = GRASS; + if (L.water[i]) + terrain[i] = WATER; + } + layBeaches(terrain, t); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); + map.rebuildTerrain(); - context.stage = "flats colonies"; - for (int team = 0; team < teams; ++team) { - std::vector home(n, 0); - for (int i = 0; i < n; ++i) - home[i] = L.islandOf[i] == team && map.isGrass(i % t.w, i / t.w); - // The swarm stands on the far side of the pond from the map's centre, a short walk from the - // fields; placeSettlement measures from the footprint's top-left tile. - const double a = std::atan2(double(t.offsetY(L.cy, L.homeY[team])), double(t.offsetX(L.cx, L.homeX[team]))); - const double back = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 6; - const MapGeneratorPoint anchor(L.pondX[team] + int(std::lround(back * std::cos(a))) - 2, - L.pondY[team] + int(std::lround(back * std::sin(a))) - 2); - if (!placeSettlement(game, context, team, home, anchor, "flats-starts")) - return false; - } + context.stage = "flats colonies"; + for (int team = 0; team < teams; ++team) + { + std::vector home(n, 0); + for (int i = 0; i < n; ++i) + home[i] = L.islandOf[i] == team && map.isGrass(i % t.w, i / t.w); + // The swarm stands on the far side of the pond from the map's centre, a short walk from the + // fields; placeSettlement measures from the footprint's top-left tile. + const double a = std::atan2(double(t.offsetY(L.cy, L.homeY[team])), + double(t.offsetX(L.cx, L.homeX[team]))); + const double back = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 6; + const MapGeneratorPoint anchor(L.pondX[team] + int(std::lround(back * std::cos(a))) - 2, + L.pondY[team] + int(std::lround(back * std::sin(a))) - 2); + if (!placeSettlement(game, context, team, home, anchor, "flats-starts")) + return false; + } - context.stage = "flats resources"; - furnishHomes(map, L, context, o); - stockIslands(map, L, context, o); - seedAlgae(map, context, t, o.algae); - clearAroundSwarms(map, context, t); - guaranteeStartingResources(game, context, 24, 32, 0); - clearAroundSwarms(map, context, t); - return true; + context.stage = "flats resources"; + furnishHomes(map, L, context, o); + stockIslands(map, L, context, o); + seedAlgae(map, context, t, o.algae); + clearAroundSwarms(map, context, t); + guaranteeStartingResources(game, context, 24, 32, 0); + clearAroundSwarms(map, context, t); + return true; } -std::string validateRequest(const GenerationRequest &r) { - if (r.nbTeams < 1) - return "Tidal flats need at least one colony."; - const Geometry g = geometryFor(r); - if (g.homeRadius < 9) - return "The home islands do not fit on this map; use a bigger map, fewer colonies or smaller islands."; - return ""; +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 1) + return "Tidal flats need at least one colony."; + const Geometry g = geometryFor(r); + if (g.homeRadius < 9) + return "The home islands do not fit on this map; use a bigger map, fewer colonies or " + "smaller islands."; + return ""; } // Checked on the finished world: every home island still carries its pond, and colony 0 can walk // to every other colony and onto the central island, with water, buildings and every resource // blocking; the flats themselves hold nothing, so that walk is always the open one. -std::string validateWorld(const Game &game, const GenerationContext &context) { - GenerationContext replay(context.request); - const Layout L = design(context.request, replay); - if (!L.failure.empty()) - return "The flats design could not be rebuilt: " + L.failure; - const Map &map = game.map; - const Torus &t = L.t; - const int n = t.w * t.h, teams = context.request.nbTeams; - if (map.getW() != t.w || map.getH() != t.h) - return "The flats design does not match the map size."; - for (int k = 0; k < teams; ++k) { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(L.pondX[k] + dx), t.y(L.pondY[k] + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s island has lost its pond."; - } - std::vector> workers(std::max(teams, 1)); - for (int i = 0; i < n; ++i) { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(i); - } - if (teams < 1 || workers[0].empty()) - return "Colony 0 has no workers to walk the flats."; - std::vector open(n, 0), source(n, 0); - for (int i = 0; i < n; ++i) { - const int x = i % t.w, y = i / t.w; - open[i] = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - } - for (int i : workers[0]) - source[i] = 1; - const std::vector steps = stepsFrom(t, source, open); - for (int team = 1; team < teams; ++team) { - bool arrived = false; - for (int i : workers[team]) - arrived = arrived || steps[i] >= 0; - if (!arrived) - return "Colony " + std::to_string(team) + " cannot walk to colony 0 over the flats."; - } - if (L.g.centralRadius > 0) { - bool arrived = false; - for (int i = 0; i < n && !arrived; ++i) - arrived = L.islandOf[i] == -2 && steps[i] >= 0; - if (!arrived) - return "The central island cannot be reached over the flats."; - } - return ""; +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (!L.failure.empty()) + return "The flats design could not be rebuilt: " + L.failure; + const Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (map.getW() != t.w || map.getH() != t.h) + return "The flats design does not match the map size."; + for (int k = 0; k < teams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(L.pondX[k] + dx), t.y(L.pondY[k] + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s island has lost its pond."; + } + std::vector> workers(std::max(teams, 1)); + for (int i = 0; i < n; ++i) + { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(i); + } + if (teams < 1 || workers[0].empty()) + return "Colony 0 has no workers to walk the flats."; + std::vector open(n, 0), source(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + open[i] = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + } + for (int i : workers[0]) + source[i] = 1; + const std::vector steps = stepsFrom(t, source, open); + for (int team = 1; team < teams; ++team) + { + bool arrived = false; + for (int i : workers[team]) + arrived = arrived || steps[i] >= 0; + if (!arrived) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 over the flats."; + } + if (L.g.centralRadius > 0) + { + bool arrived = false; + for (int i = 0; i < n && !arrived; ++i) + arrived = L.islandOf[i] == -2 && steps[i] >= 0; + if (!arrived) + return "The central island cannot be reached over the flats."; + } + return ""; } } // namespace TidalFlatsOptions::TidalFlatsOptions(const GenerationRequest &r) - : homeIslandSize(r.option("home-island-size")), extraIslands(r.option("extra-islands")), - sandbars(r.option("sandbars")), tidePools(r.option("tide-pools")), lagoons(r.option("lagoons")), - coastRoughness(r.option("coast-roughness")), centralIsland(r.option("central-island") != 0), - wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} + : homeIslandSize(r.option("home-island-size")), extraIslands(r.option("extra-islands")), + sandbars(r.option("sandbars")), tidePools(r.option("tide-pools")), + lagoons(r.option("lagoons")), coastRoughness(r.option("coast-roughness")), + centralIsland(r.option("central-island") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} -GeneratorDefinition tidalFlatsDefinition() { - return {"tidal-flats", - 18, - "Tidal flats", - 3, - false, - // The home islands' radius as a share of the half side; extra islands, sandbars and - // lagoons per colony; tide pools per 128x128 of flats. - {{"home-island-size", "Home island size", 14, 26, 1, 20, ControlGroup::Terrain}, - {"extra-islands", "Extra islands", 0, 4, 1, 3, ControlGroup::Terrain}, - {"sandbars", "Sandbars", 0, 4, 1, 3, ControlGroup::Terrain}, - {"tide-pools", "Tide pools", 0, 12, 1, 8, ControlGroup::Terrain}, - {"lagoons", "Lagoons", 0, 4, 1, 3, ControlGroup::Terrain}, - {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, - // Off, the middle of the map is flats like the rest, and there is no orchard. - GeneratorControl::toggle("central-island", "Central island", true, ControlGroup::Layout), - // Every island's ambient fields and outcrops and every prize; each home's kit is unscaled. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - validateRequest, - validateWorld}; +GeneratorDefinition tidalFlatsDefinition() +{ + return { + "tidal-flats", + 18, + "Tidal flats", + 3, + false, + // The home islands' radius as a share of the half side; extra islands, sandbars and + // lagoons per colony; tide pools per 128x128 of flats. + {{"home-island-size", "Home island size", 14, 26, 1, 20, ControlGroup::Terrain}, + {"extra-islands", "Extra islands", 0, 4, 1, 3, ControlGroup::Terrain}, + {"sandbars", "Sandbars", 0, 4, 1, 3, ControlGroup::Terrain}, + {"tide-pools", "Tide pools", 0, 12, 1, 8, ControlGroup::Terrain}, + {"lagoons", "Lagoons", 0, 4, 1, 3, ControlGroup::Terrain}, + {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, + // Off, the middle of the map is flats like the rest, and there is no orchard. + GeneratorControl::toggle("central-island", "Central island", true, ControlGroup::Layout), + // Every island's ambient fields and outcrops and every prize; each home's kit is unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; } diff --git a/src/map/generator/generators/TidalFlatsGenerator.h b/src/map/generator/generators/TidalFlatsGenerator.h index 5f4382e93..2a45499d5 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.h +++ b/src/map/generator/generators/TidalFlatsGenerator.h @@ -2,10 +2,11 @@ #pragma once #include "GenerationRequest.h" #include "GeneratorDefinition.h" -struct TidalFlatsOptions { - int homeIslandSize, extraIslands, sandbars, tidePools, lagoons, coastRoughness; - bool centralIsland; - int wheat, wood, stone, algae, fruit; // percentages of the default amounts - explicit TidalFlatsOptions(const GenerationRequest &r); +struct TidalFlatsOptions +{ + int homeIslandSize, extraIslands, sandbars, tidePools, lagoons, coastRoughness; + bool centralIsland; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit TidalFlatsOptions(const GenerationRequest &r); }; GeneratorDefinition tidalFlatsDefinition(); diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index 0f7124b0d..c56285a45 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -32,7 +32,8 @@ using namespace MapGeneration; // Terrain is stamped on the undermap with a symmetric version of Map::controlSand's shoreline // rule, then controlSand itself is run and must change nothing: the shores are already what it // would make of them, without its row-order raster pass silting narrow channels unevenly. -namespace { +namespace +{ constexpr double kPi = 3.14159265358979323846; constexpr const char *kLayoutStream = "watershed-layout"; @@ -61,1697 +62,1936 @@ constexpr int kFordSpacing[5] = {80, 60, 44, 34, 26}; constexpr int kColonyWaterNear = 5, kColonyWaterFar = 11; constexpr double kColonySpacing = 14; -double sq(double v) { return v * v; } +double sq(double v) +{ + return v * v; +} -struct Vec { - double x, y; +struct Vec +{ + double x, y; }; -Vec operator+(Vec a, Vec b) { return {a.x + b.x, a.y + b.y}; } -Vec operator-(Vec a, Vec b) { return {a.x - b.x, a.y - b.y}; } -Vec operator*(Vec a, double s) { return {a.x * s, a.y * s}; } -double dot(Vec a, Vec b) { return a.x * b.x + a.y * b.y; } -double length(Vec a) { return std::sqrt(dot(a, a)); } -Vec normalized(Vec a) { - const double l = length(a); - return l > 1e-9 ? a * (1.0 / l) : Vec{0, 1}; +Vec operator+(Vec a, Vec b) +{ + return {a.x + b.x, a.y + b.y}; +} +Vec operator-(Vec a, Vec b) +{ + return {a.x - b.x, a.y - b.y}; +} +Vec operator*(Vec a, double s) +{ + return {a.x * s, a.y * s}; +} +double dot(Vec a, Vec b) +{ + return a.x * b.x + a.y * b.y; +} +double length(Vec a) +{ + return std::sqrt(dot(a, a)); +} +Vec normalized(Vec a) +{ + const double l = length(a); + return l > 1e-9 ? a * (1.0 / l) : Vec{0, 1}; +} +double wrapped(double value, double period) +{ + return value - period * std::floor(value / period); +} +double centred(double delta, double period) +{ + return delta - period * std::round(delta / period); +} +int wrapIndex(int value, int period) +{ + return ((value % period) + period) % period; } -double wrapped(double value, double period) { return value - period * std::floor(value / period); } -double centred(double delta, double period) { return delta - period * std::round(delta / period); } -int wrapIndex(int value, int period) { return ((value % period) + period) % period; } -double unit(GenerationContext &context, const char *stream) { - return context.bounded(stream, 1u << 20) / double(1u << 20); +double unit(GenerationContext &context, const char *stream) +{ + return context.bounded(stream, 1u << 20) / double(1u << 20); } -double between(GenerationContext &context, const char *stream, double lo, double hi) { - return lo + (hi - lo) * unit(context, stream); +double between(GenerationContext &context, const char *stream, double lo, double hi) +{ + return lo + (hi - lo) * unit(context, stream); } -double channelRadius(int riverWidth) { return kMinimumChannelRadius + 0.4 * (riverWidth - 1); } +double channelRadius(int riverWidth) +{ + return kMinimumChannelRadius + 0.4 * (riverWidth - 1); +} // A coastline: a sum of sinusoids whose frequencies are whole cycles of the coast's length, so it // joins itself seamlessly across the torus seam. -struct Ripple { - double base = 0, period = 1; - std::array amplitude{}, phase{}, frequency{}; - // A smooth bulge, such as a delta building land out into the sea. - double lobeCentre = 0, lobeHeight = 0, lobeWidth = 1; - double at(double u) const { - double value = base; - for (size_t k = 0; k < amplitude.size(); ++k) - value += amplitude[k] * std::sin(2 * kPi * frequency[k] * u / period + phase[k]); - if (lobeHeight != 0) - value += lobeHeight * std::exp(-sq(centred(u - lobeCentre, period) / lobeWidth)); - return value; - } +struct Ripple +{ + double base = 0, period = 1; + std::array amplitude{}, phase{}, frequency{}; + // A smooth bulge, such as a delta building land out into the sea. + double lobeCentre = 0, lobeHeight = 0, lobeWidth = 1; + double at(double u) const + { + double value = base; + for (size_t k = 0; k < amplitude.size(); ++k) + value += amplitude[k] * std::sin(2 * kPi * frequency[k] * u / period + phase[k]); + if (lobeHeight != 0) + value += lobeHeight * std::exp(-sq(centred(u - lobeCentre, period) / lobeWidth)); + return value; + } }; -Ripple makeRipple(GenerationContext &context, double base, double amplitude, int period) { - static const double harmonics[4] = {1, 3, 5, 9}, weights[4] = {0.35, 0.3, 0.2, 0.15}; - Ripple ripple; - ripple.base = base; - ripple.period = period; - const int scale = std::max(1, period / 128); - for (size_t k = 0; k < 4; ++k) { - ripple.frequency[k] = harmonics[k] * scale; - ripple.amplitude[k] = amplitude * weights[k] * between(context, kLayoutStream, 0.5, 1.0); - ripple.phase[k] = between(context, kLayoutStream, 0, 2 * kPi); - } - return ripple; +Ripple makeRipple(GenerationContext &context, double base, double amplitude, int period) +{ + static const double harmonics[4] = {1, 3, 5, 9}, weights[4] = {0.35, 0.3, 0.2, 0.15}; + Ripple ripple; + ripple.base = base; + ripple.period = period; + const int scale = std::max(1, period / 128); + for (size_t k = 0; k < 4; ++k) + { + ripple.frequency[k] = harmonics[k] * scale; + ripple.amplitude[k] = amplitude * weights[k] * between(context, kLayoutStream, 0.5, 1.0); + ripple.phase[k] = between(context, kLayoutStream, 0, 2 * kPi); + } + return ripple; } // Smooth value noise on a lattice that tiles the map, so it wraps like the map does. -class PeriodicNoise { -public: - PeriodicNoise(int width, int height, double cell, std::mt19937 &random) - : width(width), height(height), columns(std::max(2, int(std::lround(width / cell)))), - rows(std::max(2, int(std::lround(height / cell)))), lattice(size_t(columns) * rows) { - for (double &value : lattice) - value = random() / 4294967296.0; - } - double at(double x, double y) const { - const double fx = x * columns / width, fy = y * rows / height; - const double x0 = std::floor(fx), y0 = std::floor(fy); - double tx = fx - x0, ty = fy - y0; - tx = tx * tx * (3 - 2 * tx); - ty = ty * ty * (3 - 2 * ty); - const int ix = wrapIndex(int(x0), columns), iy = wrapIndex(int(y0), rows); - const int jx = (ix + 1) % columns, jy = (iy + 1) % rows; - const double top = lattice[iy * columns + ix] * (1 - tx) + lattice[iy * columns + jx] * tx; - const double bottom = lattice[jy * columns + ix] * (1 - tx) + lattice[jy * columns + jx] * tx; - return top * (1 - ty) + bottom * ty; - } - -private: - int width, height, columns, rows; - std::vector lattice; +class PeriodicNoise +{ + public: + PeriodicNoise(int width, int height, double cell, std::mt19937 &random) + : width(width), height(height), columns(std::max(2, int(std::lround(width / cell)))), + rows(std::max(2, int(std::lround(height / cell)))), lattice(size_t(columns) * rows) + { + for (double &value : lattice) + value = random() / 4294967296.0; + } + double at(double x, double y) const + { + const double fx = x * columns / width, fy = y * rows / height; + const double x0 = std::floor(fx), y0 = std::floor(fy); + double tx = fx - x0, ty = fy - y0; + tx = tx * tx * (3 - 2 * tx); + ty = ty * ty * (3 - 2 * ty); + const int ix = wrapIndex(int(x0), columns), iy = wrapIndex(int(y0), rows); + const int jx = (ix + 1) % columns, jy = (iy + 1) % rows; + const double top = lattice[iy * columns + ix] * (1 - tx) + lattice[iy * columns + jx] * tx; + const double bottom = + lattice[jy * columns + ix] * (1 - tx) + lattice[jy * columns + jx] * tx; + return top * (1 - ty) + bottom * ty; + } + + private: + int width, height, columns, rows; + std::vector lattice; }; // The canonical frame: u runs along the coast, v across it with the sea at large v. The map is // the frame turned so the sea lies along one of its sides: orientations 0 and 1 put it south or // north, 2 and 3 east or west. -struct Frame { - int width = 0, height = 0; // the map - int along = 0, across = 0; // the frame - int orientation = 0; - Vec toMap(Vec c) const { - switch (orientation) { - case 0: - return {c.x, c.y}; - case 1: - return {c.x, across - c.y}; - case 2: - return {c.y, c.x}; - default: - return {across - c.y, c.x}; - } - } - Vec toCanonical(Vec m) const { - const double x = m.x, y = m.y; - Vec c{x, y}; - switch (orientation) { - case 1: - c = {x, across - y}; - break; - case 2: - c = {y, x}; - break; - case 3: - c = {y, across - x}; - break; - default: - break; - } - return {wrapped(c.x, along), wrapped(c.y, across)}; - } +struct Frame +{ + int width = 0, height = 0; // the map + int along = 0, across = 0; // the frame + int orientation = 0; + Vec toMap(Vec c) const + { + switch (orientation) + { + case 0: + return {c.x, c.y}; + case 1: + return {c.x, across - c.y}; + case 2: + return {c.y, c.x}; + default: + return {across - c.y, c.x}; + } + } + Vec toCanonical(Vec m) const + { + const double x = m.x, y = m.y; + Vec c{x, y}; + switch (orientation) + { + case 1: + c = {x, across - y}; + break; + case 2: + c = {y, x}; + break; + case 3: + c = {y, across - x}; + break; + default: + break; + } + return {wrapped(c.x, along), wrapped(c.y, across)}; + } }; // One stretch of river, from its upstream end (a spring, or the delta's apex for a distributary) // to where it ends: joining its parent at joinIndex, at the apex for the trunk, or in the sea. -struct Reach { - std::vector points; - std::vector radius; - // Distance across the frame to the nearer coast; small or negative where a reach meets the sea. - std::vector coastGap; - int parent = -1, joinIndex = -1; - bool distributary = false; - double length() const { return (points.size() - 1) * kSpacing; } +struct Reach +{ + std::vector points; + std::vector radius; + // Distance across the frame to the nearer coast; small or negative where a reach meets the sea. + std::vector coastGap; + int parent = -1, joinIndex = -1; + bool distributary = false; + double length() const { return (points.size() - 1) * kSpacing; } }; -struct Ford { - Vec center, along, across; - double span; +struct Ford +{ + Vec center, along, across; + double span; }; -struct Layout { - Frame frame; - Ripple north, south; - int trunk = 0; - // Everything below is in map coordinates. - std::vector reaches; - std::vector> joins; - // Lines from every spring straight upstream to the sea: they split the land into banks. - std::vector> divides; - std::vector mouths, junctions; - std::vector fords; +struct Layout +{ + Frame frame; + Ripple north, south; + int trunk = 0; + // Everything below is in map coordinates. + std::vector reaches; + std::vector> joins; + // Lines from every spring straight upstream to the sea: they split the land into banks. + std::vector> divides; + std::vector mouths, junctions; + std::vector fords; }; // River centreline points in map coordinates, bucketed so a ford can check what is near it. -class ChannelIndex { -public: - explicit ChannelIndex(const Layout &layout) - : layout(layout), w(layout.frame.width), h(layout.frame.height), - columns(std::max(1, w / 16)), rows(std::max(1, h / 16)), cells(size_t(columns) * rows) { - for (int r = 0; r < int(layout.reaches.size()); ++r) - for (int i = 0; i < int(layout.reaches[r].points.size()); ++i) - cells[cellOf(layout.reaches[r].points[i])].push_back({r, i}); - } - - // Whether p lies at least `margin` outside every channel, ignoring reach r within `skip` tiles - // of point i along it. - bool clear(Vec p, double margin, int r, int i, double skip) const { - const int cx = column(p), cy = row(p); - int xs[3], ys[3], nx = 0, ny = 0; - for (int d = -1; d <= 1; ++d) { - const int x = wrapIndex(cx + d, columns), y = wrapIndex(cy + d, rows); - if (std::find(xs, xs + nx, x) == xs + nx) - xs[nx++] = x; - if (std::find(ys, ys + ny, y) == ys + ny) - ys[ny++] = y; - } - for (int a = 0; a < ny; ++a) - for (int b = 0; b < nx; ++b) - for (const auto &entry : cells[size_t(ys[a]) * columns + xs[b]]) { - if (entry.first == r && std::abs(entry.second - i) * kSpacing <= skip) - continue; - const Reach &reach = layout.reaches[entry.first]; - double radius = reach.radius[entry.second]; - if (entry.second == 0 && !reach.distributary) - radius += 1.3; // the spring's pond - const Vec q = reach.points[entry.second]; - if (sq(centred(p.x - q.x, w)) + sq(centred(p.y - q.y, h)) < sq(radius + margin)) - return false; - } - return true; - } - -private: - int column(Vec p) const { return std::min(columns - 1, int(wrapped(p.x, w) / 16)); } - int row(Vec p) const { return std::min(rows - 1, int(wrapped(p.y, h) / 16)); } - size_t cellOf(Vec p) const { return size_t(row(p)) * columns + column(p); } - const Layout &layout; - int w, h, columns, rows; - std::vector>> cells; +class ChannelIndex +{ + public: + explicit ChannelIndex(const Layout &layout) + : layout(layout), w(layout.frame.width), h(layout.frame.height), + columns(std::max(1, w / 16)), rows(std::max(1, h / 16)), cells(size_t(columns) * rows) + { + for (int r = 0; r < int(layout.reaches.size()); ++r) + for (int i = 0; i < int(layout.reaches[r].points.size()); ++i) + cells[cellOf(layout.reaches[r].points[i])].push_back({r, i}); + } + + // Whether p lies at least `margin` outside every channel, ignoring reach r within `skip` tiles + // of point i along it. + bool clear(Vec p, double margin, int r, int i, double skip) const + { + const int cx = column(p), cy = row(p); + int xs[3], ys[3], nx = 0, ny = 0; + for (int d = -1; d <= 1; ++d) + { + const int x = wrapIndex(cx + d, columns), y = wrapIndex(cy + d, rows); + if (std::find(xs, xs + nx, x) == xs + nx) + xs[nx++] = x; + if (std::find(ys, ys + ny, y) == ys + ny) + ys[ny++] = y; + } + for (int a = 0; a < ny; ++a) + for (int b = 0; b < nx; ++b) + for (const auto &entry : cells[size_t(ys[a]) * columns + xs[b]]) + { + if (entry.first == r && std::abs(entry.second - i) * kSpacing <= skip) + continue; + const Reach &reach = layout.reaches[entry.first]; + double radius = reach.radius[entry.second]; + if (entry.second == 0 && !reach.distributary) + radius += 1.3; // the spring's pond + const Vec q = reach.points[entry.second]; + if (sq(centred(p.x - q.x, w)) + sq(centred(p.y - q.y, h)) < sq(radius + margin)) + return false; + } + return true; + } + + private: + int column(Vec p) const { return std::min(columns - 1, int(wrapped(p.x, w) / 16)); } + int row(Vec p) const { return std::min(rows - 1, int(wrapped(p.y, h) / 16)); } + size_t cellOf(Vec p) const { return size_t(row(p)) * columns + column(p); } + const Layout &layout; + int w, h, columns, rows; + std::vector>> cells; }; -std::vector resample(const std::vector &line) { - std::vector out{line.front()}; - double carried = 0; - for (size_t i = 1; i < line.size(); ++i) { - Vec a = line[i - 1]; - const Vec b = line[i]; - double segment = length(b - a); - while (segment > 0 && carried + segment >= kSpacing) { - a = a + (b - a) * ((kSpacing - carried) / segment); - out.push_back(a); - segment = length(b - a); - carried = 0; - } - carried += segment; - } - if (out.size() > 1 && length(line.back() - out.back()) < kSpacing / 2) - out.back() = line.back(); - else - out.push_back(line.back()); - return out; +std::vector resample(const std::vector &line) +{ + std::vector out{line.front()}; + double carried = 0; + for (size_t i = 1; i < line.size(); ++i) + { + Vec a = line[i - 1]; + const Vec b = line[i]; + double segment = length(b - a); + while (segment > 0 && carried + segment >= kSpacing) + { + a = a + (b - a) * ((kSpacing - carried) / segment); + out.push_back(a); + segment = length(b - a); + carried = 0; + } + carried += segment; + } + if (out.size() > 1 && length(line.back() - out.back()) < kSpacing / 2) + out.back() = line.back(); + else + out.push_back(line.back()); + return out; } // Grows the river network in the canonical frame. Springs join the nearest point downstream of // them on the network so far, nearest first, which grows a branching tree the way a spanning tree // does. A path that would run out into the sea is refused; one that runs into another river joins // that river instead. -class RiverPlanner { -public: - struct Attachment { - double distance2; - int reach, index; - }; - - RiverPlanner(const Frame &frame, const Ripple &north, const Ripple &south, double clearance, - double northMargin, double southMargin) - : along(frame.along), across(frame.across), north(north), south(south), - clearance(clearance), northMargin(northMargin), southMargin(southMargin) { - columns = std::max(1, along / 24); - rows = std::max(1, across / 24); - cellW = double(along) / columns; - cellH = double(across) / rows; - cells.resize(size_t(columns) * rows); - } - - std::vector reaches; - // Per reach: where each tributary joins it, and from which side. - std::vector>> joins; - int trunk = -1; - // 0 plans every river without its meander. - double meanderScale = 1.0; - - int sideOf(int r, int i, Vec source) const { - const Vec t = tangent(r, i), p = reaches[r].points[i]; - const Vec d{centred(source.x - p.x, along), source.y - p.y}; - return t.x * d.y - t.y * d.x >= 0 ? 1 : -1; - } - - double distance2(Vec a, Vec b) const { return sq(centred(a.x - b.x, along)) + sq(a.y - b.y); } - - int add(Reach reach) { - reaches.push_back(std::move(reach)); - joins.emplace_back(); - const int id = int(reaches.size()) - 1; - const auto &points = reaches[id].points; - for (size_t i = 0; i < points.size(); i += 2) - cells[cellOf(points[i])].push_back({id, int(i)}); - return id; - } - - // Squared distance to the nearest indexed river point within about one cell, or the largest - // double when there is none that close; `only` limits the search to one reach. - double nearest2(Vec p, int ignore = -1, int only = -1, int *nearestReach = nullptr) const { - const int cx = column(p), cy = row(p); - int xs[3], count = 0; - for (int dx = -1; dx <= 1; ++dx) { - const int x = wrapIndex(cx + dx, columns); - if (std::find(xs, xs + count, x) == xs + count) - xs[count++] = x; - } - double best = std::numeric_limits::max(); - for (int dy = -1; dy <= 1; ++dy) { - const int y = cy + dy; - if (y < 0 || y >= rows) - continue; - for (int k = 0; k < count; ++k) - for (const auto &entry : cells[size_t(y) * columns + xs[k]]) - if (entry.first != ignore && (only < 0 || entry.first == only)) { - const double d = distance2(p, reaches[entry.first].points[entry.second]); - if (d < best) { - best = d; - if (nearestReach) - *nearestReach = entry.first; - } - } - } - return best; - } - - // The point of reach r nearest to p where a spring at `source` may join it, if one is close. - int nearestAttachable(int r, Vec p, Vec source) const { - int best = -1; - double bestDistance = sq(2 * clearance); - for (int i = 0; i < int(reaches[r].points.size()); i += 2) { - if (!attachable(r, i, source)) - continue; - const Vec q = reaches[r].points[i]; - const double du = centred(q.x - source.x, along), dv = q.y - source.y; - if (dv < std::max(4.0, kSlope * std::abs(du))) - continue; - const double d = distance2(p, q); - if (d < bestDistance) { - bestDistance = d; - best = i; - } - } - return best; - } - - bool inland(Vec p, double northGap, double southGap) const { - return p.y >= north.at(p.x) + northGap && p.y <= south.at(p.x) - southGap; - } - - Vec tangent(int r, int i) const { - const auto &points = reaches[r].points; - const int a = std::max(0, i - 2), b = std::min(int(points.size()) - 1, i + 2); - return normalized(Vec{centred(points[b].x - points[a].x, along), points[b].y - points[a].y}); - } - - // A cubic curve that leaves `from` towards `to` and arrives leaning into the direction the - // river it joins is flowing, with a meander that fades to nothing at both ends. - std::vector path(Vec from, Vec to, Vec endDirection, double meander, - GenerationContext &context) const { - const Vec delta{centred(to.x - from.x, along), to.y - from.y}; - const Vec end = from + delta; - const double span = std::max(1.0, length(delta)); - const Vec blend = normalized(endDirection * 0.4 + normalized(delta) * 0.6); - const Vec c1 = from + delta * 0.33, c2 = end - blend * (0.33 * span); - const int steps = std::max(16, int(span * 3)); - std::vector raw; - raw.reserve(steps + 1); - for (int k = 0; k <= steps; ++k) { - const double t = double(k) / steps, s = 1 - t; - raw.push_back(from * (s * s * s) + c1 * (3 * s * s * t) + c2 * (3 * s * t * t) + - end * (t * t * t)); - } - const std::vector base = resample(raw); - const double amplitude = std::min(6.5, 0.1 * span) * meander * meanderScale; - const double waves = std::max(1.0, span / 34.0); - const double phase1 = between(context, kRiverStream, 0, 2 * kPi); - const double phase2 = between(context, kRiverStream, 0, 2 * kPi); - const double last = double(base.size() - 1); - std::vector bent(base.size()); - for (size_t i = 0; i < base.size(); ++i) { - const double t = last > 0 ? i / last : 0; - const Vec direction = normalized(base[std::min(base.size() - 1, i + 1)] - base[i ? i - 1 : 0]); - const double offset = amplitude * std::sin(kPi * t) * - (0.75 * std::sin(2 * kPi * waves * t + phase1) + - 0.25 * std::sin(2 * kPi * 2.3 * waves * t + phase2)); - bent[i] = base[i] + Vec{-direction.y, direction.x} * offset; - } - return resample(bent); - } - - // Where along the network a tributary may join: not on the delta, not at a spring or a - // junction, and not beside another tributary's mouth. - bool attachable(int r, int i, Vec source) const { - const Reach &reach = reaches[r]; - if (reach.distributary) - return false; - const double s = i * kSpacing; - if (s < 8 || reach.length() - s < 8) - return false; - // Junctions on the same bank well apart leave straight stretches between them to ford, and - // send later springs to join tributaries instead of all lining up along the trunk; one from - // the opposite bank only needs room for its own mouth. - const int side = sideOf(r, i, source); - for (const auto &join : joins[r]) - if (std::abs(i - join.first) * kSpacing < (join.second == side ? 20 : 8)) - return false; - return true; - } - - std::vector attachments(Vec source, int firstReach = 0) const { - std::vector out; - for (int r = firstReach; r < int(reaches.size()); ++r) - for (int i = 0; i < int(reaches[r].points.size()); i += 2) { - if (!attachable(r, i, source)) - continue; - const Vec p = reaches[r].points[i]; - const double du = centred(p.x - source.x, along), dv = p.y - source.y; - if (dv < std::max(4.0, kSlope * std::abs(du))) - continue; - out.push_back({du * du + dv * dv, r, i}); - } - std::sort(out.begin(), out.end(), [](const Attachment &a, const Attachment &b) { - if (a.distance2 != b.distance2) - return a.distance2 < b.distance2; - return a.reach != b.reach ? a.reach < b.reach : a.index < b.index; - }); - return out; - } - - struct Verdict { - bool ok = false; - int blocking = -1; // the other river the path ran into, when that is why it was refused - Vec where{0, 0}; - }; - - Verdict review(const std::vector &points, int parent) const { - Verdict verdict; - const size_t n = points.size(); - if ((n - 1) * kSpacing < 12) - return verdict; - const double joinZone = clearance + 4; - for (size_t i = 0; i < n; i += 2) { - const double toEnd = (n - 1 - i) * kSpacing; - const Vec p = points[i]; - if (toEnd > 4 && !inland(p, northMargin, southMargin)) - return verdict; - if (toEnd <= 6) - continue; - // Clear of every other river; and while it swings in to join its parent, arriving at an - // angle rather than running alongside it. - // Not near: it is a legacy macro in Windows' windef.h and expands to nothing there. - int nearestRiver = -1; - if (nearest2(p, parent, -1, &nearestRiver) < - sq(toEnd > joinZone ? clearance : 0.75 * clearance)) { - verdict.blocking = nearestRiver; - verdict.where = p; - return verdict; - } - if (nearest2(p, -1, parent) < sq(std::min(clearance, 0.35 * toEnd))) - return verdict; - } - verdict.ok = true; - return verdict; - } - - void attachSources(const std::vector &sources, GenerationContext &context) { - const double none = std::numeric_limits::max(); - std::vector cached(sources.size(), none); - std::vector alive(sources.size(), 1); - for (size_t k = 0; k < sources.size(); ++k) { - const auto list = attachments(sources[k]); - if (!list.empty()) - cached[k] = list.front().distance2; - } - while (true) { - int pick = -1; - for (size_t k = 0; k < sources.size(); ++k) - if (alive[k] && cached[k] < none && (pick < 0 || cached[k] < cached[pick])) - pick = int(k); - if (pick < 0) - return; - const auto list = attachments(sources[pick]); - if (list.empty()) { - alive[pick] = 0; - continue; - } - // A cached distance can only be optimistic (junctions since added rule points out), so a - // spring whose real best is further than cached waits its proper turn. - if (list.front().distance2 > cached[pick] + 1e-9) { - cached[pick] = list.front().distance2; - continue; - } - alive[pick] = 0; - std::vector tried; - int added = -1; - for (const Attachment &a : list) { - if (tried.size() >= 12 || added >= 0) - break; - const Vec p = reaches[a.reach].points[a.index]; - bool repeat = false; - for (Vec t : tried) - repeat = repeat || distance2(t, p) < 36; - if (repeat) - continue; - tried.push_back(p); - // A full meander first, then a gentler one. A river joins whatever river it runs into: - // a path refused for passing too near another river is tried again as its tributary. - int target = a.reach, index = a.index; - for (int hop = 0; hop < 3 && added < 0; ++hop) { - Verdict last; - for (double meander : {1.0, 0.35}) { - Reach reach; - reach.points = path(sources[pick], reaches[target].points[index], - tangent(target, index), meander, context); - last = review(reach.points, target); - if (!last.ok) - continue; - reach.parent = target; - reach.joinIndex = index; - const int side = sideOf(target, index, sources[pick]); - added = add(std::move(reach)); - joins[target].push_back({index, side}); - break; - } - if (added >= 0 || last.blocking < 0) - break; - // Nothing joins the delta itself: a path that runs into it joins the trunk above it. - const int blocking = reaches[last.blocking].distributary ? trunk : last.blocking; - const int next = blocking >= 0 ? nearestAttachable(blocking, last.where, sources[pick]) : -1; - if (next < 0) - break; - target = blocking; - index = next; - } - } - if (added < 0) - continue; - for (size_t k = 0; k < sources.size(); ++k) { - if (!alive[k]) - continue; - if (nearest2(sources[k]) < sq(clearance)) { - alive[k] = 0; // it would only add a stub beside the river that just passed it - continue; - } - const auto fresh = attachments(sources[k], added); - if (!fresh.empty()) - cached[k] = std::min(cached[k], fresh.front().distance2); - } - } - } - -private: - int column(Vec p) const { return std::min(columns - 1, int(wrapped(p.x, along) / cellW)); } - int row(Vec p) const { return std::clamp(int(std::floor(p.y / cellH)), 0, rows - 1); } - size_t cellOf(Vec p) const { return size_t(row(p)) * columns + column(p); } - - int along, across; - const Ripple &north, &south; - double clearance, northMargin, southMargin; - int columns, rows; - double cellW, cellH; - std::vector>> cells; +class RiverPlanner +{ + public: + struct Attachment + { + double distance2; + int reach, index; + }; + + RiverPlanner(const Frame &frame, const Ripple &north, const Ripple &south, double clearance, + double northMargin, double southMargin) + : along(frame.along), across(frame.across), north(north), south(south), + clearance(clearance), northMargin(northMargin), southMargin(southMargin) + { + columns = std::max(1, along / 24); + rows = std::max(1, across / 24); + cellW = double(along) / columns; + cellH = double(across) / rows; + cells.resize(size_t(columns) * rows); + } + + std::vector reaches; + // Per reach: where each tributary joins it, and from which side. + std::vector>> joins; + int trunk = -1; + // 0 plans every river without its meander. + double meanderScale = 1.0; + + int sideOf(int r, int i, Vec source) const + { + const Vec t = tangent(r, i), p = reaches[r].points[i]; + const Vec d{centred(source.x - p.x, along), source.y - p.y}; + return t.x * d.y - t.y * d.x >= 0 ? 1 : -1; + } + + double distance2(Vec a, Vec b) const { return sq(centred(a.x - b.x, along)) + sq(a.y - b.y); } + + int add(Reach reach) + { + reaches.push_back(std::move(reach)); + joins.emplace_back(); + const int id = int(reaches.size()) - 1; + const auto &points = reaches[id].points; + for (size_t i = 0; i < points.size(); i += 2) + cells[cellOf(points[i])].push_back({id, int(i)}); + return id; + } + + // Squared distance to the nearest indexed river point within about one cell, or the largest + // double when there is none that close; `only` limits the search to one reach. + double nearest2(Vec p, int ignore = -1, int only = -1, int *nearestReach = nullptr) const + { + const int cx = column(p), cy = row(p); + int xs[3], count = 0; + for (int dx = -1; dx <= 1; ++dx) + { + const int x = wrapIndex(cx + dx, columns); + if (std::find(xs, xs + count, x) == xs + count) + xs[count++] = x; + } + double best = std::numeric_limits::max(); + for (int dy = -1; dy <= 1; ++dy) + { + const int y = cy + dy; + if (y < 0 || y >= rows) + continue; + for (int k = 0; k < count; ++k) + for (const auto &entry : cells[size_t(y) * columns + xs[k]]) + if (entry.first != ignore && (only < 0 || entry.first == only)) + { + const double d = distance2(p, reaches[entry.first].points[entry.second]); + if (d < best) + { + best = d; + if (nearestReach) + *nearestReach = entry.first; + } + } + } + return best; + } + + // The point of reach r nearest to p where a spring at `source` may join it, if one is close. + int nearestAttachable(int r, Vec p, Vec source) const + { + int best = -1; + double bestDistance = sq(2 * clearance); + for (int i = 0; i < int(reaches[r].points.size()); i += 2) + { + if (!attachable(r, i, source)) + continue; + const Vec q = reaches[r].points[i]; + const double du = centred(q.x - source.x, along), dv = q.y - source.y; + if (dv < std::max(4.0, kSlope * std::abs(du))) + continue; + const double d = distance2(p, q); + if (d < bestDistance) + { + bestDistance = d; + best = i; + } + } + return best; + } + + bool inland(Vec p, double northGap, double southGap) const + { + return p.y >= north.at(p.x) + northGap && p.y <= south.at(p.x) - southGap; + } + + Vec tangent(int r, int i) const + { + const auto &points = reaches[r].points; + const int a = std::max(0, i - 2), b = std::min(int(points.size()) - 1, i + 2); + return normalized( + Vec{centred(points[b].x - points[a].x, along), points[b].y - points[a].y}); + } + + // A cubic curve that leaves `from` towards `to` and arrives leaning into the direction the + // river it joins is flowing, with a meander that fades to nothing at both ends. + std::vector path(Vec from, Vec to, Vec endDirection, double meander, + GenerationContext &context) const + { + const Vec delta{centred(to.x - from.x, along), to.y - from.y}; + const Vec end = from + delta; + const double span = std::max(1.0, length(delta)); + const Vec blend = normalized(endDirection * 0.4 + normalized(delta) * 0.6); + const Vec c1 = from + delta * 0.33, c2 = end - blend * (0.33 * span); + const int steps = std::max(16, int(span * 3)); + std::vector raw; + raw.reserve(steps + 1); + for (int k = 0; k <= steps; ++k) + { + const double t = double(k) / steps, s = 1 - t; + raw.push_back(from * (s * s * s) + c1 * (3 * s * s * t) + c2 * (3 * s * t * t) + + end * (t * t * t)); + } + const std::vector base = resample(raw); + const double amplitude = std::min(6.5, 0.1 * span) * meander * meanderScale; + const double waves = std::max(1.0, span / 34.0); + const double phase1 = between(context, kRiverStream, 0, 2 * kPi); + const double phase2 = between(context, kRiverStream, 0, 2 * kPi); + const double last = double(base.size() - 1); + std::vector bent(base.size()); + for (size_t i = 0; i < base.size(); ++i) + { + const double t = last > 0 ? i / last : 0; + const Vec direction = + normalized(base[std::min(base.size() - 1, i + 1)] - base[i ? i - 1 : 0]); + const double offset = amplitude * std::sin(kPi * t) * + (0.75 * std::sin(2 * kPi * waves * t + phase1) + + 0.25 * std::sin(2 * kPi * 2.3 * waves * t + phase2)); + bent[i] = base[i] + Vec{-direction.y, direction.x} * offset; + } + return resample(bent); + } + + // Where along the network a tributary may join: not on the delta, not at a spring or a + // junction, and not beside another tributary's mouth. + bool attachable(int r, int i, Vec source) const + { + const Reach &reach = reaches[r]; + if (reach.distributary) + return false; + const double s = i * kSpacing; + if (s < 8 || reach.length() - s < 8) + return false; + // Junctions on the same bank well apart leave straight stretches between them to ford, and + // send later springs to join tributaries instead of all lining up along the trunk; one from + // the opposite bank only needs room for its own mouth. + const int side = sideOf(r, i, source); + for (const auto &join : joins[r]) + if (std::abs(i - join.first) * kSpacing < (join.second == side ? 20 : 8)) + return false; + return true; + } + + std::vector attachments(Vec source, int firstReach = 0) const + { + std::vector out; + for (int r = firstReach; r < int(reaches.size()); ++r) + for (int i = 0; i < int(reaches[r].points.size()); i += 2) + { + if (!attachable(r, i, source)) + continue; + const Vec p = reaches[r].points[i]; + const double du = centred(p.x - source.x, along), dv = p.y - source.y; + if (dv < std::max(4.0, kSlope * std::abs(du))) + continue; + out.push_back({du * du + dv * dv, r, i}); + } + std::sort(out.begin(), out.end(), + [](const Attachment &a, const Attachment &b) + { + if (a.distance2 != b.distance2) + return a.distance2 < b.distance2; + return a.reach != b.reach ? a.reach < b.reach : a.index < b.index; + }); + return out; + } + + struct Verdict + { + bool ok = false; + int blocking = -1; // the other river the path ran into, when that is why it was refused + Vec where{0, 0}; + }; + + Verdict review(const std::vector &points, int parent) const + { + Verdict verdict; + const size_t n = points.size(); + if ((n - 1) * kSpacing < 12) + return verdict; + const double joinZone = clearance + 4; + for (size_t i = 0; i < n; i += 2) + { + const double toEnd = (n - 1 - i) * kSpacing; + const Vec p = points[i]; + if (toEnd > 4 && !inland(p, northMargin, southMargin)) + return verdict; + if (toEnd <= 6) + continue; + // Clear of every other river; and while it swings in to join its parent, arriving at an + // angle rather than running alongside it. + // Not near: it is a legacy macro in Windows' windef.h and expands to nothing there. + int nearestRiver = -1; + if (nearest2(p, parent, -1, &nearestRiver) < + sq(toEnd > joinZone ? clearance : 0.75 * clearance)) + { + verdict.blocking = nearestRiver; + verdict.where = p; + return verdict; + } + if (nearest2(p, -1, parent) < sq(std::min(clearance, 0.35 * toEnd))) + return verdict; + } + verdict.ok = true; + return verdict; + } + + void attachSources(const std::vector &sources, GenerationContext &context) + { + const double none = std::numeric_limits::max(); + std::vector cached(sources.size(), none); + std::vector alive(sources.size(), 1); + for (size_t k = 0; k < sources.size(); ++k) + { + const auto list = attachments(sources[k]); + if (!list.empty()) + cached[k] = list.front().distance2; + } + while (true) + { + int pick = -1; + for (size_t k = 0; k < sources.size(); ++k) + if (alive[k] && cached[k] < none && (pick < 0 || cached[k] < cached[pick])) + pick = int(k); + if (pick < 0) + return; + const auto list = attachments(sources[pick]); + if (list.empty()) + { + alive[pick] = 0; + continue; + } + // A cached distance can only be optimistic (junctions since added rule points out), so a + // spring whose real best is further than cached waits its proper turn. + if (list.front().distance2 > cached[pick] + 1e-9) + { + cached[pick] = list.front().distance2; + continue; + } + alive[pick] = 0; + std::vector tried; + int added = -1; + for (const Attachment &a : list) + { + if (tried.size() >= 12 || added >= 0) + break; + const Vec p = reaches[a.reach].points[a.index]; + bool repeat = false; + for (Vec t : tried) + repeat = repeat || distance2(t, p) < 36; + if (repeat) + continue; + tried.push_back(p); + // A full meander first, then a gentler one. A river joins whatever river it runs into: + // a path refused for passing too near another river is tried again as its tributary. + int target = a.reach, index = a.index; + for (int hop = 0; hop < 3 && added < 0; ++hop) + { + Verdict last; + for (double meander : {1.0, 0.35}) + { + Reach reach; + reach.points = path(sources[pick], reaches[target].points[index], + tangent(target, index), meander, context); + last = review(reach.points, target); + if (!last.ok) + continue; + reach.parent = target; + reach.joinIndex = index; + const int side = sideOf(target, index, sources[pick]); + added = add(std::move(reach)); + joins[target].push_back({index, side}); + break; + } + if (added >= 0 || last.blocking < 0) + break; + // Nothing joins the delta itself: a path that runs into it joins the trunk above it. + const int blocking = + reaches[last.blocking].distributary ? trunk : last.blocking; + const int next = + blocking >= 0 ? nearestAttachable(blocking, last.where, sources[pick]) : -1; + if (next < 0) + break; + target = blocking; + index = next; + } + } + if (added < 0) + continue; + for (size_t k = 0; k < sources.size(); ++k) + { + if (!alive[k]) + continue; + if (nearest2(sources[k]) < sq(clearance)) + { + alive[k] = 0; // it would only add a stub beside the river that just passed it + continue; + } + const auto fresh = attachments(sources[k], added); + if (!fresh.empty()) + cached[k] = std::min(cached[k], fresh.front().distance2); + } + } + } + + private: + int column(Vec p) const { return std::min(columns - 1, int(wrapped(p.x, along) / cellW)); } + int row(Vec p) const { return std::clamp(int(std::floor(p.y / cellH)), 0, rows - 1); } + size_t cellOf(Vec p) const { return size_t(row(p)) * columns + column(p); } + + int along, across; + const Ripple &north, &south; + double clearance, northMargin, southMargin; + int columns, rows; + double cellW, cellH; + std::vector>> cells; }; // Fords go where a river can be crossed cleanly: the middle of every straight, uncluttered run of // river, longest runs first, about one per `spacing` tiles of each reach. -void chooseFords(Layout &layout, const WatershedOptions &o) { - const double spacing = kFordSpacing[std::clamp(o.fords, 1, 5) - 1]; - const Frame &frame = layout.frame; - auto distance2 = [&](Vec a, Vec b) { - return sq(centred(a.x - b.x, frame.width)) + sq(centred(a.y - b.y, frame.height)); - }; - const int count = int(layout.reaches.size()); - const ChannelIndex channels(layout); - std::vector order; - for (int r = layout.trunk; r < count; ++r) - order.push_back(r); - for (int r = 0; r < layout.trunk; ++r) - order.push_back(r); - for (int r : order) { - const Reach &reach = layout.reaches[r]; - const int n = int(reach.points.size()); - const double len = reach.length(); - if (len < std::max(20.0, spacing / 2)) - continue; - auto valid = [&](int i) { - if (i < 16 || i >= n - 16) - return false; - const double radius = reach.radius[i], span = radius + kFordReach; - if (reach.coastGap[i] < span + 6) - return false; - for (int j : layout.joins[r]) - if (std::abs(i - j) * kSpacing < radius + 8) - return false; - // Only on a straight stretch, eight tiles either way, where the channel runs on in line - // beyond both sides of the ford and is no wider across than its radius. - const Vec before = reach.points[i] - reach.points[i - 16]; - const Vec after = reach.points[i + 16] - reach.points[i]; - if (dot(normalized(before), normalized(after)) < std::cos(25 * kPi / 180)) - return false; - // The crossing, and dry ground a little past both of its ends, touch no other channel: - // a ford crosses one river, not a junction, a spring's pond or a neighbouring bend. - const Vec along = normalized(reach.points[i + 2] - reach.points[i - 2]); - const Vec across{-along.y, along.x}; - for (double s = -(span + 1); s <= span + 1 + 1e-9; s += 1.0) - for (double a : {-kFordHalfWidth, 0.0, kFordHalfWidth}) - if (!channels.clear(reach.points[i] + across * s + along * a, - std::abs(s) <= span ? 1.5 : 0.5, r, i, span + 6)) - return false; - return true; - }; - std::vector> runs; // first and last fordable point, a tile apart - for (int i = 12; i < n - 12; i += 2) { - if (!valid(i)) - continue; - if (!runs.empty() && runs.back().second == i - 2) - runs.back().second = i; - else - runs.push_back({i, i}); - } - struct Spot { - int index; - int run; - }; - std::vector spots; - for (const auto &run : runs) { - const int steps = (run.second - run.first) / 2; - const int extra = int(steps / spacing); - for (int k = 0; k <= extra; ++k) - spots.push_back({run.first + 2 * int(steps * (k + 0.5) / (extra + 1)), steps}); - } - std::stable_sort(spots.begin(), spots.end(), - [](const Spot &a, const Spot &b) { return a.run > b.run; }); - const int limit = std::max(1, int(std::lround(len / spacing))); - std::vector chosen; - for (const Spot &spot : spots) { - if (int(chosen.size()) >= limit) - break; - const double span = reach.radius[spot.index] + kFordReach; - bool apart = true; - for (int c : chosen) - apart = apart && std::abs(c - spot.index) * kSpacing >= 0.75 * spacing; - for (const Ford &f : layout.fords) - apart = apart && distance2(reach.points[spot.index], f.center) >= sq(f.span + span + 10); - if (!apart) - continue; - chosen.push_back(spot.index); - const Vec along = normalized(reach.points[spot.index + 2] - reach.points[spot.index - 2]); - layout.fords.push_back({reach.points[spot.index], along, Vec{-along.y, along.x}, span}); - } - } +void chooseFords(Layout &layout, const WatershedOptions &o) +{ + const double spacing = kFordSpacing[std::clamp(o.fords, 1, 5) - 1]; + const Frame &frame = layout.frame; + auto distance2 = [&](Vec a, Vec b) + { return sq(centred(a.x - b.x, frame.width)) + sq(centred(a.y - b.y, frame.height)); }; + const int count = int(layout.reaches.size()); + const ChannelIndex channels(layout); + std::vector order; + for (int r = layout.trunk; r < count; ++r) + order.push_back(r); + for (int r = 0; r < layout.trunk; ++r) + order.push_back(r); + for (int r : order) + { + const Reach &reach = layout.reaches[r]; + const int n = int(reach.points.size()); + const double len = reach.length(); + if (len < std::max(20.0, spacing / 2)) + continue; + auto valid = [&](int i) + { + if (i < 16 || i >= n - 16) + return false; + const double radius = reach.radius[i], span = radius + kFordReach; + if (reach.coastGap[i] < span + 6) + return false; + for (int j : layout.joins[r]) + if (std::abs(i - j) * kSpacing < radius + 8) + return false; + // Only on a straight stretch, eight tiles either way, where the channel runs on in line + // beyond both sides of the ford and is no wider across than its radius. + const Vec before = reach.points[i] - reach.points[i - 16]; + const Vec after = reach.points[i + 16] - reach.points[i]; + if (dot(normalized(before), normalized(after)) < std::cos(25 * kPi / 180)) + return false; + // The crossing, and dry ground a little past both of its ends, touch no other channel: + // a ford crosses one river, not a junction, a spring's pond or a neighbouring bend. + const Vec along = normalized(reach.points[i + 2] - reach.points[i - 2]); + const Vec across{-along.y, along.x}; + for (double s = -(span + 1); s <= span + 1 + 1e-9; s += 1.0) + for (double a : {-kFordHalfWidth, 0.0, kFordHalfWidth}) + if (!channels.clear(reach.points[i] + across * s + along * a, + std::abs(s) <= span ? 1.5 : 0.5, r, i, span + 6)) + return false; + return true; + }; + std::vector> runs; // first and last fordable point, a tile apart + for (int i = 12; i < n - 12; i += 2) + { + if (!valid(i)) + continue; + if (!runs.empty() && runs.back().second == i - 2) + runs.back().second = i; + else + runs.push_back({i, i}); + } + struct Spot + { + int index; + int run; + }; + std::vector spots; + for (const auto &run : runs) + { + const int steps = (run.second - run.first) / 2; + const int extra = int(steps / spacing); + for (int k = 0; k <= extra; ++k) + spots.push_back({run.first + 2 * int(steps * (k + 0.5) / (extra + 1)), steps}); + } + std::stable_sort(spots.begin(), spots.end(), + [](const Spot &a, const Spot &b) { return a.run > b.run; }); + const int limit = std::max(1, int(std::lround(len / spacing))); + std::vector chosen; + for (const Spot &spot : spots) + { + if (int(chosen.size()) >= limit) + break; + const double span = reach.radius[spot.index] + kFordReach; + bool apart = true; + for (int c : chosen) + apart = apart && std::abs(c - spot.index) * kSpacing >= 0.75 * spacing; + for (const Ford &f : layout.fords) + apart = apart && + distance2(reach.points[spot.index], f.center) >= sq(f.span + span + 10); + if (!apart) + continue; + chosen.push_back(spot.index); + const Vec along = + normalized(reach.points[spot.index + 2] - reach.points[spot.index - 2]); + layout.fords.push_back({reach.points[spot.index], along, Vec{-along.y, along.x}, span}); + } + } } // The whole plan: a pure function of the request's size, options and seed. -Layout planLayout(int width, int height, const WatershedOptions &o, GenerationContext &context) { - Layout layout; - Frame &frame = layout.frame; - frame.width = width; - frame.height = height; - // Any of the four sides on a square map. On a rectangular one the rivers run the long way, with - // the sea across one of its short ends: one network cannot gather a coast twice as long as the - // land is deep, and would leave most of the map a desert. - frame.orientation = int(context.bounded(kLayoutStream, width == height ? 4 : 2)); - if (width > height) - frame.orientation += 2; - frame.along = frame.orientation < 2 ? width : height; - frame.across = frame.orientation < 2 ? height : width; - const double along = frame.along, across = frame.across; - - // One band of sea along the map's edge: deep off the delta, and a narrow strait along the - // uplands on the far side of the torus seam. - const double sea = std::clamp(std::round(8 + 0.14 * across), 16.0, 64.0); - layout.north = makeRipple(context, 0.35 * sea, 0.08 * sea + 1.0, frame.along); - layout.south = makeRipple(context, across - 0.65 * sea, 0.2 * sea, frame.along); - - const double minimumRadius = channelRadius(o.riverWidth); - const double gain = 0.35 + 0.1 * o.riverWidth; - const double estimate = minimumRadius + 0.5; - const double clearance = 2 * estimate + 5; - const double northMargin = estimate + 8, southMargin = estimate + 6; - RiverPlanner planner(frame, layout.north, layout.south, clearance, northMargin, southMargin); - planner.meanderScale = o.meanders ? 1.0 : 0.0; - - // The delta: one to three distributaries fanning out from an apex to the coast. Without a - // delta there is no lobe and only the middle mouth is kept; the others are still planned, so - // every later draw is the one it would have been. - const double deltaLength = std::clamp(0.12 * across + 6, 12.0, 44.0); - const double spread = std::clamp(0.16 * along, 18.0, 44.0); - const int mouthLimit = std::max(1, int(0.7 * along / spread) + 1); - const int mouthCount = std::min(mouthLimit, 1 + int(context.bounded(kLayoutStream, 3))); - const double u0 = between(context, kLayoutStream, 0, along); - layout.south.lobeCentre = u0; - layout.south.lobeHeight = o.delta ? 0.3 * sea : 0.0; - layout.south.lobeWidth = 1.1 * spread + 6; - const Vec apex{u0, layout.south.at(u0) - deltaLength}; - static const double shares[3][3] = {{1, 0, 0}, {0.5, 0.5, 0}, {0.32, 0.36, 0.32}}; - std::vector mouths; - for (int m = 0; m < mouthCount; ++m) { - const double u = u0 + (m - (mouthCount - 1) / 2.0) * spread * - between(context, kLayoutStream, 0.85, 1.15); - Reach reach; - reach.points = - planner.path(apex, {u, layout.south.at(u) + std::min(8.0, 0.25 * sea)}, {0, 1}, 0.5, context); - reach.distributary = true; - if (!o.delta && m != mouthCount / 2) - continue; - mouths.push_back({u, layout.south.at(u)}); - planner.add(std::move(reach)); - } - - // The trunk rises well up in the uplands; every other spring joins the network it starts. - const double headU = u0 + between(context, kLayoutStream, -0.15, 0.15) * along; - const double top = layout.north.at(headU) + northMargin; - const Vec head{headU, top + std::max(0.0, apex.y - top) * between(context, kLayoutStream, 0.1, 0.3)}; - { - Reach reach; - reach.points = planner.path(head, apex, {0, 1}, 1.0, context); - layout.trunk = planner.add(std::move(reach)); - planner.trunk = layout.trunk; - } - const int trunk = layout.trunk; - - const double landArea = along * (across - sea); - const int wanted = int(std::lround(landArea * o.riverDensity * 1.5e-4)); - const double sourceSpacing = 0.7 * std::sqrt(landArea / (wanted + 1.0)); - // A spring that finds no way onto the network is dropped, so springs are sampled again wherever - // there is still room until enough have joined it. - for (int round = 0; round < 6; ++round) { - const int missing = wanted - (int(planner.reaches.size()) - trunk - 1); - if (missing <= 0) - break; - std::vector sources; - for (int attempt = 0; attempt < 60 * missing && int(sources.size()) < missing; ++attempt) { - const double u = between(context, kRiverStream, 0, along); - // Upstream of the delta's apex: nearer the sea than that, there is no river to run down to. - const double lo = layout.north.at(u) + northMargin; - const double hi = std::min(layout.south.at(u) - (deltaLength + southMargin), apex.y - 6); - const double v = between(context, kRiverStream, 0, 1); - if (hi <= lo) - continue; - const Vec s{u, lo + (hi - lo) * v}; - bool clear = planner.nearest2(s) >= sq(clearance); - for (size_t k = 0; clear && k < sources.size(); ++k) - clear = planner.distance2(s, sources[k]) >= sq(sourceSpacing); - for (int r = trunk + 1; clear && r < int(planner.reaches.size()); ++r) - clear = planner.distance2(s, planner.reaches[r].points.front()) >= sq(sourceSpacing); - if (clear) - sources.push_back(s); - } - planner.attachSources(sources, context); - } - - // Width grows with the number of springs upstream. - const int count = int(planner.reaches.size()); - std::vector outflow(count, 1.0); - std::vector> children(count); - for (int r = count - 1; r > trunk; --r) { - outflow[planner.reaches[r].parent] += outflow[r]; - children[planner.reaches[r].parent].push_back(r); - } - auto radiusFor = [&](double flow) { - return std::min(minimumRadius + 3.5, minimumRadius + gain * (std::sqrt(flow) - 1)); - }; - for (int r = trunk; r < count; ++r) { - Reach &reach = planner.reaches[r]; - std::vector> inflows; - for (int c : children[r]) - inflows.push_back({planner.reaches[c].joinIndex, outflow[c]}); - std::sort(inflows.begin(), inflows.end()); - double flow = 1; - size_t next = 0; - reach.radius.resize(reach.points.size()); - for (size_t i = 0; i < reach.points.size(); ++i) { - while (next < inflows.size() && inflows[next].first <= int(i)) - flow += inflows[next++].second; - reach.radius[i] = radiusFor(flow); - } - } - const int distributaries = int(mouths.size()); - for (int m = 0; m < distributaries; ++m) { - Reach &reach = planner.reaches[m]; - const double radius = radiusFor(std::max(1.0, outflow[trunk] * shares[distributaries - 1][m])); - const double last = double(reach.points.size() - 1); - reach.radius.resize(reach.points.size()); - for (size_t i = 0; i < reach.points.size(); ++i) - reach.radius[i] = radius + 0.8 * (i / last); - } - - // Into map coordinates. - for (Reach &reach : planner.reaches) { - reach.coastGap.resize(reach.points.size()); - for (size_t i = 0; i < reach.points.size(); ++i) { - const Vec c = reach.points[i]; - reach.coastGap[i] = std::min(c.y - layout.north.at(c.x), layout.south.at(c.x) - c.y); - } - } - for (int r = trunk + 1; r < count; ++r) { - const Reach &reach = planner.reaches[r]; - layout.junctions.push_back(frame.toMap(planner.reaches[reach.parent].points[reach.joinIndex])); - } - layout.junctions.push_back(frame.toMap(apex)); - for (int r = trunk; r < count; ++r) { - std::vector line; - Vec p = planner.reaches[r].points.front(); - for (int k = 0; k < frame.across * 2; ++k) { - line.push_back(frame.toMap(p)); - if (p.y < layout.north.at(p.x) - 1) - break; - p.y -= kSpacing; - } - layout.divides.push_back(std::move(line)); - } - for (Vec m : mouths) - layout.mouths.push_back(frame.toMap(m)); - for (Reach &reach : planner.reaches) - for (Vec &p : reach.points) - p = frame.toMap(p); - layout.reaches = std::move(planner.reaches); - layout.joins.resize(planner.joins.size()); - for (size_t r = 0; r < planner.joins.size(); ++r) - for (const auto &join : planner.joins[r]) - layout.joins[r].push_back(join.first); - chooseFords(layout, o); - return layout; +Layout planLayout(int width, int height, const WatershedOptions &o, GenerationContext &context) +{ + Layout layout; + Frame &frame = layout.frame; + frame.width = width; + frame.height = height; + // Any of the four sides on a square map. On a rectangular one the rivers run the long way, with + // the sea across one of its short ends: one network cannot gather a coast twice as long as the + // land is deep, and would leave most of the map a desert. + frame.orientation = int(context.bounded(kLayoutStream, width == height ? 4 : 2)); + if (width > height) + frame.orientation += 2; + frame.along = frame.orientation < 2 ? width : height; + frame.across = frame.orientation < 2 ? height : width; + const double along = frame.along, across = frame.across; + + // One band of sea along the map's edge: deep off the delta, and a narrow strait along the + // uplands on the far side of the torus seam. + const double sea = std::clamp(std::round(8 + 0.14 * across), 16.0, 64.0); + layout.north = makeRipple(context, 0.35 * sea, 0.08 * sea + 1.0, frame.along); + layout.south = makeRipple(context, across - 0.65 * sea, 0.2 * sea, frame.along); + + const double minimumRadius = channelRadius(o.riverWidth); + const double gain = 0.35 + 0.1 * o.riverWidth; + const double estimate = minimumRadius + 0.5; + const double clearance = 2 * estimate + 5; + const double northMargin = estimate + 8, southMargin = estimate + 6; + RiverPlanner planner(frame, layout.north, layout.south, clearance, northMargin, southMargin); + planner.meanderScale = o.meanders ? 1.0 : 0.0; + + // The delta: one to three distributaries fanning out from an apex to the coast. Without a + // delta there is no lobe and only the middle mouth is kept; the others are still planned, so + // every later draw is the one it would have been. + const double deltaLength = std::clamp(0.12 * across + 6, 12.0, 44.0); + const double spread = std::clamp(0.16 * along, 18.0, 44.0); + const int mouthLimit = std::max(1, int(0.7 * along / spread) + 1); + const int mouthCount = std::min(mouthLimit, 1 + int(context.bounded(kLayoutStream, 3))); + const double u0 = between(context, kLayoutStream, 0, along); + layout.south.lobeCentre = u0; + layout.south.lobeHeight = o.delta ? 0.3 * sea : 0.0; + layout.south.lobeWidth = 1.1 * spread + 6; + const Vec apex{u0, layout.south.at(u0) - deltaLength}; + static const double shares[3][3] = {{1, 0, 0}, {0.5, 0.5, 0}, {0.32, 0.36, 0.32}}; + std::vector mouths; + for (int m = 0; m < mouthCount; ++m) + { + const double u = u0 + (m - (mouthCount - 1) / 2.0) * spread * + between(context, kLayoutStream, 0.85, 1.15); + Reach reach; + reach.points = planner.path(apex, {u, layout.south.at(u) + std::min(8.0, 0.25 * sea)}, + {0, 1}, 0.5, context); + reach.distributary = true; + if (!o.delta && m != mouthCount / 2) + continue; + mouths.push_back({u, layout.south.at(u)}); + planner.add(std::move(reach)); + } + + // The trunk rises well up in the uplands; every other spring joins the network it starts. + const double headU = u0 + between(context, kLayoutStream, -0.15, 0.15) * along; + const double top = layout.north.at(headU) + northMargin; + const Vec head{headU, + top + std::max(0.0, apex.y - top) * between(context, kLayoutStream, 0.1, 0.3)}; + { + Reach reach; + reach.points = planner.path(head, apex, {0, 1}, 1.0, context); + layout.trunk = planner.add(std::move(reach)); + planner.trunk = layout.trunk; + } + const int trunk = layout.trunk; + + const double landArea = along * (across - sea); + const int wanted = int(std::lround(landArea * o.riverDensity * 1.5e-4)); + const double sourceSpacing = 0.7 * std::sqrt(landArea / (wanted + 1.0)); + // A spring that finds no way onto the network is dropped, so springs are sampled again wherever + // there is still room until enough have joined it. + for (int round = 0; round < 6; ++round) + { + const int missing = wanted - (int(planner.reaches.size()) - trunk - 1); + if (missing <= 0) + break; + std::vector sources; + for (int attempt = 0; attempt < 60 * missing && int(sources.size()) < missing; ++attempt) + { + const double u = between(context, kRiverStream, 0, along); + // Upstream of the delta's apex: nearer the sea than that, there is no river to run down to. + const double lo = layout.north.at(u) + northMargin; + const double hi = + std::min(layout.south.at(u) - (deltaLength + southMargin), apex.y - 6); + const double v = between(context, kRiverStream, 0, 1); + if (hi <= lo) + continue; + const Vec s{u, lo + (hi - lo) * v}; + bool clear = planner.nearest2(s) >= sq(clearance); + for (size_t k = 0; clear && k < sources.size(); ++k) + clear = planner.distance2(s, sources[k]) >= sq(sourceSpacing); + for (int r = trunk + 1; clear && r < int(planner.reaches.size()); ++r) + clear = + planner.distance2(s, planner.reaches[r].points.front()) >= sq(sourceSpacing); + if (clear) + sources.push_back(s); + } + planner.attachSources(sources, context); + } + + // Width grows with the number of springs upstream. + const int count = int(planner.reaches.size()); + std::vector outflow(count, 1.0); + std::vector> children(count); + for (int r = count - 1; r > trunk; --r) + { + outflow[planner.reaches[r].parent] += outflow[r]; + children[planner.reaches[r].parent].push_back(r); + } + auto radiusFor = [&](double flow) + { return std::min(minimumRadius + 3.5, minimumRadius + gain * (std::sqrt(flow) - 1)); }; + for (int r = trunk; r < count; ++r) + { + Reach &reach = planner.reaches[r]; + std::vector> inflows; + for (int c : children[r]) + inflows.push_back({planner.reaches[c].joinIndex, outflow[c]}); + std::sort(inflows.begin(), inflows.end()); + double flow = 1; + size_t next = 0; + reach.radius.resize(reach.points.size()); + for (size_t i = 0; i < reach.points.size(); ++i) + { + while (next < inflows.size() && inflows[next].first <= int(i)) + flow += inflows[next++].second; + reach.radius[i] = radiusFor(flow); + } + } + const int distributaries = int(mouths.size()); + for (int m = 0; m < distributaries; ++m) + { + Reach &reach = planner.reaches[m]; + const double radius = + radiusFor(std::max(1.0, outflow[trunk] * shares[distributaries - 1][m])); + const double last = double(reach.points.size() - 1); + reach.radius.resize(reach.points.size()); + for (size_t i = 0; i < reach.points.size(); ++i) + reach.radius[i] = radius + 0.8 * (i / last); + } + + // Into map coordinates. + for (Reach &reach : planner.reaches) + { + reach.coastGap.resize(reach.points.size()); + for (size_t i = 0; i < reach.points.size(); ++i) + { + const Vec c = reach.points[i]; + reach.coastGap[i] = std::min(c.y - layout.north.at(c.x), layout.south.at(c.x) - c.y); + } + } + for (int r = trunk + 1; r < count; ++r) + { + const Reach &reach = planner.reaches[r]; + layout.junctions.push_back( + frame.toMap(planner.reaches[reach.parent].points[reach.joinIndex])); + } + layout.junctions.push_back(frame.toMap(apex)); + for (int r = trunk; r < count; ++r) + { + std::vector line; + Vec p = planner.reaches[r].points.front(); + for (int k = 0; k < frame.across * 2; ++k) + { + line.push_back(frame.toMap(p)); + if (p.y < layout.north.at(p.x) - 1) + break; + p.y -= kSpacing; + } + layout.divides.push_back(std::move(line)); + } + for (Vec m : mouths) + layout.mouths.push_back(frame.toMap(m)); + for (Reach &reach : planner.reaches) + for (Vec &p : reach.points) + p = frame.toMap(p); + layout.reaches = std::move(planner.reaches); + layout.joins.resize(planner.joins.size()); + for (size_t r = 0; r < planner.joins.size(); ++r) + for (const auto &join : planner.joins[r]) + layout.joins[r].push_back(join.first); + chooseFords(layout, o); + return layout; } // Distance in fifths of a tile (5 across, 7 diagonally) from every vertex to the nearest water. -std::vector waterDistance(const std::vector &terrain, int w, int h) { - const int unreached = std::numeric_limits::max(); - std::vector distance(terrain.size(), unreached); - std::array, 8> buckets; - size_t pending = 0; - for (size_t i = 0; i < terrain.size(); ++i) - if (terrain[i] == WATER) { - distance[i] = 0; - buckets[0].push_back(int(i)); - ++pending; - } - for (int current = 0; pending > 0; ++current) { - std::vector items; - items.swap(buckets[current % 8]); - pending -= items.size(); - for (int p : items) { - if (distance[p] != current) - continue; - const int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (!dx && !dy) - continue; - const int q = wrapIndex(y + dy, h) * w + wrapIndex(x + dx, w); - const int next = current + (dx && dy ? 7 : 5); - if (next < distance[q]) { - distance[q] = next; - buckets[next % 8].push_back(q); - ++pending; - } - } - } - } - return distance; +std::vector waterDistance(const std::vector &terrain, int w, int h) +{ + const int unreached = std::numeric_limits::max(); + std::vector distance(terrain.size(), unreached); + std::array, 8> buckets; + size_t pending = 0; + for (size_t i = 0; i < terrain.size(); ++i) + if (terrain[i] == WATER) + { + distance[i] = 0; + buckets[0].push_back(int(i)); + ++pending; + } + for (int current = 0; pending > 0; ++current) + { + std::vector items; + items.swap(buckets[current % 8]); + pending -= items.size(); + for (int p : items) + { + if (distance[p] != current) + continue; + const int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int q = wrapIndex(y + dy, h) * w + wrapIndex(x + dx, w); + const int next = current + (dx && dy ? 7 : 5); + if (next < distance[q]) + { + distance[q] = next; + buckets[next % 8].push_back(q); + ++pending; + } + } + } + } + return distance; } -void stampFord(std::vector &terrain, int w, int h, const Ford &f) { - const int reach = int(std::ceil(f.span + kFordHalfWidth)) + 1; - const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); - for (int dy = -reach; dy <= reach; ++dy) - for (int dx = -reach; dx <= reach; ++dx) { - const Vec d{cx + dx - f.center.x, cy + dy - f.center.y}; - if (std::abs(dot(d, f.along)) > kFordHalfWidth || std::abs(dot(d, f.across)) > f.span) - continue; - unsigned char &t = terrain[size_t(wrapIndex(cy + dy, h)) * w + wrapIndex(cx + dx, w)]; - if (t == WATER) - t = SAND; - } +void stampFord(std::vector &terrain, int w, int h, const Ford &f) +{ + const int reach = int(std::ceil(f.span + kFordHalfWidth)) + 1; + const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + { + const Vec d{cx + dx - f.center.x, cy + dy - f.center.y}; + if (std::abs(dot(d, f.along)) > kFordHalfWidth || std::abs(dot(d, f.across)) > f.span) + continue; + unsigned char &t = terrain[size_t(wrapIndex(cy + dy, h)) * w + wrapIndex(cx + dx, w)]; + if (t == WATER) + t = SAND; + } } std::vector stampTerrain(const Layout &layout, const WatershedOptions &o, - GenerationContext &context) { - const Frame &frame = layout.frame; - const int w = frame.width, h = frame.height; - std::vector terrain(size_t(w) * h, GRASS); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const Vec c = frame.toCanonical({double(x), double(y)}); - if (c.y < layout.north.at(c.x) || c.y > layout.south.at(c.x)) - terrain[size_t(y) * w + x] = WATER; - } - auto disc = [&](Vec centre, double radius) { - const int x0 = int(std::floor(centre.x - radius)), x1 = int(std::ceil(centre.x + radius)); - const int y0 = int(std::floor(centre.y - radius)), y1 = int(std::ceil(centre.y + radius)); - for (int y = y0; y <= y1; ++y) - for (int x = x0; x <= x1; ++x) - if (sq(x - centre.x) + sq(y - centre.y) <= radius * radius) - terrain[size_t(wrapIndex(y, h)) * w + wrapIndex(x, w)] = WATER; - }; - for (const Reach &reach : layout.reaches) { - for (size_t i = 0; i < reach.points.size(); ++i) - disc(reach.points[i], reach.radius[i]); - if (!reach.distributary) - disc(reach.points.front(), reach.radius.front() + 1.3); // the spring's pond - } - - // Uplands far from any water dry out to sand. The floor keeps a floodplain of grass wide enough - // to farm and build on beside every river, whatever the setting. - if (o.dryness > 0) { - const std::vector distance = waterDistance(terrain, w, h); - std::mt19937 &random = context.stream("watershed-terrain"); - const PeriodicNoise coarse(w, h, 28, random), fine(w, h, 9, random); - const double reach = 24.0 - 1.3 * o.dryness; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - if (terrain[i] != GRASS) - continue; - const double n = 0.7 * coarse.at(x, y) + 0.3 * fine.at(x, y); - if (distance[i] > 5 * std::max(11.0, reach + 12.0 * (n - 0.5))) - terrain[i] = SAND; - } - } - - // Map::controlSand's rule, applied to every vertex at once instead of in raster order: water - // touching grass and grass touching water both become sand, so every shore gets the same sand - // on both sides and the result is already a fixed point of controlSand. - const std::vector before = terrain; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - if (before[i] == SAND) - continue; - const unsigned char other = before[i] == WATER ? GRASS : WATER; - for (int dy = -1; dy <= 1 && terrain[i] != SAND; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (before[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] == other) { - terrain[i] = SAND; - break; - } - } - for (const Ford &f : layout.fords) - stampFord(terrain, w, h, f); - return terrain; + GenerationContext &context) +{ + const Frame &frame = layout.frame; + const int w = frame.width, h = frame.height; + std::vector terrain(size_t(w) * h, GRASS); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Vec c = frame.toCanonical({double(x), double(y)}); + if (c.y < layout.north.at(c.x) || c.y > layout.south.at(c.x)) + terrain[size_t(y) * w + x] = WATER; + } + auto disc = [&](Vec centre, double radius) + { + const int x0 = int(std::floor(centre.x - radius)), x1 = int(std::ceil(centre.x + radius)); + const int y0 = int(std::floor(centre.y - radius)), y1 = int(std::ceil(centre.y + radius)); + for (int y = y0; y <= y1; ++y) + for (int x = x0; x <= x1; ++x) + if (sq(x - centre.x) + sq(y - centre.y) <= radius * radius) + terrain[size_t(wrapIndex(y, h)) * w + wrapIndex(x, w)] = WATER; + }; + for (const Reach &reach : layout.reaches) + { + for (size_t i = 0; i < reach.points.size(); ++i) + disc(reach.points[i], reach.radius[i]); + if (!reach.distributary) + disc(reach.points.front(), reach.radius.front() + 1.3); // the spring's pond + } + + // Uplands far from any water dry out to sand. The floor keeps a floodplain of grass wide enough + // to farm and build on beside every river, whatever the setting. + if (o.dryness > 0) + { + const std::vector distance = waterDistance(terrain, w, h); + std::mt19937 &random = context.stream("watershed-terrain"); + const PeriodicNoise coarse(w, h, 28, random), fine(w, h, 9, random); + const double reach = 24.0 - 1.3 * o.dryness; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + if (terrain[i] != GRASS) + continue; + const double n = 0.7 * coarse.at(x, y) + 0.3 * fine.at(x, y); + if (distance[i] > 5 * std::max(11.0, reach + 12.0 * (n - 0.5))) + terrain[i] = SAND; + } + } + + // Map::controlSand's rule, applied to every vertex at once instead of in raster order: water + // touching grass and grass touching water both become sand, so every shore gets the same sand + // on both sides and the result is already a fixed point of controlSand. + const std::vector before = terrain; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + if (before[i] == SAND) + continue; + const unsigned char other = before[i] == WATER ? GRASS : WATER; + for (int dy = -1; dy <= 1 && terrain[i] != SAND; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (before[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] == other) + { + terrain[i] = SAND; + break; + } + } + for (const Ford &f : layout.fords) + stampFord(terrain, w, h, f); + return terrain; } -bool vertexTileWater(const std::vector &t, int w, int h, int x, int y) { - const int x1 = (x + 1) % w, y1 = (y + 1) % h; - return t[size_t(y) * w + x] == WATER && t[size_t(y) * w + x1] == WATER && - t[size_t(y1) * w + x] == WATER && t[size_t(y1) * w + x1] == WATER; +bool vertexTileWater(const std::vector &t, int w, int h, int x, int y) +{ + const int x1 = (x + 1) % w, y1 = (y + 1) % h; + return t[size_t(y) * w + x] == WATER && t[size_t(y) * w + x1] == WATER && + t[size_t(y1) * w + x] == WATER && t[size_t(y1) * w + x1] == WATER; } // Every ford interrupts a real channel - open water runs on in line just upstream and downstream // of it - is open from bank to bank along three parallel lines, and reaches dry land at both // ends. `water(x, y)` answers for a tile. -template std::string checkFords(const Layout &layout, Water water) { - const int w = layout.frame.width, h = layout.frame.height; - auto wet = [&](Vec p) { - return water(wrapIndex(int(std::floor(p.x)), w), wrapIndex(int(std::floor(p.y)), h)); - }; - auto where = [&](const Ford &f) { - return " at (" + std::to_string(wrapIndex(int(f.center.x), w)) + ", " + - std::to_string(wrapIndex(int(f.center.y), h)) + ")"; - }; - // Open water within a tile of a point: a thin or diagonal channel's water core need not cover - // any one exact tile once its shores are sanded. - auto wetNear = [&](Vec p) { - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (wet(p + Vec{double(dx), double(dy)})) - return true; - return false; - }; - for (const Ford &f : layout.fords) { - for (int side : {-1, 1}) - if (!wetNear(f.center + f.along * (side * (kFordHalfWidth + 2.5)))) - return "A ford" + where(f) + " does not cross a channel."; - for (int a = -1; a <= 1; ++a) - for (double s = -f.span; s <= f.span + 1e-9; s += kSpacing) - if (wet(f.center + f.along * a + f.across * s)) - return "A ford" + where(f) + " is cut by open water."; - for (int side : {-1, 1}) - if (wet(f.center + f.across * (side * (f.span + 1.0)))) - return "A ford" + where(f) + " does not reach its bank."; - } - return ""; +template std::string checkFords(const Layout &layout, Water water) +{ + const int w = layout.frame.width, h = layout.frame.height; + auto wet = [&](Vec p) + { return water(wrapIndex(int(std::floor(p.x)), w), wrapIndex(int(std::floor(p.y)), h)); }; + auto where = [&](const Ford &f) + { + return " at (" + std::to_string(wrapIndex(int(f.center.x), w)) + ", " + + std::to_string(wrapIndex(int(f.center.y), h)) + ")"; + }; + // Open water within a tile of a point: a thin or diagonal channel's water core need not cover + // any one exact tile once its shores are sanded. + auto wetNear = [&](Vec p) + { + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (wet(p + Vec{double(dx), double(dy)})) + return true; + return false; + }; + for (const Ford &f : layout.fords) + { + for (int side : {-1, 1}) + if (!wetNear(f.center + f.along * (side * (kFordHalfWidth + 2.5)))) + return "A ford" + where(f) + " does not cross a channel."; + for (int a = -1; a <= 1; ++a) + for (double s = -f.span; s <= f.span + 1e-9; s += kSpacing) + if (wet(f.center + f.along * a + f.across * s)) + return "A ford" + where(f) + " is cut by open water."; + for (int side : {-1, 1}) + if (wet(f.center + f.across * (side * (f.span + 1.0)))) + return "A ford" + where(f) + " does not reach its bank."; + } + return ""; } // Every channel keeps a 4-connected core of open water from its spring to where it ends, except // where a ford crosses it - so the fords are the only way over on foot. -template std::string checkChannels(const Layout &layout, Water water) { - const int w = layout.frame.width, h = layout.frame.height; - for (const Reach &reach : layout.reaches) { - bool previous = false; - int px = 0, py = 0; - for (size_t i = 0; i < reach.points.size(); ++i) { - const Vec p = reach.points[i]; - bool skip = reach.coastGap[i] < 2.0; - for (size_t k = 0; !skip && k < layout.fords.size(); ++k) { - const Ford &f = layout.fords[k]; - const Vec d{centred(p.x - f.center.x, w), centred(p.y - f.center.y, h)}; - skip = std::abs(dot(d, f.along)) <= kFordHalfWidth + 2 && - std::abs(dot(d, f.across)) <= f.span + 1; - } - if (skip) { - previous = false; - continue; - } - const int x = wrapIndex(int(std::floor(p.x)), w), y = wrapIndex(int(std::floor(p.y)), h); - if (!water(x, y)) - return "A river channel silts up at (" + std::to_string(x) + ", " + std::to_string(y) + ")."; - if (previous && x != px && y != py && !water(x, py) && !water(px, y)) - return "A river channel can be stepped across at (" + std::to_string(x) + ", " + - std::to_string(y) + ")."; - previous = true; - px = x; - py = y; - } - } - return ""; +template std::string checkChannels(const Layout &layout, Water water) +{ + const int w = layout.frame.width, h = layout.frame.height; + for (const Reach &reach : layout.reaches) + { + bool previous = false; + int px = 0, py = 0; + for (size_t i = 0; i < reach.points.size(); ++i) + { + const Vec p = reach.points[i]; + bool skip = reach.coastGap[i] < 2.0; + for (size_t k = 0; !skip && k < layout.fords.size(); ++k) + { + const Ford &f = layout.fords[k]; + const Vec d{centred(p.x - f.center.x, w), centred(p.y - f.center.y, h)}; + skip = std::abs(dot(d, f.along)) <= kFordHalfWidth + 2 && + std::abs(dot(d, f.across)) <= f.span + 1; + } + if (skip) + { + previous = false; + continue; + } + const int x = wrapIndex(int(std::floor(p.x)), w), + y = wrapIndex(int(std::floor(p.y)), h); + if (!water(x, y)) + return "A river channel silts up at (" + std::to_string(x) + ", " + + std::to_string(y) + ")."; + if (previous && x != px && y != py && !water(x, py) && !water(px, y)) + return "A river channel can be stepped across at (" + std::to_string(x) + ", " + + std::to_string(y) + ")."; + previous = true; + px = x; + py = y; + } + } + return ""; } // Labels 8-connected components of open tiles; closed tiles get -1. -std::vector components(const std::vector &open, int w, int h, int &count) { - std::vector label(open.size(), -1); - std::vector queue; - queue.reserve(open.size()); - count = 0; - for (size_t start = 0; start < open.size(); ++start) { - if (!open[start] || label[start] >= 0) - continue; - queue.clear(); - queue.push_back(int(start)); - label[start] = count; - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); - if (open[q] && label[q] < 0) { - label[q] = count; - queue.push_back(int(q)); - } - } - } - ++count; - } - return label; +std::vector components(const std::vector &open, int w, int h, int &count) +{ + std::vector label(open.size(), -1); + std::vector queue; + queue.reserve(open.size()); + count = 0; + for (size_t start = 0; start < open.size(); ++start) + { + if (!open[start] || label[start] >= 0) + continue; + queue.clear(); + queue.push_back(int(start)); + label[start] = count; + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); + if (open[q] && label[q] < 0) + { + label[q] = count; + queue.push_back(int(q)); + } + } + } + ++count; + } + return label; } // The finished terrain seen tile by tile. -struct Tiles { - int w = 0, h = 0; - std::vector water, grass; - std::vector waterSteps; // 8-neighbour steps to the nearest water tile - std::vector grassDepth; // 8-neighbour steps to the nearest tile that isn't pure grass - std::vector component; // walkable component, -1 on water - int mainComponent = -1; +struct Tiles +{ + int w = 0, h = 0; + std::vector water, grass; + std::vector waterSteps; // 8-neighbour steps to the nearest water tile + std::vector grassDepth; // 8-neighbour steps to the nearest tile that isn't pure grass + std::vector component; // walkable component, -1 on water + int mainComponent = -1; }; // Steps from every tile to the nearest tile where `seed` holds. -std::vector stepsFrom(const std::vector &seed, int w, int h) { - std::vector steps(seed.size(), -1), queue; - queue.reserve(seed.size()); - for (size_t i = 0; i < seed.size(); ++i) - if (seed[i]) { - steps[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); - if (steps[q] < 0) { - steps[q] = steps[queue[head]] + 1; - queue.push_back(int(q)); - } - } - } - return steps; +std::vector stepsFrom(const std::vector &seed, int w, int h) +{ + std::vector steps(seed.size(), -1), queue; + queue.reserve(seed.size()); + for (size_t i = 0; i < seed.size(); ++i) + if (seed[i]) + { + steps[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); + if (steps[q] < 0) + { + steps[q] = steps[queue[head]] + 1; + queue.push_back(int(q)); + } + } + } + return steps; } -Tiles tileView(const std::vector &terrain, int w, int h) { - Tiles t; - t.w = w; - t.h = h; - const size_t n = size_t(w) * h; - t.water.assign(n, 0); - t.grass.assign(n, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - const int x1 = (x + 1) % w, y1 = (y + 1) % h; - const unsigned char a = terrain[i], b = terrain[size_t(y) * w + x1]; - const unsigned char c = terrain[size_t(y1) * w + x], d = terrain[size_t(y1) * w + x1]; - t.water[i] = a == WATER && b == WATER && c == WATER && d == WATER; - t.grass[i] = a == GRASS && b == GRASS && c == GRASS && d == GRASS; - } - t.waterSteps = stepsFrom(t.water, w, h); - std::vector land(n), notGrass(n); - for (size_t i = 0; i < n; ++i) { - land[i] = !t.water[i]; - notGrass[i] = !t.grass[i]; - } - t.grassDepth = stepsFrom(notGrass, w, h); - int count = 0; - t.component = components(land, w, h, count); - std::vector sizes(count, 0); - for (int c : t.component) - if (c >= 0) - ++sizes[c]; - if (count) - t.mainComponent = int(std::max_element(sizes.begin(), sizes.end()) - sizes.begin()); - return t; +Tiles tileView(const std::vector &terrain, int w, int h) +{ + Tiles t; + t.w = w; + t.h = h; + const size_t n = size_t(w) * h; + t.water.assign(n, 0); + t.grass.assign(n, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + const int x1 = (x + 1) % w, y1 = (y + 1) % h; + const unsigned char a = terrain[i], b = terrain[size_t(y) * w + x1]; + const unsigned char c = terrain[size_t(y1) * w + x], d = terrain[size_t(y1) * w + x1]; + t.water[i] = a == WATER && b == WATER && c == WATER && d == WATER; + t.grass[i] = a == GRASS && b == GRASS && c == GRASS && d == GRASS; + } + t.waterSteps = stepsFrom(t.water, w, h); + std::vector land(n), notGrass(n); + for (size_t i = 0; i < n; ++i) + { + land[i] = !t.water[i]; + notGrass[i] = !t.grass[i]; + } + t.grassDepth = stepsFrom(notGrass, w, h); + int count = 0; + t.component = components(land, w, h, count); + std::vector sizes(count, 0); + for (int c : t.component) + if (c >= 0) + ++sizes[c]; + if (count) + t.mainComponent = int(std::max_element(sizes.begin(), sizes.end()) - sizes.begin()); + return t; } // The banks: land split by the rivers, by every ford as if it were still water, and by a line // from each spring upstream to the sea, so the ground either side of a river counts as two banks // even though a colony could walk around its spring. -std::vector bankRegions(const Layout &layout, const Tiles &t) { - const int w = t.w, h = t.h; - std::vector open(t.water.size()); - for (size_t i = 0; i < open.size(); ++i) - open[i] = !t.water[i]; - auto close = [&](int x, int y) { open[size_t(wrapIndex(y, h)) * w + wrapIndex(x, w)] = 0; }; - for (const Ford &f : layout.fords) { - const int reach = int(std::ceil(f.span + kFordHalfWidth)) + 2; - const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); - for (int dy = -reach; dy <= reach; ++dy) - for (int dx = -reach; dx <= reach; ++dx) { - const Vec d{cx + dx + 0.5 - f.center.x, cy + dy + 0.5 - f.center.y}; - if (std::abs(dot(d, f.along)) <= kFordHalfWidth + 1 && - std::abs(dot(d, f.across)) <= f.span + 1) - close(cx + dx, cy + dy); - } - } - for (const auto &line : layout.divides) - for (Vec p : line) - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - close(int(std::floor(p.x)) + dx, int(std::floor(p.y)) + dy); - int count = 0; - return components(open, w, h, count); +std::vector bankRegions(const Layout &layout, const Tiles &t) +{ + const int w = t.w, h = t.h; + std::vector open(t.water.size()); + for (size_t i = 0; i < open.size(); ++i) + open[i] = !t.water[i]; + auto close = [&](int x, int y) { open[size_t(wrapIndex(y, h)) * w + wrapIndex(x, w)] = 0; }; + for (const Ford &f : layout.fords) + { + const int reach = int(std::ceil(f.span + kFordHalfWidth)) + 2; + const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + { + const Vec d{cx + dx + 0.5 - f.center.x, cy + dy + 0.5 - f.center.y}; + if (std::abs(dot(d, f.along)) <= kFordHalfWidth + 1 && + std::abs(dot(d, f.across)) <= f.span + 1) + close(cx + dx, cy + dy); + } + } + for (const auto &line : layout.divides) + for (Vec p : line) + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + close(int(std::floor(p.x)) + dx, int(std::floor(p.y)) + dy); + int count = 0; + return components(open, w, h, count); } // Swarm anchors on the floodplain of the main landmass, clear of the fords, spread as far apart // as possible and, where the map allows, each on a bank of its own. std::vector chooseSites(const Layout &layout, const Tiles &t, - const std::vector ®ions, - const Fertility::Field &fertility, int teams, - GenerationContext &context, std::string &detail) { - const int w = t.w, h = t.h; - struct Site { - int x, y, region; - double fertility; - }; - std::vector candidates; - for (int y = 0; y < h; y += 2) - for (int x = 0; x < w; x += 2) { - const size_t c = size_t((y + 2) % h) * w + (x + 2) % w; - if (t.component[c] != t.mainComponent || regions[c] < 0 || - t.waterSteps[c] < kColonyWaterNear || t.waterSteps[c] > kColonyWaterFar) - continue; - bool open = true; - for (int dy = -2; dy <= 5 && open; ++dy) - for (int dx = -2; dx <= 5 && open; ++dx) - open = t.grass[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] != 0; - const Vec centre{x + 2.0, y + 2.0}; - for (size_t k = 0; open && k < layout.fords.size(); ++k) { - const Ford &f = layout.fords[k]; - open = sq(centred(centre.x - f.center.x, w)) + sq(centred(centre.y - f.center.y, h)) >= - sq(f.span + 12); - } - if (!open) - continue; - // How fertile the ground a young colony works is, around the swarm. - double sum = 0; - int samples = 0; - for (int dy = -10; dy <= 13; dy += 3) - for (int dx = -10; dx <= 13; dx += 3, ++samples) - sum += fertility.at(wrapIndex(x + dx, w), wrapIndex(y + dy, h)); - candidates.push_back({x, y, regions[c], sum / samples}); - } - if (int(candidates.size()) < teams) { - detail = "only " + std::to_string(candidates.size()) + " floodplain sites for " + - std::to_string(teams) + " colonies"; - return {}; - } - // Every colony starts on the more fertile half of the floodplain, so none is handed the dry - // fringe while another gets the river bank; spacing is then chosen within that half. - std::stable_sort(candidates.begin(), candidates.end(), - [](const Site &a, const Site &b) { return a.fertility > b.fertility; }); - const std::vector all = candidates; - candidates.resize(std::max(size_t(teams) * 6, candidates.size() / 2)); - if (candidates.size() > all.size()) - candidates = all; - auto distance2 = [&](const Site &a, const Site &b) { - return sq(centred(double(a.x - b.x), w)) + sq(centred(double(a.y - b.y), h)); - }; - std::vector best; - double bestScore = -1; - std::vector nearest(candidates.size()); - for (int trial = 0; trial < 32; ++trial) { - // Half the trials in the fertile half; if no spacing fits there, the rest use every site. - if (trial == 16) { - if (!best.empty()) - break; - candidates = all; - nearest.assign(candidates.size(), 0); - } - std::vector chosen{int(context.bounded(kStartStream, candidates.size()))}; - std::fill(nearest.begin(), nearest.end(), std::numeric_limits::max()); - auto claim = [&](int k) { - for (size_t c = 0; c < candidates.size(); ++c) - nearest[c] = std::min(nearest[c], distance2(candidates[c], candidates[k])); - }; - claim(chosen.front()); - while (int(chosen.size()) < teams) { - int pick = -1; - double pickScore = -1; - for (size_t c = 0; c < candidates.size(); ++c) { - if (nearest[c] < sq(kColonySpacing)) - continue; - bool shared = false; - for (int k : chosen) - shared = shared || candidates[k].region == candidates[c].region; - const double score = nearest[c] * (shared ? 0.3 : 1.0); - if (score > pickScore) { - pickScore = score; - pick = int(c); - } - } - if (pick < 0) - break; - chosen.push_back(pick); - claim(pick); - } - if (int(chosen.size()) < teams) - continue; - double closest = std::numeric_limits::max(); - int shared = 0; - for (size_t a = 0; a < chosen.size(); ++a) - for (size_t b = a + 1; b < chosen.size(); ++b) { - closest = std::min(closest, distance2(candidates[chosen[a]], candidates[chosen[b]])); - shared += candidates[chosen[a]].region == candidates[chosen[b]].region; - } - const double score = (teams > 1 ? closest : 1.0) * std::pow(0.5, shared); - if (score > bestScore) { - bestScore = score; - best = chosen; - } - } - if (best.empty()) { - detail = "the floodplain sites are too close together for " + std::to_string(teams) + - " colonies"; - return {}; - } - std::vector sites; - for (int k : best) - sites.emplace_back(candidates[k].x, candidates[k].y); - return sites; + const std::vector ®ions, + const Fertility::Field &fertility, int teams, + GenerationContext &context, std::string &detail) +{ + const int w = t.w, h = t.h; + struct Site + { + int x, y, region; + double fertility; + }; + std::vector candidates; + for (int y = 0; y < h; y += 2) + for (int x = 0; x < w; x += 2) + { + const size_t c = size_t((y + 2) % h) * w + (x + 2) % w; + if (t.component[c] != t.mainComponent || regions[c] < 0 || + t.waterSteps[c] < kColonyWaterNear || t.waterSteps[c] > kColonyWaterFar) + continue; + bool open = true; + for (int dy = -2; dy <= 5 && open; ++dy) + for (int dx = -2; dx <= 5 && open; ++dx) + open = t.grass[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] != 0; + const Vec centre{x + 2.0, y + 2.0}; + for (size_t k = 0; open && k < layout.fords.size(); ++k) + { + const Ford &f = layout.fords[k]; + open = + sq(centred(centre.x - f.center.x, w)) + sq(centred(centre.y - f.center.y, h)) >= + sq(f.span + 12); + } + if (!open) + continue; + // How fertile the ground a young colony works is, around the swarm. + double sum = 0; + int samples = 0; + for (int dy = -10; dy <= 13; dy += 3) + for (int dx = -10; dx <= 13; dx += 3, ++samples) + sum += fertility.at(wrapIndex(x + dx, w), wrapIndex(y + dy, h)); + candidates.push_back({x, y, regions[c], sum / samples}); + } + if (int(candidates.size()) < teams) + { + detail = "only " + std::to_string(candidates.size()) + " floodplain sites for " + + std::to_string(teams) + " colonies"; + return {}; + } + // Every colony starts on the more fertile half of the floodplain, so none is handed the dry + // fringe while another gets the river bank; spacing is then chosen within that half. + std::stable_sort(candidates.begin(), candidates.end(), + [](const Site &a, const Site &b) { return a.fertility > b.fertility; }); + const std::vector all = candidates; + candidates.resize(std::max(size_t(teams) * 6, candidates.size() / 2)); + if (candidates.size() > all.size()) + candidates = all; + auto distance2 = [&](const Site &a, const Site &b) + { return sq(centred(double(a.x - b.x), w)) + sq(centred(double(a.y - b.y), h)); }; + std::vector best; + double bestScore = -1; + std::vector nearest(candidates.size()); + for (int trial = 0; trial < 32; ++trial) + { + // Half the trials in the fertile half; if no spacing fits there, the rest use every site. + if (trial == 16) + { + if (!best.empty()) + break; + candidates = all; + nearest.assign(candidates.size(), 0); + } + std::vector chosen{int(context.bounded(kStartStream, candidates.size()))}; + std::fill(nearest.begin(), nearest.end(), std::numeric_limits::max()); + auto claim = [&](int k) + { + for (size_t c = 0; c < candidates.size(); ++c) + nearest[c] = std::min(nearest[c], distance2(candidates[c], candidates[k])); + }; + claim(chosen.front()); + while (int(chosen.size()) < teams) + { + int pick = -1; + double pickScore = -1; + for (size_t c = 0; c < candidates.size(); ++c) + { + if (nearest[c] < sq(kColonySpacing)) + continue; + bool shared = false; + for (int k : chosen) + shared = shared || candidates[k].region == candidates[c].region; + const double score = nearest[c] * (shared ? 0.3 : 1.0); + if (score > pickScore) + { + pickScore = score; + pick = int(c); + } + } + if (pick < 0) + break; + chosen.push_back(pick); + claim(pick); + } + if (int(chosen.size()) < teams) + continue; + double closest = std::numeric_limits::max(); + int shared = 0; + for (size_t a = 0; a < chosen.size(); ++a) + for (size_t b = a + 1; b < chosen.size(); ++b) + { + closest = + std::min(closest, distance2(candidates[chosen[a]], candidates[chosen[b]])); + shared += candidates[chosen[a]].region == candidates[chosen[b]].region; + } + const double score = (teams > 1 ? closest : 1.0) * std::pow(0.5, shared); + if (score > bestScore) + { + bestScore = score; + best = chosen; + } + } + if (best.empty()) + { + detail = "the floodplain sites are too close together for " + std::to_string(teams) + + " colonies"; + return {}; + } + std::vector sites; + for (int k : best) + sites.emplace_back(candidates[k].x, candidates[k].y); + return sites; } // Walking steps from the given tiles over land that is free of resources and buildings. -std::vector walk(const Map &map, const std::vector &from) { - const int w = map.getW(), h = map.getH(); - std::vector distance(size_t(w) * h, -1); - std::vector queue; - queue.reserve(distance.size()); - for (int p : from) - if (distance[p] < 0) { - distance[p] = 0; - queue.push_back(p); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const size_t q = size_t(ny) * w + nx; - if (distance[q] < 0 && !map.isWater(nx, ny) && !map.isResource(nx, ny) && - map.getBuilding(nx, ny) == NOGBID) { - distance[q] = distance[queue[head]] + 1; - queue.push_back(int(q)); - } - } - } - return distance; +std::vector walk(const Map &map, const std::vector &from) +{ + const int w = map.getW(), h = map.getH(); + std::vector distance(size_t(w) * h, -1); + std::vector queue; + queue.reserve(distance.size()); + for (int p : from) + if (distance[p] < 0) + { + distance[p] = 0; + queue.push_back(p); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % w, y = queue[head] / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const size_t q = size_t(ny) * w + nx; + if (distance[q] < 0 && !map.isWater(nx, ny) && !map.isResource(nx, ny) && + map.getBuilding(nx, ny) == NOGBID) + { + distance[q] = distance[queue[head]] + 1; + queue.push_back(int(q)); + } + } + } + return distance; } -std::vector> workersByTeam(const Map &map, int teams) { - std::vector> workers(teams); - for (int y = 0; y < map.getH(); ++y) - for (int x = 0; x < map.getW(); ++x) { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid == NOGUID) - continue; - const int team = Unit::GIDtoTeam(gid); - if (team >= 0 && team < teams) - workers[team].push_back(y * map.getW() + x); - } - return workers; +std::vector> workersByTeam(const Map &map, int teams) +{ + std::vector> workers(teams); + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid == NOGUID) + continue; + const int team = Unit::GIDtoTeam(gid); + if (team >= 0 && team < teams) + workers[team].push_back(y * map.getW() + x); + } + return workers; } // Clumps keep a gap of open ground between them, so no two can join into a wall. -class Reservations { -public: - Reservations(int w, int h) : w(w), h(h), taken(size_t(w) * h, 0) {} - bool free(int x, int y, int radius) const { - const int reach = radius + 2; - for (int dy = -reach; dy <= reach; ++dy) - for (int dx = -reach; dx <= reach; ++dx) - if (dx * dx + dy * dy <= reach * reach && - taken[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]) - return false; - return true; - } - void reserve(int x, int y, int radius) { - const int reach = radius + 1; - for (int dy = -reach; dy <= reach; ++dy) - for (int dx = -reach; dx <= reach; ++dx) - if (dx * dx + dy * dy <= reach * reach) - taken[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] = 1; - } - -private: - int w, h; - std::vector taken; +class Reservations +{ + public: + Reservations(int w, int h) : w(w), h(h), taken(size_t(w) * h, 0) {} + bool free(int x, int y, int radius) const + { + const int reach = radius + 2; + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + if (dx * dx + dy * dy <= reach * reach && + taken[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]) + return false; + return true; + } + void reserve(int x, int y, int radius) + { + const int reach = radius + 1; + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + if (dx * dx + dy * dy <= reach * reach) + taken[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] = 1; + } + + private: + int w, h; + std::vector taken; }; // Every colony gets the same kit on its own floodplain: wheat and wood where the ground is most // fertile, on different sides of the swarm, and a little stone on the drier side. void placeStarterKits(Game &game, GenerationContext &context, int teams, const Tiles &t, - const Fertility::Field &fertility, Reservations &reservations) { - Map &map = game.map; - const int w = map.getW(), h = map.getH(); - const auto workers = workersByTeam(map, teams); - for (int team = 0; team < teams; ++team) { - const std::vector distance = walk(map, workers[team]); - const int bx = context.bootX[team], by = context.bootY[team]; - const Vec centre{bx + 2.0, by + 2.0}; - struct Spot { - int x, y, steps; - Vec offset; - std::uint32_t fertility; - }; - std::vector spots; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const int steps = distance[size_t(y) * w + x]; - if (steps < 6 || steps > 22 || !map.isGrass(x, y) || !map.isResourceAllowed(x, y, CORN)) - continue; - const Vec offset{centred(x - centre.x, w), centred(y - centre.y, h)}; - if (std::max(std::abs(offset.x), std::abs(offset.y)) < 7.5) - continue; - spots.push_back({x, y, steps, offset, fertility.at(x, y)}); - } - // On the preferred side at full size if possible; then on any side; then a smaller clump a - // little further out that may sit beside another, for a colony on a narrow strip of - // floodplain. Any route a kit closes is opened again by connectColonies. - auto pick = [&](int nearSteps, int farSteps, bool fertile, const std::vector &avoid, - double minimumAngle, int radius) -> std::pair { - for (int pass = 0; pass < 3; ++pass) { - const int size = pass < 2 ? radius : 1, reach = pass < 2 ? farSteps : farSteps + 8; - const Spot *chosen = nullptr; - for (const Spot &s : spots) { - if (s.steps < nearSteps || s.steps > reach || - t.grassDepth[size_t(s.y) * w + s.x] <= size || - (pass < 2 && !reservations.free(s.x, s.y, size))) - continue; - bool apart = true; - for (Vec a : avoid) - apart = apart && (pass > 0 || dot(normalized(a), normalized(s.offset)) <= - std::cos(minimumAngle * kPi / 180)); - if (!apart) - continue; - if (!chosen || (fertile ? s.fertility > chosen->fertility : s.fertility < chosen->fertility)) - chosen = &s; - } - if (chosen) - return {chosen, size}; - } - return {nullptr, 0}; - }; - std::vector used; - for (const auto &kit : {std::pair{CORN, 2}, std::pair{WOOD, 2}, - std::pair{STONE, 1}}) { - const bool stone = kit.first == STONE; - const auto [spot, size] = pick(stone ? 10 : 6, stone ? 14 : 10, !stone, used, - stone ? 60 : 100, kit.second); - if (!spot) - continue; - placeResourceClump(map, context, MapGeneratorPoint(spot->x, spot->y), kit.first, size); - reservations.reserve(spot->x, spot->y, size); - used.push_back(spot->offset); - } - } + const Fertility::Field &fertility, Reservations &reservations) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const auto workers = workersByTeam(map, teams); + for (int team = 0; team < teams; ++team) + { + const std::vector distance = walk(map, workers[team]); + const int bx = context.bootX[team], by = context.bootY[team]; + const Vec centre{bx + 2.0, by + 2.0}; + struct Spot + { + int x, y, steps; + Vec offset; + std::uint32_t fertility; + }; + std::vector spots; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const int steps = distance[size_t(y) * w + x]; + if (steps < 6 || steps > 22 || !map.isGrass(x, y) || + !map.isResourceAllowed(x, y, CORN)) + continue; + const Vec offset{centred(x - centre.x, w), centred(y - centre.y, h)}; + if (std::max(std::abs(offset.x), std::abs(offset.y)) < 7.5) + continue; + spots.push_back({x, y, steps, offset, fertility.at(x, y)}); + } + // On the preferred side at full size if possible; then on any side; then a smaller clump a + // little further out that may sit beside another, for a colony on a narrow strip of + // floodplain. Any route a kit closes is opened again by connectColonies. + auto pick = [&](int nearSteps, int farSteps, bool fertile, const std::vector &avoid, + double minimumAngle, int radius) -> std::pair + { + for (int pass = 0; pass < 3; ++pass) + { + const int size = pass < 2 ? radius : 1, reach = pass < 2 ? farSteps : farSteps + 8; + const Spot *chosen = nullptr; + for (const Spot &s : spots) + { + if (s.steps < nearSteps || s.steps > reach || + t.grassDepth[size_t(s.y) * w + s.x] <= size || + (pass < 2 && !reservations.free(s.x, s.y, size))) + continue; + bool apart = true; + for (Vec a : avoid) + apart = apart && (pass > 0 || dot(normalized(a), normalized(s.offset)) <= + std::cos(minimumAngle * kPi / 180)); + if (!apart) + continue; + if (!chosen || (fertile ? s.fertility > chosen->fertility + : s.fertility < chosen->fertility)) + chosen = &s; + } + if (chosen) + return {chosen, size}; + } + return {nullptr, 0}; + }; + std::vector used; + for (const auto &kit : {std::pair{CORN, 2}, std::pair{WOOD, 2}, + std::pair{STONE, 1}}) + { + const bool stone = kit.first == STONE; + const auto [spot, size] = + pick(stone ? 10 : 6, stone ? 14 : 10, !stone, used, stone ? 60 : 100, kit.second); + if (!spot) + continue; + placeResourceClump(map, context, MapGeneratorPoint(spot->x, spot->y), kit.first, size); + reservations.reserve(spot->x, spot->y, size); + used.push_back(spot->offset); + } + } } // Farmland in ribbons along the rivers: clumps seeded on the most fertile free ground, 2:1 wheat // to wood, with the crop chosen by a coarse field so stretches of bank favour one or the other. void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, - const Fertility::Field &fertility, const std::vector &keepClear, - Reservations &reservations, const WatershedOptions &o) { - Map &map = game.map; - const int w = t.w, h = t.h; - std::mt19937 &random = context.stream("watershed-farmland"); - const PeriodicNoise patch(w, h, 6, random), crop(w, h, 20, random); - struct Candidate { - double score; - int index; - }; - std::vector order; - const std::uint32_t floor = Fertility::kScale / 20; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - const std::uint32_t f = fertility.at(x, y); - if (t.grassDepth[i] < 3 || keepClear[i] || f < floor || !map.isResourceAllowed(x, y, CORN) || - map.isResource(x, y)) - continue; - order.push_back({f * (0.5 + patch.at(x + 0.5, y + 0.5)), int(i)}); - } - std::sort(order.begin(), order.end(), [](const Candidate &a, const Candidate &b) { - return a.score != b.score ? a.score > b.score : a.index < b.index; - }); - int budget = int(order.size()) / 5; - // Other wheat or wood amounts than the default give each crop a budget of its own: its share - // of the candidates the crop field gives it, scaled. - const bool shared = o.wheat == 100 && o.wood == 100; - int cornBudget = 0, woodBudget = 0; - if (!shared && !order.empty()) { - std::int64_t cornCandidates = 0; - for (const Candidate &c : order) - cornCandidates += crop.at(c.index % w + 0.5, c.index / w + 0.5) < 0.62; - const int cornShare = int(budget * cornCandidates / std::int64_t(order.size())); - cornBudget = int(scaledCount(cornShare, o.wheat)); - woodBudget = int(scaledCount(budget - cornShare, o.wood)); - } - for (const Candidate &c : order) { - if (shared ? budget <= 0 : cornBudget <= 0 && woodBudget <= 0) - break; - const int x = c.index % w, y = c.index / w; - const int radius = context.bounded("resources", 5) < 2 && t.grassDepth[c.index] > 3 ? 3 : 2; - if (!reservations.free(x, y, radius)) - continue; - const int type = crop.at(x + 0.5, y + 0.5) < 0.62 ? CORN : WOOD; - int &remaining = shared ? budget : type == CORN ? cornBudget : woodBudget; - if (remaining <= 0) - continue; - remaining -= placeResourceClump(map, context, MapGeneratorPoint(x, y), type, radius); - reservations.reserve(x, y, radius); - } + const Fertility::Field &fertility, const std::vector &keepClear, + Reservations &reservations, const WatershedOptions &o) +{ + Map &map = game.map; + const int w = t.w, h = t.h; + std::mt19937 &random = context.stream("watershed-farmland"); + const PeriodicNoise patch(w, h, 6, random), crop(w, h, 20, random); + struct Candidate + { + double score; + int index; + }; + std::vector order; + const std::uint32_t floor = Fertility::kScale / 20; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + const std::uint32_t f = fertility.at(x, y); + if (t.grassDepth[i] < 3 || keepClear[i] || f < floor || + !map.isResourceAllowed(x, y, CORN) || map.isResource(x, y)) + continue; + order.push_back({f * (0.5 + patch.at(x + 0.5, y + 0.5)), int(i)}); + } + std::sort(order.begin(), order.end(), [](const Candidate &a, const Candidate &b) + { return a.score != b.score ? a.score > b.score : a.index < b.index; }); + int budget = int(order.size()) / 5; + // Other wheat or wood amounts than the default give each crop a budget of its own: its share + // of the candidates the crop field gives it, scaled. + const bool shared = o.wheat == 100 && o.wood == 100; + int cornBudget = 0, woodBudget = 0; + if (!shared && !order.empty()) + { + std::int64_t cornCandidates = 0; + for (const Candidate &c : order) + cornCandidates += crop.at(c.index % w + 0.5, c.index / w + 0.5) < 0.62; + const int cornShare = int(budget * cornCandidates / std::int64_t(order.size())); + cornBudget = int(scaledCount(cornShare, o.wheat)); + woodBudget = int(scaledCount(budget - cornShare, o.wood)); + } + for (const Candidate &c : order) + { + if (shared ? budget <= 0 : cornBudget <= 0 && woodBudget <= 0) + break; + const int x = c.index % w, y = c.index / w; + const int radius = context.bounded("resources", 5) < 2 && t.grassDepth[c.index] > 3 ? 3 : 2; + if (!reservations.free(x, y, radius)) + continue; + const int type = crop.at(x + 0.5, y + 0.5) < 0.62 ? CORN : WOOD; + int &remaining = shared ? budget : type == CORN ? cornBudget : woodBudget; + if (remaining <= 0) + continue; + remaining -= placeResourceClump(map, context, MapGeneratorPoint(x, y), type, radius); + reservations.reserve(x, y, radius); + } } // Stone outcrops at the edge of the dry uplands, well away from the rivers. void placeStone(Game &game, GenerationContext &context, const Tiles &t, - const std::vector &keepClear, Reservations &reservations, - int stonePercent) { - Map &map = game.map; - const int w = t.w, h = t.h; - std::vector candidates; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - if (!t.grass[i] || keepClear[i] || t.waterSteps[i] < 9) - continue; - bool edge = false; - for (int dy = -2; dy <= 2 && !edge; ++dy) - for (int dx = -2; dx <= 2 && !edge; ++dx) - edge = !t.grass[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]; - if (edge || t.waterSteps[i] >= 14) - candidates.push_back(int(i)); - } - const int outcrops = int(scaledCount(std::max(2, w * h / 3000), stonePercent)); - std::vector placed; - for (int attempt = 0; attempt < outcrops * 20 && int(placed.size()) < outcrops && !candidates.empty(); - ++attempt) { - const int i = candidates[context.bounded("resources", candidates.size())]; - const int x = i % w, y = i / w, radius = 1 + int(context.bounded("resources", 2)); - bool apart = reservations.free(x, y, radius) && map.isResourceAllowed(x, y, STONE); - for (const auto &p : placed) - apart = apart && sq(centred(double(x - p.x), w)) + sq(centred(double(y - p.y), h)) >= sq(20); - if (!apart) - continue; - placeResourceClump(map, context, MapGeneratorPoint(x, y), STONE, radius); - reservations.reserve(x, y, radius); - placed.emplace_back(x, y); - } + const std::vector &keepClear, Reservations &reservations, + int stonePercent) +{ + Map &map = game.map; + const int w = t.w, h = t.h; + std::vector candidates; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + if (!t.grass[i] || keepClear[i] || t.waterSteps[i] < 9) + continue; + bool edge = false; + for (int dy = -2; dy <= 2 && !edge; ++dy) + for (int dx = -2; dx <= 2 && !edge; ++dx) + edge = !t.grass[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]; + if (edge || t.waterSteps[i] >= 14) + candidates.push_back(int(i)); + } + const int outcrops = int(scaledCount(std::max(2, w * h / 3000), stonePercent)); + std::vector placed; + for (int attempt = 0; + attempt < outcrops * 20 && int(placed.size()) < outcrops && !candidates.empty(); ++attempt) + { + const int i = candidates[context.bounded("resources", candidates.size())]; + const int x = i % w, y = i / w, radius = 1 + int(context.bounded("resources", 2)); + bool apart = reservations.free(x, y, radius) && map.isResourceAllowed(x, y, STONE); + for (const auto &p : placed) + apart = apart && + sq(centred(double(x - p.x), w)) + sq(centred(double(y - p.y), h)) >= sq(20); + if (!apart) + continue; + placeResourceClump(map, context, MapGeneratorPoint(x, y), STONE, radius); + reservations.reserve(x, y, radius); + placed.emplace_back(x, y); + } } // Fruit is rare: a small grove in the fertile wedge where two rivers meet, one kind at a time. void placeFruit(Game &game, GenerationContext &context, const Layout &layout, const Tiles &t, - const Fertility::Field &fertility, const std::vector &keepClear, - Reservations &reservations, int fruitPercent) { - Map &map = game.map; - const int w = t.w, h = t.h; - std::vector junctions = layout.junctions; - for (size_t i = junctions.size(); i > 1; --i) - std::swap(junctions[i - 1], junctions[context.bounded("resources", i)]); - const int groves = int(scaledCount(std::max(3, w * h / 6000), fruitPercent)); - int type = int(context.bounded("resources", 3)), placed = 0; - for (size_t j = 0; j < junctions.size() && placed < groves; ++j) { - const int jx = int(std::floor(junctions[j].x)), jy = int(std::floor(junctions[j].y)); - int bestX = 0, bestY = 0; - std::uint32_t bestFertility = 0; - for (int dy = -12; dy <= 12; ++dy) - for (int dx = -12; dx <= 12; ++dx) { - const int d2 = dx * dx + dy * dy; - const int x = wrapIndex(jx + dx, w), y = wrapIndex(jy + dy, h); - const size_t i = size_t(y) * w + x; - if (d2 < 25 || d2 > 144 || !t.grass[i] || keepClear[i] || fertility.at(x, y) <= bestFertility || - !map.isResourceAllowed(x, y, CHERRY) || !reservations.free(x, y, 1)) - continue; - bestFertility = fertility.at(x, y); - bestX = x; - bestY = y; - } - if (!bestFertility) - continue; - placeResourceClump(map, context, MapGeneratorPoint(bestX, bestY), CHERRY + type, 1); - reservations.reserve(bestX, bestY, 1); - type = (type + 1) % 3; - ++placed; - } + const Fertility::Field &fertility, const std::vector &keepClear, + Reservations &reservations, int fruitPercent) +{ + Map &map = game.map; + const int w = t.w, h = t.h; + std::vector junctions = layout.junctions; + for (size_t i = junctions.size(); i > 1; --i) + std::swap(junctions[i - 1], junctions[context.bounded("resources", i)]); + const int groves = int(scaledCount(std::max(3, w * h / 6000), fruitPercent)); + int type = int(context.bounded("resources", 3)), placed = 0; + for (size_t j = 0; j < junctions.size() && placed < groves; ++j) + { + const int jx = int(std::floor(junctions[j].x)), jy = int(std::floor(junctions[j].y)); + int bestX = 0, bestY = 0; + std::uint32_t bestFertility = 0; + for (int dy = -12; dy <= 12; ++dy) + for (int dx = -12; dx <= 12; ++dx) + { + const int d2 = dx * dx + dy * dy; + const int x = wrapIndex(jx + dx, w), y = wrapIndex(jy + dy, h); + const size_t i = size_t(y) * w + x; + if (d2 < 25 || d2 > 144 || !t.grass[i] || keepClear[i] || + fertility.at(x, y) <= bestFertility || !map.isResourceAllowed(x, y, CHERRY) || + !reservations.free(x, y, 1)) + continue; + bestFertility = fertility.at(x, y); + bestX = x; + bestY = y; + } + if (!bestFertility) + continue; + placeResourceClump(map, context, MapGeneratorPoint(bestX, bestY), CHERRY + type, 1); + reservations.reserve(bestX, bestY, 1); + type = (type + 1) % 3; + ++placed; + } } // Algae off every mouth and in the sea's shallows, where there is sand near enough for it to // grow back. void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, const Tiles &t, - int algaePercent) { - Map &map = game.map; - const int w = t.w, h = t.h; - for (Vec mouth : layout.mouths) { - std::vector water; - const int mx = int(std::floor(mouth.x)), my = int(std::floor(mouth.y)); - for (int dy = -10; dy <= 10; ++dy) - for (int dx = -10; dx <= 10; ++dx) { - const int x = wrapIndex(mx + dx, w), y = wrapIndex(my + dy, h); - const size_t i = size_t(y) * w + x; - if (dx * dx + dy * dy <= 100 && t.water[i] && t.waterSteps[i] == 0) - water.push_back(int(i)); - } - for (int clump = 0; clump < scaledCount(3, algaePercent) && !water.empty(); ++clump) { - const int i = water[context.bounded("resources", water.size())]; - placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 2); - } - } - std::vector shallows; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const size_t i = size_t(y) * w + x; - if (!t.water[i]) - continue; - bool shore = false; - for (int dy = -3; dy <= 3 && !shore; ++dy) - for (int dx = -3; dx <= 3 && !shore; ++dx) - shore = !t.water[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]; - if (!shore) - continue; - const Vec c = layout.frame.toCanonical({x + 0.5, y + 0.5}); - if (c.y < layout.north.at(c.x) - 1 || c.y > layout.south.at(c.x) + 1) - shallows.push_back(int(i)); - } - for (int clump = 0; clump < scaledCount(int(shallows.size()) / 60, algaePercent); ++clump) { - const int i = shallows[context.bounded("resources", shallows.size())]; - placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 1); - } + int algaePercent) +{ + Map &map = game.map; + const int w = t.w, h = t.h; + for (Vec mouth : layout.mouths) + { + std::vector water; + const int mx = int(std::floor(mouth.x)), my = int(std::floor(mouth.y)); + for (int dy = -10; dy <= 10; ++dy) + for (int dx = -10; dx <= 10; ++dx) + { + const int x = wrapIndex(mx + dx, w), y = wrapIndex(my + dy, h); + const size_t i = size_t(y) * w + x; + if (dx * dx + dy * dy <= 100 && t.water[i] && t.waterSteps[i] == 0) + water.push_back(int(i)); + } + for (int clump = 0; clump < scaledCount(3, algaePercent) && !water.empty(); ++clump) + { + const int i = water[context.bounded("resources", water.size())]; + placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 2); + } + } + std::vector shallows; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const size_t i = size_t(y) * w + x; + if (!t.water[i]) + continue; + bool shore = false; + for (int dy = -3; dy <= 3 && !shore; ++dy) + for (int dx = -3; dx <= 3 && !shore; ++dx) + shore = !t.water[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)]; + if (!shore) + continue; + const Vec c = layout.frame.toCanonical({x + 0.5, y + 0.5}); + if (c.y < layout.north.at(c.x) - 1 || c.y > layout.south.at(c.x) + 1) + shallows.push_back(int(i)); + } + for (int clump = 0; clump < scaledCount(int(shallows.size()) / 60, algaePercent); ++clump) + { + const int i = shallows[context.bounded("resources", shallows.size())]; + placeResourceClump(map, context, MapGeneratorPoint(i % w, i / w), ALGA, 1); + } } // Clears the fewest resource tiles that let every colony walk to colony 0: a 0-1 search where // stepping onto a resource costs one and open ground nothing. -bool connectColonies(Game &game, int teams, std::string &detail) { - Map &map = game.map; - const int w = map.getW(), h = map.getH(); - const auto workers = workersByTeam(map, teams); - for (int team = 1; team < teams; ++team) { - std::vector reach = walk(map, workers[0]); - bool connected = false; - for (int p : workers[team]) - connected = connected || reach[p] >= 0; - if (connected) - continue; - std::vector cost(size_t(w) * h, std::numeric_limits::max()), from(size_t(w) * h, -1); - std::vector target(size_t(w) * h, 0); - for (int p : workers[team]) - target[p] = 1; - std::deque queue; - for (int p : workers[0]) { - cost[p] = 0; - queue.push_back(p); - } - int reached = -1; - while (!queue.empty() && reached < 0) { - const int p = queue.front(); - queue.pop_front(); - if (target[p]) { - reached = p; - break; - } - const int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const int q = ny * w + nx; - if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID) - continue; - const int step = map.isResource(nx, ny) ? 1 : 0; - if (cost[p] + step < cost[q]) { - cost[q] = cost[p] + step; - from[q] = p; - if (step) - queue.push_back(q); - else - queue.push_front(q); - } - } - } - if (reached < 0) { - detail = "colony " + std::to_string(team) + " has no land route to colony 0"; - return false; - } - for (int p = reached; p >= 0; p = from[p]) - if (map.isResource(p % w, p / w)) - map.setNoResource(p % w, p / w, 1); - } - return true; +bool connectColonies(Game &game, int teams, std::string &detail) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const auto workers = workersByTeam(map, teams); + for (int team = 1; team < teams; ++team) + { + std::vector reach = walk(map, workers[0]); + bool connected = false; + for (int p : workers[team]) + connected = connected || reach[p] >= 0; + if (connected) + continue; + std::vector cost(size_t(w) * h, std::numeric_limits::max()), + from(size_t(w) * h, -1); + std::vector target(size_t(w) * h, 0); + for (int p : workers[team]) + target[p] = 1; + std::deque queue; + for (int p : workers[0]) + { + cost[p] = 0; + queue.push_back(p); + } + int reached = -1; + while (!queue.empty() && reached < 0) + { + const int p = queue.front(); + queue.pop_front(); + if (target[p]) + { + reached = p; + break; + } + const int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + const int q = ny * w + nx; + if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID) + continue; + const int step = map.isResource(nx, ny) ? 1 : 0; + if (cost[p] + step < cost[q]) + { + cost[q] = cost[p] + step; + from[q] = p; + if (step) + queue.push_back(q); + else + queue.push_front(q); + } + } + } + if (reached < 0) + { + detail = "colony " + std::to_string(team) + " has no land route to colony 0"; + return false; + } + for (int p = reached; p >= 0; p = from[p]) + if (map.isResource(p % w, p / w)) + map.setNoResource(p % w, p / w, 1); + } + return true; } -bool generate(Game &game, GenerationContext &context) { - context.stage = "watershed layout"; - const WatershedOptions o(context.request); - Map &map = game.map; - const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; - const Layout layout = planLayout(w, h, o, context); - - context.stage = "watershed terrain"; - const std::vector terrain = stampTerrain(layout, o, context); - const auto stampedWater = [&](int x, int y) { return vertexTileWater(terrain, w, h, x, y); }; - context.detail = checkFords(layout, stampedWater); - if (context.detail.empty()) - context.detail = checkChannels(layout, stampedWater); - if (!context.detail.empty()) - return false; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * w + x])); - map.controlSand(); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - if (map.getUMTerrain(x, y) != terrain[size_t(y) * w + x]) { - context.detail = "Map::controlSand changed the stamped shoreline"; - return false; - } - map.rebuildTerrain(); - - context.stage = "watershed colonies"; - const Tiles tiles = tileView(terrain, w, h); - const std::vector regions = bankRegions(layout, tiles); - const Fertility::Field fertility = Fertility::forMap(map, false); - const std::vector sites = - chooseSites(layout, tiles, regions, fertility, teams, context, context.detail); - if (int(sites.size()) != teams) - return false; - for (int team = 0; team < teams; ++team) - game.addTeam(); - for (int team = 0; team < teams; ++team) { - std::vector home(size_t(w) * h, 0); - for (int dy = -6; dy <= 9; ++dy) - for (int dx = -6; dx <= 9; ++dx) { - const int x = wrapIndex(sites[team].x + dx, w), y = wrapIndex(sites[team].y + dy, h); - home[size_t(y) * w + x] = !tiles.water[size_t(y) * w + x]; - } - if (!placeSettlement(game, context, team, home, sites[team], "starts")) - return false; - } - - context.stage = "watershed resources"; - std::vector keepClear(size_t(w) * h, 0); - for (const Ford &f : layout.fords) { - const int reach = int(std::ceil(f.span + 5)); - const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); - for (int dy = -reach; dy <= reach; ++dy) - for (int dx = -reach; dx <= reach; ++dx) - if (sq(cx + dx + 0.5 - f.center.x) + sq(cy + dy + 0.5 - f.center.y) <= sq(f.span + 5)) - keepClear[size_t(wrapIndex(cy + dy, h)) * w + wrapIndex(cx + dx, w)] = 1; - } - for (int team = 0; team < teams; ++team) - for (int dy = -3; dy <= 6; ++dy) - for (int dx = -3; dx <= 6; ++dx) - keepClear[size_t(wrapIndex(context.bootY[team] + dy, h)) * w + - wrapIndex(context.bootX[team] + dx, w)] = 1; - Reservations reservations(w, h); - placeStarterKits(game, context, teams, tiles, fertility, reservations); - placeFruit(game, context, layout, tiles, fertility, keepClear, reservations, o.fruit); - placeStone(game, context, tiles, keepClear, reservations, o.stone); - placeFarmland(game, context, tiles, fertility, keepClear, reservations, o); - placeAlgae(game, context, layout, tiles, o.algae); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - if (keepClear[size_t(y) * w + x] && map.isResource(x, y) && !map.isWater(x, y)) - map.setNoResource(x, y, 1); - context.stage = "watershed connectivity"; - if (!connectColonies(game, teams, context.detail)) - return false; - // Opening a route can clear part of a starter kit, so the kits are topped up afterwards - held - // to the kit's own distance rather than the shared defaults, so a colony whose kit could not be - // placed still gets wheat and wood as near as its rivals do - and the routes checked once more - // in case a top-up closed one. - guaranteeStartingResources(game, context, 16, 16); - return connectColonies(game, teams, context.detail); +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "watershed layout"; + const WatershedOptions o(context.request); + Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + const Layout layout = planLayout(w, h, o, context); + + context.stage = "watershed terrain"; + const std::vector terrain = stampTerrain(layout, o, context); + const auto stampedWater = [&](int x, int y) { return vertexTileWater(terrain, w, h, x, y); }; + context.detail = checkFords(layout, stampedWater); + if (context.detail.empty()) + context.detail = checkChannels(layout, stampedWater); + if (!context.detail.empty()) + return false; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * w + x])); + map.controlSand(); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (map.getUMTerrain(x, y) != terrain[size_t(y) * w + x]) + { + context.detail = "Map::controlSand changed the stamped shoreline"; + return false; + } + map.rebuildTerrain(); + + context.stage = "watershed colonies"; + const Tiles tiles = tileView(terrain, w, h); + const std::vector regions = bankRegions(layout, tiles); + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector sites = + chooseSites(layout, tiles, regions, fertility, teams, context, context.detail); + if (int(sites.size()) != teams) + return false; + for (int team = 0; team < teams; ++team) + game.addTeam(); + for (int team = 0; team < teams; ++team) + { + std::vector home(size_t(w) * h, 0); + for (int dy = -6; dy <= 9; ++dy) + for (int dx = -6; dx <= 9; ++dx) + { + const int x = wrapIndex(sites[team].x + dx, w), + y = wrapIndex(sites[team].y + dy, h); + home[size_t(y) * w + x] = !tiles.water[size_t(y) * w + x]; + } + if (!placeSettlement(game, context, team, home, sites[team], "starts")) + return false; + } + + context.stage = "watershed resources"; + std::vector keepClear(size_t(w) * h, 0); + for (const Ford &f : layout.fords) + { + const int reach = int(std::ceil(f.span + 5)); + const int cx = int(std::floor(f.center.x)), cy = int(std::floor(f.center.y)); + for (int dy = -reach; dy <= reach; ++dy) + for (int dx = -reach; dx <= reach; ++dx) + if (sq(cx + dx + 0.5 - f.center.x) + sq(cy + dy + 0.5 - f.center.y) <= + sq(f.span + 5)) + keepClear[size_t(wrapIndex(cy + dy, h)) * w + wrapIndex(cx + dx, w)] = 1; + } + for (int team = 0; team < teams; ++team) + for (int dy = -3; dy <= 6; ++dy) + for (int dx = -3; dx <= 6; ++dx) + keepClear[size_t(wrapIndex(context.bootY[team] + dy, h)) * w + + wrapIndex(context.bootX[team] + dx, w)] = 1; + Reservations reservations(w, h); + placeStarterKits(game, context, teams, tiles, fertility, reservations); + placeFruit(game, context, layout, tiles, fertility, keepClear, reservations, o.fruit); + placeStone(game, context, tiles, keepClear, reservations, o.stone); + placeFarmland(game, context, tiles, fertility, keepClear, reservations, o); + placeAlgae(game, context, layout, tiles, o.algae); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (keepClear[size_t(y) * w + x] && map.isResource(x, y) && !map.isWater(x, y)) + map.setNoResource(x, y, 1); + context.stage = "watershed connectivity"; + if (!connectColonies(game, teams, context.detail)) + return false; + // Opening a route can clear part of a starter kit, so the kits are topped up afterwards - held + // to the kit's own distance rather than the shared defaults, so a colony whose kit could not be + // placed still gets wheat and wood as near as its rivals do - and the routes checked once more + // in case a top-up closed one. + guaranteeStartingResources(game, context, 16, 16); + return connectColonies(game, teams, context.detail); } -std::string validate(const GenerationRequest &r) { - if (r.nbTeams > (1 << (r.wDec + r.hDec)) / kTilesPerColony) - return "The watershed has too little floodplain for this many colonies; use a bigger map or " - "fewer colonies."; - return ""; +std::string validate(const GenerationRequest &r) +{ + if (r.nbTeams > (1 << (r.wDec + r.hDec)) / kTilesPerColony) + return "The watershed has too little floodplain for this many colonies; use a bigger map " + "or " + "fewer colonies."; + return ""; } // Checked on the finished world rather than trusted. Walking treats water, buildings and every @@ -1759,102 +1999,113 @@ std::string validate(const GenerationRequest &r) { // stand beside wheat and wood that way. The layout is planned again from the request to find the // fords and channels: every ford must interrupt a real channel, be open from bank to bank and have // walkable land at both ends, and every channel must keep its core of open water everywhere else. -std::string validateWorld(const Game &game, const GenerationContext &context) { - const Map &map = game.map; - const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; - const auto workers = workersByTeam(map, teams); - for (int team = 0; team < teams; ++team) - if (workers[team].empty()) - return "Colony " + std::to_string(team) + " has no workers."; - std::vector fromFirst; - for (int team = 0; team < teams; ++team) { - const std::vector reach = walk(map, workers[team]); - if (team == 0) - fromFirst = reach; - bool met = false; - for (int p : workers[team]) - met = met || fromFirst[p] >= 0; - if (!met) - return "Colony " + std::to_string(team) + " cannot walk to colony 0 without swimming."; - bool wheat = false, wood = false; - for (int y = 0; y < h && !(wheat && wood); ++y) - for (int x = 0; x < w; ++x) { - const int type = map.getResource(x, y).type; - if (type != CORN && type != WOOD) - continue; - bool beside = false; - for (int dy = -1; dy <= 1 && !beside; ++dy) - for (int dx = -1; dx <= 1 && !beside; ++dx) - beside = reach[size_t(map.normalizeY(y + dy)) * w + map.normalizeX(x + dx)] >= 0; - (type == CORN ? wheat : wood) = (type == CORN ? wheat : wood) || beside; - } - if (!wheat || !wood) - return "Colony " + std::to_string(team) + " cannot walk to " + (wheat ? "wood." : "wheat."); - } - - const WatershedOptions o(context.request); - GenerationContext replay(context.request); - const Layout layout = planLayout(w, h, o, replay); - const auto water = [&](int x, int y) { return map.isWater(x, y); }; - std::string error = checkFords(layout, water); - if (!error.empty()) - return error; - for (const Ford &f : layout.fords) - for (int side : {-1, 1}) { - const Vec end = f.center + f.across * (side * (f.span + 1.0)); - bool walkable = false; - for (int dy = -1; dy <= 1 && !walkable; ++dy) - for (int dx = -1; dx <= 1 && !walkable; ++dx) { - const int x = map.normalizeX(int(std::floor(end.x)) + dx); - const int y = map.normalizeY(int(std::floor(end.y)) + dy); - walkable = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - } - if (!walkable) - return "A ford at (" + std::to_string(map.normalizeX(int(f.center.x))) + ", " + - std::to_string(map.normalizeY(int(f.center.y))) + - ") has no walkable land on one bank."; - } - return checkChannels(layout, water); +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + const Map &map = game.map; + const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + const auto workers = workersByTeam(map, teams); + for (int team = 0; team < teams; ++team) + if (workers[team].empty()) + return "Colony " + std::to_string(team) + " has no workers."; + std::vector fromFirst; + for (int team = 0; team < teams; ++team) + { + const std::vector reach = walk(map, workers[team]); + if (team == 0) + fromFirst = reach; + bool met = false; + for (int p : workers[team]) + met = met || fromFirst[p] >= 0; + if (!met) + return "Colony " + std::to_string(team) + " cannot walk to colony 0 without swimming."; + bool wheat = false, wood = false; + for (int y = 0; y < h && !(wheat && wood); ++y) + for (int x = 0; x < w; ++x) + { + const int type = map.getResource(x, y).type; + if (type != CORN && type != WOOD) + continue; + bool beside = false; + for (int dy = -1; dy <= 1 && !beside; ++dy) + for (int dx = -1; dx <= 1 && !beside; ++dx) + beside = + reach[size_t(map.normalizeY(y + dy)) * w + map.normalizeX(x + dx)] >= 0; + (type == CORN ? wheat : wood) = (type == CORN ? wheat : wood) || beside; + } + if (!wheat || !wood) + return "Colony " + std::to_string(team) + " cannot walk to " + + (wheat ? "wood." : "wheat."); + } + + const WatershedOptions o(context.request); + GenerationContext replay(context.request); + const Layout layout = planLayout(w, h, o, replay); + const auto water = [&](int x, int y) { return map.isWater(x, y); }; + std::string error = checkFords(layout, water); + if (!error.empty()) + return error; + for (const Ford &f : layout.fords) + for (int side : {-1, 1}) + { + const Vec end = f.center + f.across * (side * (f.span + 1.0)); + bool walkable = false; + for (int dy = -1; dy <= 1 && !walkable; ++dy) + for (int dx = -1; dx <= 1 && !walkable; ++dx) + { + const int x = map.normalizeX(int(std::floor(end.x)) + dx); + const int y = map.normalizeY(int(std::floor(end.y)) + dy); + walkable = !map.isWater(x, y) && !map.isResource(x, y) && + map.getBuilding(x, y) == NOGBID; + } + if (!walkable) + return "A ford at (" + std::to_string(map.normalizeX(int(f.center.x))) + ", " + + std::to_string(map.normalizeY(int(f.center.y))) + + ") has no walkable land on one bank."; + } + return checkChannels(layout, water); } } // namespace WatershedOptions::WatershedOptions(const GenerationRequest &r) - : riverDensity(r.option("river-density")), riverWidth(r.option("river-width")), - dryness(r.option("dryness")), fords(r.option("fords")), delta(r.option("river-delta") != 0), - meanders(r.option("meanders") != 0), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) {} - -GeneratorDefinition watershedDefinition() { - return {"watershed", - 13, - "Watershed", - 1, - false, - {// Springs per area of land; each one that finds its way to the network is a tributary. - {"river-density", "River density", 1, 10, 1, 5, ControlGroup::Terrain}, - // The narrowest channel, at a spring; rivers widen downstream as tributaries join. - {"river-width", "River width", 1, 5, 1, 2, ControlGroup::Terrain}, - // How near to the rivers the uplands dry out to sand; 0 leaves them grass, and no - // setting dries ground within 11 tiles of water, so every river keeps a floodplain. - {"dryness", "Dryness", 0, 10, 1, 5, ControlGroup::Terrain}, - // How often a ford crosses a river: about one per 80 tiles of river at 1, one per 26 - // at 5, and at least one on every reach long enough to hold one. - {"fords", "Fords", 1, 5, 1, 3, ControlGroup::Layout}, - // Off, the trunk reaches the sea through a single mouth, with no delta lobe. - GeneratorControl::toggle("river-delta", "River delta", true, ControlGroup::Terrain), - // Off, rivers run without their meanders. - GeneratorControl::toggle("meanders", "Meandering rivers", true, ControlGroup::Terrain), - // The farmland along the rivers (wheat and wood each), stone outcrops, fruit groves at - // the confluences and algae off the mouths and shallows. Every colony's starter kit - // stays as it is. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - validate, - validateWorld}; + : riverDensity(r.option("river-density")), riverWidth(r.option("river-width")), + dryness(r.option("dryness")), fords(r.option("fords")), delta(r.option("river-delta") != 0), + meanders(r.option("meanders") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition watershedDefinition() +{ + return {"watershed", + 13, + "Watershed", + 1, + false, + {// Springs per area of land; each one that finds its way to the network is a tributary. + {"river-density", "River density", 1, 10, 1, 5, ControlGroup::Terrain}, + // The narrowest channel, at a spring; rivers widen downstream as tributaries join. + {"river-width", "River width", 1, 5, 1, 2, ControlGroup::Terrain}, + // How near to the rivers the uplands dry out to sand; 0 leaves them grass, and no + // setting dries ground within 11 tiles of water, so every river keeps a floodplain. + {"dryness", "Dryness", 0, 10, 1, 5, ControlGroup::Terrain}, + // How often a ford crosses a river: about one per 80 tiles of river at 1, one per 26 + // at 5, and at least one on every reach long enough to hold one. + {"fords", "Fords", 1, 5, 1, 3, ControlGroup::Layout}, + // Off, the trunk reaches the sea through a single mouth, with no delta lobe. + GeneratorControl::toggle("river-delta", "River delta", true, ControlGroup::Terrain), + // Off, rivers run without their meanders. + GeneratorControl::toggle("meanders", "Meandering rivers", true, ControlGroup::Terrain), + // The farmland along the rivers (wheat and wood each), stone outcrops, fruit groves at + // the confluences and algae off the mouths and shallows. Every colony's starter kit + // stays as it is. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validate, + validateWorld}; } diff --git a/src/map/generator/generators/WatershedGenerator.h b/src/map/generator/generators/WatershedGenerator.h index 75377205c..f6dc0f0cd 100644 --- a/src/map/generator/generators/WatershedGenerator.h +++ b/src/map/generator/generators/WatershedGenerator.h @@ -2,10 +2,11 @@ #pragma once #include "GenerationRequest.h" #include "GeneratorDefinition.h" -struct WatershedOptions { - int riverDensity, riverWidth, dryness, fords; - bool delta, meanders; - int wheat, wood, stone, algae, fruit; // percentages of the default amounts - explicit WatershedOptions(const GenerationRequest &r); +struct WatershedOptions +{ + int riverDensity, riverWidth, dryness, fords; + bool delta, meanders; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit WatershedOptions(const GenerationRequest &r); }; GeneratorDefinition watershedDefinition(); diff --git a/src/map/generator/shared/Distances.cpp b/src/map/generator/shared/Distances.cpp index 714b9d56c..7476db193 100644 --- a/src/map/generator/shared/Distances.cpp +++ b/src/map/generator/shared/Distances.cpp @@ -102,7 +102,7 @@ void computeDistances(Map &map, std::vector &sources, deltaAddrC[6] = (yd << wDec) | xl; deltaAddrC[7] = (y << wDec) | xl; for (int ci = 0; ci < 8; ci++) // Check for each of this fields if we - { // can improve its gradient value + { // can improve its gradient value addr = &heightmap[deltaAddrC[ci]]; side = *addr; if (side == 0) diff --git a/src/map/generator/shared/HeightMap.h b/src/map/generator/shared/HeightMap.h index 803f44d25..b033bd86e 100644 --- a/src/map/generator/shared/HeightMap.h +++ b/src/map/generator/shared/HeightMap.h @@ -6,12 +6,12 @@ #include "Noise.h" class HeightMap /// class to generate heightmaps to decide where to put resources, water, sand and - /// grass later +/// grass later { - float *_map; /// height values are always [0,1]. + float *_map; /// height values are always [0,1]. unsigned int _w, _h; /// map size - float *_stamp; /// smooth 0 to 1 gradient lookup to generate craters, islands and rivers - unsigned int _r; /// radius of the _stamp + float *_stamp; /// smooth 0 to 1 gradient lookup to generate craters, islands and rivers + unsigned int _r; /// radius of the _stamp GenerationNoise _pn; std::mt19937 &random; unsigned oldLowerX = ~0u, oldLowerY = ~0u, oldDifferenceX = ~0u, @@ -75,5 +75,5 @@ class HeightMap /// class to generate heightmaps to decide where to put resource /// 0..255 values of stamp to it. to see it, use convert -size /// [width]x[height] -depth 8 gray:[filename] test.png where /// with==height as _stamp is always a square - void normalize(); /// fits the values of _map to [0, 1] + void normalize(); /// fits the values of _map to [0, 1] }; diff --git a/src/map/generator/shared/Noise.cpp b/src/map/generator/shared/Noise.cpp index 0c01f2f66..ab6ae3196 100644 --- a/src/map/generator/shared/Noise.cpp +++ b/src/map/generator/shared/Noise.cpp @@ -30,7 +30,10 @@ d1 = d0 - 1.0; // Constructor -GenerationNoise::GenerationNoise(unsigned int seed) { reseed(seed); } +GenerationNoise::GenerationNoise(unsigned int seed) +{ + reseed(seed); +} // Destructor GenerationNoise::~GenerationNoise() {} @@ -216,7 +219,10 @@ float GenerationNoise::Noise3d(float pos[3]) // you can call noise component-wise, too. -float GenerationNoise::Noise(float x) { return Noise1d(&x); } +float GenerationNoise::Noise(float x) +{ + return Noise1d(&x); +} float GenerationNoise::Noise(float x, float y) { diff --git a/src/map/generator/shared/Regions.cpp b/src/map/generator/shared/Regions.cpp index 994ad31b9..1994eb267 100644 --- a/src/map/generator/shared/Regions.cpp +++ b/src/map/generator/shared/Regions.cpp @@ -104,8 +104,8 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, std::vector startingPoints; { const int w = map.getW(); - collectPointsColumnOrder(w, map.getH(), [&](int x, int y) { return grid[y * w + x] == areaN; }, - startingPoints); + collectPointsColumnOrder( + w, map.getH(), [&](int x, int y) { return grid[y * w + x] == areaN; }, startingPoints); } if (startingPoints.size() < points.size()) @@ -134,8 +134,8 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, for (int x = 0; x < w; ++x) max = std::max(max, heights[y * w + x]); std::vector possible; - collectPointsColumnOrder(w, h2, [&](int x, int y) { return heights[y * w + x] >= max; }, - possible); + collectPointsColumnOrder( + w, h2, [&](int x, int y) { return heights[y * w + x] >= max; }, possible); int n = context.stream("regions")() % possible.size(); points[i] = possible[n]; sources.push_back(points[i]); @@ -451,7 +451,7 @@ void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, i } for (int ci = 0; ci < 8; ci++) // Check for each of this fields if we - { // can improve its gradient value + { // can improve its gradient value addr = &gradient[deltaAddrC[ci]]; side = *addr; if (side == 0 && grid[deltaAddrC[ci]] == areaN) @@ -480,16 +480,16 @@ void getAllPoints(Map &map, std::vector &grid, int areaN, std::vector &points) { const int w = map.getW(); - collectPointsColumnOrder(w, map.getH(), [&](int x, int y) { return grid[y * w + x] == areaN; }, - points); + collectPointsColumnOrder( + w, map.getH(), [&](int x, int y) { return grid[y * w + x] == areaN; }, points); } void getAllOtherPoints(Map &map, std::vector &grid, int areaN, std::vector &points) { const int w = map.getW(); - collectPointsColumnOrder(w, map.getH(), [&](int x, int y) { return grid[y * w + x] != areaN; }, - points); + collectPointsColumnOrder( + w, map.getH(), [&](int x, int y) { return grid[y * w + x] != areaN; }, points); } void getAllPointsLine(Map &map, int x1, int y1, int x2, int y2, diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 171fa53c8..207cf90fd 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -20,79 +20,87 @@ #include using namespace MapGeneration; -namespace MapGeneration { -int placeResourceClump(Map &map, GenerationContext &context, - MapGeneratorPoint center, int resourceType, int radius) { - int placed = 0; - for (int dy = -radius; dy <= radius; ++dy) - for (int dx = -radius; dx <= radius; ++dx) { - const int d2 = dx * dx + dy * dy; - if (d2 > radius * radius || (d2 > (radius - 1) * (radius - 1) && - context.bounded("resources", 4) == 0)) - continue; - const int x = map.normalizeX(center.x + dx), - y = map.normalizeY(center.y + dy); - const int existingType = map.getResource(x, y).type; - if (map.isResourceAllowed(x, y, resourceType) && - (existingType == NO_RES_TYPE || existingType == resourceType)) { - map.setResource(x, y, resourceType, 1); - ++placed; - } - } - return placed; +namespace MapGeneration +{ +int placeResourceClump(Map &map, GenerationContext &context, MapGeneratorPoint center, + int resourceType, int radius) +{ + int placed = 0; + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) + { + const int d2 = dx * dx + dy * dy; + if (d2 > radius * radius || + (d2 > (radius - 1) * (radius - 1) && context.bounded("resources", 4) == 0)) + continue; + const int x = map.normalizeX(center.x + dx), y = map.normalizeY(center.y + dy); + const int existingType = map.getResource(x, y).type; + if (map.isResourceAllowed(x, y, resourceType) && + (existingType == NO_RES_TYPE || existingType == resourceType)) + { + map.setResource(x, y, resourceType, 1); + ++placed; + } + } + return placed; } -void setScaledResource(Map &map, int x, int y, int resourceType, int size, int percent) { - if (percent == 100) { - map.setResource(x, y, resourceType, size); - return; - } - const int half = size >> 1; - const int target = int(scaledCount(std::int64_t(2 * half + 1) * (2 * half + 1), percent)); - if (target <= 0) - return; - int reach = 0; - while ((2 * reach + 1) * (2 * reach + 1) < target) - ++reach; - const int side = 2 * reach + 1; - // Offset i is column i / side, row i % side, the order setResource visits its square in. Keep - // the `target` offsets nearest the centre (by ring, then by distance round it). - const auto rank = [side, reach](int i) { - const int dx = std::abs(i / side - reach), dy = std::abs(i % side - reach); - return std::make_pair(std::max(dx, dy), dx + dy); - }; - std::vector order(size_t(side) * side); - for (size_t i = 0; i < order.size(); ++i) - order[i] = int(i); - std::stable_sort(order.begin(), order.end(), [&](int a, int b) { return rank(a) < rank(b); }); - std::vector chosen(order.size(), 0); - for (int k = 0; k < target; ++k) - chosen[size_t(order[size_t(k)])] = 1; - for (int i = 0; i < int(chosen.size()); ++i) - if (chosen[size_t(i)]) - map.setResource(map.normalizeX(x + i / side - reach), map.normalizeY(y + i % side - reach), - resourceType, 1); +void setScaledResource(Map &map, int x, int y, int resourceType, int size, int percent) +{ + if (percent == 100) + { + map.setResource(x, y, resourceType, size); + return; + } + const int half = size >> 1; + const int target = int(scaledCount(std::int64_t(2 * half + 1) * (2 * half + 1), percent)); + if (target <= 0) + return; + int reach = 0; + while ((2 * reach + 1) * (2 * reach + 1) < target) + ++reach; + const int side = 2 * reach + 1; + // Offset i is column i / side, row i % side, the order setResource visits its square in. Keep + // the `target` offsets nearest the centre (by ring, then by distance round it). + const auto rank = [side, reach](int i) + { + const int dx = std::abs(i / side - reach), dy = std::abs(i % side - reach); + return std::make_pair(std::max(dx, dy), dx + dy); + }; + std::vector order(size_t(side) * side); + for (size_t i = 0; i < order.size(); ++i) + order[i] = int(i); + std::stable_sort(order.begin(), order.end(), [&](int a, int b) { return rank(a) < rank(b); }); + std::vector chosen(order.size(), 0); + for (int k = 0; k < target; ++k) + chosen[size_t(order[size_t(k)])] = 1; + for (int i = 0; i < int(chosen.size()); ++i) + if (chosen[size_t(i)]) + map.setResource(map.normalizeX(x + i / side - reach), + map.normalizeY(y + i % side - reach), resourceType, 1); } int placeResourceClumpInArea(Map &map, GenerationContext &context, - const std::vector &points, - int resourceType, int radius) { - if (points.empty()) - return 0; - const size_t first = context.bounded("resources", points.size()); - for (size_t offset = 0; offset < points.size(); ++offset) { - const MapGeneratorPoint ¢er = points[(first + offset) % points.size()]; - if (!map.isResourceAllowed(center.x, center.y, resourceType)) - continue; - const int placed = - placeResourceClump(map, context, center, resourceType, radius); - if (placed) - return placed; - } - return 0; + const std::vector &points, int resourceType, + int radius) +{ + if (points.empty()) + return 0; + const size_t first = context.bounded("resources", points.size()); + for (size_t offset = 0; offset < points.size(); ++offset) + { + const MapGeneratorPoint ¢er = points[(first + offset) % points.size()]; + if (!map.isResourceAllowed(center.x, center.y, resourceType)) + continue; + const int placed = placeResourceClump(map, context, center, resourceType, radius); + if (placed) + return placed; + } + return 0; } -namespace { +namespace +{ // generateHeightField (Terrain.cpp) carves its own resource bands from the same noise field // that carved the terrain: every tile's resource is a threshold on a value it already has, so // stone and algae come out following the same contours the water and grass do, rather than @@ -123,90 +131,100 @@ namespace { // water into separate bodies (a lake apart from the sea). Algae only places on water, so an algae // band has to be shared out between water bodies the same way a stone band is shared out between // landmasses; drawn over land components it would never find a single candidate. -std::vector computeComponents(const Map &map, bool water, int &numComponents) { - const int w = map.getW(), h = map.getH(); - std::vector component(size_t(w) * h, -1); - // Bounded by the component[np] < 0 guard below to at most w*h enqueues per component, and - // every tile belongs to exactly one component - a flat preallocated FIFO reused across - // components, the same fix as floodReach below and computeDistances in Distances.cpp. - std::vector queue(size_t(w) * h); - int nextId = 0; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const int start = y * w + x; - if (component[start] != -1 || map.isWater(x, y) != water) - continue; - size_t qHead = 0, qTail = 0; - component[start] = nextId; - queue[qTail++] = start; - while (qHead < qTail) { - const int p = queue[qHead++]; - const int px = p % w, py = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (!dx && !dy) - continue; - const int nx = map.normalizeX(px + dx), ny = map.normalizeY(py + dy); - const int np = ny * w + nx; - if (component[np] == -1 && map.isWater(nx, ny) == water) { - component[np] = nextId; - queue[qTail++] = np; - } - } - } - ++nextId; - } - numComponents = nextId; - return component; +std::vector computeComponents(const Map &map, bool water, int &numComponents) +{ + const int w = map.getW(), h = map.getH(); + std::vector component(size_t(w) * h, -1); + // Bounded by the component[np] < 0 guard below to at most w*h enqueues per component, and + // every tile belongs to exactly one component - a flat preallocated FIFO reused across + // components, the same fix as floodReach below and computeDistances in Distances.cpp. + std::vector queue(size_t(w) * h); + int nextId = 0; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const int start = y * w + x; + if (component[start] != -1 || map.isWater(x, y) != water) + continue; + size_t qHead = 0, qTail = 0; + component[start] = nextId; + queue[qTail++] = start; + while (qHead < qTail) + { + const int p = queue[qHead++]; + const int px = p % w, py = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int nx = map.normalizeX(px + dx), ny = map.normalizeY(py + dy); + const int np = ny * w + nx; + if (component[np] == -1 && map.isWater(nx, ny) == water) + { + component[np] = nextId; + queue[qTail++] = np; + } + } + } + ++nextId; + } + numComponents = nextId; + return component; } void scatterBand(Map &map, HeightMap &noise, int resourceType, int targetTiles, - const std::vector &component, int numComponents) { - if (targetTiles <= 0 || numComponents <= 0) - return; - const int width = map.getW(), height = map.getH(); - constexpr unsigned kBuckets = 2048; - std::vector> candidates(numComponents); - std::vector> level(numComponents); - std::vector> histogram(numComponents, std::vector(kBuckets, 0)); - int totalCandidates = 0; - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - if (map.getResource(x, y).type != NO_RES_TYPE) - continue; - if (!map.isResourceAllowed(x, y, resourceType)) - continue; - const int comp = component[y * width + x]; - if (comp < 0) - continue; - const unsigned lvl = noise.uiLevel(x, y, kBuckets); - candidates[comp].emplace_back(x, y); - level[comp].push_back(lvl); - ++histogram[comp][lvl]; - ++totalCandidates; - } - if (totalCandidates == 0) - return; - for (int c = 0; c < numComponents; ++c) { - const auto &compCandidates = candidates[c]; - if (compCandidates.empty()) - continue; - const int share = - int((std::int64_t(targetTiles) * std::int64_t(compCandidates.size())) / totalCandidates); - const int wanted = std::min(compCandidates.size(), std::max(1, share)); - unsigned threshold = kBuckets - 1; - int accumulated = 0; - for (unsigned b = 0; b < kBuckets; ++b) { - accumulated += histogram[c][b]; - if (accumulated >= wanted) { - threshold = b; - break; - } - } - for (size_t i = 0; i < compCandidates.size(); ++i) - if (level[c][i] <= threshold) - map.setResource(compCandidates[i].x, compCandidates[i].y, resourceType, 1); - } + const std::vector &component, int numComponents) +{ + if (targetTiles <= 0 || numComponents <= 0) + return; + const int width = map.getW(), height = map.getH(); + constexpr unsigned kBuckets = 2048; + std::vector> candidates(numComponents); + std::vector> level(numComponents); + std::vector> histogram(numComponents, std::vector(kBuckets, 0)); + int totalCandidates = 0; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + if (map.getResource(x, y).type != NO_RES_TYPE) + continue; + if (!map.isResourceAllowed(x, y, resourceType)) + continue; + const int comp = component[y * width + x]; + if (comp < 0) + continue; + const unsigned lvl = noise.uiLevel(x, y, kBuckets); + candidates[comp].emplace_back(x, y); + level[comp].push_back(lvl); + ++histogram[comp][lvl]; + ++totalCandidates; + } + if (totalCandidates == 0) + return; + for (int c = 0; c < numComponents; ++c) + { + const auto &compCandidates = candidates[c]; + if (compCandidates.empty()) + continue; + const int share = int((std::int64_t(targetTiles) * std::int64_t(compCandidates.size())) / + totalCandidates); + const int wanted = std::min(compCandidates.size(), std::max(1, share)); + unsigned threshold = kBuckets - 1; + int accumulated = 0; + for (unsigned b = 0; b < kBuckets; ++b) + { + accumulated += histogram[c][b]; + if (accumulated >= wanted) + { + threshold = b; + break; + } + } + for (size_t i = 0; i < compCandidates.size(); ++i) + if (level[c][i] <= threshold) + map.setResource(compCandidates[i].x, compCandidates[i].y, resourceType, 1); + } } // Map::growResources only regrows a wheat or wood tile near water (the same triangular kernel @@ -222,198 +240,222 @@ void scatterBand(Map &map, HeightMap &noise, int resourceType, int targetTiles, // unrelated noise field splits that region into corn and wood, so which crop lands where varies // along the coast rather than with distance from it. void scatterFarmland(Map &map, const Fertility::Field &fertility, HeightMap &splitNoise, - int cornTarget, int woodTarget, const std::vector &landComponent, - int numComponents) { - const int totalTarget = cornTarget + woodTarget; - if (totalTarget <= 0 || numComponents <= 0) - return; - const int width = map.getW(), height = map.getH(); - constexpr unsigned kBuckets = 2048; - constexpr int kWiden = 3; // the fertile region reaches roughly 3x past the requested tile count - std::vector> candidates(numComponents); - std::vector> fertLevel(numComponents); - std::vector> fertHistogram(numComponents, std::vector(kBuckets, 0)); - int totalCandidates = 0; - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) { - if (map.getResource(x, y).type != NO_RES_TYPE) - continue; - // Wood and corn share the same terrain requirement (grass), so either stands in for - // eligibility here - which of the two a tile ends up with is decided below, by the split. - if (!map.isResourceAllowed(x, y, CORN)) - continue; - const std::uint32_t f = fertility.at(x, y); - if (f == 0) - continue; - const int comp = landComponent[y * width + x]; - if (comp < 0) - continue; - const unsigned lvl = - kBuckets - 1 - std::min(kBuckets - 1, unsigned((f * kBuckets) / Fertility::kScale)); - candidates[comp].emplace_back(x, y); - fertLevel[comp].push_back(lvl); - ++fertHistogram[comp][lvl]; - ++totalCandidates; - } - if (totalCandidates == 0) - return; - for (int c = 0; c < numComponents; ++c) { - const auto &compCandidates = candidates[c]; - if (compCandidates.empty()) - continue; - const int share = - int((std::int64_t(totalTarget) * std::int64_t(compCandidates.size())) / totalCandidates); - // Widening is proportionally huge on a landmass with little eligible ground to begin with - - // a small island can have its whole candidate pool absorbed by kWiden's multiplier even - // though the unwidened share alone would have left most of it free. Capping the region - // at 2/3 of the component's own pool guarantees slack to route around it regardless of - // component size, instead of only checking against the (unrelated) map-wide total above. - const int widenedShare = std::max(1, share) * kWiden; - const int slackCap = std::max(1, int(compCandidates.size()) * 2 / 3); - const int regionWanted = std::min(compCandidates.size(), std::min(widenedShare, slackCap)); - unsigned threshold = kBuckets - 1; - int accumulated = 0; - for (unsigned b = 0; b < kBuckets; ++b) { - accumulated += fertHistogram[c][b]; - if (accumulated >= regionWanted) { - threshold = b; - break; - } - } - std::vector region; - for (size_t i = 0; i < compCandidates.size(); ++i) - if (fertLevel[c][i] <= threshold) - region.push_back(compCandidates[i]); - if (region.empty()) - continue; - // Split the region by an independent noise field instead of fertility - sort it into that - // field's order and slice off the lowest share for corn, the next share for wood, so the - // two form separate patches following the noise field's own organic contours rather than - // corn's band always sitting closer to the water than wood's. - std::vector order(region.size()); - for (size_t i = 0; i < order.size(); ++i) - order[i] = i; - std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { - return splitNoise.uiLevel(region[a].x, region[a].y, kBuckets) < - splitNoise.uiLevel(region[b].x, region[b].y, kBuckets); - }); - const int cornWanted = std::min( - region.size(), - std::max(0, int((std::int64_t(cornTarget) * std::int64_t(region.size())) / totalTarget))); - const int woodWanted = std::min( - int(region.size()) - cornWanted, - std::max(0, int((std::int64_t(woodTarget) * std::int64_t(region.size())) / totalTarget))); - for (int i = 0; i < cornWanted; ++i) - map.setResource(region[order[i]].x, region[order[i]].y, CORN, 1); - for (int i = cornWanted; i < cornWanted + woodWanted; ++i) - map.setResource(region[order[i]].x, region[order[i]].y, WOOD, 1); - } + int cornTarget, int woodTarget, const std::vector &landComponent, + int numComponents) +{ + const int totalTarget = cornTarget + woodTarget; + if (totalTarget <= 0 || numComponents <= 0) + return; + const int width = map.getW(), height = map.getH(); + constexpr unsigned kBuckets = 2048; + constexpr int kWiden = 3; // the fertile region reaches roughly 3x past the requested tile count + std::vector> candidates(numComponents); + std::vector> fertLevel(numComponents); + std::vector> fertHistogram(numComponents, std::vector(kBuckets, 0)); + int totalCandidates = 0; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + if (map.getResource(x, y).type != NO_RES_TYPE) + continue; + // Wood and corn share the same terrain requirement (grass), so either stands in for + // eligibility here - which of the two a tile ends up with is decided below, by the split. + if (!map.isResourceAllowed(x, y, CORN)) + continue; + const std::uint32_t f = fertility.at(x, y); + if (f == 0) + continue; + const int comp = landComponent[y * width + x]; + if (comp < 0) + continue; + const unsigned lvl = + kBuckets - 1 - + std::min(kBuckets - 1, unsigned((f * kBuckets) / Fertility::kScale)); + candidates[comp].emplace_back(x, y); + fertLevel[comp].push_back(lvl); + ++fertHistogram[comp][lvl]; + ++totalCandidates; + } + if (totalCandidates == 0) + return; + for (int c = 0; c < numComponents; ++c) + { + const auto &compCandidates = candidates[c]; + if (compCandidates.empty()) + continue; + const int share = int((std::int64_t(totalTarget) * std::int64_t(compCandidates.size())) / + totalCandidates); + // Widening is proportionally huge on a landmass with little eligible ground to begin with - + // a small island can have its whole candidate pool absorbed by kWiden's multiplier even + // though the unwidened share alone would have left most of it free. Capping the region + // at 2/3 of the component's own pool guarantees slack to route around it regardless of + // component size, instead of only checking against the (unrelated) map-wide total above. + const int widenedShare = std::max(1, share) * kWiden; + const int slackCap = std::max(1, int(compCandidates.size()) * 2 / 3); + const int regionWanted = + std::min(compCandidates.size(), std::min(widenedShare, slackCap)); + unsigned threshold = kBuckets - 1; + int accumulated = 0; + for (unsigned b = 0; b < kBuckets; ++b) + { + accumulated += fertHistogram[c][b]; + if (accumulated >= regionWanted) + { + threshold = b; + break; + } + } + std::vector region; + for (size_t i = 0; i < compCandidates.size(); ++i) + if (fertLevel[c][i] <= threshold) + region.push_back(compCandidates[i]); + if (region.empty()) + continue; + // Split the region by an independent noise field instead of fertility - sort it into that + // field's order and slice off the lowest share for corn, the next share for wood, so the + // two form separate patches following the noise field's own organic contours rather than + // corn's band always sitting closer to the water than wood's. + std::vector order(region.size()); + for (size_t i = 0; i < order.size(); ++i) + order[i] = i; + std::sort(order.begin(), order.end(), + [&](size_t a, size_t b) + { + return splitNoise.uiLevel(region[a].x, region[a].y, kBuckets) < + splitNoise.uiLevel(region[b].x, region[b].y, kBuckets); + }); + const int cornWanted = + std::min(region.size(), + std::max(0, int((std::int64_t(cornTarget) * std::int64_t(region.size())) / + totalTarget))); + const int woodWanted = + std::min(int(region.size()) - cornWanted, + std::max(0, int((std::int64_t(woodTarget) * std::int64_t(region.size())) / + totalTarget))); + for (int i = 0; i < cornWanted; ++i) + map.setResource(region[order[i]].x, region[order[i]].y, CORN, 1); + for (int i = cornWanted; i < cornWanted + woodWanted; ++i) + map.setResource(region[order[i]].x, region[order[i]].y, WOOD, 1); + } } } // namespace -void scatterResources(Game &game, GenerationContext &context, - const ResourceDensities &density) { - Map &map = game.map; - const int width = map.getW(), height = map.getH(), area = width * height; - const Fertility::Field fertility = Fertility::forMap(map, false); - HeightMap noise(width, height, context.stream("scatter-noise")); - noise.makePlain(24); - // A much finer scale than the terrain-shaping noise above: this one only decides which of two - // *adjacent* crops a farmland tile becomes, not where the coastline itself bends, so its - // features need to be sized like a player's local working area (tens of tiles), not like a - // continent (hundreds). Reusing 24 here first produced patches wide enough that a zoomed-in - // view could sit entirely inside one - corn and wood alternated across the whole map, but not - // within reach of any one colony, so a colony's own farmland still read as a solid wall of one - // crop with the other hidden behind it. - HeightMap splitNoise(width, height, context.stream("scatter-split")); - splitNoise.makePlain(6); - int numComponents = 0; - const std::vector landComponent = computeComponents(map, false, numComponents); +void scatterResources(Game &game, GenerationContext &context, const ResourceDensities &density) +{ + Map &map = game.map; + const int width = map.getW(), height = map.getH(), area = width * height; + const Fertility::Field fertility = Fertility::forMap(map, false); + HeightMap noise(width, height, context.stream("scatter-noise")); + noise.makePlain(24); + // A much finer scale than the terrain-shaping noise above: this one only decides which of two + // *adjacent* crops a farmland tile becomes, not where the coastline itself bends, so its + // features need to be sized like a player's local working area (tens of tiles), not like a + // continent (hundreds). Reusing 24 here first produced patches wide enough that a zoomed-in + // view could sit entirely inside one - corn and wood alternated across the whole map, but not + // within reach of any one colony, so a colony's own farmland still read as a solid wall of one + // crop with the other hidden behind it. + HeightMap splitNoise(width, height, context.stream("scatter-split")); + splitNoise.makePlain(6); + int numComponents = 0; + const std::vector landComponent = computeComponents(map, false, numComponents); - scatterFarmland(map, fertility, splitNoise, density.corn * area / 1600, - density.wood * area / 1600, landComponent, numComponents); - scatterBand(map, noise, STONE, density.stone * area / 3000, landComponent, numComponents); - if (density.algae > 0) { - int numWaterBodies = 0; - const std::vector waterBody = computeComponents(map, true, numWaterBodies); - scatterBand(map, noise, ALGA, density.algae * area / 800, waterBody, numWaterBodies); - } + scatterFarmland(map, fertility, splitNoise, density.corn * area / 1600, + density.wood * area / 1600, landComponent, numComponents); + scatterBand(map, noise, STONE, density.stone * area / 3000, landComponent, numComponents); + if (density.algae > 0) + { + int numWaterBodies = 0; + const std::vector waterBody = computeComponents(map, true, numWaterBodies); + scatterBand(map, noise, ALGA, density.algae * area / 800, waterBody, numWaterBodies); + } - // Fruit stays a rare, discrete find rather than a background band - "a distinct little - // prize", the same role it already plays elsewhere in these generators - so it keeps the - // original single-clump-at-a-random-point search instead of joining the noise bands above. - for (int i = 0; i < density.fruit; ++i) { - const int type = CHERRY + context.bounded("resources", 3); - MapGeneratorPoint center(0, 0); - bool found = false; - for (int attempt = 0; attempt < 100 && !found; ++attempt) { - center = {int(context.bounded("resources", width)), int(context.bounded("resources", height))}; - found = map.isResourceAllowed(center.x, center.y, type); - } - if (found) - placeResourceClump(map, context, center, type, 1); - } + // Fruit stays a rare, discrete find rather than a background band - "a distinct little + // prize", the same role it already plays elsewhere in these generators - so it keeps the + // original single-clump-at-a-random-point search instead of joining the noise bands above. + for (int i = 0; i < density.fruit; ++i) + { + const int type = CHERRY + context.bounded("resources", 3); + MapGeneratorPoint center(0, 0); + bool found = false; + for (int attempt = 0; attempt < 100 && !found; ++attempt) + { + center = {int(context.bounded("resources", width)), + int(context.bounded("resources", height))}; + found = map.isResourceAllowed(center.x, center.y, type); + } + if (found) + placeResourceClump(map, context, center, type, 1); + } } -void fillInResource(Map &map, GenerationContext &context, - std::vector &points, int resourceType, - int maxFillSize) { - if (maxFillSize < 1 || maxFillSize >= std::min(map.getW(), map.getH())) - throw GenerationFailure("Invalid resource patch size"); - for (unsigned int n = 0; n < points.size(); ++n) { - map.setResource(points[n].x, points[n].y, resourceType, - 1 + context.stream("regions")() % maxFillSize); - } +void fillInResource(Map &map, GenerationContext &context, std::vector &points, + int resourceType, int maxFillSize) +{ + if (maxFillSize < 1 || maxFillSize >= std::min(map.getW(), map.getH())) + throw GenerationFailure("Invalid resource patch size"); + for (unsigned int n = 0; n < points.size(); ++n) + { + map.setResource(points[n].x, points[n].y, resourceType, + 1 + context.stream("regions")() % maxFillSize); + } } -namespace { +namespace +{ // Ground units can't walk onto a tile carrying a resource (Map::isHardSpaceForGroundUnit // excludes them), so a solid, unbroken band from the noise-band resource painting can wall a // team's boot tile off from the rest of an otherwise perfectly connected landmass. floodReach // walks that real, resource-respecting space; dist is kept (not just aggregated) so a caller // can compare it against a wall-blind flood and find exactly where such a wall runs. -struct ReachResult { - int wheatDist = -1, woodDist = -1; - std::vector closeGrass, farGrass; - std::vector dist; +struct ReachResult +{ + int wheatDist = -1, woodDist = -1; + std::vector closeGrass, farGrass; + std::vector dist; }; ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int closeRange, - int clearRadius) { - const int w = map.getW(), h = map.getH(); - ReachResult r; - r.dist.assign(size_t(w) * h, -1); - // Bounded by the r.dist[np] < 0 guard below to at most w*h enqueues - a flat preallocated - // FIFO instead of std::queue's std::deque, which grows by separately heap-allocated - // blocks (same fix as computeDistances in Distances.cpp). - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - int start = bootY * w + bootX; - r.dist[start] = 0; - q[qTail++] = start; - while (qHead < qTail) { - int p = q[qHead++]; - int x = p % w, y = p / w; - if (map.getUMTerrain(x, y) == GRASS && r.dist[p] >= clearRadius && r.dist[p] <= exploreLimit) - (r.dist[p] <= closeRange ? r.closeGrass : r.farGrass).push_back(MapGeneratorPoint(x, y)); - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int np = ny * w + nx; - int resType = map.getResource(nx, ny).type; - if (resType == CORN && r.wheatDist < 0) - r.wheatDist = r.dist[p] + 1; - if (resType == WOOD && r.woodDist < 0) - r.woodDist = r.dist[p] + 1; - if (r.dist[np] < 0 && r.dist[p] < exploreLimit && - map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { - r.dist[np] = r.dist[p] + 1; - q[qTail++] = np; - } - } - } - return r; + int clearRadius) +{ + const int w = map.getW(), h = map.getH(); + ReachResult r; + r.dist.assign(size_t(w) * h, -1); + // Bounded by the r.dist[np] < 0 guard below to at most w*h enqueues - a flat preallocated + // FIFO instead of std::queue's std::deque, which grows by separately heap-allocated + // blocks (same fix as computeDistances in Distances.cpp). + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; + int start = bootY * w + bootX; + r.dist[start] = 0; + q[qTail++] = start; + while (qHead < qTail) + { + int p = q[qHead++]; + int x = p % w, y = p / w; + if (map.getUMTerrain(x, y) == GRASS && r.dist[p] >= clearRadius && + r.dist[p] <= exploreLimit) + (r.dist[p] <= closeRange ? r.closeGrass : r.farGrass) + .push_back(MapGeneratorPoint(x, y)); + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * w + nx; + int resType = map.getResource(nx, ny).type; + if (resType == CORN && r.wheatDist < 0) + r.wheatDist = r.dist[p] + 1; + if (resType == WOOD && r.woodDist < 0) + r.woodDist = r.dist[p] + 1; + if (r.dist[np] < 0 && r.dist[p] < exploreLimit && + map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + r.dist[np] = r.dist[p] + 1; + q[qTail++] = np; + } + } + } + return r; } // The same flood, but blocked only by water — as if resources didn't exist. Diffing this @@ -422,32 +464,37 @@ ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int clo // entire neighborhood) opens the way while disturbing nothing else nearby. Protected walls are // part of the terrain, so they block this flood as well. std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit, - const std::vector *protectedWalls) { - const int w = map.getW(), h = map.getH(); - std::vector dist(size_t(w) * h, -1); - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - int start = bootY * w + bootX; - dist[start] = 0; - q[qTail++] = start; - while (qHead < qTail) { - int p = q[qHead++]; - if (dist[p] >= limit) - continue; - int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int np = ny * w + nx; - if (dist[np] < 0 && !map.isWater(nx, ny) && !(protectedWalls && (*protectedWalls)[np])) { - dist[np] = dist[p] + 1; - q[qTail++] = np; - } - } - } - return dist; + const std::vector *protectedWalls) +{ + const int w = map.getW(), h = map.getH(); + std::vector dist(size_t(w) * h, -1); + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; + int start = bootY * w + bootX; + dist[start] = 0; + q[qTail++] = start; + while (qHead < qTail) + { + int p = q[qHead++]; + if (dist[p] >= limit) + continue; + int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int np = ny * w + nx; + if (dist[np] < 0 && !map.isWater(nx, ny) && + !(protectedWalls && (*protectedWalls)[np])) + { + dist[np] = dist[p] + 1; + q[qTail++] = np; + } + } + } + return dist; } // Clears the resource tiles directly responsible for a team's cramped pocket: exactly the @@ -458,167 +505,192 @@ std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit, // few times by the caller in case a wall is thicker still. A protected wall tile is never // cleared. bool clearResourceWall(Map &map, const std::vector &boxedDist, - const std::vector &openDist, int minGain, - const std::vector *protectedWalls) { - const int w = map.getW(), h = map.getH(); - int cleared = 0; - std::vector> toClear; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - int p = y * w + x; - if (boxedDist[p] >= 0 || openDist[p] < 0 || !map.isResource(x, y) || - (protectedWalls && (*protectedWalls)[p])) - continue; - bool touchesBoxed = false; - for (int dy = -1; dy <= 1 && !touchesBoxed; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - if (boxedDist[ny * w + nx] >= 0) { - touchesBoxed = true; - break; - } - } - if (touchesBoxed) - toClear.push_back({x, y}); - } - if ((int)toClear.size() < minGain) - return false; - for (auto [x, y] : toClear) { - map.setNoResource(x, y, 1); - ++cleared; - } - return cleared > 0; + const std::vector &openDist, int minGain, + const std::vector *protectedWalls) +{ + const int w = map.getW(), h = map.getH(); + int cleared = 0; + std::vector> toClear; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + int p = y * w + x; + if (boxedDist[p] >= 0 || openDist[p] < 0 || !map.isResource(x, y) || + (protectedWalls && (*protectedWalls)[p])) + continue; + bool touchesBoxed = false; + for (int dy = -1; dy <= 1 && !touchesBoxed; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + if (boxedDist[ny * w + nx] >= 0) + { + touchesBoxed = true; + break; + } + } + if (touchesBoxed) + toClear.push_back({x, y}); + } + if ((int)toClear.size() < minGain) + return false; + for (auto [x, y] : toClear) + { + map.setNoResource(x, y, 1); + ++cleared; + } + return cleared > 0; } } // namespace void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, - int woodRange, int clearRadius, - const std::vector *protectedWalls) { - Map &map = game.map; - if (protectedWalls && protectedWalls->size() != size_t(map.getW()) * map.getH()) - throw GenerationFailure("Protected wall mask does not match the map size"); - const int exploreLimit = std::max(wheatRange, woodRange) * 5 / 2; - const int closeRange = wheatRange / 2; - // Below this many reached tiles a team is badly boxed in, but that has two very different - // causes: a resource wall sealing off an otherwise fine landmass (swamp and river both paint - // resources with no regard for what they might enclose), or the boot search simply landing on - // a genuinely small spot — a real little island on a water-heavy map, say, where there's - // nothing bigger on the other side to reach at all. Only the first is fixable, and only its - // exact wall tiles are cleared, so a genuinely small spot is correctly left untouched. - const int minPocketTiles = 60; - for (int team = 0; team < context.request.nbTeams; ++team) { - // A caller can hand over a boot tile it has not wrapped onto the map yet: the height-field - // generators' fallback site search does, before placeStarts normalizes it. The floods below - // index by it directly. - int bootX = map.normalizeX(context.bootX[team]), bootY = map.normalizeY(context.bootY[team]); - ReachResult reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); - auto pocketSize = [&] { - int n = 0; - for (int v : reach.dist) - if (v >= 0) - ++n; - return n; - }; - bool underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || - reach.woodDist < 0 || reach.woodDist > woodRange; - for (int attempt = 0; underServed && pocketSize() < minPocketTiles && attempt < 6; ++attempt) { - std::vector open = terrainOnlyReach(map, bootX, bootY, exploreLimit, protectedWalls); - if (!clearResourceWall(map, reach.dist, open, /*minGain=*/1, protectedWalls)) - break; - reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); - underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || reach.woodDist < 0 || - reach.woodDist > woodRange; - } - auto placeReachable = [&](int resourceType) { - if (!reach.closeGrass.empty() && - placeResourceClumpInArea(map, context, reach.closeGrass, resourceType, 2)) - return; - if (!reach.farGrass.empty()) - placeResourceClumpInArea(map, context, reach.farGrass, resourceType, 2); - }; - if (reach.wheatDist < 0 || reach.wheatDist > wheatRange) - placeReachable(CORN); - if (reach.woodDist < 0 || reach.woodDist > woodRange) - placeReachable(WOOD); - } + int woodRange, int clearRadius, + const std::vector *protectedWalls) +{ + Map &map = game.map; + if (protectedWalls && protectedWalls->size() != size_t(map.getW()) * map.getH()) + throw GenerationFailure("Protected wall mask does not match the map size"); + const int exploreLimit = std::max(wheatRange, woodRange) * 5 / 2; + const int closeRange = wheatRange / 2; + // Below this many reached tiles a team is badly boxed in, but that has two very different + // causes: a resource wall sealing off an otherwise fine landmass (swamp and river both paint + // resources with no regard for what they might enclose), or the boot search simply landing on + // a genuinely small spot — a real little island on a water-heavy map, say, where there's + // nothing bigger on the other side to reach at all. Only the first is fixable, and only its + // exact wall tiles are cleared, so a genuinely small spot is correctly left untouched. + const int minPocketTiles = 60; + for (int team = 0; team < context.request.nbTeams; ++team) + { + // A caller can hand over a boot tile it has not wrapped onto the map yet: the height-field + // generators' fallback site search does, before placeStarts normalizes it. The floods below + // index by it directly. + int bootX = map.normalizeX(context.bootX[team]), + bootY = map.normalizeY(context.bootY[team]); + ReachResult reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); + auto pocketSize = [&] + { + int n = 0; + for (int v : reach.dist) + if (v >= 0) + ++n; + return n; + }; + bool underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || + reach.woodDist < 0 || reach.woodDist > woodRange; + for (int attempt = 0; underServed && pocketSize() < minPocketTiles && attempt < 6; + ++attempt) + { + std::vector open = + terrainOnlyReach(map, bootX, bootY, exploreLimit, protectedWalls); + if (!clearResourceWall(map, reach.dist, open, /*minGain=*/1, protectedWalls)) + break; + reach = floodReach(map, bootX, bootY, exploreLimit, closeRange, clearRadius); + underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || + reach.woodDist < 0 || reach.woodDist > woodRange; + } + auto placeReachable = [&](int resourceType) + { + if (!reach.closeGrass.empty() && + placeResourceClumpInArea(map, context, reach.closeGrass, resourceType, 2)) + return; + if (!reach.farGrass.empty()) + placeResourceClumpInArea(map, context, reach.farGrass, resourceType, 2); + }; + if (reach.wheatDist < 0 || reach.wheatDist > wheatRange) + placeReachable(CORN); + if (reach.woodDist < 0 || reach.woodDist > woodRange) + placeReachable(WOOD); + } } -namespace { +namespace +{ // Where a team's own workers can walk within `range` — resources, water and buildings all block a // unit — and how many of the tiles reached start a 4x4 building. Seeded from the workers rather // than the boot tile, which is the swarm's own tile, so this counts the room a colony actually has. -struct WorkerReach { - std::vector dist; - int sites = 0; +struct WorkerReach +{ + std::vector dist; + int sites = 0; }; -WorkerReach reachFromWorkers(Map &map, int team, int range) { - const int w = map.getW(), h = map.getH(); - WorkerReach r; - r.dist.assign(size_t(w) * h, -1); - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) { - r.dist[size_t(y) * w + x] = 0; - q[qTail++] = y * w + x; - } - } - while (qHead < qTail) { - const int p = q[qHead++]; - const int x = p % w, y = p / w; - if (map.isFreeForBuilding(x, y, 4, 4)) - ++r.sites; - if (r.dist[p] >= range) - continue; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (dx == 0 && dy == 0) - continue; - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy), np = ny * w + nx; - if (r.dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) { - r.dist[np] = r.dist[p] + 1; - q[qTail++] = np; - } - } - } - return r; +WorkerReach reachFromWorkers(Map &map, int team, int range) +{ + const int w = map.getW(), h = map.getH(); + WorkerReach r; + r.dist.assign(size_t(w) * h, -1); + std::vector q(size_t(w) * h); + size_t qHead = 0, qTail = 0; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) + { + r.dist[size_t(y) * w + x] = 0; + q[qTail++] = y * w + x; + } + } + while (qHead < qTail) + { + const int p = q[qHead++]; + const int x = p % w, y = p / w; + if (map.isFreeForBuilding(x, y, 4, 4)) + ++r.sites; + if (r.dist[p] >= range) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy), + np = ny * w + nx; + if (r.dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) + { + r.dist[np] = r.dist[p] + 1; + q[qTail++] = np; + } + } + } + return r; } } // namespace void openCrampedStarts(Game &game, GenerationContext &context, int sites, int range, - const std::vector *protectedWalls) { - Map &map = game.map; - const int w = map.getW(), h = map.getH(); - if (protectedWalls && protectedWalls->size() != size_t(w) * h) - throw GenerationFailure("Protected wall mask does not match the map size"); - for (int team = 0; team < context.request.nbTeams; ++team) { - WorkerReach reach = reachFromWorkers(map, team, range); - if (reach.sites >= sites) - continue; - // Rings measured with resources ignored, so they walk out through the wall itself instead of - // stopping at its inner face the way the colony's own flood does. - const int bootX = map.normalizeX(context.bootX[team]), - bootY = map.normalizeY(context.bootY[team]); - const std::vector open = terrainOnlyReach(map, bootX, bootY, range, protectedWalls); - std::vector> rings(size_t(range) + 1); - for (int p = 0; p < w * h; ++p) - if (open[p] >= 1 && open[p] <= range && map.isResource(p % w, p / w) && - !(protectedWalls && (*protectedWalls)[p])) - rings[open[p]].push_back(p); - // One ring at a time, nearest first, re-measuring after each: the ring that finally gives the - // colony its room is the last one cleared, so nothing further out is touched. A colony on a - // genuinely small spot — a real islet, with nothing to open up — just runs out of rings. - for (int ring = 1; ring <= range && reach.sites < sites; ++ring) { - if (rings[ring].empty()) - continue; - for (const int p : rings[ring]) - map.setNoResource(p % w, p / w, 1); - reach = reachFromWorkers(map, team, range); - } - } + const std::vector *protectedWalls) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + if (protectedWalls && protectedWalls->size() != size_t(w) * h) + throw GenerationFailure("Protected wall mask does not match the map size"); + for (int team = 0; team < context.request.nbTeams; ++team) + { + WorkerReach reach = reachFromWorkers(map, team, range); + if (reach.sites >= sites) + continue; + // Rings measured with resources ignored, so they walk out through the wall itself instead of + // stopping at its inner face the way the colony's own flood does. + const int bootX = map.normalizeX(context.bootX[team]), + bootY = map.normalizeY(context.bootY[team]); + const std::vector open = terrainOnlyReach(map, bootX, bootY, range, protectedWalls); + std::vector> rings(size_t(range) + 1); + for (int p = 0; p < w * h; ++p) + if (open[p] >= 1 && open[p] <= range && map.isResource(p % w, p / w) && + !(protectedWalls && (*protectedWalls)[p])) + rings[open[p]].push_back(p); + // One ring at a time, nearest first, re-measuring after each: the ring that finally gives the + // colony its room is the last one cleared, so nothing further out is touched. A colony on a + // genuinely small spot — a real islet, with nothing to open up — just runs out of rings. + for (int ring = 1; ring <= range && reach.sites < sites; ++ring) + { + if (rings[ring].empty()) + continue; + for (const int p : rings[ring]) + map.setNoResource(p % w, p / w, 1); + reach = reachFromWorkers(map, team, range); + } + } } } // namespace MapGeneration diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h index 98e6198b7..4aebd650d 100644 --- a/src/map/generator/shared/Resources.h +++ b/src/map/generator/shared/Resources.h @@ -1,26 +1,28 @@ #pragma once #include "Regions.h" #include -namespace MapGeneration { +namespace MapGeneration +{ // A resource-amount percentage (GeneratorControl::percentage) applied to a count or a share, // rounding to nearest. 100 returns the input unchanged, so a default map is exactly what it was. -inline std::int64_t scaledCount(std::int64_t count, int percent) { - return percent == 100 ? count : (count * percent + 50) / 100; +inline std::int64_t scaledCount(std::int64_t count, int percent) +{ + return percent == 100 ? count : (count * percent + 50) / 100; } -inline double scaledShare(double share, int percent) { - return percent == 100 ? share : share * percent / 100.0; +inline double scaledShare(double share, int percent) +{ + return percent == 100 ? share : share * percent / 100.0; } -struct ResourceDensities { - int corn, wood, stone, algae, fruit; +struct ResourceDensities +{ + int corn, wood, stone, algae, fruit; }; -void fillInResource(Map &map, GenerationContext &context, - std::vector &points, int resourceType, - int maxFillSize); -int placeResourceClump(Map &, GenerationContext &, MapGeneratorPoint center, - int resourceType, int radius); -int placeResourceClumpInArea(Map &, GenerationContext &, - const std::vector &, - int resourceType, int radius); +void fillInResource(Map &map, GenerationContext &context, std::vector &points, + int resourceType, int maxFillSize); +int placeResourceClump(Map &, GenerationContext &, MapGeneratorPoint center, int resourceType, + int radius); +int placeResourceClumpInArea(Map &, GenerationContext &, const std::vector &, + int resourceType, int radius); // Map::setResource(x, y, type, size) scaled to `percent` of that square's tiles: the tiles // nearest its centre, placed in setResource's own order, so 100 is exactly that call. void setScaledResource(Map &, int x, int y, int resourceType, int size, int percent); @@ -35,8 +37,8 @@ void scatterResources(Game &, GenerationContext &, const ResourceDensities &); // the map's design (a generator's stone ridgelines, say). They are treated like terrain: never // cleared, and never looked past when deciding whether a colony is walled into a pocket. void guaranteeStartingResources(Game &game, GenerationContext &context, int wheatRange, - int woodRange, int clearRadius = 0, - const std::vector *protectedWalls = nullptr); + int woodRange, int clearRadius = 0, + const std::vector *protectedWalls = nullptr); // A colony with nowhere to build is unplayable however rich the ground around it is: resources // block ground units and buildings alike, so a resource amount well above the default can wall a // swarm into a pocket with no room for a 4x4 building. guaranteeStartingResources above only opens @@ -49,5 +51,5 @@ void guaranteeStartingResources(Game &game, GenerationContext &context, int whea // Call guaranteeStartingResources again afterwards: the clearing can take away the nearest wheat // or wood along with the wall. void openCrampedStarts(Game &game, GenerationContext &context, int sites = 16, int range = 24, - const std::vector *protectedWalls = nullptr); + const std::vector *protectedWalls = nullptr); } // namespace MapGeneration diff --git a/src/map/generator/shared/StartQuality.cpp b/src/map/generator/shared/StartQuality.cpp index 80b4ddb90..696cd322b 100644 --- a/src/map/generator/shared/StartQuality.cpp +++ b/src/map/generator/shared/StartQuality.cpp @@ -12,7 +12,10 @@ namespace MapGeneration { namespace { -double clampUnit(double v) { return v < 0 ? 0 : (v > 1 ? 1 : v); } +double clampUnit(double v) +{ + return v < 0 ? 0 : (v > 1 ? 1 : v); +} /// Distance in walking steps from a colony's starting workers to every tile it can reach. /// Workers, not the boot tile: the swarm occupies the boot tile and nobody walks out of it. @@ -92,7 +95,8 @@ StartQualityReport scoreStarts(Game &game, int requestedTeams, const StartQualit for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { - const Uint16 value = static_cast(std::min(fertility.at(x, y), kFertilityCeiling)); + const Uint16 value = + static_cast(std::min(fertility.at(x, y), kFertilityCeiling)); map.getTile(x, y).fertility = value; fertilityMax = std::max(fertilityMax, value); } @@ -173,19 +177,21 @@ StartQualityReport scoreStarts(Game &game, int requestedTeams, const StartQualit colony.room = clampUnit(double(colony.buildSites) / scale.roomReference); // No reachable rival is the safest a colony can be; being one of several crowded into // the same neighbourhood is the case the raw distance alone does not describe. - const double spacing = colony.rivalDistance < 0 - ? 1.0 - : clampUnit(double(colony.rivalDistance) / scale.isolationReference); + const double spacing = + colony.rivalDistance < 0 + ? 1.0 + : clampUnit(double(colony.rivalDistance) / scale.isolationReference); const int crowd = std::max(0, colony.rivalsWithinThreat - 1); colony.isolation = spacing * std::max(0.0, 1.0 - scale.crowdPenalty * crowd); const double sum = weights.wheat + weights.wood + weights.fertility + weights.depth + weights.room + weights.isolation; - colony.total = sum <= 0 ? 0 - : (weights.wheat * colony.wheat + weights.wood * colony.wood + - weights.fertility * colony.fertility + weights.depth * colony.depth + - weights.room * colony.room + weights.isolation * colony.isolation) / - sum; + colony.total = sum <= 0 + ? 0 + : (weights.wheat * colony.wheat + weights.wood * colony.wood + + weights.fertility * colony.fertility + weights.depth * colony.depth + + weights.room * colony.room + weights.isolation * colony.isolation) / + sum; // A colony that cannot reach one of its primary resources has not got a start at all, // whatever room and fertility it was given. if (colony.wheatDistance < 0 || colony.woodDistance < 0) diff --git a/src/map/generator/shared/StartQuality.h b/src/map/generator/shared/StartQuality.h index c833c0709..34c451666 100644 --- a/src/map/generator/shared/StartQuality.h +++ b/src/map/generator/shared/StartQuality.h @@ -14,8 +14,7 @@ namespace MapGeneration // clearing is cleared and the workers are standing where they will actually start walking. struct StartQualityWeights { - double wheat = 0.22, wood = 0.22, fertility = 0.25, depth = 0.12, room = 0.10, - isolation = 0.09; + double wheat = 0.22, wood = 0.22, fertility = 0.25, depth = 0.12, room = 0.10, isolation = 0.09; }; // Every factor is normalised against an absolute reference, never against the map's own best @@ -55,8 +54,8 @@ struct StartQualityReport { std::vector colonies; double worst = 0, best = 0; - double fairness = 0; ///< worst/best, 1 when a map has a single colony - double score = 0; ///< worst * pow(fairness, fairnessExponent) + double fairness = 0; ///< worst/best, 1 when a map has a single colony + double score = 0; ///< worst * pow(fairness, fairnessExponent) bool measured = false; }; diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index 27bb938f0..c7428e3d8 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -324,7 +324,7 @@ std::vector buildHardSpaceGrid(Map &map) // on any map this engine supports fit comfortably in 16 bits, halving the footprint of an array // this flood (and every scoreAsBuilt call after it) touches over and over. std::vector distanceToResource(Map &map, const std::vector &hard, - int resourceType) + int resourceType) { const int w = map.getW(), h = map.getH(); std::vector dist(size_t(w) * h, -1); diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index aa4cddd10..61571c761 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -37,8 +37,7 @@ void MapGeneration::readResourceControls(HeightFieldOptions &options, void MapGeneration::openStartsBuriedByAmounts(Game &game, GenerationContext &context, const HeightFieldOptions &options) { - if (options.wheat == 100 && options.wood == 100 && options.stone == 100 && - options.algae == 100) + if (options.wheat == 100 && options.wood == 100 && options.stone == 100 && options.algae == 100) return; // The resource bands are painted from map-wide noise levels with no awareness of where any team // starts, so an amount well above the default widens them until they can wall a colony in with @@ -103,16 +102,15 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, // each band is scaled on its own and none reaches past the top of the grass. Hilltop stone // takes the highest grass instead, and farmland then starts at the beach. const unsigned int landTop = waterTiles + sandTiles + grassTiles; - const unsigned int stoneTiles = - unsigned(std::min(grassTiles, scaledCount(wheatWoodTiles / 3, options.stone))); + const unsigned int stoneTiles = unsigned( + std::min(grassTiles, scaledCount(wheatWoodTiles / 3, options.stone))); const unsigned int farmTiles = wheatWoodTiles - wheatWoodTiles / 3; - const unsigned int stoneTop = options.hilltopStone - ? landTop - : std::min(landTop, waterTiles + sandTiles + stoneTiles); + const unsigned int stoneTop = + options.hilltopStone ? landTop : std::min(landTop, waterTiles + sandTiles + stoneTiles); const unsigned int stoneFloor = landTop - stoneTiles; const unsigned int farmStart = options.hilltopStone ? waterTiles + sandTiles : stoneTop; - const unsigned int wheatTop = - unsigned(std::min(landTop, farmStart + scaledCount(farmTiles, options.wheat))); + const unsigned int wheatTop = unsigned( + std::min(landTop, farmStart + scaledCount(farmTiles, options.wheat))); const unsigned int woodTop = unsigned(std::min(landTop, farmStart + scaledCount(farmTiles, options.wood))); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index cdf992589..6069d0fbc 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -106,7 +106,8 @@ class MapGeneratorDefaultsTest Game repeat(nullptr); auto b = service.generate(repeat, request); if (bool(a) != bool(b) || a.stage != b.stage || hash != mapFingerprint(repeat)) - std::fprintf(stderr, "Not repeatable after an intervening map: generator %d (%s, %s)\n", + std::fprintf(stderr, + "Not repeatable after an intervening map: generator %d (%s, %s)\n", method, a.diagnostic().c_str(), b.diagnostic().c_str()); assert(bool(a) == bool(b) && a.stage == b.stage && hash == mapFingerprint(repeat)); assert(checksum == repeat.checkSum(nullptr, nullptr, nullptr, true)); @@ -130,8 +131,10 @@ class MapGeneratorDefaultsTest assert(world.map.getH() == (1 << dimensions.second)); for (int team = 0; team < rectangular.nbTeams; ++team) { - assert(world.teams[team]->startPosX >= 0 && world.teams[team]->startPosX < world.map.getW()); - assert(world.teams[team]->startPosY >= 0 && world.teams[team]->startPosY < world.map.getH()); + assert(world.teams[team]->startPosX >= 0 && + world.teams[team]->startPosX < world.map.getW()); + assert(world.teams[team]->startPosY >= 0 && + world.teams[team]->startPosY < world.map.getH()); } } for (const auto &c : D::controls(method)) @@ -187,7 +190,7 @@ class MapGeneratorDefaultsTest legacy.riverDiameter = 50; auto converted = fromLegacyDescriptor(legacy, 42); const char *key = pair.first == D::eCRATERLAKES ? "lake-size" - : pair.first == D::eISLES ? "bridge-width" + : pair.first == D::eISLES ? "bridge-width" : "channel-width"; assert(converted.option(key) == pair.second); } @@ -195,43 +198,47 @@ class MapGeneratorDefaultsTest std::vector definitions; for (int id : GeneratorRegistry::builtins().methods()) definitions.push_back(GeneratorRegistry::builtins().at(id)); - definitions.push_back({"test-plateau", - 101, - "uniform terrain", - 1, - false, - {{"test-elevation", "Smoothing", 2, 10, 2, 6}, - {"test-cell", - "Island size", - 4, - 16, - 1, - 8, - ControlGroup::Layout, - false, - false, - {4, 8, 16}}, - {"test-gap", "Channel width", 1, 5, 2, 3, ControlGroup::Layout}, - GeneratorControl::toggle("test-switch", "Lake connects to fjords", - false)}, - [](Game &game, GenerationContext &context) - { - const int elevation = context.request.option("test-elevation"); - assert(elevation == 8); - assert(context.request.option("test-switch") == 1); - game.map.makeHomogenMap(GRASS); - for (int team = 0; team < context.request.nbTeams; ++team) - { - context.bootX[team] = elevation + (team % 2) * 40; - context.bootY[team] = elevation + (team / 2) * 40; - } - return MapGeneration::placeStarts(game, context); - }}); + definitions.push_back( + {"test-plateau", + 101, + "uniform terrain", + 1, + false, + {{"test-elevation", "Smoothing", 2, 10, 2, 6}, + {"test-cell", + "Island size", + 4, + 16, + 1, + 8, + ControlGroup::Layout, + false, + false, + {4, 8, 16}}, + {"test-gap", "Channel width", 1, 5, 2, 3, ControlGroup::Layout}, + GeneratorControl::toggle("test-switch", "Lake connects to fjords", false)}, + [](Game &game, GenerationContext &context) + { + const int elevation = context.request.option("test-elevation"); + assert(elevation == 8); + assert(context.request.option("test-switch") == 1); + game.map.makeHomogenMap(GRASS); + for (int team = 0; team < context.request.nbTeams; ++team) + { + context.bootX[team] = elevation + (team % 2) * 40; + context.bootY[team] = elevation + (team / 2) * 40; + } + return MapGeneration::placeStarts(game, context); + }}); // A toggle is exactly 0 or 1, shown as a checkbox: any other domain is a registration error. { const auto rejected = [](GeneratorControl control) { - GeneratorDefinition definition{"test-toggle", 102, "uniform terrain", 1, false, + GeneratorDefinition definition{"test-toggle", + 102, + "uniform terrain", + 1, + false, {std::move(control)}, [](Game &, GenerationContext &) { return false; }}; try @@ -245,7 +252,8 @@ class MapGeneratorDefaultsTest return false; }; const auto on = GeneratorControl::toggle("switch", "Lake connects to fjords", true); - assert(on.isToggle() && on.defaultValue == 1 && on.values() == std::vector({0, 1})); + assert(on.isToggle() && on.defaultValue == 1 && + on.values() == std::vector({0, 1})); assert(!rejected(on)); const auto amount = GeneratorControl::percentage("amount", "Fruit"); assert(!amount.isToggle() && amount.defaultValue == 100 && !rejected(amount)); @@ -301,7 +309,8 @@ class MapGeneratorDefaultsTest for (int attempt = 0; attempt < GenerationService::kSampledCandidates; ++attempt) { D roll = sampled; - roll.seed = GenerationContext::deriveSeed(root, "attempt/" + std::to_string(attempt)); + roll.seed = + GenerationContext::deriveSeed(root, "attempt/" + std::to_string(attempt)); sawChosen = sawChosen || roll.seed == chosen; Game world(nullptr); const auto rolled = service.generate(world, roll); @@ -422,14 +431,16 @@ class MapGeneratorDefaultsTest for (auto &w : s.controlWidgets) if (w.method == m) { - assert(w.definition.isToggle() == bool(w.toggle) && bool(w.number) != bool(w.toggle)); + assert(w.definition.isToggle() == bool(w.toggle) && + bool(w.number) != bool(w.toggle)); assert(w.field()->visible && w.label->visible); if (!w.toggle) continue; const int before = w.definition.get(s.descriptor); assert(w.toggle->getState() == (before != 0)); click(s, w.toggle); - assert(w.definition.get(s.descriptor) == 1 - before && w.toggle->getState() == !before); + assert(w.definition.get(s.descriptor) == 1 - before && + w.toggle->getState() == !before); click(s, w.toggle); assert(w.definition.get(s.descriptor) == before); } diff --git a/test/MapGeneratorFrameworkChecks.h b/test/MapGeneratorFrameworkChecks.h index f8e750a2c..75ae6da56 100644 --- a/test/MapGeneratorFrameworkChecks.h +++ b/test/MapGeneratorFrameworkChecks.h @@ -92,9 +92,9 @@ inline void dispersionChecks() std::int64_t best = std::numeric_limits::max(); for (int j = 0; j < count; ++j) if (j != i) - best = std::min(best, std::int64_t(map.warpDistSquare(x, y, points[j].x, - points[j].y)) * - weights[j]); + best = std::min( + best, std::int64_t(map.warpDistSquare(x, y, points[j].x, points[j].y)) * + weights[j]); return best; }; std::set> occupied; @@ -121,7 +121,8 @@ inline void dispersionChecks() else for (int dx = -3; dx <= 3; ++dx) for (int dy = -3; dy <= 3; ++dy) - consider(map.normalizeX(points[i].x + dx), map.normalizeY(points[i].y + dy)); + consider(map.normalizeX(points[i].x + dx), + map.normalizeY(points[i].y + dy)); } } } @@ -174,7 +175,7 @@ inline void frameworkChecks() dispersionChecks(); distanceChecks(); using namespace MapGeneration; - GeneratorControl cells{"cell", "Cell", 4, 16, 1, 8, ControlGroup::Layout, + GeneratorControl cells{"cell", "Cell", 4, 16, 1, 8, ControlGroup::Layout, false, false, {4, 8, 16}}; assert(cells.normalize(6) == 8 && cells.normalize(11) == 8 && cells.normalize(12) == 16); assert(cells.normalize(-100) == 4 && cells.normalize(100) == 16); @@ -298,15 +299,22 @@ inline void frameworkChecks() for (size_t i = 0; i < points.size(); ++i) { int score = 2147483647; - for (size_t j = 0; j < points.size(); ++j) if (i != j) - score = std::min(score, game.map.warpDistSquare(points[i].x, points[i].y, points[j].x, points[j].y) * weights[j]); - for (int y = 0; y < 64; ++y) for (int x = 0; x < 64; ++x) - { - int candidate = 2147483647; - for (size_t j = 0; j < points.size(); ++j) if (i != j) - candidate = std::min(candidate, game.map.warpDistSquare(x, y, points[j].x, points[j].y) * weights[j]); - assert(candidate <= score); - } + for (size_t j = 0; j < points.size(); ++j) + if (i != j) + score = std::min(score, game.map.warpDistSquare(points[i].x, points[i].y, + points[j].x, points[j].y) * + weights[j]); + for (int y = 0; y < 64; ++y) + for (int x = 0; x < 64; ++x) + { + int candidate = 2147483647; + for (size_t j = 0; j < points.size(); ++j) + if (i != j) + candidate = std::min( + candidate, + game.map.warpDistSquare(x, y, points[j].x, points[j].y) * weights[j]); + assert(candidate <= score); + } } weights.clear(); rejected = false; diff --git a/test/MapGeneratorGoldenTest.cpp b/test/MapGeneratorGoldenTest.cpp index a32855df0..08f737c83 100644 --- a/test/MapGeneratorGoldenTest.cpp +++ b/test/MapGeneratorGoldenTest.cpp @@ -85,8 +85,8 @@ std::vector readTable(const std::string &path) continue; std::istringstream fields(line); Row row; - if (fields >> row.platform >> row.id >> row.revision >> row.wDec >> row.hDec >> - row.teams >> row.seed >> row.status >> row.hash) + if (fields >> row.platform >> row.id >> row.revision >> row.wDec >> row.hDec >> row.teams >> + row.seed >> row.status >> row.hash) rows.push_back(row); else throw std::runtime_error("Malformed golden row: " + line); @@ -176,8 +176,8 @@ int check(const std::string &path) auto it = revisions.find(id); if (it == revisions.end()) { - std::printf("MISSING generator %s (%d): no golden rows; run --update\n", - definition.id, id); + std::printf("MISSING generator %s (%d): no golden rows; run --update\n", definition.id, + id); ++failures; } else if (it->second != definition.revision) @@ -212,8 +212,7 @@ int check(const std::string &path) ++failures; } } - std::printf("%s: %d golden rows compared, %d failures\n", platform.c_str(), compared, - failures); + std::printf("%s: %d golden rows compared, %d failures\n", platform.c_str(), compared, failures); return failures ? 1 : 0; } @@ -297,7 +296,7 @@ int sweep() }; const std::vector five{1, 2, 3, 4, 5}, three{1, 2, 3}; const std::vector cells = { - {7, 2, five}, {7, 4, five}, {8, 2, five}, {8, 3, five}, {8, 4, five}, + {7, 2, five}, {7, 4, five}, {8, 2, five}, {8, 3, five}, {8, 4, five}, {8, 6, five}, {8, 8, five}, {8, 12, five}, {9, 4, three}, {9, 12, three}, }; int failures = 0; diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 7a746e814..f5e1721ab 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -102,10 +102,10 @@ std::uint64_t colonySignature(const Game &game, int team) hash = fnv(hash, std::uint64_t(v)); for (int i = 0; i < Unit::MAX_COUNT; ++i) if (const Unit *u = colony->myUnits[i]) - for (std::int64_t v : {std::int64_t(i), std::int64_t(Unit::GIDtoID(u->gid)), - std::int64_t(u->typeNum), std::int64_t(u->posX), - std::int64_t(u->posY), std::int64_t(u->hp), - std::int64_t(u->hungry), std::int64_t(u->direction)}) + for (std::int64_t v : + {std::int64_t(i), std::int64_t(Unit::GIDtoID(u->gid)), std::int64_t(u->typeNum), + std::int64_t(u->posX), std::int64_t(u->posY), std::int64_t(u->hp), + std::int64_t(u->hungry), std::int64_t(u->direction)}) hash = fnv(hash, std::uint64_t(v)); for (int i = 0; i < Building::MAX_COUNT; ++i) if (const Building *b = colony->myBuildings[i]) @@ -182,9 +182,9 @@ void shiftTeams(Game &game, int shift) }; auto buildingGid = [&](Uint16 gid) { - return gid == NOGBID - ? gid - : Uint16(Building::GIDfrom(Building::GIDtoID(gid), to(Building::GIDtoTeam(gid)))); + return gid == NOGBID ? gid + : Uint16(Building::GIDfrom(Building::GIDtoID(gid), + to(Building::GIDtoTeam(gid)))); }; struct Seat { @@ -347,12 +347,13 @@ int main(int argc, char **argv) for (size_t i = 0; i < controls.size(); ++i) { const auto &c = controls[i]; - std::printf("%s{\"id\":\"%s\",\"label\":\"%s\",\"kind\":\"%s\",\"min\":%d,\"max\":%d," - "\"step\":%d,\"default\":%d," - "\"group\":%d,\"powerOfTwo\":%s,\"values\":[", - i ? "," : "", c.id.c_str(), c.label, c.isToggle() ? "toggle" : "range", - c.minimum, c.maximum, c.step, c.defaultValue, int(c.group), - c.powerOfTwo ? "true" : "false"); + std::printf( + "%s{\"id\":\"%s\",\"label\":\"%s\",\"kind\":\"%s\",\"min\":%d,\"max\":%d," + "\"step\":%d,\"default\":%d," + "\"group\":%d,\"powerOfTwo\":%s,\"values\":[", + i ? "," : "", c.id.c_str(), c.label, c.isToggle() ? "toggle" : "range", + c.minimum, c.maximum, c.step, c.defaultValue, int(c.group), + c.powerOfTwo ? "true" : "false"); const auto domain = c.values(); for (size_t j = 0; j < domain.size(); ++j) std::printf("%s%d", j ? "," : "", domain[j]); @@ -572,9 +573,9 @@ int main(int argc, char **argv) if (local >= 16 && wheat <= 24 && wood <= 32) ++viableTeams; } - std::printf("TUNE,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d\n", minLocal, minWheat, minWood, viableTeams, - resources[CORN], resources[WOOD], resources[STONE], resources[ALGA], bestWheat, - bestWood); + std::printf("TUNE,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d\n", minLocal, minWheat, minWood, + viableTeams, resources[CORN], resources[WOOD], resources[STONE], + resources[ALGA], bestWheat, bestWood); } if (headroom) { @@ -765,7 +766,8 @@ int main(int argc, char **argv) c = 5; if (map.getResource(x, y).type == STONE) c = 6; - if (map.getResource(x, y).type >= CHERRY && map.getResource(x, y).type <= CHERRY + 2) + if (map.getResource(x, y).type >= CHERRY && + map.getResource(x, y).type <= CHERRY + 2) c = 8; if (map.getResource(x, y).type == ALGA) c = 9; @@ -779,9 +781,9 @@ int main(int argc, char **argv) } if (!savePrefix.empty()) return saveRotatedMaps(game, result, descriptor, savePrefix, - mapName.empty() ? "study-" + std::to_string(method) + "-" + - std::to_string(seed) - : mapName, + mapName.empty() + ? "study-" + std::to_string(method) + "-" + std::to_string(seed) + : mapName, rotations); return 0; } From 9875966ab5cb264eb30636e312ef728e406a63e3 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 13:57:03 -0400 Subject: [PATCH 089/136] Retire the height map's debug dumps and the noise macros HeightMap held its field and its stamp in raw new[] arrays and carried two debug functions that wrote them to a file through the file manager, which nothing called; the arrays are std::vector now, the dumps and the file-manager includes are gone, and the field starts zeroed (every builder overwrites it before reading it, so nothing changes). Noise's five interpolation macros become inline functions with the same float and double arithmetic in the same order. Two "if (false) printf" diagnostics in start placement, dead since they were written, become the generation detail a failed roll reports. Verified: the golden table compares 126 rows with 0 failures, so every noise-driven generator produces bit-identical maps, and MapGeneratorDefaultsTest passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- src/map/generator/shared/HeightMap.cpp | 48 ++----------------- src/map/generator/shared/HeightMap.h | 28 +++++------ src/map/generator/shared/Noise.cpp | 43 +++++++++++------ .../generator/shared/StartingPositions.cpp | 12 ++--- 4 files changed, 50 insertions(+), 81 deletions(-) diff --git a/src/map/generator/shared/HeightMap.cpp b/src/map/generator/shared/HeightMap.cpp index fc6ac1f10..b4e085ea3 100644 --- a/src/map/generator/shared/HeightMap.cpp +++ b/src/map/generator/shared/HeightMap.cpp @@ -5,9 +5,6 @@ #include "GenerationResult.h" #include "GlobalContainer.h" #include "Noise.h" -#include -#include -#include #include #include @@ -44,28 +41,19 @@ HeightMap::HeightMap(unsigned int width, unsigned int height, std::mt19937 &rng) init(width, height); } -HeightMap::~HeightMap() -{ - delete[] _map; - if (_stamp) - delete[] _stamp; -} - void HeightMap::init(unsigned int width, unsigned int height) { _w = width; _h = height; - _map = new float[_w * _h]; - _stamp = NULL; + _map.assign(size_t(_w) * _h, 0.f); + _stamp.clear(); } void HeightMap::makeStamp(unsigned int radius) { _r = std::max(1u, radius); oldLowerX = oldLowerY = oldDifferenceX = oldDifferenceY = ~0u; - if (_stamp) - delete[] _stamp; - _stamp = new float[(2 * _r + 1) * (2 * _r + 1)]; + _stamp.assign(size_t(2 * _r + 1) * (2 * _r + 1), 0.f); for (unsigned int x = 0; x < 2 * _r + 1; x++) { for (unsigned int y = 0; y < 2 * _r + 1; y++) @@ -85,7 +73,7 @@ inline void HeightMap::lower(unsigned int coordX, unsigned int coordY) (coordY != oldLowerY)) // don't stamp the same spot again. if stamp is moved like in // rivermaps this saves a lot of time { - assert(_stamp); + assert(!_stamp.empty()); for (unsigned int x = 0; x < 2 * _r + 1; x++) { /// this loop can be replaced by a somehow complicated memcpy @@ -108,7 +96,7 @@ inline void HeightMap::differenceStamp(unsigned int coordX, unsigned int coordY) (coordY != oldDifferenceY)) // don't stamp the same spot again. if stamp is moved like in // rivermaps this saves a lot of time { - assert(_stamp); + assert(!_stamp.empty()); for (unsigned int x = 0; x < 2 * _r + 1; x++) { for (unsigned int y = 0; y < 2 * _r + 1; y++) @@ -262,32 +250,6 @@ void HeightMap::makeRiver(unsigned int maxDiameter, float smoothingFactor, bool normalize(); } -void HeightMap::stampOutput(char *filename) -{ - char *hm2 = new char[(2 * _r + 1) * (2 * _r + 1)]; - StreamBackend *stream = Toolkit::getFileManager()->openOutputStreamBackend(filename); - - for (unsigned int i = 0; i < (2 * _r + 1) * (2 * _r + 1); i++) - hm2[i] = (char)(_stamp[i] * 256); - - stream->write(hm2, (2 * _r + 1) * (2 * _r + 1) * sizeof(char)); - delete stream; - delete[] hm2; -} - -void HeightMap::mapOutput(char *filename) -{ - char *hm2 = new char[_w * _h]; - StreamBackend *stream = Toolkit::getFileManager()->openOutputStreamBackend(filename); - - for (unsigned int i = 0; i < _w * _h; i++) - hm2[i] = (char)(_map[i] * 256); - - stream->write(hm2, _w * _h * sizeof(char)); - delete stream; - delete[] hm2; -} - void HeightMap::makeCraters(unsigned int craterCount, unsigned int craterRadius, float smoothingFactor) { diff --git a/src/map/generator/shared/HeightMap.h b/src/map/generator/shared/HeightMap.h index b033bd86e..98e3f5d73 100644 --- a/src/map/generator/shared/HeightMap.h +++ b/src/map/generator/shared/HeightMap.h @@ -4,14 +4,17 @@ #pragma once #include "Noise.h" +#include +#include class HeightMap /// class to generate heightmaps to decide where to put resources, water, sand and /// grass later { - float *_map; /// height values are always [0,1]. - unsigned int _w, _h; /// map size - float *_stamp; /// smooth 0 to 1 gradient lookup to generate craters, islands and rivers - unsigned int _r; /// radius of the _stamp + std::vector _map; /// height values are always [0,1]. + unsigned int _w, _h; /// map size + std::vector + _stamp; /// smooth 0 to 1 gradient lookup to generate craters, islands and rivers + unsigned int _r; /// radius of the _stamp GenerationNoise _pn; std::mt19937 &random; unsigned oldLowerX = ~0u, oldLowerY = ~0u, oldDifferenceX = ~0u, @@ -29,7 +32,6 @@ class HeightMap /// class to generate heightmaps to decide where to put resource HeightMap(unsigned int width, unsigned int height, std::mt19937 &random); HeightMap(const HeightMap &) = delete; HeightMap &operator=(const HeightMap &) = delete; - ~HeightMap(); inline unsigned int uiLevel(unsigned int i, unsigned int scale) { return std::min(scale - 1, (unsigned int)(_map[i] * scale)); @@ -43,9 +45,6 @@ class HeightMap /// class to generate heightmaps to decide where to put resource { return _map[x % _w + (y % _h) * _w]; } - void mapOutput(char *filename); /// generates the file ~/.glob2/filename and writes the raw - /// 0..255 values of map to it. to see it, use convert -size - /// [width]x[height] -depth 8 gray:[filename] test.png void makePlain(float smoothingFactor); /// a plain perlin height field void makeSwamp(float smoothingFactor); /// a plain perlin height field @@ -56,7 +55,7 @@ class HeightMap /// class to generate heightmaps to decide where to put resource void makeCraters(unsigned int craterCount, unsigned int craterRadius, float smoothingFactor); /// generates a 'swamp' with craterCount craters private: - void operator=(float value) { std::fill(_map, _map + _w * _h, value); } + void operator=(float value) { std::fill(_map.begin(), _map.end(), value); } void init(unsigned int width, unsigned int height); void makeStamp(unsigned int radius); /// generates the stamp (smooth 0 to 1 gradient lookup to /// generate craters, islands and rivers) @@ -70,10 +69,9 @@ class HeightMap /// class to generate heightmaps to decide where to put resource /// away from other hills. inline void addNoise(float weight, - float smoothingFactor); /// adds noise to the map: map=noise*weight+map*(1-weight) - void stampOutput(char *filename); /// generates the file ~/.glob2/filename and writes the raw - /// 0..255 values of stamp to it. to see it, use convert -size - /// [width]x[height] -depth 8 gray:[filename] test.png where - /// with==height as _stamp is always a square - void normalize(); /// fits the values of _map to [0, 1] + float smoothingFactor); /// adds noise to the map: map=noise*weight+map*(1-weight) + /// 0..255 values of stamp to it. to see it, use convert -size + /// [width]x[height] -depth 8 gray:[filename] test.png where + /// with==height as _stamp is always a square + void normalize(); /// fits the values of _map to [0, 1] }; diff --git a/src/map/generator/shared/Noise.cpp b/src/map/generator/shared/Noise.cpp index ab6ae3196..9fd38fd30 100644 --- a/src/map/generator/shared/Noise.cpp +++ b/src/map/generator/shared/Noise.cpp @@ -7,27 +7,40 @@ // with no warranty. Please email me at mazucker@vassar.edu if // you find it useful. -#include - #include "Noise.h" #include -// TODO: change these preprocessor macros into inline functions - +namespace +{ // S curve is (3x^2 - 2x^3) because it's quick to calculate // though -cos(x * PI) * 0.5 + 0.5 would work too - -#define easeCurve(t) (t * t * (3.0 - 2.0 * t)) -#define linearInterp(t, a, b) (a + t * (b - a)) -#define dot2(rx, ry, q) (rx * q[0] + ry * q[1]) -#define dot3(rx, ry, rz, q) (rx * q[0] + ry * q[1] + rz * q[2]) - -#define setupValues(t, axis, g0, g1, d0, d1, pos) \ - t = pos[axis] + NOISE_LARGE_PWR2; \ - g0 = ((int)t) & NOISE_MOD_MASK; \ - g1 = (g0 + 1) & NOISE_MOD_MASK; \ - d0 = t - (int)t; \ +inline float easeCurve(float t) +{ + return t * t * (3.0 - 2.0 * t); +} +inline float linearInterp(float t, float a, float b) +{ + return a + t * (b - a); +} +inline float dot2(float rx, float ry, const float *q) +{ + return rx * q[0] + ry * q[1]; +} +inline float dot3(float rx, float ry, float rz, const float *q) +{ + return rx * q[0] + ry * q[1] + rz * q[2]; +} +// The grid points either side of pos on one axis, and the signed distances to them. +inline void setupValues(float &t, int axis, int &g0, int &g1, float &d0, float &d1, + const float *pos) +{ + t = pos[axis] + NOISE_LARGE_PWR2; + g0 = ((int)t) & NOISE_MOD_MASK; + g1 = (g0 + 1) & NOISE_MOD_MASK; + d0 = t - (int)t; d1 = d0 - 1.0; +} +} // namespace // Constructor GenerationNoise::GenerationNoise(unsigned int seed) diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index c7428e3d8..f797af897 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -580,8 +580,7 @@ bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSi if (!game.checkRoomForBuilding(context.bootX[s], context.bootY[s], globalContainer->buildingsTypes.get(typeNum), s, false)) { - if (false) - printf("Failed to add swarm of team %d\n", s); + context.detail = "No room for a colony's swarm"; return false; } game.teams[s]->startPosX = context.bootX[s]; @@ -593,8 +592,7 @@ bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSi if (game.addUnit(context.bootX[s] + (i % 4), context.bootY[s] - 1 - (i / 4), s, WORKER, 0, 0, 0, 0) == NULL) { - if (false) - printf("Failed to add unit %d of team %d\n", i, s); + context.detail = "No room for a colony's starting workers"; return false; } game.teams[s]->createLists(); @@ -624,8 +622,7 @@ bool placeStarts(Game &game, GenerationContext &context) if (!game.checkRoomForBuilding(context.bootX[s], context.bootY[s], globalContainer->buildingsTypes.get(typeNum), s, false)) { - if (false) - printf("Failed to add swarm of team %d\n", s); + context.detail = "No room for a colony's swarm"; return false; } game.teams[s]->startPosX = context.bootX[s]; @@ -637,8 +634,7 @@ bool placeStarts(Game &game, GenerationContext &context) if (game.addUnit(context.bootX[s] + (i % 4), context.bootY[s] - 1 - (i / 4), s, WORKER, 0, 0, 0, 0) == NULL) { - if (false) - printf("Failed to add unit %d of team %d\n", i, s); + context.detail = "No room for a colony's starting workers"; return false; } game.teams[s]->createLists(); From acd1590322e3bd05cf89010d71f96d9af82d2be3 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:08:01 -0400 Subject: [PATCH 090/136] Share the torus grid, flood and reachability check between generators Three generators carried the same Torus struct, five the same eight- connected breadth-first flood, three the same "which tiles hold each team's units" scan and most validators the same "every colony's workers reach colony 0's" loop, each written out again. shared/Grid now holds one Torus (wrap, offset, squared and Chebyshev distance), stepsFrom over an open mask, tileMask, unitTilesByTeam, walkableTiles and firstColonyCutOff, plus the cardinal step table six files declared. Tidal flats, City states, Stone highlands, Ring world, Symmetric arena, Watershed and Maze use them; Watershed's own component labelling is the topology toolkit's connectedRegions, which labels in the same scan order. Every message a validator returns is worded as before. A breadth-first field with unit steps is the same whatever order tiles are visited in, and connected components labelled in scan order get the same numbers whatever the traversal, so no map or verdict changes: Verified: the golden table compares 126 rows with 0 failures, the sweep reports 0 failing cells, and MapGeneratorDefaultsTest passes, whose dispersion and distance oracles cover the shared flood's neighbourhood. Net 442 lines removed from the generators for 150 added to the toolkit. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 1 + src/SConscript | 1 + .../generators/CityStatesGenerator.cpp | 74 +--------- .../generator/generators/MazeGenerator.cpp | 49 ++----- .../generators/RingWorldGenerator.cpp | 67 ++-------- .../generators/StoneHighlandsGenerator.cpp | 74 ++-------- .../generators/SymmetricArenaGenerator.cpp | 33 +---- .../generators/TidalFlatsGenerator.cpp | 78 +---------- .../generators/WatershedGenerator.cpp | 126 +++--------------- src/map/generator/shared/Grid.cpp | 91 +++++++++++++ src/map/generator/shared/Grid.h | 67 ++++++++++ 11 files changed, 219 insertions(+), 442 deletions(-) create mode 100644 src/map/generator/shared/Grid.cpp create mode 100644 src/map/generator/shared/Grid.h diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index a86717a82..295ef5fa4 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -17,6 +17,7 @@ leans on the shared modules below only where doing so is actually less work than | Cross-control and dimension constraints | `GeneratorDefinition::validateRequest` | A pure check run before generation; no silent settings rewrites | | Jagged islands, stretched/rotated continents, coastline shape | `shared/Geometry` (`ShapeTransform`, `RadialShape`, `stampShape`) | An invertible shape transform and a seeded radial shape with a per-angle `radiusAt()` query — checking a specific candidate against its own angle is exact, where a single global "furthest possible" bound is only ever a conservative overestimate | | Distances through corridors, connected components, resource-role adjacency | `shared/Topology`, `shared/Distances` | Graph distances and grid-component labelling with explicit wrapping/neighbor policy | +| Torus grid arithmetic, breadth-first steps, walkable tiles, units by team and the colony reachability check (every torus generator and validator) | `shared/Grid` | One `Torus` with wrap and offset arithmetic, one eight-connected flood, one walkable mask and one "each colony reaches colony 0" check, in place of the copies each generator carried | | Whole-region or local point dispersion (Contested Commons, Concrete Islands, Isles) | `shared/Regions` | Paired weight/point shuffling, a bounded local search and an opt-in whole-region search that scores every tile from its two nearest weighted distances, kept current as points move, under an explicit pass budget rather than silently treating an unfinished search as converged | | Settlements restricted to a home island or room | `shared/Settlements` | Whole-footprint mask, nearest legal anchor, exact worker count, per-colony diagnostics | | Generator-specific connectivity guarantees | `GeneratorDefinition::validateWorld` | Runs after structural checks, against the actual finished terrain/movement mask | diff --git a/src/SConscript b/src/SConscript index bbb607b72..459104124 100644 --- a/src/SConscript +++ b/src/SConscript @@ -201,6 +201,7 @@ map/generator/shared/Distances.cpp map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp map/generator/shared/Geometry.cpp +map/generator/shared/Grid.cpp map/generator/shared/Topology.cpp map/generator/shared/Settlements.cpp map/generator/shared/Regions.cpp diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 4ee834b15..126215611 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -3,6 +3,7 @@ #include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" +#include "Grid.h" #include "Geometry.h" #include "HeightMap.h" #include "Resources.h" @@ -109,55 +110,6 @@ enum HeartKind kHeartKinds }; -struct Torus -{ - int w, h; - int x(int v) const { return ((v % w) + w) % w; } - int y(int v) const { return ((v % h) + h) % h; } - int at(int px, int py) const { return y(py) * w + x(px); } - int offsetX(int from, int to) const - { - const int d = x(to - from); - return d > w / 2 ? d - w : d; - } - int offsetY(int from, int to) const - { - const int d = y(to - from); - return d > h / 2 ? d - h : d; - } -}; - -// Breadth-first 8-neighbour steps on the torus from every source tile through open tiles; -1 -// where the flood never arrives. -std::vector stepsFrom(const Torus &t, const std::vector &source, - const std::vector &open) -{ - std::vector dist(size_t(t.w) * t.h, -1); - std::vector queue; - queue.reserve(dist.size()); - for (size_t i = 0; i < dist.size(); ++i) - if (source[i]) - { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % t.w, y = queue[head] / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int n = t.at(x + dx, y + dy); - if (dist[n] < 0 && open[n]) - { - dist[n] = dist[queue[head]] + 1; - queue.push_back(n); - } - } - } - return dist; -} - // A smooth random curve round a ring, periodic in u over [0, 1): a few harmonics with random // phases, scaled so its peak is one. struct Profile @@ -923,7 +875,7 @@ int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible std::vector frontier{seed}; queued[seed] = 1; int placed = 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + const auto &steps = kCardinalSteps; for (size_t head = 0; head < frontier.size() && placed < count; ++head) { const int i = frontier[head], x = i % t.w, y = i / t.w; @@ -1498,29 +1450,15 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (L.ford[i] && !walkable(i)) return "The ford at " + where(i) + " is blocked."; } - std::vector> workers(std::max(teams, 1)); - for (int i = 0; i < n; ++i) - { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(i); - } + const auto workers = unitTilesByTeam(map, teams); if (teams < 1 || workers[0].empty()) return "Colony 0 has no workers to walk the map."; std::vector open(n, 0), source(n, 0); for (int i = 0; i < n; ++i) open[i] = walkable(i); - for (int i : workers[0]) - source[i] = 1; - const std::vector fromFirst = stepsFrom(t, source, open); - for (int team = 1; team < teams; ++team) - { - bool arrived = false; - for (int i : workers[team]) - arrived = arrived || fromFirst[i] >= 0; - if (!arrived) - return "Colony " + std::to_string(team) + " cannot walk to colony 0."; - } + const std::vector fromFirst = stepsFrom(t, tileMask(t, workers[0]), open); + if (const int cut = firstColonyCutOff(fromFirst, workers); cut >= 0) + return "Colony " + std::to_string(cut) + " cannot walk to colony 0."; const std::vector heart = heartTiles(map, L); bool heartReached = false; for (int i = 0; i < n && !heartReached; ++i) diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index d56b7d88e..7f61d14a8 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -2,6 +2,7 @@ #include "MazeGenerator.h" #include "Game.h" #include "GenerationContext.h" +#include "Grid.h" #include "Resources.h" #include "Settlements.h" #include "Unit.h" @@ -504,7 +505,7 @@ void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, } if (seed < 0) return 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + const auto &steps = kCardinalSteps; std::vector frontier{seed}; free[seed] = 0; int placed = 0; @@ -742,46 +743,12 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { const Map &map = game.map; const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; - std::vector reached(size_t(w) * h, 0); - std::vector queue; - queue.reserve(size_t(w) * h); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) - { - reached[size_t(y) * w + x] = 1; - queue.push_back(y * w + x); - } - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const size_t n = size_t(ny) * w + nx; - if (!reached[n] && !map.isWater(nx, ny) && !map.isResource(nx, ny) && - map.getBuilding(nx, ny) == NOGBID) - { - reached[n] = 1; - queue.push_back(int(n)); - } - } - } - std::vector teamReached(teams, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && reached[size_t(y) * w + x] && Unit::GIDtoTeam(gid) < teams) - teamReached[Unit::GIDtoTeam(gid)] = 1; - } - for (int team = 0; team < teams; ++team) - if (!teamReached[team]) - return "Colony " + std::to_string(team) + " cannot walk to colony 0 through the maze."; + const Torus t(map); + const auto units = unitTilesByTeam(map, teams); + const std::vector reached = + stepsFrom(t, tileMask(t, teams > 0 ? units[0] : std::vector{}), walkableTiles(map)); + if (const int cut = firstColonyCutOff(reached, units, 0); cut >= 0) + return "Colony " + std::to_string(cut) + " cannot walk to colony 0 through the maze."; return ""; } } // namespace diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index d5d3145e6..19493aff5 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -2,6 +2,7 @@ #include "RingWorldGenerator.h" #include "Game.h" #include "GenerationContext.h" +#include "Grid.h" #include "Geometry.h" #include "Resources.h" #include "Settlements.h" @@ -89,38 +90,6 @@ int wrapSquare(int width, int height, int ax, int ay, int bx, int by) return dx * dx + dy * dy; } -// Breadth-first 8-neighbour steps on the torus from every source tile through open tiles; -1 -// where the flood never arrives. -std::vector stepsFrom(int width, int height, const std::vector &source, - const std::vector &open) -{ - std::vector dist(size_t(width) * height, -1); - std::vector queue; - queue.reserve(dist.size()); - for (size_t i = 0; i < dist.size(); ++i) - if (source[i]) - { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % width, y = queue[head] / width; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const size_t n = - size_t((y + dy + height) % height) * width + (x + dx + width) % width; - if (dist[n] < 0 && open[n]) - { - dist[n] = dist[queue[head]] + 1; - queue.push_back(int(n)); - } - } - } - return dist; -} - // A smooth closed curve along the belt, one sample per tile. Its harmonics are whole numbers of // cycles per map length, so it meets itself exactly at the seam. Normalised to [-1, 1]; `slope` // receives its steepest step between neighbouring samples. @@ -270,7 +239,7 @@ void carveLakes(std::vector &terrain, const Belt &belt, Generatio } if (density <= 0 || dry.empty()) return; - const std::vector shore = stepsFrom(width, height, water, land); + const std::vector shore = stepsFrom(Torus{width, height}, water, land); const int wanted = int(std::lround(density * double(dry.size()) / 4096.0)); const double scale = std::clamp(std::sqrt(belt.axes.breadth() / 128.0), 0.8, 1.6); struct Lake @@ -336,7 +305,7 @@ std::vector raiseIslands(std::vector &terrain, int width, } if (sea.empty()) return islands; - const std::vector offshore = stepsFrom(width, height, land, water); + const std::vector offshore = stepsFrom(Torus{width, height}, land, water); const int wanted = std::max(1, int(std::lround(perStandardMap * double(area) / 16384.0))); // Never smaller than on a 128-tile map: any smaller and the beach leaves no grass for a prize. const double scale = std::clamp(std::sqrt(std::min(width, height) / 128.0), 1.0, 1.6); @@ -430,7 +399,7 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, boo water[size_t(y) * width + x] = map.isWater(x, y); dry[size_t(y) * width + x] = !map.isWater(x, y); } - const std::vector shore = stepsFrom(width, height, water, dry); + const std::vector shore = stepsFrom(Torus{width, height}, water, dry); // Only the belt itself: an island or a stretch of rough coast cut off at sea can offer the same // shore, but a colony there would have no land neighbours at all. The spine is always dry, so // the belt is whatever land is reached from it. @@ -439,7 +408,7 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, boo for (int x = 0; x < width; ++x) spine[size_t(y) * width + x] = !map.isWater(x, y) && std::abs(belt.across(x, y)) < kSpineHalf - 1; - const std::vector onBelt = stepsFrom(width, height, spine, dry); + const std::vector onBelt = stepsFrom(Torus{width, height}, spine, dry); const double slot = double(length) / teams; const double first = context.bounded("colonies", 3600) / 3600.0 * length; const int firstSide = int(context.bounded("colonies", 2)); @@ -519,7 +488,7 @@ int growPatch(Map &map, int seed, int type, int count, Eligible eligible) std::vector frontier{seed}; queued[seed] = 1; int placed = 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + const auto &steps = kCardinalSteps; for (size_t head = 0; head < frontier.size() && placed < count; ++head) { const int i = frontier[head], x = i % width, y = i / width; @@ -559,7 +528,7 @@ void furnishHomes(Game &game, GenerationContext &context) for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) reserved[size_t(map.normalizeY(context.bootY[team] + dy)) * width + map.normalizeX(context.bootX[team] + dx)] = 1; - const std::vector shore = stepsFrom(width, height, water, dry); + const std::vector shore = stepsFrom(Torus{width, height}, water, dry); const auto eligible = [&](int i) { const int x = i % width, y = i / width; @@ -573,7 +542,7 @@ void furnishHomes(Game &game, GenerationContext &context) for (int dx = 0; dx < 4; ++dx) footprint[size_t(map.normalizeY(context.bootY[team] + dy)) * width + map.normalizeX(context.bootX[team] + dx)] = 1; - const std::vector walk = stepsFrom(width, height, footprint, dry); + const std::vector walk = stepsFrom(Torus{width, height}, footprint, dry); // Wettest (or, for stone, driest) eligible tile in a walking band, nearest first on ties. const auto pick = [&](int nearest, int farthest, bool wet, int avoid) { @@ -649,7 +618,7 @@ void seedAlgae(Map &map, GenerationContext &context, int algaePercent) water[size_t(y) * width + x] = map.isWater(x, y); dry[size_t(y) * width + x] = !map.isWater(x, y); } - const std::vector offshore = stepsFrom(width, height, dry, water); + const std::vector offshore = stepsFrom(Torus{width, height}, dry, water); std::vector shallows; for (int i = 0; i < int(area); ++i) if (offshore[i] >= 2 && offshore[i] <= 5) @@ -688,13 +657,8 @@ bool openBeltRoad(Game &game, GenerationContext &context, const Axes &axes) const int area = width * height; constexpr int kLayers = 4; // windings -1 to 2 along the belt std::vector> workers(teams); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); - } + for (int team = 0; team < teams; ++team) + workers[team] = unitTilesByTeam(map, teams)[team]; std::vector> order; for (int team = 0; team < teams; ++team) order.emplace_back( @@ -921,13 +885,8 @@ std::string validateWorld(const Game &game, const GenerationContext &context) const Axes axes = axesFor(width, height); const size_t area = size_t(width) * height; std::vector> workers(std::max(teams, 1)); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(y * width + x); - } + for (int team = 0; team < teams; ++team) + workers[team] = unitTilesByTeam(map, teams)[team]; if (teams < 1 || workers[0].empty()) return "Colony 0 has no workers to walk the belt."; std::vector winding(area, kUnreached), reached; diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 8b8b6b6da..de2b61296 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -3,6 +3,7 @@ #include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" +#include "Grid.h" #include "Resources.h" #include "Settlements.h" #include "Topology.h" @@ -75,34 +76,6 @@ constexpr int kFruitGrove = 6; constexpr int kAreaPerColony = 1024; constexpr int kFar = INT_MAX / 4; -struct Torus -{ - int w, h; - int x(int v) const { return ((v % w) + w) % w; } - int y(int v) const { return ((v % h) + h) % h; } - int at(int px, int py) const { return y(py) * w + x(px); } - // Signed shortest offset from a to b. - int offsetX(int a, int b) const - { - const int d = x(b - a); - return d > w / 2 ? d - w : d; - } - int offsetY(int a, int b) const - { - const int d = y(b - a); - return d > h / 2 ? d - h : d; - } - int dist2(int ax, int ay, int bx, int by) const - { - const int dx = offsetX(ax, bx), dy = offsetY(ay, by); - return dx * dx + dy * dy; - } - int chebyshev(int ax, int ay, int bx, int by) const - { - return std::max(std::abs(offsetX(ax, bx)), std::abs(offsetY(ay, by))); - } -}; - template void shuffle(std::vector &values, GenerationContext &context, const char *stream) { @@ -611,7 +584,7 @@ void growPond(Layout &L, const std::vector &depth, const std::vector & std::priority_queue, std::greater> frontier; frontier.push({key(seed), seed}); queued[seed] = queuedStamp; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + const auto &steps = kCardinalSteps; int grown = 0; while (!frontier.empty() && grown < target) { @@ -818,7 +791,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const int thickShare = int(std::min(100, scaledCount(45, o.stone))); const int thickLevel = percentile(thickness, 100 - thickShare); std::vector thick = L.ridge; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + const auto &steps = kCardinalSteps; for (int i = 0; i < n; ++i) { if (!L.ridge[i] || thickShare <= 0 || thickness[i] < thickLevel) @@ -1322,42 +1295,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) return "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + ", " + std::to_string(p.y) + ") is blocked."; } - std::vector reached(n, 0); - std::vector queue; - for (int i = 0; i < n; ++i) - { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == 0) - { - reached[i] = 1; - queue.push_back(i); - } - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int x = t.x(px + dx), y = t.y(py + dy), q = y * t.w + x; - if (!reached[q] && walkable(x, y)) - { - reached[q] = 1; - queue.push_back(q); - } - } - } - std::vector teamReached(teams, 0); - for (int i = 0; i < n; ++i) - { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && reached[i] && Unit::GIDtoTeam(gid) < teams) - teamReached[Unit::GIDtoTeam(gid)] = 1; - } - for (int team = 0; team < teams; ++team) - if (!teamReached[team]) - return "Colony " + std::to_string(team) + - " cannot walk to colony 0 over the highlands."; + const auto units = unitTilesByTeam(map, teams); + const std::vector reached = + stepsFrom(t, tileMask(t, teams > 0 ? units[0] : std::vector{}), walkableTiles(map)); + if (const int cut = firstColonyCutOff(reached, units, 0); cut >= 0) + return "Colony " + std::to_string(cut) + " cannot walk to colony 0 over the highlands."; // Each pass must be on the colonies' walkable network and lead straight through: walking out // of its box, without going far, reaches both valleys it joins. for (size_t k = 0; k < L.passes.size(); ++k) diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index d8c069065..a968f58e0 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -4,6 +4,7 @@ #include "BuildingType.h" #include "Game.h" #include "GenerationContext.h" +#include "Grid.h" #include "GlobalContainer.h" #include "HeightMap.h" #include "Resources.h" @@ -610,35 +611,6 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La return true; } -// Walking steps (8-neighbour) from every tile to the nearest source tile: exact integers, so -// symmetric terrain gives symmetric distances. -1 where no source is reachable. -std::vector stepsFrom(int w, int h, const std::vector &source) -{ - std::vector dist(source.size(), -1), queue; - queue.reserve(source.size()); - for (size_t i = 0; i < source.size(); ++i) - if (source[i]) - { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); - if (dist[j] < 0) - { - dist[j] = dist[size_t(queue[head])] + 1; - queue.push_back(int(j)); - } - } - } - return dist; -} - // Every swarm, then every worker, from colony 0's choices mapped by each colony's symmetry: // its footprint centred on its home corner (the image of a footprint needs its own top-left // anchor, since a turn or mirror moves which corner is top-left), and a random choice of the @@ -748,7 +720,8 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou blocked[i] = homeClear[i] || landing[i] || onPath(x, y) || onPath(x + 1, y) || onPath(x, y + 1) || onPath(x + 1, y + 1); } - const std::vector waterSteps = stepsFrom(w, h, water), landSteps = stepsFrom(w, h, land); + const std::vector waterSteps = MapGeneration::stepsFrom(MapGeneration::Torus{w, h}, water), + landSteps = MapGeneration::stepsFrom(MapGeneration::Torus{w, h}, land); const float coarse = std::min(24.0f, std::max(8.0f, std::min(w, h) / 10.0f)); std::vector plan(n, -1); diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index 459f8ec03..9e5ea6e91 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -3,6 +3,7 @@ #include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" +#include "Grid.h" #include "Geometry.h" #include "HeightMap.h" #include "Resources.h" @@ -55,53 +56,6 @@ constexpr double kCentralShare = 0.13; // Sandbars are just big enough to hold a tower or an inn. constexpr double kSandbarLow = 3.0, kSandbarHigh = 4.5; -struct Torus -{ - int w, h; - int x(int v) const { return ((v % w) + w) % w; } - int y(int v) const { return ((v % h) + h) % h; } - int at(int px, int py) const { return y(py) * w + x(px); } - int offsetX(int from, int to) const - { - const int d = x(to - from); - return d > w / 2 ? d - w : d; - } - int offsetY(int from, int to) const - { - const int d = y(to - from); - return d > h / 2 ? d - h : d; - } -}; - -std::vector stepsFrom(const Torus &t, const std::vector &source, - const std::vector &open) -{ - std::vector dist(size_t(t.w) * t.h, -1); - std::vector queue; - queue.reserve(dist.size()); - for (size_t i = 0; i < dist.size(); ++i) - if (source[i]) - { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % t.w, y = queue[head] / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int n = t.at(x + dx, y + dy); - if (dist[n] < 0 && open[n]) - { - dist[n] = dist[queue[head]] + 1; - queue.push_back(n); - } - } - } - return dist; -} - struct Geometry { int teams, half; @@ -339,7 +293,7 @@ int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible std::vector frontier{seed}; queued[seed] = 1; int placed = 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + const auto &steps = kCardinalSteps; for (size_t head = 0; head < frontier.size() && placed < count; ++head) { const int i = frontier[head], x = i % t.w, y = i / t.w; @@ -671,32 +625,12 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (!pond) return "Colony " + std::to_string(k) + "'s island has lost its pond."; } - std::vector> workers(std::max(teams, 1)); - for (int i = 0; i < n; ++i) - { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(i); - } + const auto workers = unitTilesByTeam(map, teams); if (teams < 1 || workers[0].empty()) return "Colony 0 has no workers to walk the flats."; - std::vector open(n, 0), source(n, 0); - for (int i = 0; i < n; ++i) - { - const int x = i % t.w, y = i / t.w; - open[i] = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - } - for (int i : workers[0]) - source[i] = 1; - const std::vector steps = stepsFrom(t, source, open); - for (int team = 1; team < teams; ++team) - { - bool arrived = false; - for (int i : workers[team]) - arrived = arrived || steps[i] >= 0; - if (!arrived) - return "Colony " + std::to_string(team) + " cannot walk to colony 0 over the flats."; - } + const std::vector steps = stepsFrom(t, tileMask(t, workers[0]), walkableTiles(map)); + if (const int cut = firstColonyCutOff(steps, workers); cut >= 0) + return "Colony " + std::to_string(cut) + " cannot walk to colony 0 over the flats."; if (L.g.centralRadius > 0) { bool arrived = false; diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index c56285a45..14bec5796 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -3,6 +3,8 @@ #include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" +#include "Topology.h" +#include "Grid.h" #include "Resources.h" #include "Settlements.h" #include "Unit.h" @@ -1210,33 +1212,8 @@ template std::string checkChannels(const Layout &layout, Water // Labels 8-connected components of open tiles; closed tiles get -1. std::vector components(const std::vector &open, int w, int h, int &count) { - std::vector label(open.size(), -1); - std::vector queue; - queue.reserve(open.size()); - count = 0; - for (size_t start = 0; start < open.size(); ++start) - { - if (!open[start] || label[start] >= 0) - continue; - queue.clear(); - queue.push_back(int(start)); - label[start] = count; - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); - if (open[q] && label[q] < 0) - { - label[q] = count; - queue.push_back(int(q)); - } - } - } - ++count; - } + const std::vector label = connectedRegions(open, w, h, true, GridNeighbors::Eight); + count = label.empty() ? 0 : *std::max_element(label.begin(), label.end()) + 1; return label; } @@ -1251,34 +1228,6 @@ struct Tiles int mainComponent = -1; }; -// Steps from every tile to the nearest tile where `seed` holds. -std::vector stepsFrom(const std::vector &seed, int w, int h) -{ - std::vector steps(seed.size(), -1), queue; - queue.reserve(seed.size()); - for (size_t i = 0; i < seed.size(); ++i) - if (seed[i]) - { - steps[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const size_t q = size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w); - if (steps[q] < 0) - { - steps[q] = steps[queue[head]] + 1; - queue.push_back(int(q)); - } - } - } - return steps; -} - Tiles tileView(const std::vector &terrain, int w, int h) { Tiles t; @@ -1297,14 +1246,14 @@ Tiles tileView(const std::vector &terrain, int w, int h) t.water[i] = a == WATER && b == WATER && c == WATER && d == WATER; t.grass[i] = a == GRASS && b == GRASS && c == GRASS && d == GRASS; } - t.waterSteps = stepsFrom(t.water, w, h); + t.waterSteps = stepsFrom(Torus{w, h}, t.water); std::vector land(n), notGrass(n); for (size_t i = 0; i < n; ++i) { land[i] = !t.water[i]; notGrass[i] = !t.grass[i]; } - t.grassDepth = stepsFrom(notGrass, w, h); + t.grassDepth = stepsFrom(Torus{w, h}, notGrass); int count = 0; t.component = components(land, w, h, count); std::vector sizes(count, 0); @@ -1481,54 +1430,6 @@ std::vector chooseSites(const Layout &layout, const Tiles &t, return sites; } -// Walking steps from the given tiles over land that is free of resources and buildings. -std::vector walk(const Map &map, const std::vector &from) -{ - const int w = map.getW(), h = map.getH(); - std::vector distance(size_t(w) * h, -1); - std::vector queue; - queue.reserve(distance.size()); - for (int p : from) - if (distance[p] < 0) - { - distance[p] = 0; - queue.push_back(p); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const size_t q = size_t(ny) * w + nx; - if (distance[q] < 0 && !map.isWater(nx, ny) && !map.isResource(nx, ny) && - map.getBuilding(nx, ny) == NOGBID) - { - distance[q] = distance[queue[head]] + 1; - queue.push_back(int(q)); - } - } - } - return distance; -} - -std::vector> workersByTeam(const Map &map, int teams) -{ - std::vector> workers(teams); - for (int y = 0; y < map.getH(); ++y) - for (int x = 0; x < map.getW(); ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid == NOGUID) - continue; - const int team = Unit::GIDtoTeam(gid); - if (team >= 0 && team < teams) - workers[team].push_back(y * map.getW() + x); - } - return workers; -} - // Clumps keep a gap of open ground between them, so no two can join into a wall. class Reservations { @@ -1565,10 +1466,11 @@ void placeStarterKits(Game &game, GenerationContext &context, int teams, const T { Map &map = game.map; const int w = map.getW(), h = map.getH(); - const auto workers = workersByTeam(map, teams); + const auto workers = unitTilesByTeam(map, teams); for (int team = 0; team < teams; ++team) { - const std::vector distance = walk(map, workers[team]); + const std::vector distance = + stepsFrom(Torus(map), tileMask(Torus(map), workers[team]), walkableTiles(map)); const int bx = context.bootX[team], by = context.bootY[team]; const Vec centre{bx + 2.0, by + 2.0}; struct Spot @@ -1832,10 +1734,11 @@ bool connectColonies(Game &game, int teams, std::string &detail) { Map &map = game.map; const int w = map.getW(), h = map.getH(); - const auto workers = workersByTeam(map, teams); + const auto workers = unitTilesByTeam(map, teams); for (int team = 1; team < teams; ++team) { - std::vector reach = walk(map, workers[0]); + std::vector reach = + stepsFrom(Torus(map), tileMask(Torus(map), workers[0]), walkableTiles(map)); bool connected = false; for (int p : workers[team]) connected = connected || reach[p] >= 0; @@ -2003,14 +1906,15 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { const Map &map = game.map; const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; - const auto workers = workersByTeam(map, teams); + const auto workers = unitTilesByTeam(map, teams); for (int team = 0; team < teams; ++team) if (workers[team].empty()) return "Colony " + std::to_string(team) + " has no workers."; std::vector fromFirst; for (int team = 0; team < teams; ++team) { - const std::vector reach = walk(map, workers[team]); + const std::vector reach = + stepsFrom(Torus(map), tileMask(Torus(map), workers[team]), walkableTiles(map)); if (team == 0) fromFirst = reach; bool met = false; diff --git a/src/map/generator/shared/Grid.cpp b/src/map/generator/shared/Grid.cpp new file mode 100644 index 000000000..0828cb2c9 --- /dev/null +++ b/src/map/generator/shared/Grid.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Grid.h" +#include "Map.h" +#include "Unit.h" +#include +namespace MapGeneration +{ +Torus::Torus(const Map &map) : w(map.getW()), h(map.getH()) {} + +std::vector stepsFrom(const Torus &t, const std::vector &source, + const std::vector &open) +{ + std::vector dist(size_t(t.size()), -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) + { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % t.w, y = queue[head] / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int n = t.at(x + dx, y + dy); + if (dist[n] < 0 && open[n]) + { + dist[n] = dist[queue[head]] + 1; + queue.push_back(n); + } + } + } + return dist; +} + +std::vector stepsFrom(const Torus &t, const std::vector &source) +{ + return stepsFrom(t, source, std::vector(size_t(t.size()), 1)); +} + +std::vector tileMask(const Torus &t, const std::vector &tiles) +{ + std::vector mask(size_t(t.size()), 0); + for (int i : tiles) + mask[i] = 1; + return mask; +} + +std::vector> unitTilesByTeam(const Map &map, int teams) +{ + std::vector> units(size_t(std::max(teams, 0))); + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + { + const Uint16 gid = map.getGroundUnit(x, y); + if (gid == NOGUID) + continue; + const int team = Unit::GIDtoTeam(gid); + if (team >= 0 && team < teams) + units[team].push_back(y * map.getW() + x); + } + return units; +} + +std::vector walkableTiles(const Map &map) +{ + std::vector open(size_t(map.getW()) * map.getH(), 0); + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + open[size_t(y) * map.getW() + x] = + !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + return open; +} + +int firstColonyCutOff(const std::vector &steps, const std::vector> &units, + int first) +{ + for (size_t team = size_t(std::max(first, 0)); team < units.size(); ++team) + { + bool arrived = false; + for (int i : units[team]) + arrived = arrived || steps[i] >= 0; + if (!arrived) + return int(team); + } + return -1; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Grid.h b/src/map/generator/shared/Grid.h new file mode 100644 index 000000000..7dd01ab54 --- /dev/null +++ b/src/map/generator/shared/Grid.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +#include +#include +#include +class Map; +namespace MapGeneration +{ +/// A width by height torus addressed row-major by tile index: the wrap arithmetic every +/// generator needs, written once. +struct Torus +{ + int w, h; + Torus(int width, int height) : w(width), h(height) {} + explicit Torus(const Map &); + int size() const { return w * h; } + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + /// Signed shortest offset from one column to another across the wrap. + int offsetX(int from, int to) const + { + const int d = x(to - from); + return d > w / 2 ? d - w : d; + } + /// Signed shortest offset from one row to another across the wrap. + int offsetY(int from, int to) const + { + const int d = y(to - from); + return d > h / 2 ? d - h : d; + } + /// Squared distance between two tiles, the short way round. + int dist2(int ax, int ay, int bx, int by) const + { + const int dx = offsetX(ax, bx), dy = offsetY(ay, by); + return dx * dx + dy * dy; + } + /// Steps between two tiles when a step may be diagonal, the short way round. + int chebyshev(int ax, int ay, int bx, int by) const + { + return std::max(std::abs(offsetX(ax, bx)), std::abs(offsetY(ay, by))); + } +}; +/// The four cardinal steps, in the order the generators have always listed them. +constexpr int kCardinalSteps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + +/// Breadth-first steps from every source tile, eight-connected across the wrap, through open +/// tiles only: 0 at a source, -1 where nothing was reached. Every step costs one, so the field +/// is the same whatever order the tiles were visited in. +std::vector stepsFrom(const Torus &, const std::vector &source, + const std::vector &open); +/// The same flood with every tile open. +std::vector stepsFrom(const Torus &, const std::vector &source); +/// A mask with one at each listed tile. +std::vector tileMask(const Torus &, const std::vector &tiles); +/// Where each of the first `teams` teams' ground units stand, as tile indices in row-major +/// order. +std::vector> unitTilesByTeam(const Map &, int teams); +/// The tiles a ground unit can stand on and walk through: land carrying no deposit and no +/// building. +std::vector walkableTiles(const Map &); +/// The lowest colony from `first` on none of whose units the flood reached, or -1 when every +/// one was: the check every validator makes that colonies can walk to colony 0. +int firstColonyCutOff(const std::vector &steps, const std::vector> &units, + int first = 1); +} // namespace MapGeneration From ab4f28b648cb7a31d965a2fef5adcf190d83f4fa Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:11:13 -0400 Subject: [PATCH 091/136] Use the shared flood for Stone highlands' and Maze's remaining floods Stone highlands' Chebyshev distance, its pond fill and its ridge- junction distance, and Maze's shore depth, were each the same eight- connected flood again with different masks; they now call stepsFrom with those masks. A unit-step flood gives the same field whatever order it visits tiles in, so nothing a generator reads from it changes. Verified: the golden table compares 126 rows with 0 failures, the sweep reports 0 failing cells, and MapGeneratorDefaultsTest passes. 88 lines become 18. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generator/generators/MazeGenerator.cpp | 32 ++------ .../generators/StoneHighlandsGenerator.cpp | 74 +++---------------- 2 files changed, 18 insertions(+), 88 deletions(-) diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 7f61d14a8..119be3be1 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -346,32 +346,12 @@ TileRect wallFor(const MazeGrid &g, int cell, int direction) // grass - not counting the sand road, which runs down the middle of passages, not their edges. std::vector shoreDepth(const Map &map, const std::vector &onRoad) { - const int w = map.getW(), h = map.getH(); - std::vector depth(size_t(w) * h, -1); - std::vector queue; - queue.reserve(size_t(w) * h); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - if (!map.isGrass(x, y) && !onRoad[size_t(y) * w + x]) - { - depth[size_t(y) * w + x] = 0; - queue.push_back(y * w + x); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const size_t n = size_t(map.normalizeY(y + dy)) * w + map.normalizeX(x + dx); - if (depth[n] < 0) - { - depth[n] = depth[size_t(queue[head])] + 1; - queue.push_back(int(n)); - } - } - } - return depth; + const Torus t(map); + std::vector shore(size_t(t.size()), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + shore[size_t(y) * t.w + x] = !map.isGrass(x, y) && !onRoad[size_t(y) * t.w + x]; + return stepsFrom(t, shore); } struct CellTile diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index de2b61296..07bcc3512 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -144,29 +144,10 @@ int percentile(std::vector samples, int percent) // Chebyshev steps from every tile to the nearest source tile; kFar with no sources. std::vector chebyshevDistance(const Torus &t, const std::vector &source) { - std::vector distance(source.size(), kFar); - std::vector queue; - queue.reserve(source.size()); - for (size_t i = 0; i < source.size(); ++i) - if (source[i]) - { - distance[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int q = t.at(px + dx, py + dy); - if (distance[q] == kFar) - { - distance[q] = distance[p] + 1; - queue.push_back(q); - } - } - } + std::vector distance = stepsFrom(t, source); + for (int &d : distance) + if (d < 0) + d = kFar; return distance; } @@ -730,30 +711,15 @@ void placePonds(Layout &L, GenerationContext &context, int spacing, int pondSize } // Fill any ground a pond has closed in: everything still walkable is reachable from the band // along the ridges, which no pond comes near. - std::vector reached(n, 0); - std::vector queue; + std::vector open(n, 0), band(n, 0); for (int i = 0; i < n; ++i) - if (!L.ridge[i] && !L.pond[i] && L.ridgeDistance[i] <= 2) - { - reached[i] = 1; - queue.push_back(i); - } - for (size_t head = 0; head < queue.size(); ++head) { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int q = t.at(px + dx, py + dy); - if (!reached[q] && !L.ridge[q] && !L.pond[q]) - { - reached[q] = 1; - queue.push_back(q); - } - } + open[i] = !L.ridge[i] && !L.pond[i]; + band[i] = open[i] && L.ridgeDistance[i] <= 2; } + const std::vector reached = stepsFrom(t, band, open); for (int i = 0; i < n; ++i) - if (!L.ridge[i] && !L.pond[i] && !reached[i]) + if (open[i] && reached[i] < 0) L.pond[i] = 1; } @@ -841,7 +807,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // the nearest junction ranks how central a site is on its ridgeline. const int reach = o.passWidth / 2 + 2; std::map, std::vector> exclusive, anyPair; - std::vector alongRidge(n, -1), queue; + std::vector junction(n, 0); for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) { @@ -853,30 +819,14 @@ Layout design(const GenerationRequest &request, GenerationContext &context) int nearIds[3], farIds[3]; const int count = nearbyValleys(L, x, y, 2, nearIds, 3); if (count >= 3) - { - alongRidge[i] = 0; - queue.push_back(i); - } + junction[i] = 1; for (int p = 0; p < count; ++p) for (int q = p + 1; q < count; ++q) anyPair[{nearIds[p], nearIds[q]}].push_back(i); if (count == 2 && nearbyValleys(L, x, y, reach, farIds, 3) == 2) exclusive[{nearIds[0], nearIds[1]}].push_back(i); } - for (size_t head = 0; head < queue.size(); ++head) - { - const int p = queue[head], px = p % t.w, py = p / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int q = t.at(px + dx, py + dy); - if (L.ridge[q] && alongRidge[q] < 0) - { - alongRidge[q] = alongRidge[p] + 1; - queue.push_back(q); - } - } - } + const std::vector alongRidge = stepsFrom(t, junction, L.ridge); // A random spanning tree of passes joins every valley; loopiness then opens that share of the // remaining shared ridgelines as well. From 0e1bf687612f09345a20998a2ee8e305f65f6b3e Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:17:08 -0400 Subject: [PATCH 092/136] Split Contested commons' generate into its five stages The 404-line function becomes disperseSeeds, sizeIslands, paintTerrain, stockCommons and settleHomes around a Layout struct holding what they hand each other: the working area grid, the next free area number, the seed points, the radii and the area numbers of the commons and its bridges. Each stage runs in the same order and makes the same draws from the same streams, so the maps are unchanged; the paint stage takes the layout by const reference, which the compiler now enforces. Maze's validator drops two locals the shared flood made unused, and the contract test's scoped-generator check on a failed roll, which saved the generator and never compared it, now compares it. Verified: the golden table compares 126 rows with 0 failures and MapGeneratorDefaultsTest passes, including the restored assertion. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generators/ContestedCommonsGenerator.cpp | 192 +++++++++++------- .../generator/generators/MazeGenerator.cpp | 2 +- test/MapGeneratorDefaultsTest.cpp | 1 + 3 files changed, 120 insertions(+), 75 deletions(-) diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp index 7c9fcfdce..421137eae 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.cpp +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -33,20 +33,23 @@ void createJaggedIsland(Map &map, GenerationContext &context, std::vector & stampShape(grid, map.getW(), map.getH(), area, ShapeTransform({double(x), double(y)}, 0), shape, true); } -static bool generate(Game &game, GenerationContext &context) +// What the stages of a roll hand each other: the working area grid and the next free area +// number, where the colonies and the commons were seeded, and how big everything came out. +struct Layout { - context.stage = "layout"; - const ContestedCommonsOptions options(context.request); - game.map.makeHomogenMap(WATER); - for (int i = 0; i < context.request.nbTeams; ++i) - game.addTeam(); - - const int W = game.map.getW(); - const int H = game.map.getH(); - + std::vector grid; int areaNumber = 1; - std::vector grid(W * H, 0); - + std::vector teamPoints; + std::vector teamAreaNumbers; + MapGeneratorPoint commonsCenter{0, 0}; + int homeRadius = 0, commonsRadius = 0, moatWidth = 0; + int commonsAreaNumber = 0, bridgeAreaNumber = 0; + explicit Layout(const Map &map) : grid(size_t(map.getW()) * map.getH(), 0) {} +}; + +// Spread the colony seeds and the commons seed jointly, then tell them apart. +static bool disperseSeeds(Game &game, GenerationContext &context, Layout &L) +{ // Spread the team seeds AND the commons seed jointly, as one dispersion problem, instead // of placing the teams first and hunting for a leftover gap afterward: the commons is an // (N+1)th point in the exact same "push apart until nobody can move to a better spot" @@ -69,40 +72,45 @@ static bool generate(Game &game, GenerationContext &context) allPoints.push_back(MapGeneratorPoint(0, 0)); // the commons seed allWeights.push_back(commonsWeight); - if (splitUpPoints(game.map, context, grid, 0, allPoints, allWeights, + if (splitUpPoints(game.map, context, L.grid, 0, allPoints, allWeights, PointSearch::WholeRegion) == 0) return false; // splitUpPoints shuffles the whole array, but weights travel with their point, so the // one point that kept commonsWeight is still identifiable regardless of where it landed. - std::vector teamPoints; - std::vector teamAreaNumbers; - MapGeneratorPoint commonsCenter(0, 0); for (unsigned int i = 0; i < allPoints.size(); ++i) { if (allWeights[i] == commonsWeight) { - commonsCenter = allPoints[i]; + L.commonsCenter = allPoints[i]; } else { - teamPoints.push_back(allPoints[i]); - teamAreaNumbers.push_back(areaNumber); - areaNumber += 1; + L.teamPoints.push_back(allPoints[i]); + L.teamAreaNumbers.push_back(L.areaNumber); + L.areaNumber += 1; } } + return true; +} +// Size the home islands and the commons off the room the dispersion bought, draw them into the +// grid and lay the bridge strips across the moat. +static void sizeIslands(Game &game, GenerationContext &context, + const ContestedCommonsOptions &options, Layout &L) +{ + const int W = game.map.getW(); // Home islands are sized off actual team-to-team separation, not the joint figure // splitUpPoints returns (which is dominated by the deliberately-tighter team/commons gap). int minDist = std::numeric_limits::max(); - for (unsigned int i = 0; i < teamPoints.size(); ++i) - for (unsigned int j = i + 1; j < teamPoints.size(); ++j) - minDist = std::min( - minDist, (int)std::sqrt((double)game.map.warpDistSquare( - teamPoints[i].x, teamPoints[i].y, teamPoints[j].x, teamPoints[j].y))); + for (unsigned int i = 0; i < L.teamPoints.size(); ++i) + for (unsigned int j = i + 1; j < L.teamPoints.size(); ++j) + minDist = std::min(minDist, (int)std::sqrt((double)game.map.warpDistSquare( + L.teamPoints[i].x, L.teamPoints[i].y, L.teamPoints[j].x, + L.teamPoints[j].y))); if (minDist == std::numeric_limits::max()) // a single team: no pair to measure - minDist = (int)std::sqrt((double)game.map.warpDistSquare(teamPoints[0].x, teamPoints[0].y, - commonsCenter.x, commonsCenter.y)); + minDist = (int)std::sqrt((double)game.map.warpDistSquare( + L.teamPoints[0].x, L.teamPoints[0].y, L.commonsCenter.x, L.commonsCenter.y)); // Both islands are jagged, which means both can reach noticeably past their nominal // radius in a lucky direction -- comfortably more than a flat few-tile fudge factor once @@ -112,78 +120,85 @@ static bool generate(Game &game, GenerationContext &context) const double teamJaggedness = options.jaggedCoasts ? 0.35 : 0.0; const double commonsJaggedness = options.jaggedCoasts ? 0.32 : 0.0; - const int homeRadius = std::max(10, minDist * options.homeSize / 100); - for (unsigned int i = 0; i < teamPoints.size(); ++i) - createJaggedIsland(game.map, context, grid, teamAreaNumbers[i], teamPoints[i].x, - teamPoints[i].y, homeRadius, teamJaggedness); + L.homeRadius = std::max(10, minDist * options.homeSize / 100); + for (unsigned int i = 0; i < L.teamPoints.size(); ++i) + createJaggedIsland(game.map, context, L.grid, L.teamAreaNumbers[i], L.teamPoints[i].x, + L.teamPoints[i].y, L.homeRadius, teamJaggedness); // Real Euclidean room at the chosen commons spot: the same metric createOval/ // createJaggedIsland use to draw the islands, measured straight from the seed points. int minGapSquared = std::numeric_limits::max(); - for (unsigned int i = 0; i < teamPoints.size(); ++i) + for (unsigned int i = 0; i < L.teamPoints.size(); ++i) minGapSquared = - std::min(minGapSquared, game.map.warpDistSquare(commonsCenter.x, commonsCenter.y, - teamPoints[i].x, teamPoints[i].y)); + std::min(minGapSquared, game.map.warpDistSquare(L.commonsCenter.x, L.commonsCenter.y, + L.teamPoints[i].x, L.teamPoints[i].y)); const int centerGap = (int)std::sqrt((double)minGapSquared); // Budget the gap as: nearest team's worst-case reach, a real moat, the commons' own // worst-case reach, and a small flat buffer on top of all that jaggedness math. - const int teamReach = (int)std::ceil(homeRadius * (1.0 + teamJaggedness * 1.4)); - const int moatWidth = std::max(4, homeRadius * options.moatWidth / 100); - const int roomAvailable = std::max(0, centerGap - teamReach - moatWidth - 3); + const int teamReach = (int)std::ceil(L.homeRadius * (1.0 + teamJaggedness * 1.4)); + L.moatWidth = std::max(4, L.homeRadius * options.moatWidth / 100); + const int roomAvailable = std::max(0, centerGap - teamReach - L.moatWidth - 3); const int roomLimitedRadius = - std::max(homeRadius, (int)(roomAvailable / (1.0 + commonsJaggedness * 1.4))); + std::max(L.homeRadius, (int)(roomAvailable / (1.0 + commonsJaggedness * 1.4))); // The commons is the whole point of the map, so it should use up the room the joint // dispersion just bought it: aim for 4x a home island's radius (roughly what's actually // available once teams are pushed off it -- measured, not guessed), and let the min() // below be the real safety net for whatever a given layout can't quite support. - const int desiredCommonsRadius = homeRadius * options.commonsSize / 100; - const int commonsRadius = std::min(desiredCommonsRadius, roomLimitedRadius); + const int desiredCommonsRadius = L.homeRadius * options.commonsSize / 100; + L.commonsRadius = std::min(desiredCommonsRadius, roomLimitedRadius); const int bridgeCount = options.bridgeCount; std::vector bridgeAngles; for (int i = 0; i < bridgeCount; ++i) bridgeAngles.push_back(context.bounded("layout", 360)); - int commonsAreaNumber = areaNumber; - areaNumber += 1; - createJaggedIsland(game.map, context, grid, commonsAreaNumber, commonsCenter.x, commonsCenter.y, - commonsRadius, commonsJaggedness); + L.commonsAreaNumber = L.areaNumber; + L.areaNumber += 1; + createJaggedIsland(game.map, context, L.grid, L.commonsAreaNumber, L.commonsCenter.x, + L.commonsCenter.y, L.commonsRadius, commonsJaggedness); // Extend the commons island out across the moat along each bridge angle, so those strips // get boosted into land by the heightmap pass below just like the island itself. Bridges // get their own area number, kept separate from the island body, so the resource-field // split further down only ever touches the round island, never a narrow connecting strip. - int bridgeAreaNumber = areaNumber; - areaNumber += 1; + L.bridgeAreaNumber = L.areaNumber; + L.areaNumber += 1; for (unsigned int b = 0; b < bridgeAngles.size() && options.moatBridges; ++b) { double theta = bridgeAngles[b] * pi / 180.0; - for (int r = commonsRadius - 2; r <= commonsRadius + moatWidth + 2; ++r) + for (int r = L.commonsRadius - 2; r <= L.commonsRadius + L.moatWidth + 2; ++r) { - int cx = commonsCenter.x + (int)round(r * cos(theta)); - int cy = commonsCenter.y + (int)round(r * sin(theta)); + int cx = L.commonsCenter.x + (int)round(r * cos(theta)); + int cy = L.commonsCenter.y + (int)round(r * sin(theta)); for (int dx = -1; dx <= 1; ++dx) { for (int dy = -1; dy <= 1; ++dy) { int nx = game.map.normalizeX(cx + dx); int ny = game.map.normalizeY(cy + dy); - if (grid[ny * W + nx] == 0) - grid[ny * W + nx] = bridgeAreaNumber; + if (L.grid[ny * W + nx] == 0) + L.grid[ny * W + nx] = L.bridgeAreaNumber; } } } } +} +// Turn the grid into terrain: noise, islands raised to land, the moat forced back to water. +static void paintTerrain(Game &game, GenerationContext &context, + const ContestedCommonsOptions &options, const Layout &L) +{ + const int W = game.map.getW(); + const int H = game.map.getH(); // Base heightmap: noise everywhere, boosted wherever land has been carved out, so // every island (and every bridge strip) comes out dry and everything else stays ocean. std::vector heights(W * H, 40); adjustHeightmapFromPerlinNoise(game.map, context, heights, 20); for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) - if (grid[y * W + x] != 0) + if (L.grid[y * W + x] != 0) heights[y * W + x] += 60; for (int x = 0; x < W; ++x) @@ -211,12 +226,12 @@ static bool generate(Game &game, GenerationContext &context) { for (int x = 0; x < W; ++x) { - if (grid[y * W + x] == commonsAreaNumber || grid[y * W + x] == bridgeAreaNumber) + if (L.grid[y * W + x] == L.commonsAreaNumber || L.grid[y * W + x] == L.bridgeAreaNumber) continue; - int dx = wrapDelta(x, commonsCenter.x, W); - int dy = wrapDelta(y, commonsCenter.y, H); + int dx = wrapDelta(x, L.commonsCenter.x, W); + int dy = wrapDelta(y, L.commonsCenter.y, H); double r = sqrt((double)(dx * dx + dy * dy)); - if (r >= commonsRadius - 1 && r < commonsRadius + moatWidth) + if (r >= L.commonsRadius - 1 && r < L.commonsRadius + L.moatWidth) { game.map.setUMatPos(x, y, WATER, 1); // A quarter of the ring gets algae. The algae amount thins or thickens that with @@ -233,7 +248,14 @@ static bool generate(Game &game, GenerationContext &context) } } game.map.controlSand(); +} +// Split the commons into zones and give each a role: wood, wheat, the quarry, fruit, or open. +static void stockCommons(Game &game, GenerationContext &context, + const ContestedCommonsOptions &options, Layout &L) +{ + const int W = game.map.getW(); + const int H = game.map.getH(); // Stock the commons with real fields, not a scattering of single tiles -- but the commons // is several times the area of a home island, so splitting it into the same handful of // zones a home island uses would read as one giant forest next to one giant farm. Scale @@ -241,16 +263,16 @@ static bool generate(Game &game, GenerationContext &context) // comes out as a patchwork of many small groves and fields instead of two big blobs. { double areaRatio = - (double)(commonsRadius * commonsRadius) / (double)(homeRadius * homeRadius); + (double)(L.commonsRadius * L.commonsRadius) / (double)(L.homeRadius * L.homeRadius); int zoneCount = (int)std::round(7.0 * std::sqrt(std::max(1.0, areaRatio))); zoneCount = std::max(7, std::min(24, zoneCount)); std::vector zoneWeights(zoneCount, 1); std::vector zoneAreas; - const int firstZoneArea = areaNumber; + const int firstZoneArea = L.areaNumber; for (int z = 0; z < zoneCount; ++z) - zoneAreas.push_back(areaNumber++); - if (divideUpArea(game.map, context, grid, commonsAreaNumber, zoneWeights, zoneAreas)) + zoneAreas.push_back(L.areaNumber++); + if (divideUpArea(game.map, context, L.grid, L.commonsAreaNumber, zoneWeights, zoneAreas)) { // Which zone borders which: zoneAreas is a contiguous run of area numbers, so a // tile's zone index is just its area number minus the first one. Two adjacent @@ -260,15 +282,15 @@ static bool generate(Game &game, GenerationContext &context) { for (int x = 0; x < W; ++x) { - int a = grid[y * W + x] - firstZoneArea; + int a = L.grid[y * W + x] - firstZoneArea; if (a < 0 || a >= zoneCount) continue; int xr = game.map.normalizeX(x + 1); - int b = grid[y * W + xr] - firstZoneArea; + int b = L.grid[y * W + xr] - firstZoneArea; if (b >= 0 && b < zoneCount && b != a) adjacent[a][b] = adjacent[b][a] = true; int yd = game.map.normalizeY(y + 1); - int c = grid[yd * W + x] - firstZoneArea; + int c = L.grid[yd * W + x] - firstZoneArea; if (c >= 0 && c < zoneCount && c != a) adjacent[a][c] = adjacent[c][a] = true; } @@ -338,7 +360,7 @@ static bool generate(Game &game, GenerationContext &context) for (int z = 0; z < zoneCount; ++z) { std::vector pts; - getAllPoints(game.map, grid, zoneAreas[z], pts); + getAllPoints(game.map, L.grid, zoneAreas[z], pts); if (role[z] == ROLE_QUARRY) { // One compact quarry, not a scatter: a single setResource call already @@ -367,7 +389,14 @@ static bool generate(Game &game, GenerationContext &context) } } } +} +// Each colony's starter kit, swarm and workers on its own island. +static bool settleHomes(Game &game, GenerationContext &context, + const ContestedCommonsOptions &options, Layout &L) +{ + const int W = game.map.getW(); + const int H = game.map.getH(); // Give each team a small starter kit, then a swarm and workers, exactly the way the // other generators do it: enough to bootstrap, not enough to make the commons optional. for (int i = 0; i < context.request.nbTeams; ++i) @@ -378,24 +407,24 @@ static bool generate(Game &game, GenerationContext &context) std::vector zoneWeights(5, 1); std::vector zoneAreas; for (int z = 0; z < 5; ++z) - zoneAreas.push_back(areaNumber++); + zoneAreas.push_back(L.areaNumber++); std::vector homePoints; - if (divideUpArea(game.map, context, grid, teamAreaNumbers[i], zoneWeights, zoneAreas)) + if (divideUpArea(game.map, context, L.grid, L.teamAreaNumbers[i], zoneWeights, zoneAreas)) { std::vector pts; - getAllPoints(game.map, grid, zoneAreas[0], pts); + getAllPoints(game.map, L.grid, zoneAreas[0], pts); fillInResource(game.map, context, pts, WOOD, 2); homePoints.insert(homePoints.end(), pts.begin(), pts.end()); pts.clear(); - getAllPoints(game.map, grid, zoneAreas[1], pts); + getAllPoints(game.map, L.grid, zoneAreas[1], pts); fillInResource(game.map, context, pts, CORN, 2); homePoints.insert(homePoints.end(), pts.begin(), pts.end()); pts.clear(); // One compact deposit, not a scatter across the whole zone -- leaves the rest of // it clear to build on instead of peppering it with single-tile boulders. - getAllPoints(game.map, grid, zoneAreas[2], pts); + getAllPoints(game.map, L.grid, zoneAreas[2], pts); if (!pts.empty()) { MapGeneratorPoint quarry = pts[context.bounded("layout", pts.size())]; @@ -404,17 +433,17 @@ static bool generate(Game &game, GenerationContext &context) homePoints.insert(homePoints.end(), pts.begin(), pts.end()); pts.clear(); - getAllPoints(game.map, grid, zoneAreas[3], pts); + getAllPoints(game.map, L.grid, zoneAreas[3], pts); homePoints.insert(homePoints.end(), pts.begin(), pts.end()); pts.clear(); - getAllPoints(game.map, grid, zoneAreas[4], pts); + getAllPoints(game.map, L.grid, zoneAreas[4], pts); homePoints.insert(homePoints.end(), pts.begin(), pts.end()); } else { // The island came out too small to subdivide -- fall back to a light scatter // over the whole thing rather than failing the map. - getAllPoints(game.map, grid, teamAreaNumbers[i], homePoints); + getAllPoints(game.map, L.grid, L.teamAreaNumbers[i], homePoints); if (homePoints.empty()) return false; std::vector cornPts = homePoints; @@ -432,13 +461,28 @@ static bool generate(Game &game, GenerationContext &context) std::vector home(size_t(W) * H, 0); for (const auto &point : homePoints) home[point.y * W + point.x] = 1; - if (!placeSettlement(game, context, i, home, teamPoints[i], "starts")) + if (!placeSettlement(game, context, i, home, L.teamPoints[i], "starts")) return false; } - return true; } +static bool generate(Game &game, GenerationContext &context) +{ + context.stage = "layout"; + const ContestedCommonsOptions options(context.request); + game.map.makeHomogenMap(WATER); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + Layout L(game.map); + if (!disperseSeeds(game, context, L)) + return false; + sizeIslands(game, context, options, L); + paintTerrain(game, context, options, L); + stockCommons(game, context, options, L); + return settleHomes(game, context, options, L); +} + } // namespace ContestedCommonsOptions::ContestedCommonsOptions(const GenerationRequest &r) diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 119be3be1..0db09367f 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -722,7 +722,7 @@ bool generate(Game &game, GenerationContext &context) std::string validateWorld(const Game &game, const GenerationContext &context) { const Map &map = game.map; - const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; + const int teams = context.request.nbTeams; const Torus t(map); const auto units = unitTilesByTeam(map, teams); const std::vector reached = diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 6069d0fbc..238d254f9 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -181,6 +181,7 @@ class MapGeneratorDefaultsTest auto failed = service.generate(fresh, invalid); assert(!failed && failed.error == GenerationError::PlacementFailed && !failed.stage.empty()); + assert(randomGenerator == savedFailureRng); // Legacy sentinel conversion belongs exclusively to the adapter. for (auto pair : {std::pair{D::eCRATERLAKES, 30}, std::pair{D::eCONCRETEISLANDS, 6}, std::pair{D::eISLES, 4}}) From c870aa280d5aa610be4313e22f13fd4ddbfbffef Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:18:55 -0400 Subject: [PATCH 093/136] Split Rugged archipelago's terrain into its growth passes The 274-line function becomes plantBootstraps, expandIslands, smoothGrass and spreadBeaches, and the eight near-identical switch cases in the growth and beach passes become one direction table indexed by the same draw, in the same order: each tile still draws once and tests the same pair of opposite neighbours, and each condition is symmetric in that pair, so the terrain is unchanged. Verified: the golden table compares 126 rows with 0 failures and MapGeneratorDefaultsTest passes. 206 lines become 75. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generators/RuggedArchipelagoGenerator.cpp | 281 +++++------------- 1 file changed, 75 insertions(+), 206 deletions(-) diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index 4bf0778e2..fd4e0172f 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -18,21 +18,18 @@ using namespace MapGeneration; #include "RuggedArchipelagoGenerator.h" -static bool terrain(Game &game, GenerationContext &context, const RuggedArchipelagoOptions &options) +// The eight directions the growth passes below draw from: a pair of opposite neighbours at one +// or two tiles, in the order the draw has always indexed them. +static const int kGrowthDirections[8][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}, + {2, 0}, {0, 2}, {2, 2}, {2, -2}}; + +// Seed one grass patch per colony, each at least a colony's share of the map from the others, +// halving that spacing if a placement keeps failing. Returns how many growth passes the islands +// get, from the map size and the island size control. +static int plantBootstraps(Map &map, GenerationContext &context, + const RuggedArchipelagoOptions &options, std::mt19937 &rng) { - Map &map = game.map; const int w = map.getW(), h = map.getH(); - // context.stream() looks up a named std::mt19937 by string key on every call; every draw in - // this function names the same "terrain" stream, so look it up once and reuse the reference - // through the loops below instead of repeating the lookup per tile per draw. - std::mt19937 &rng = context.stream("terrain"); - - // First, fill with water: - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) - map.setUMTerrain(x, y, WATER); - - // Two, plants "bootstraps" int *bootX = context.bootX.data(); int *bootY = context.bootY.data(); int nbIslands = context.request.nbTeams; @@ -74,220 +71,92 @@ static bool terrain(Game &game, GenerationContext &context, const RuggedArchipel map.setUMTerrain(x + dx, y + dy, GRASS); } } + return islandsSize; +} - // Three, expands islands - for (int s = 0; s < islandsSize; s++) - { +// Grow the islands: for so many passes, every non-grass tile on a checkerboard draws one of +// sixteen values and, for the eight that name a direction, turns to grass when either +// neighbour that way already is. +static void expandIslands(Map &map, std::mt19937 &rng, int passes) +{ + const int w = map.getW(), h = map.getH(); + for (int s = 0; s < passes; s++) for (int oddEven = 0; oddEven < 2; oddEven++) - { for (int y = oddEven; y < h; y += 2) - { for (int x = oddEven; x < w; x += 2) { - TerrainType umt = map.getUMTerrain(x, y); - if (umt == GRASS) + if (map.getUMTerrain(x, y) == GRASS) continue; - - int a, b; - switch (rng() & 15) - { - case 0: - a = map.getUMTerrain(x + 1, y); - b = map.getUMTerrain(x - 1, y); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - case 1: - a = map.getUMTerrain(x, y - 1); - b = map.getUMTerrain(x, y + 1); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - case 2: - a = map.getUMTerrain(x + 1, y + 1); - b = map.getUMTerrain(x - 1, y - 1); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - case 3: - a = map.getUMTerrain(x + 1, y - 1); - b = map.getUMTerrain(x - 1, y + 1); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - case 4: - a = map.getUMTerrain(x + 2, y); - b = map.getUMTerrain(x - 2, y); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - case 5: - a = map.getUMTerrain(x, y - 2); - b = map.getUMTerrain(x, y + 2); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - case 6: - a = map.getUMTerrain(x + 2, y + 2); - b = map.getUMTerrain(x - 2, y - 2); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - case 7: - a = map.getUMTerrain(x + 2, y - 2); - b = map.getUMTerrain(x - 2, y + 2); - if ((a == GRASS) || (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - } - break; - default: - break; - } + const unsigned draw = rng() & 15; + if (draw >= 8) + continue; + const int dx = kGrowthDirections[draw][0], dy = kGrowthDirections[draw][1]; + if (map.getUMTerrain(x + dx, y + dy) == GRASS || + map.getUMTerrain(x - dx, y - dy) == GRASS) + map.setUMTerrain(x, y, GRASS); } - } - } - } +} - // Four, avoid too much sand. Let's smooth +// Close single-tile gaps: a tile between two grass tiles, in any of the four directions, +// becomes grass, twice over. +static void smoothGrass(Map &map) +{ + const int w = map.getW(), h = map.getH(); for (int s = 0; s < 2; s++) for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) - { - int a, b; - a = map.getUMTerrain(x + 1, y); - b = map.getUMTerrain(x - 1, y); - if ((a == GRASS) && (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - continue; - } - a = map.getUMTerrain(x, y - 1); - b = map.getUMTerrain(x, y + 1); - if ((a == GRASS) && (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - continue; - } - a = map.getUMTerrain(x + 1, y + 1); - b = map.getUMTerrain(x - 1, y - 1); - if ((a == GRASS) && (b == GRASS)) + for (int d = 0; d < 4; d++) { - map.setUMTerrain(x, y, GRASS); - continue; - } - a = map.getUMTerrain(x + 1, y - 1); - b = map.getUMTerrain(x - 1, y + 1); - if ((a == GRASS) && (b == GRASS)) - { - map.setUMTerrain(x, y, GRASS); - continue; + const int dx = kGrowthDirections[d][0], dy = kGrowthDirections[d][1]; + if (map.getUMTerrain(x + dx, y + dy) == GRASS && + map.getUMTerrain(x - dx, y - dy) == GRASS) + { + map.setUMTerrain(x, y, GRASS); + break; + } } - } - - map.controlSand(); +} - // Five, add some sand - for (int s = 0; s < options.beach_size; s++) +// Widen the beaches: for so many passes, every tile on a four-by-four lattice draws one of +// eight values; the first four turn it to sand between sand and water along that direction, +// the last four between sand and sand. +static void spreadBeaches(Map &map, std::mt19937 &rng, int passes) +{ + const int w = map.getW(), h = map.getH(); + for (int s = 0; s < passes; s++) for (int dy = 0; dy < 4; dy++) for (int dx = 0; dx < 4; dx++) for (int y = dy; y < h; y += 4) for (int x = dx; x < w; x += 4) { - int a, b; - switch (rng() & 7) - { - case 0: - a = map.getUMTerrain(x + 1, y); - b = map.getUMTerrain(x - 1, y); - if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - case 1: - a = map.getUMTerrain(x, y - 1); - b = map.getUMTerrain(x, y + 1); - if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - case 2: - a = map.getUMTerrain(x + 1, y + 1); - b = map.getUMTerrain(x - 1, y - 1); - if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - case 3: - a = map.getUMTerrain(x + 1, y - 1); - b = map.getUMTerrain(x - 1, y + 1); - if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - - case 4: - a = map.getUMTerrain(x + 1, y); - b = map.getUMTerrain(x - 1, y); - if ((a == SAND) && (b == SAND)) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - case 5: - a = map.getUMTerrain(x, y - 1); - b = map.getUMTerrain(x, y + 1); - if ((a == SAND) && (b == SAND)) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - case 6: - a = map.getUMTerrain(x + 1, y + 1); - b = map.getUMTerrain(x - 1, y - 1); - if ((a == SAND) && (b == SAND)) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - case 7: - a = map.getUMTerrain(x + 1, y - 1); - b = map.getUMTerrain(x - 1, y + 1); - if ((a == SAND) && (b == SAND)) - { - map.setUMTerrain(x, y, SAND); - continue; - } - break; - } + const unsigned draw = rng() & 7; + const int ddx = kGrowthDirections[draw & 3][0], + ddy = kGrowthDirections[draw & 3][1]; + const int a = map.getUMTerrain(x + ddx, y + ddy), + b = map.getUMTerrain(x - ddx, y - ddy); + const bool shore = + draw < 4 ? (a == SAND && b == WATER) || (a == WATER && b == SAND) + : a == SAND && b == SAND; + if (shore) + map.setUMTerrain(x, y, SAND); } +} - // map.controlSand(); +static bool terrain(Game &game, GenerationContext &context, const RuggedArchipelagoOptions &options) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + // Every draw below names the "terrain" stream; one lookup serves all of them. + std::mt19937 &rng = context.stream("terrain"); + + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + map.setUMTerrain(x, y, WATER); + const int passes = plantBootstraps(map, context, options, rng); + expandIslands(map, rng, passes); + smoothGrass(map); + map.controlSand(); + spreadBeaches(map, rng, options.beach_size); map.rebuildTerrain(); return true; } From ffadd74365061e246290b7f21336700a6a7469c6 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:20:24 -0400 Subject: [PATCH 094/136] Split Isles' generate into its stages The 213-line function becomes layoutIslands, raiseIslands, buildBridges, paintTerrain and placeAlgae around a Layout struct holding the area grid, the colonies' seeds, weights and areas, the spacing the dispersion found, the height field, the last distance field and the bridges' tiles and area; generate keeps the resource division and the cramped-start opening that close a roll. Same stages in the same order with the same draws, so the maps are unchanged. Verified: the golden table compares 126 rows with 0 failures and MapGeneratorDefaultsTest passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generator/generators/IslesGenerator.cpp | 156 ++++++++++++------ 1 file changed, 101 insertions(+), 55 deletions(-) diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index 1973e8580..cde293179 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -17,65 +17,81 @@ #include using namespace MapGeneration; #include "IslesGenerator.h" -static bool generate(Game &game, GenerationContext &context) +// What the stages of a roll hand each other: the area grid and the next free area number, each +// colony's seed point, weight and area, the spacing the dispersion found, the height field the +// islands and bridges are raised in, the last distance field, and the bridges' tiles and area. +struct Layout { - context.stage = "layout"; - const IslesOptions options(context.request); - const int islandSize = options.island_size, bridgeWidth = options.bridge_width, - bridgeRadius = bridgeWidth - 2; - game.map.makeHomogenMap(context.request.terrainType); - for (int i = 0; i < context.request.nbTeams; ++i) - game.addTeam(); - + std::vector grid; int areaNumber = 1; - std::vector grid(game.map.getW() * game.map.getH(), 0); + std::vector teamPoints; + std::vector teamWeights, teamAreaNumbers; + int minDist = 0; + std::vector heightmap, distances; + std::vector connectorPoints; + int connectorArea = 0; + explicit Layout(const Map &map) + : grid(size_t(map.getW()) * map.getH(), 0), heightmap(size_t(map.getW()) * map.getH(), 50) + { + } +}; +// Spread the colonies apart and give each an oval island of its own. +static void layoutIslands(Game &game, GenerationContext &context, const IslesOptions &options, + Layout &L) +{ + const int islandSize = options.island_size; // Do the starting locations of the teams - std::vector teamPoints; - std::vector teamWeights; - std::vector teamAreaNumbers; for (int i = 0; i < context.request.nbTeams; ++i) { - teamPoints.push_back(MapGeneratorPoint(0, 0)); - teamWeights.push_back(1); - teamAreaNumbers.push_back(areaNumber); - areaNumber += 1; + L.teamPoints.push_back(MapGeneratorPoint(0, 0)); + L.teamWeights.push_back(1); + L.teamAreaNumbers.push_back(L.areaNumber); + L.areaNumber += 1; } - int minDist = splitUpPoints(game.map, context, grid, 0, teamPoints, teamWeights); + L.minDist = splitUpPoints(game.map, context, L.grid, 0, L.teamPoints, L.teamWeights); // Construct the areas for the teams for (int i = 0; i < context.request.nbTeams; ++i) { - createOval(game.map, grid, teamAreaNumbers[i], teamPoints[i].x, teamPoints[i].y, - minDist * islandSize / 100, minDist * islandSize / 100); + createOval(game.map, L.grid, L.teamAreaNumbers[i], L.teamPoints[i].x, L.teamPoints[i].y, + L.minDist * islandSize / 100, L.minDist * islandSize / 100); } +} - // Construct a heightmap - std::vector heightmap(game.map.getW() * game.map.getH(), 50); +// Raise the islands in the height field, by distance from their edges. +static void raiseIslands(Game &game, Layout &L) +{ + // Construct a L.heightmap std::vector teamAreaPoints; - getAllOtherPoints(game.map, grid, 0, teamAreaPoints); + getAllOtherPoints(game.map, L.grid, 0, teamAreaPoints); std::vector obstacles; - std::vector distances; - computeDistances(game.map, teamAreaPoints, obstacles, distances); + computeDistances(game.map, teamAreaPoints, obstacles, L.distances); // Stamp out the team areas for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) { - int d = distances[y * game.map.getW() + x]; + int d = L.distances[y * game.map.getW() + x]; if (d > 1 && d <= 11) - heightmap[y * game.map.getW() + x] += (11 - d) * 10; + L.heightmap[y * game.map.getW() + x] += (11 - d) * 10; else if (d == 1) - heightmap[y * game.map.getW() + x] += 100; + L.heightmap[y * game.map.getW() + x] += 100; } } +} +// Join every pair of islands whose straight line crosses no third island with a land bridge. +static void buildBridges(Game &game, GenerationContext &context, const IslesOptions &options, + Layout &L) +{ + const int bridgeWidth = options.bridge_width, bridgeRadius = bridgeWidth - 2; // Connect each teams area to each other players area - std::vector connectorPoints; - int connectorArea = areaNumber; - areaNumber += 1; + L.connectorArea = L.areaNumber; + L.areaNumber += 1; + std::vector obstacles; for (int i = 0; i < context.request.nbTeams; ++i) { for (int j = i + 1; j < context.request.nbTeams; ++j) @@ -83,8 +99,8 @@ static bool generate(Game &game, GenerationContext &context) // Choose one random point from each players area std::vector teamI; std::vector teamJ; - getAllPoints(game.map, grid, teamAreaNumbers[i], teamI); - getAllPoints(game.map, grid, teamAreaNumbers[j], teamJ); + getAllPoints(game.map, L.grid, L.teamAreaNumbers[i], teamI); + getAllPoints(game.map, L.grid, L.teamAreaNumbers[j], teamJ); chooseRandomPoints(game.map, context, teamI, 1); chooseRandomPoints(game.map, context, teamJ, 1); @@ -101,9 +117,9 @@ static bool generate(Game &game, GenerationContext &context) for (int y = -bridgeRadius; y <= bridgeRadius && !failed; ++y) { int ny = game.map.normalizeY(linePoints[p].y + y); - int g = grid[ny * game.map.getW() + nx]; - if (g != 0 && g != teamAreaNumbers[i] && g != teamAreaNumbers[j] && - g != connectorArea) + int g = L.grid[ny * game.map.getW() + nx]; + if (g != 0 && g != L.teamAreaNumbers[i] && g != L.teamAreaNumbers[j] && + g != L.connectorArea) { failed = true; } @@ -116,17 +132,17 @@ static bool generate(Game &game, GenerationContext &context) { for (unsigned int p = 0; p < linePoints.size(); ++p) { - connectorPoints.push_back(linePoints[p]); + L.connectorPoints.push_back(linePoints[p]); for (int x = -bridgeRadius; x <= bridgeRadius; ++x) { int nx = game.map.normalizeX(linePoints[p].x + x); for (int y = -bridgeRadius; y <= bridgeRadius; ++y) { int ny = game.map.normalizeY(linePoints[p].y + y); - int d = distances[ny * game.map.getW() + nx]; + int d = L.distances[ny * game.map.getW() + nx]; if (d > bridgeWidth + 1) { - grid[ny * game.map.getW() + nx] = connectorArea; + L.grid[ny * game.map.getW() + nx] = L.connectorArea; } } } @@ -134,28 +150,35 @@ static bool generate(Game &game, GenerationContext &context) } } } - computeDistances(game.map, connectorPoints, obstacles, distances); + computeDistances(game.map, L.connectorPoints, obstacles, L.distances); +} +// Raise the bridges, add noise, and turn the height field into water, sand and grass. +static void paintTerrain(Game &game, GenerationContext &context, const IslesOptions &options, + Layout &L) +{ + const int bridgeWidth = options.bridge_width; // Stamp out the connectors for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) { - int d = distances[y * game.map.getW() + x]; + int d = L.distances[y * game.map.getW() + x]; if (d > 1 && d <= bridgeWidth) - heightmap[y * game.map.getW() + x] += (bridgeWidth - d) * (100 / (bridgeWidth - 1)); + L.heightmap[y * game.map.getW() + x] += + (bridgeWidth - d) * (100 / (bridgeWidth - 1)); else if (d == 1) - heightmap[y * game.map.getW() + x] += 100; + L.heightmap[y * game.map.getW() + x] += 100; } } - // Use the heightmap to put in water, grass, and sand - adjustHeightmapFromPerlinNoise(game.map, context, heightmap, 45); + // Use the L.heightmap to put in water, grass, and sand + adjustHeightmapFromPerlinNoise(game.map, context, L.heightmap, 45); for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) { - int total_height = heightmap[y * game.map.getW() + x]; + int total_height = L.heightmap[y * game.map.getW() + x]; if (total_height < 90) game.map.setUMatPos(x, y, WATER, 1); else if (options.sandy_beaches && total_height > 95 && total_height < 105) @@ -165,32 +188,39 @@ static bool generate(Game &game, GenerationContext &context) } } game.map.controlSand(); +} - // Reset the grid, and recompute within the boundaries of the various islands +// Re-divide the land between the colonies and give each an algae patch just off its coast. +static bool placeAlgae(Game &game, GenerationContext &context, const IslesOptions &options, + Layout &L) +{ + const int bridgeWidth = options.bridge_width; + // Reset the L.grid, and recompute within the boundaries of the various islands for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) { - if (grid[y * game.map.getW() + x] != connectorArea) - grid[y * game.map.getW() + x] = 0; + if (L.grid[y * game.map.getW() + x] != L.connectorArea) + L.grid[y * game.map.getW() + x] = 0; } } - splitUpArea(game.map, context, grid, 0, teamPoints, teamWeights, teamAreaNumbers, true); + splitUpArea(game.map, context, L.grid, 0, L.teamPoints, L.teamWeights, L.teamAreaNumbers, true); - std::vector connectorDistances = distances; + std::vector connectorDistances = L.distances; + std::vector obstacles; // For each team, find a point just off the coast and place algae there for (int i = 0; i < context.request.nbTeams; ++i) { std::vector sources; - getAllPoints(game.map, grid, teamAreaNumbers[i], sources); - computeDistances(game.map, sources, obstacles, distances); + getAllPoints(game.map, L.grid, L.teamAreaNumbers[i], sources); + computeDistances(game.map, sources, obstacles, L.distances); std::vector possible; for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) { - int d = distances[y * game.map.getW() + x]; + int d = L.distances[y * game.map.getW() + x]; int d2 = connectorDistances[y * game.map.getW() + x]; if (d == 8 && (!options.land_bridges || d2 > bridgeWidth)) { @@ -206,8 +236,24 @@ static bool generate(Game &game, GenerationContext &context) // A five by five patch of algae at the default amount. setScaledResource(game.map, possible[r].x, possible[r].y, ALGA, 5, options.algae); } + return true; +} - if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber, +static bool generate(Game &game, GenerationContext &context) +{ + context.stage = "layout"; + const IslesOptions options(context.request); + game.map.makeHomogenMap(context.request.terrainType); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + Layout L(game.map); + layoutIslands(game, context, options, L); + raiseIslands(game, L); + buildBridges(game, context, options, L); + paintTerrain(game, context, options, L); + if (!placeAlgae(game, context, options, L)) + return false; + if (!divideUpPlayerLands(game, context, L.grid, L.teamAreaNumbers, L.areaNumber, {options.wheat, options.wood, options.stone})) { return false; From d801eadef36f40d993d2a569909ef734f7bd90b9 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:23:21 -0400 Subject: [PATCH 095/136] Split Shattered coast's terrain into its stages The 448-line function becomes fitTerrainMix (the numeric search for painting shares whose smoothed result lands on the requested ones), paintPatchwork, smoothPatchwork and placeColonies, with one countTerrain in place of three copies of the same tally. The smoothing pass's four pairs of hand-written direction blocks become a direction table indexed by the same draw in the same order, each condition symmetric in its pair of neighbours, and the colony spacing still uses the grass count taken before sand control. A final simulation whose result nothing read is gone; the simulation stream feeds nothing after it. Verified: the golden table compares 126 rows with 0 failures, the sweep reports 0 failing cells with Shattered coast at 5/5 everywhere, and MapGeneratorDefaultsTest passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generators/ShatteredCoastGenerator.cpp | 279 ++++++------------ 1 file changed, 95 insertions(+), 184 deletions(-) diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 3b99e6d31..1d7699499 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -185,32 +185,40 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas *finalGrass = ((double)grassCount) / ((double)totalCount); } -static bool terrain(Game &game, GenerationContext &context, const ShatteredCoastOptions &options) +// The share of water, sand and grass a map is asked for, and what a patchwork actually ends +// up with once smoothed. +struct TerrainMix { - Map &map = game.map; - const int w = map.getW(), h = map.getH(); - // Same reasoning as simulateRandomMap's rng: every draw below names the "terrain" stream, - // so look it up once rather than on every one of the many draws per tile in the loops below. - std::mt19937 &rng = context.stream("terrain"); + double water, sand, grass; +}; - int waterRatio = options.water; - int sandRatio = options.sand; - int grassRatio = options.grass; - int totalRatio = waterRatio + sandRatio + grassRatio; - int smooth = options.smoothing; - double baseWater, baseSand, baseGrass; +static void countTerrain(const Map &map, int &waterCount, int &sandCount, int &grassCount) +{ + waterCount = sandCount = grassCount = 0; + for (int y = 0; y < map.getH(); y++) + for (int x = 0; x < map.getW(); x++) + { + switch (map.getUMTerrain(x, y)) + { + case WATER: + waterCount++; + continue; + case SAND: + sandCount++; + continue; + case GRASS: + grassCount++; + continue; + } + } +} - if (totalRatio == 0) - { - baseWater = baseSand = baseGrass = 1.0 / 3.0; - } - else - { - baseWater = (float)waterRatio / (float)totalRatio; - baseSand = (float)sandRatio / (float)totalRatio; - baseGrass = (float)grassRatio / (float)totalRatio; - } - // Sorry, the equation is too complex for me. We use a numeric approach: +// Smoothing eats into whichever terrain is rarest, so the shares the patchwork is painted with +// are not the shares the map should end up with. There is no closed form; this searches for +// painting shares whose simulated result lands on the requested ones. +static TerrainMix fitTerrainMix(GenerationContext &context, const TerrainMix &base, int smooth) +{ + const double baseWater = base.water, baseSand = base.sand, baseGrass = base.grass; double alphaWater = baseWater; double alphaSand = baseSand; double alphaGrass = baseGrass; @@ -311,14 +319,16 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast } } - double simWater, simSand, simGrass; - simulateRandomMap(context, smooth, alphaWater, alphaSand, alphaGrass, &simWater, &simSand, - &simGrass); + return {alphaWater, alphaSand, alphaGrass}; +} - totalRatio = 0x7FFF; - waterRatio = (int)(((double)alphaWater) * ((double)totalRatio)); - sandRatio = (int)(((double)alphaSand) * ((double)totalRatio)); - grassRatio = (int)(((double)alphaGrass) * ((double)totalRatio)); +// Every tile draws its terrain independently, in the fitted shares. +static void paintPatchwork(Map &map, std::mt19937 &rng, const TerrainMix &mix) +{ + int totalRatio = 0x7FFF; + int waterRatio = (int)(((double)mix.water) * ((double)totalRatio)); + int sandRatio = (int)(((double)mix.sand) * ((double)totalRatio)); + int grassRatio = (int)(((double)mix.grass) * ((double)totalRatio)); if (waterRatio < 0) waterRatio = 0; if (sandRatio < 0) @@ -327,9 +337,8 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast grassRatio = 0; totalRatio = waterRatio + sandRatio + grassRatio; - // First, we create a fully random patchwork: - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) + for (int y = 0; y < map.getH(); y++) + for (int x = 0; x < map.getW(); x++) { int r = rng() % totalRatio; r -= waterRatio; @@ -352,59 +361,31 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast } assert(false); // Want's to sing ? } +} - // What's finally in ? - int waterCount = 0; - int sandCount = 0; - int grassCount = 0; - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) - { - switch (map.getUMTerrain(x, y)) - { - case WATER: - waterCount++; - continue; - case SAND: - sandCount++; - continue; - case GRASS: - grassCount++; - continue; - } - } - double totalCount = (double)(waterCount + sandCount + grassCount); +// The pairs of opposite neighbours a smoothing pass looks across: one draw picks the first or +// the second of each pair of directions, in the order the passes have always used. +static const int kSmoothingDirections[8][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}, + {2, 0}, {0, 2}, {2, 2}, {2, -2}}; +// Each pass makes a tile take the terrain of two matching opposite neighbours, one direction +// after another, but a terrain already over its requested share is allowed to spread only +// rarely, the more so the further over it is. +static void smoothPatchwork(Map &map, std::mt19937 &rng, int smooth, const TerrainMix &base) +{ + const int w = map.getW(), h = map.getH(); for (int i = 0; i < smooth; i++) { - // What's in now? - waterCount = 0; - sandCount = 0; - grassCount = 0; - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) - { - switch (map.getUMTerrain(x, y)) - { - case WATER: - waterCount++; - continue; - case SAND: - sandCount++; - continue; - case GRASS: - grassCount++; - continue; - } - } + int waterCount, sandCount, grassCount; + countTerrain(map, waterCount, sandCount, grassCount); double totalRatioCount = (double)(waterCount + sandCount + grassCount); double waterRatioCount = waterCount / totalRatioCount; double sandRatioCount = sandCount / totalRatioCount; double grassRatioCount = grassCount / totalRatioCount; - double errWaterRatioCount = waterRatioCount - baseWater; - double errSandRatioCount = sandRatioCount - baseSand; - double errGrassRatioCount = grassRatioCount - baseGrass; + double errWaterRatioCount = waterRatioCount - base.water; + double errSandRatioCount = sandRatioCount - base.sand; + double errGrassRatioCount = grassRatioCount - base.grass; Uint32 allowed[3]; if (errWaterRatioCount > 0) @@ -420,10 +401,6 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast else allowed[2] = 0; - assert(allowed[0] <= (Uint32)0xFFFFFFFF); - assert(allowed[1] <= (Uint32)0xFFFFFFFF); - assert(allowed[2] <= (Uint32)0xFFFFFFFF); - if (i == 0) { allowed[0] = 0; @@ -433,114 +410,28 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) - { - if (rng() & 4) + for (int pair = 0; pair < 4; pair++) { - int a = map.getUMTerrain(x + 1, y); - int b = map.getUMTerrain(x - 1, y); + const int d = (rng() & 4) ? 2 * pair : 2 * pair + 1; + const int dx = kSmoothingDirections[d][0], dy = kSmoothingDirections[d][1]; + const int a = map.getUMTerrain(x + dx, y + dy); + const int b = map.getUMTerrain(x - dx, y - dy); if ((a == b) && (allowed[a] <= rng())) { map.setUMTerrain(x, y, (TerrainType)a); - continue; + break; } } - else - { - int a = map.getUMTerrain(x, y - 1); - int b = map.getUMTerrain(x, y + 1); - if ((a == b) && (allowed[a] <= rng())) - { - map.setUMTerrain(x, y, (TerrainType)a); - continue; - } - } - if (rng() & 4) - { - int a = map.getUMTerrain(x + 1, y + 1); - int b = map.getUMTerrain(x - 1, y - 1); - if ((a == b) && (allowed[a] <= rng())) - { - map.setUMTerrain(x, y, (TerrainType)a); - continue; - } - } - else - { - int a = map.getUMTerrain(x + 1, y - 1); - int b = map.getUMTerrain(x - 1, y + 1); - if ((a == b) && (allowed[a] <= rng())) - { - map.setUMTerrain(x, y, (TerrainType)a); - continue; - } - } - if (rng() & 4) - { - int a = map.getUMTerrain(x + 2, y); - int b = map.getUMTerrain(x - 2, y); - if ((a == b) && (allowed[a] <= rng())) - { - map.setUMTerrain(x, y, (TerrainType)a); - continue; - } - } - else - { - int a = map.getUMTerrain(x, y - 2); - int b = map.getUMTerrain(x, y + 2); - if ((a == b) && (allowed[a] <= rng())) - { - map.setUMTerrain(x, y, (TerrainType)a); - continue; - } - } - if (rng() & 4) - { - int a = map.getUMTerrain(x + 2, y + 2); - int b = map.getUMTerrain(x - 2, y - 2); - if ((a == b) && (allowed[a] <= rng())) - { - map.setUMTerrain(x, y, (TerrainType)a); - continue; - } - } - else - { - int a = map.getUMTerrain(x + 2, y - 2); - int b = map.getUMTerrain(x - 2, y + 2); - if ((a == b) && (allowed[a] <= rng())) - { - map.setUMTerrain(x, y, (TerrainType)a); - continue; - } - } - } } - // What's finally in ? - waterCount = 0; - sandCount = 0; - grassCount = 0; - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) - { - switch (map.getUMTerrain(x, y)) - { - case WATER: - waterCount++; - continue; - case SAND: - sandCount++; - continue; - case GRASS: - grassCount++; - continue; - } - } - totalCount = (double)(waterCount + sandCount + grassCount); - - map.controlSand(); +} - // Now, we have to find suitable places for teams: +// Seat every colony on grass, each a colony's share of the grass from the others, and give +// each a meadow unless the colonies are to start in the shattered terrain itself. The counts +// are the map's before sand control ran. +static bool placeColonies(Map &map, GenerationContext &context, + const ShatteredCoastOptions &options, int grassCount, double totalCount) +{ + const int w = map.getW(), h = map.getH(); int nbTeams = context.request.nbTeams; int minDistSquare = (int)((double)((double)w * (double)h * (double)grassCount) / (double)((double)nbTeams * (double)totalCount)); @@ -633,18 +524,38 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast map.setUMTerrain(maxX + dx, maxY + dy, GRASS); } - // Let's add some green space for teams: a meadow around every colony, unless the colonies are - // to start in the shattered terrain itself. int squareSize = 5 + (int)(sqrt((double)minDistSquare) / 4.5); for (int team = 0; team < nbTeams && options.colony_meadows; team++) { map.setUMatPos(context.bootX[team] + 2, context.bootY[team] + 0, GRASS, squareSize); map.setUMatPos(context.bootX[team] + 2, context.bootY[team] + 2, GRASS, squareSize); } + return true; +} +static bool terrain(Game &game, GenerationContext &context, const ShatteredCoastOptions &options) +{ + Map &map = game.map; + // Every draw below names the "terrain" stream; one lookup serves all of them. + std::mt19937 &rng = context.stream("terrain"); + + const int totalRatio = options.water + options.sand + options.grass; + TerrainMix base{1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0}; + if (totalRatio != 0) + base = {(float)options.water / (float)totalRatio, (float)options.sand / (float)totalRatio, + (float)options.grass / (float)totalRatio}; + const TerrainMix mix = fitTerrainMix(context, base, options.smoothing); + paintPatchwork(map, rng, mix); + smoothPatchwork(map, rng, options.smoothing, base); + int waterCount, sandCount, grassCount; + countTerrain(map, waterCount, sandCount, grassCount); + const double totalCount = (double)(waterCount + sandCount + grassCount); + + map.controlSand(); + if (!placeColonies(map, context, options, grassCount, totalCount)) + return false; map.controlSand(); map.rebuildTerrain(); - return true; } From 9493124c5254d0996c71395dd511a692ff54d2c2 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:30:36 -0400 Subject: [PATCH 096/136] Split City states' design into coasts, features and rasterization The 389-line function becomes a Coasts struct (the profiles and amplitudes a roll draws and where the commons' shore, the strait's far side and the homes' outer edge lie at any point of a wedge), a Features struct with rollFeatures (the homes' lakes, creek, ridges and sand, and the commons' heart and sand) and rasterize (the per-tile classification and each home's causeway ends); design itself is 33 lines. Every kind of feature draws from a stream of its own, so the heart, now rolled first for its shape, draws what it drew after the sand. Verified: the golden table compares 126 rows with 0 failures and MapGeneratorDefaultsTest passes. Along the way the table caught a rename that had turned the stream name "city-home-lakes" into another name and changed every City states map; that is the check working as intended. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generators/CityStatesGenerator.cpp | 298 ++++++++++-------- 1 file changed, 173 insertions(+), 125 deletions(-) diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 126215611..8062c59c4 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -244,91 +244,118 @@ struct Layout std::string failure; }; -Layout design(const GenerationRequest &request, GenerationContext &context) +// A roll's coasts: the profiles and amplitudes drawn for it, and where the commons' shore, the +// strait's far side and the homes' outer edge lie at any point of a wedge. All periodic in the +// wedge, so every home gets the same. +struct Coasts { - Layout L; - L.t = {1 << request.wDec, 1 << request.hDec}; - L.g = geometryFor(request); - const Torus &t = L.t; - const Geometry &g = L.g; - const int n = t.w * t.h, teams = g.teams; - const CityStatesOptions o(request); - L.cx = t.w / 2; - L.cy = t.h / 2; - L.phase = context.bounded("city-layout", 3600) / 3600.0 * 2 * pi; - const double wedge = 2 * pi / teams; - const double inner = g.innerRadius(), depth = g.depth(); - - // What this map is made of. - L.homeKind = int(context.bounded("city-kind", kHomeKinds)); - L.heartKind = int(context.bounded("city-kind", kHeartKinds)); - if (L.heartKind == Delta && teams < 2) - L.heartKind = LakeHeart; - if (depth < kFeatureDepth && - (L.homeKind == Riverside || L.homeKind == Highland || L.homeKind == Barrens)) - L.homeKind = Lakeland; - - // The coasts: bays and headlands round the commons, a ripple on top, bays into the homes' - // flanks, and a bow in every channel. All periodic in the wedge, so every home gets the same. - const Profile coast = rollProfile(context, "city-coast", 1, 4, 1.2); - const Profile ripple = rollProfile(context, "city-coast", 6, 6, 0.6); - const Profile innerBays = rollProfile(context, "city-coast", 1, 3, 1.0); - const Profile outerBays = rollProfile(context, "city-coast", 1, 3, 1.0); - const double coastAmp = kCoastAmplitude * g.amplitude * g.commonsRadius; - const double rippleAmp = kRippleAmplitude * g.amplitude * g.commonsRadius; - const double innerBayAmp = kInnerBayShare * g.amplitude * depth; - const double outerBayAmp = kOuterBayShare * g.amplitude * depth; - const double bend = (context.bounded("city-coast", 2001) / 1000.0 - 1) * kChannelBend; - // The coasts hold flat for a stretch either side of every causeway, measured in tiles of arc so - // the stretch is the same however many homes share the ring, and the flanks' bays begin only - // beyond the lake and its clearance. - const double lakeRadius = std::clamp(0.1 * depth, 2.0, 6.0); - const double lakeReach = 1.3 * lakeRadius; - const double flatArc = g.causeway / 2.0 + kShoulder + kLane + 4; - const double bayArc = lakeReach + kLakeSeaGap + 6; - const auto coastAt = [&](double u, double d) + const Geometry &g; + double wedge, inner, depth; + Profile coast, ripple, innerBays, outerBays; + double coastAmp, rippleAmp, innerBayAmp, outerBayAmp, bend; + // The coasts hold flat for a stretch either side of every causeway, measured in tiles of + // arc so the stretch is the same however many homes share the ring, and the flanks' bays + // begin only beyond the lake and its clearance. + double lakeRadius, lakeReach, flatArc, bayArc; + Coasts(const Geometry &g, GenerationContext &context) + : g(g), wedge(2 * pi / g.teams), inner(g.innerRadius()), depth(g.depth()), + coast(rollProfile(context, "city-coast", 1, 4, 1.2)), + ripple(rollProfile(context, "city-coast", 6, 6, 0.6)), + innerBays(rollProfile(context, "city-coast", 1, 3, 1.0)), + outerBays(rollProfile(context, "city-coast", 1, 3, 1.0)), + coastAmp(kCoastAmplitude * g.amplitude * g.commonsRadius), + rippleAmp(kRippleAmplitude * g.amplitude * g.commonsRadius), + innerBayAmp(kInnerBayShare * g.amplitude * depth), + outerBayAmp(kOuterBayShare * g.amplitude * depth), + bend((context.bounded("city-coast", 2001) / 1000.0 - 1) * kChannelBend), + lakeRadius(std::clamp(0.1 * depth, 2.0, 6.0)), lakeReach(1.3 * lakeRadius), + flatArc(g.causeway / 2.0 + kShoulder + kLane + 4), bayArc(lakeReach + kLakeSeaGap + 6) + { + } + double coastAt(double u, double d) const { const double away = awayFromMiddle(d * std::abs(u - 0.5) * wedge, flatArc, 12); return g.commonsRadius + (coastAmp * coast.at(u) + rippleAmp * ripple.at(u)) * away; - }; - const auto innerAt = [&](double u, double d) + } + double innerAt(double u, double d) const { return coastAt(u, d) + g.strait + innerBayAmp * std::max(0.0, innerBays.at(u)) * awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc, 10); - }; - const auto outerAt = [&](double u, double d) + } + double outerAt(double u, double d) const { return std::min(double(g.half - 3), g.outerRadius - outerBayAmp * std::max(0.0, outerBays.at(u)) * awayFromMiddle(d * std::abs(u - 0.5) * wedge, bayArc - 2, 10)); - }; - const auto bendAt = [&](double d) + } + double bendAt(double d) const { const double x = (d - inner) / std::max(1.0, depth); return x <= 0 || x >= 1 ? 0.0 : bend * std::sin(pi * x); - }; + } +}; - for (int k = 0; k < teams; ++k) +struct Patch +{ + int x, y; + RadialShape shape; +}; + +// What a roll puts in its homes and on its commons, in the wedge frame: the lakes (the main one +// first), a creek and its ford, the ridges' arcs and the pass radius, how much of a flank the +// barrens take, the sand patches, and the commons' heart with its own sand. +struct Features +{ + double mainLakeR = 0; + std::vector lakes, sandBlobs; + std::vector> creek; + int fordSegment = -1; + double ridgeU[2] = {0.28, 0.72}; + double passR = 0; + double barrenShare = 0.45; + double deltaFordR = 0; + RadialShape heart; + std::vector commonsSand; + Features(RadialShape heart, double deltaFordR) : deltaFordR(deltaFordR), heart(heart) {} +}; + +// Roll the features. Each kind of feature draws from a stream of its own, so the heart, rolled +// first here for its shape, draws the same as it did after the sand. +static Features rollFeatures(const GenerationRequest &request, GenerationContext &context, + const Geometry &g, const Coasts &c, Layout &L) +{ + const CityStatesOptions o(request); + const Torus &t = L.t; + const double wedge = c.wedge, inner = c.inner, depth = c.depth; + // The heart of the commons. + L.lakeR = std::max(3.0, kHeartShare * g.commonsRadius); + L.ringR = std::max(8.0, kHeartShare * g.commonsRadius); + L.islandR = 0; + if (L.heartKind == Crag) + L.lakeR = std::max(2.5, 0.06 * g.commonsRadius); + if (L.heartKind == IslandHeart) { - const double a = L.phase + wedge * (k + 0.5); - L.homes.push_back({a, g.commonsRadius, 0, 0, -1, -1}); + L.lakeR = std::max(6.0, 0.3 * g.commonsRadius); + L.islandR = std::max(5.0, 0.12 * g.commonsRadius); } - - // The home's features in the wedge's frame: lakes first (the main one first), then whatever + L.forestR = L.lakeR + 4 + 0.09 * g.commonsRadius; + Features f(RadialShape(L.lakeR, L.heartKind == IslandHeart ? 0.2 : 0.3, context, "city-heart"), + 0.6 * g.commonsRadius); + // The home's features in the wedge's frame: f.lakes first (the main one first), then whatever // the kind adds. A feature keeps clear of the coasts as bays can bring them, of the channels, // of the causeway's approach and of other features. - const double mainLakeR = inner + lakeOffset(g, lakeRadius); - std::vector lakes, sandBlobs; - lakes.push_back({0.0, mainLakeR, RadialShape(lakeRadius, 0.3, context, "city-home-lakes")}); + f.mainLakeR = inner + lakeOffset(g, c.lakeRadius); + f.lakes.push_back( + {0.0, f.mainLakeR, RadialShape(c.lakeRadius, 0.3, context, "city-home-lakes")}); const auto fits = [&](double s0, double r0, double reach, double gap, const std::vector &others) { if (std::abs(s0) + reach + gap > g.arcHalf(r0)) return false; const double u = 0.5 + s0 / (wedge * r0); - if (r0 - reach - gap < innerAt(u, r0) || r0 + reach + gap > outerAt(u, r0)) + if (r0 - reach - gap < c.innerAt(u, r0) || r0 + reach + gap > c.outerAt(u, r0)) return false; if (r0 - reach < inner + kLanding + 2 && std::abs(s0) < g.causeway + reach + 4) return false; @@ -338,7 +365,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) return true; }; if (L.homeKind == Marsh) - for (int attempt = 0; attempt < 400 && lakes.size() < 4; ++attempt) + for (int attempt = 0; attempt < 400 && f.lakes.size() < 4; ++attempt) { const double r0 = inner + 8 + context.bounded("city-home-lakes", 1000) / 1000.0 * (depth - 16); @@ -346,69 +373,45 @@ Layout design(const GenerationRequest &request, GenerationContext &context) (context.bounded("city-home-lakes", 2001) / 1000.0 - 1) * 0.6 * g.arcHalf(r0); RadialShape shape(2.0 + context.bounded("city-home-lakes", 1000) / 1000.0 * 2.0, 0.3, context, "city-home-lakes"); - if (fits(s0, r0, shape.maximumRadius(), kLakeSeaGap, lakes)) - lakes.push_back({s0, r0, shape}); + if (fits(s0, r0, shape.maximumRadius(), kLakeSeaGap, f.lakes)) + f.lakes.push_back({s0, r0, shape}); } - // Riverside: a creek from the main lake towards one flank of the inner coast, with a ford. - std::vector> creek; - int fordSegment = -1; + // Riverside: a f.creek from the main lake towards one flank of the inner coast, with a ford. if (L.homeKind == Riverside) { const double side = context.bounded("city-features", 2) ? 1.0 : -1.0; L.creekSide = side; - const double reachArc = 0.32 * g.arcHalf(mainLakeR); + const double reachArc = 0.32 * g.arcHalf(f.mainLakeR); const double endR = inner + kLakeSeaGap + 6.0; - creek = {{0.0, mainLakeR}, - {side * 0.35 * reachArc, mainLakeR - 0.3 * (mainLakeR - endR)}, - {side * 0.75 * reachArc, mainLakeR - 0.65 * (mainLakeR - endR)}, - {side * reachArc, endR}}; - fordSegment = 1; + f.creek = {{0.0, f.mainLakeR}, + {side * 0.35 * reachArc, f.mainLakeR - 0.3 * (f.mainLakeR - endR)}, + {side * 0.75 * reachArc, f.mainLakeR - 0.65 * (f.mainLakeR - endR)}, + {side * reachArc, endR}}; + f.fordSegment = 1; } // Highland: two ridges out from the strait to the sea, a pass through each. - const double ridgeU[2] = {0.28, 0.72}; - const double passR = inner + 0.5 * depth; + f.passR = inner + 0.5 * depth; // Barrens: both flanks of the home are sand, leaving a strip of grass down the middle from the // causeway past the lake; the sand keeps clear of every coast so the wall stays on the shore. - const double barrenShare = 0.45; // Sand patches, in every kind of home. const double homeArea = wedge * (g.outerRadius * g.outerRadius - inner * inner) / 2; const int homeSand = int(std::lround(o.sand * homeArea / 8192.0)); - for (int attempt = 0; attempt < 400 && int(sandBlobs.size()) < homeSand; ++attempt) + for (int attempt = 0; attempt < 400 && int(f.sandBlobs.size()) < homeSand; ++attempt) { const double r0 = inner + 8 + context.bounded("city-sand", 1000) / 1000.0 * (depth - 16); const double s0 = (context.bounded("city-sand", 2001) / 1000.0 - 1) * 0.75 * g.arcHalf(r0); RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 4.0, 0.5, context, "city-sand"); - std::vector avoid = lakes; - avoid.insert(avoid.end(), sandBlobs.begin(), sandBlobs.end()); + std::vector avoid = f.lakes; + avoid.insert(avoid.end(), f.sandBlobs.begin(), f.sandBlobs.end()); if (fits(s0, r0, shape.maximumRadius(), 5, avoid)) - sandBlobs.push_back({s0, r0, shape}); + f.sandBlobs.push_back({s0, r0, shape}); } - // The heart of the commons. - L.lakeR = std::max(3.0, kHeartShare * g.commonsRadius); - L.ringR = std::max(8.0, kHeartShare * g.commonsRadius); - L.islandR = 0; - if (L.heartKind == Crag) - L.lakeR = std::max(2.5, 0.06 * g.commonsRadius); - if (L.heartKind == IslandHeart) - { - L.lakeR = std::max(6.0, 0.3 * g.commonsRadius); - L.islandR = std::max(5.0, 0.12 * g.commonsRadius); - } - L.forestR = L.lakeR + 4 + 0.09 * g.commonsRadius; - const double deltaFordR = 0.6 * g.commonsRadius; - const RadialShape heart(L.lakeR, L.heartKind == IslandHeart ? 0.2 : 0.3, context, "city-heart"); - // Sand on the commons: anywhere between the heart and the shore, off the landings. - struct Patch - { - int x, y; - RadialShape shape; - }; - std::vector commonsSand; + // Sand on the commons: anywhere between the f.heart and the shore, off the landings. const double commonsArea = pi * g.commonsRadius * g.commonsRadius; const int wantedSand = int(std::lround(o.sand * commonsArea / 8192.0)); - for (int attempt = 0; attempt < 400 && int(commonsSand.size()) < wantedSand; ++attempt) + for (int attempt = 0; attempt < 400 && int(f.commonsSand.size()) < wantedSand; ++attempt) { const double a = context.bounded("city-sand", 3600) / 3600.0 * 2 * pi; const double r0 = 0.45 * g.commonsRadius + context.bounded("city-sand", 1000) / 1000.0 * @@ -421,10 +424,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (r0 * std::abs(u - 0.5) * wedge < reach + g.causeway + 4 && r0 > g.commonsRadius - kLanding - reach - 4) continue; - if (r0 + reach > coastAt(u, r0) - 8) + if (r0 + reach > c.coastAt(u, r0) - 8) continue; bool open = true; - for (const Patch &p : commonsSand) + for (const Patch &p : f.commonsSand) { const int px = L.cx + int(std::lround(r0 * std::cos(a))), py = L.cy + int(std::lround(r0 * std::sin(a))); @@ -432,10 +435,20 @@ Layout design(const GenerationRequest &request, GenerationContext &context) open = open && std::hypot(dx, dy) >= reach + p.shape.maximumRadius() + 4; } if (open) - commonsSand.push_back({t.x(L.cx + int(std::lround(r0 * std::cos(a)))), - t.y(L.cy + int(std::lround(r0 * std::sin(a)))), shape}); + f.commonsSand.push_back({t.x(L.cx + int(std::lround(r0 * std::cos(a)))), + t.y(L.cy + int(std::lround(r0 * std::sin(a)))), shape}); } + return f; +} + +// Classify every tile: which region and home it lies in, the causeway and its clearances, and +// each feature's mask, the same in every wedge; then each home's causeway ends. +static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const Features &f) +{ + const Torus &t = L.t; + const int n = t.w * t.h, teams = g.teams; + const double wedge = coasts.wedge; L.region.assign(n, Sea); L.homeOf.assign(n, -1); for (auto *mask : @@ -458,14 +471,14 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // The wedge frame: which home, how far across it and how far along the arc from its middle. double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); if (teams >= 2) - turn = std::fmod(turn - bendAt(d) / std::max(1.0, d) + 4 * pi, 2 * pi); + turn = std::fmod(turn - coasts.bendAt(d) / std::max(1.0, d) + 4 * pi, 2 * pi); const int k = std::min(teams - 1, int(turn / wedge)); const double u = turn / wedge - k; const double s = d * (u - 0.5) * wedge; const double toBoundary = teams < 2 ? 1e9 : d * std::min(u, 1 - u) * wedge; const double toLanding = d * std::abs(u - 0.5) * wedge; - const double c = coastAt(u, d); - const double in = innerAt(u, d), out = outerAt(u, d); + const double c = coasts.coastAt(u, d); + const double in = coasts.innerAt(u, d), out = coasts.outerAt(u, d); if (d < c) { L.region[i] = Commons; @@ -516,13 +529,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // The home's features, the same in every wedge. if (L.region[i] == HomeLand && !L.strip[i]) { - for (const Blob &b : lakes) + for (const Blob &b : f.lakes) if (b.holds(s - b.s, d - b.r)) L.lake[i] = 1; - for (size_t seg = 0; seg + 1 < creek.size(); ++seg) + for (size_t seg = 0; seg + 1 < f.creek.size(); ++seg) { - const double ax = creek[seg].first, ay = creek[seg].second; - const double bx = creek[seg + 1].first, by = creek[seg + 1].second; + const double ax = f.creek[seg].first, ay = f.creek[seg].second; + const double bx = f.creek[seg + 1].first, by = f.creek[seg + 1].second; const double len2 = (bx - ax) * (bx - ax) + (by - ay) * (by - ay); const double tt = std::clamp(((s - ax) * (bx - ax) + (d - ay) * (by - ay)) / std::max(1e-6, len2), @@ -531,7 +544,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) std::hypot(s - (ax + tt * (bx - ax)), d - (ay + tt * (by - ay))); if (dist < 1.6) { - if (int(seg) == fordSegment && std::abs(tt - 0.5) * std::sqrt(len2) < 2.5) + if (int(seg) == f.fordSegment && std::abs(tt - 0.5) * std::sqrt(len2) < 2.5) { L.ford[i] = 1; L.clear[i] = 1; @@ -541,39 +554,39 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.lake[i] = 1; } } - else if (int(seg) == fordSegment && dist < 3.5 && + else if (int(seg) == f.fordSegment && dist < 3.5 && std::abs(tt - 0.5) * std::sqrt(len2) < 3.5) { L.clear[i] = 1; } } if (L.homeKind == Highland && d >= in + 1 && d <= out - 1) - for (double ur : ridgeU) + for (double ur : f.ridgeU) { const double sr = (ur - 0.5) * wedge * d; if (std::abs(s - sr) < 1.25) { - if (std::abs(d - passR) < 2.5) + if (std::abs(d - f.passR) < 2.5) L.clear[i] = 1; else L.ridge[i] = 1; } - else if (std::abs(s - sr) < 3.5 && std::abs(d - passR) < 4.5) + else if (std::abs(s - sr) < 3.5 && std::abs(d - f.passR) < 4.5) { L.clear[i] = 1; } } if (L.homeKind == Barrens && d >= in + 8 && d <= out - 8 && - std::abs(s) > barrenShare * g.arcHalf(d) && std::abs(s) < g.arcHalf(d) - 8) + std::abs(s) > f.barrenShare * g.arcHalf(d) && std::abs(s) < g.arcHalf(d) - 8) L.sand[i] = 1; - for (const Blob &b : sandBlobs) + for (const Blob &b : f.sandBlobs) if (b.holds(s - b.s, d - b.r)) L.sand[i] = 1; } - // The heart of the commons. + // The f.heart of the commons. if (L.region[i] == Commons) { - const bool inHeart = d < heart.radiusAt(theta); + const bool inHeart = d < f.heart.radiusAt(theta); if (L.heartKind == IslandHeart) { if (inHeart && d >= L.islandR) @@ -601,16 +614,16 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { if (toBoundary < 2) { - if (std::abs(d - deltaFordR) < 2.5) + if (std::abs(d - f.deltaFordR) < 2.5) L.ford[i] = 1; else L.river[i] = 1; } - if (toBoundary < 3.5 && std::abs(d - deltaFordR) < 4) + if (toBoundary < 3.5 && std::abs(d - f.deltaFordR) < 4) L.clear[i] = 1; } if (!L.lake[i] && !L.river[i] && !L.ford[i] && !L.clear[i] && !L.causeway[i]) - for (const Patch &p : commonsSand) + for (const Patch &p : f.commonsSand) { const double px = t.offsetX(p.x, x), py = t.offsetY(p.y, y); if (std::hypot(px, py) < p.shape.radiusAt(std::atan2(py, px))) @@ -625,12 +638,47 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (L.homes[k].landing < 0 || L.homes[k].shore < 0) { L.failure = "home " + std::to_string(k) + " has no causeway"; - return L; + return; } Home &h = L.homes[k]; - h.lakeX = t.x(L.cx + int(std::lround(mainLakeR * std::cos(h.angle)))); - h.lakeY = t.y(L.cy + int(std::lround(mainLakeR * std::sin(h.angle)))); + h.lakeX = t.x(L.cx + int(std::lround(f.mainLakeR * std::cos(h.angle)))); + h.lakeY = t.y(L.cy + int(std::lround(f.mainLakeR * std::sin(h.angle)))); + } +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const int teams = g.teams; + const CityStatesOptions o(request); + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("city-layout", 3600) / 3600.0 * 2 * pi; + const double wedge = 2 * pi / teams; + const double depth = g.depth(); + + // What this map is made of. + L.homeKind = int(context.bounded("city-kind", kHomeKinds)); + L.heartKind = int(context.bounded("city-kind", kHeartKinds)); + if (L.heartKind == Delta && teams < 2) + L.heartKind = LakeHeart; + if (depth < kFeatureDepth && + (L.homeKind == Riverside || L.homeKind == Highland || L.homeKind == Barrens)) + L.homeKind = Lakeland; + + const Coasts c(g, context); + for (int k = 0; k < teams; ++k) + { + const double a = L.phase + wedge * (k + 0.5); + L.homes.push_back({a, g.commonsRadius, 0, 0, -1, -1}); } + + const Features f = rollFeatures(request, context, g, c, L); + rasterize(L, g, c, f); return L; } From 43b54099b8f97d3b0400949edc27c333682f623c Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:35:09 -0400 Subject: [PATCH 097/136] Roll the lobby's preview candidates off the UI thread The lobby's preview rolled its five candidate seeds on the UI thread, which froze it for as long as the slowest landscape took five times over: seconds on a 512 map with twelve colonies. The five rolls now run on the landscape previewer's worker pool, which gains a per-roll start score and a restart that swaps in new requests without stopping its workers; when they are in, the best-scoring seed is rolled once more on the UI thread for the snapshot, the same seed the candidate produced. An edit made while candidates roll discards them for a fresh pass. A map the picker showed, and a draft that fails validation, still resolve at once, and Start waits for candidates in flight rather than rolling again. The preview placeholder and the Randomize button follow the new state. The harness drives the preview through a helper that runs the timer and waits for the workers where it used to rely on the roll being synchronous. Verified: CustomGameSetupHarness passes headless, windowed at 640x480 and 1000x700, in the SDL-driven flow that launches a match from a lobby whose candidates may still be rolling, and in both preferences modes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 5 +- src/CustomGameScreen.cpp | 69 ++++++++++++++++++- src/CustomGameScreen.h | 10 +++ src/LandscapePreviewer.cpp | 37 +++++++--- src/LandscapePreviewer.h | 6 ++ test/CustomGameSetupHarness.cpp | 25 ++++--- 6 files changed, 129 insertions(+), 23 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 295ef5fa4..2e16f0fbd 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -217,7 +217,10 @@ playable landscape as a freshly generated map at the draft's current size and co a Regenerate all button. `LandscapePreviewer` rolls those previews on background threads (one roll per landscape, up to three seeds before a tile reports no preview) and hands back the seed each shown map came from; the lobby then rolls that one seed instead of sampling five, so the map -a player picked by sight is the map the preview shows and the match starts on. Any later edit to +a player picked by sight is the map the preview shows and the match starts on. The lobby's own +five candidate rolls run on the same workers: the best-scoring seed is rolled once more on the +UI thread for the snapshot, so a large map with many colonies no longer freezes the lobby while +its candidates roll. Any later edit to the draft drops the remembered seed and returns to candidate sampling. The picker takes only a list of localized names and requests, so the editor or a multiplayer lobby can run it too. Background rolls are safe because `syncRand()`'s state is per thread: a worker seeds its own diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index b14a66bb4..1fa6b4efe 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -7,6 +7,7 @@ #include "CustomGameScreen.h" #include "LobbyMapPreview.h" #include "LandscapePickerScreen.h" +#include "LandscapePreviewer.h" #include "Game.h" #include "GenerationService.h" #include "GlobalContainer.h" @@ -548,6 +549,8 @@ void CustomGameScreen::onAction(Widget *widget, Action action, int code, int val endExecute(CANCEL); if (code == OK && setup.validation().empty()) { + if (setup.random && candidates) + finishPreview(); if (setup.random && (!validMap || previewRevision != setup.mapRevision)) if (!generateMap()) return; @@ -565,7 +568,67 @@ void CustomGameScreen::onTimer(Uint32 tick) // Wait until the last edit settles and a dragged control/menu is released. if (previewPending && setup.random && Sint32(tick - previewDue) >= 0 && controls->pressed.empty() && !controls->popup.open) - generateMap(); + { + // A map the picker showed, or a draft that fails validation, resolves at once; anything + // else rolls its candidates off this thread first. + if (chosenSeed || !setup.validation().empty()) + generateMap(); + else + startCandidates(); + } + if (candidates && !candidates->busy()) + collectCandidates(); +} +void CustomGameScreen::startCandidates() +{ + previewPending = false; + setup.generator.nbTeams = setup.capacity; + std::vector requests(GenerationService::kSampledCandidates, setup.generator); + if (candidates) + candidates->restart(std::move(requests)); + else + candidates = std::make_unique(std::move(requests)); + candidateRevision = setup.mapRevision; + message = tr("Updating map preview..."); +} +// The candidates are in: keep the best-scoring one by rolling its seed for the snapshot, or +// report that none seated every colony. Returns whether a map came of it. +bool CustomGameScreen::collectCandidates() +{ + if (!candidates) + return false; + std::optional best; + double bestScore = -1.0; + for (std::size_t i = 0; i < candidates->size(); ++i) + { + const auto preview = candidates->preview(i); + if (preview.state == LandscapePreviewer::State::Ready && preview.score > bestScore) + { + bestScore = preview.score; + best = preview.seed; + } + } + const bool stale = candidateRevision != setup.mapRevision; + candidates.reset(); + if (stale) + return false; // an edit meanwhile already asked for a new preview + if (!best) + { + validMap = false; + message = tr("Generation failed. Adjust settings or press Start to retry."); + return false; + } + chosenSeed = best; + return generateMap(); +} +// Wait for candidates in flight and apply them: for a launch that cannot wait for the timer, +// and for tests that drive the timer by hand. +void CustomGameScreen::finishPreview() +{ + while (candidates && candidates->busy()) + SDL_Delay(5); + if (candidates) + collectCandidates(); } void CustomGameScreen::setMapMode(bool random) { @@ -1070,7 +1133,7 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) { ui.box({px, py, size, size}, Color(211, 223, 197)); ui.paragraph(px + 16, py + size / 2 - 20, size - 32, - tr(previewPending + tr(previewBusy() ? "Updating map preview..." : "Preview unavailable. Adjust settings or start to retry.")); } @@ -1088,6 +1151,6 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) invalidate(); previewDue = SDL_GetTicks(); }, - false, setup.validation().empty() && !previewPending); + false, setup.validation().empty() && !previewBusy()); } } diff --git a/src/CustomGameScreen.h b/src/CustomGameScreen.h index 2dca956f3..d1fea2970 100644 --- a/src/CustomGameScreen.h +++ b/src/CustomGameScreen.h @@ -9,6 +9,7 @@ #include class LobbyControls; class LandscapePickerScreen; +class LandscapePreviewer; class CustomGameChoiceScreen : public Glob2Screen { friend struct CustomGameSetupHarness; @@ -66,6 +67,15 @@ class CustomGameScreen : public Glob2TabScreen Uint32 previewDue = 0; // A seed the landscape picker showed a map for: the next preview reproduces that map. std::optional chosenSeed; + // The preview's candidate rolls run on the previewer's workers; the best-scoring one is then + // rolled again on this thread for the snapshot. candidateRevision is the draft they were + // rolled for, so an edit made meanwhile discards them. + std::unique_ptr candidates; + unsigned candidateRevision = 0; + void startCandidates(); + bool collectCandidates(); + void finishPreview(); + bool previewBusy() const { return previewPending || candidates != nullptr; } bool validMap = false, userMaps = false; bool separateMapLibraries = true; int currentTab = 0; diff --git a/src/LandscapePreviewer.cpp b/src/LandscapePreviewer.cpp index 94da14d97..434f6eccf 100644 --- a/src/LandscapePreviewer.cpp +++ b/src/LandscapePreviewer.cpp @@ -33,19 +33,37 @@ LandscapePreviewer::~LandscapePreviewer() worker.join(); } +// Caller holds the mutex. +void LandscapePreviewer::beginPass() +{ + ++pass; + next = 0; + const auto root = GenerationContext::randomSeed(); + for (std::size_t i = 0; i < slots.size(); ++i) + { + seeds[i] = GenerationContext::deriveSeed(root, "landscape/" + std::to_string(i)); + slots[i].state = State::Pending; + ++slots[i].revision; + } +} + void LandscapePreviewer::regenerate() { { std::lock_guard lock(mutex); - ++pass; - next = 0; - const auto root = GenerationContext::randomSeed(); - for (std::size_t i = 0; i < slots.size(); ++i) - { - seeds[i] = GenerationContext::deriveSeed(root, "landscape/" + std::to_string(i)); - slots[i].state = State::Pending; - ++slots[i].revision; - } + beginPass(); + } + wake.notify_all(); +} + +void LandscapePreviewer::restart(std::vector fresh) +{ + { + std::lock_guard lock(mutex); + requests = std::move(fresh); + slots.resize(requests.size()); + seeds.resize(requests.size()); + beginPass(); } wake.notify_all(); } @@ -100,6 +118,7 @@ LandscapePreviewer::Preview LandscapePreviewer::roll(const GenerationRequest &re result.starts.push_back( {game.teams[i]->startPosX, game.teams[i]->startPosY, game.teams[i]->color}); result.seed = roll.seed; + result.score = outcome.quality.score; result.width = game.map.getW(); result.height = game.map.getH(); result.state = State::Ready; diff --git a/src/LandscapePreviewer.h b/src/LandscapePreviewer.h index 8bda145c1..724a4068b 100644 --- a/src/LandscapePreviewer.h +++ b/src/LandscapePreviewer.h @@ -32,6 +32,8 @@ class LandscapePreviewer std::vector starts; std::uint32_t seed = 0; int width = 0, height = 0; + /// How good a start each colony got, as GenerationService scores it; 0 unless Ready. + double score = 0; /// The last generation diagnostic, when Failed. std::string detail; /// Increments on every state change, so a viewer polling cheaply knows when to copy. @@ -46,6 +48,9 @@ class LandscapePreviewer /// Rolls every request again with fresh seeds. Rolls already under way finish, then are /// dropped rather than shown. void regenerate(); + /// Replaces the requests and rolls them with fresh seeds, on the same workers; rolls of the + /// old requests already under way finish, then are dropped. + void restart(std::vector requests); std::size_t size() const { return requests.size(); } unsigned revision(std::size_t index) const; Preview preview(std::size_t index) const; @@ -58,6 +63,7 @@ class LandscapePreviewer static Preview roll(const GenerationRequest &request, std::uint32_t rootSeed); private: + void beginPass(); void work(); std::vector requests; std::vector slots; diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index 3e89bd312..8b1f3a825 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -443,6 +443,11 @@ struct CustomGameSetupHarness screen.listMaps(); auto paint = [&] { screen.dispatchPaint(false); }; + // The preview rolls its candidates on worker threads; wait for them as the timer would. + auto preview = [&] { + screen.onTimer(screen.previewDue); + screen.finishPreview(); + }; auto keyEvent = [&](SDL_Keycode key, Uint16 modifiers = KMOD_NONE) { SDL_Event e = {}; e.type = SDL_KEYDOWN; @@ -601,10 +606,10 @@ struct CustomGameSetupHarness screen.onTimer(screen.previewDue - 1); assert(!screen.validMap); screen.controls->pressed = "generator/water"; - screen.onTimer(screen.previewDue); + preview(); assert(!screen.validMap); screen.controls->pressed.clear(); - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap && !screen.previewPending); auto first = screen.snapshot; auto revision = screen.previewRevision; @@ -638,7 +643,7 @@ struct CustomGameSetupHarness const auto replaced = screen.snapshot; clickControl("map/randomize"); assert(!screen.validMap && screen.previewPending); - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap && screen.snapshot != replaced && !std::filesystem::exists(replaced)); assert(screen.setup.generator.method == settings.method && @@ -667,7 +672,7 @@ struct CustomGameSetupHarness screen.controls->hits.begin(), screen.controls->hits.end(), [](const auto &h) { return h.id == "generator/reset"; }); assert(reset != screen.controls->hits.end() && !reset->enabled); - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap); capture("reset-640"); } @@ -679,7 +684,7 @@ struct CustomGameSetupHarness screen.setup.generator.options["sand"] = screen.setup.generator.options["grass"] = screen.setup.generator.options["desert"] = 0; - screen.onTimer(screen.previewDue); + preview(); assert(!screen.validMap && !screen.previewPending); for (int i = 0; i < 4; ++i) assert(screen.setup.colonies[i].alliance == assignments[i].alliance); @@ -688,7 +693,7 @@ struct CustomGameSetupHarness screen.setup.generator.options["grass"] = screen.setup.generator.options["desert"] = 50; screen.invalidate(); - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap); // Apply a landscape the way the picker's result does, then drive the same steppers used @@ -728,7 +733,7 @@ struct CustomGameSetupHarness // either edit invalidates the preview like any other generator control. landscape(GenerationRequest::eFJORDCONTINENT); screen.expanded[0] = screen.expanded[1] = screen.expanded[2] = true; - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap); { auto &options = screen.setup.generator.options; @@ -747,7 +752,7 @@ struct CustomGameSetupHarness assert(screen.controls->focus != "generator/lake-connected"); keyEvent(SDLK_TAB, KMOD_SHIFT); assert(screen.controls->focus == "generator/lake-connected"); - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap); capture("map-checkboxes"); clickControl("generator/lake-connected"); @@ -939,7 +944,7 @@ struct CustomGameSetupHarness screen.applyLandscape(entries[other].first, seed); assert(screen.previewPending && screen.chosenSeed == seed && screen.setup.generator.method == entries[other].first); - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap && !screen.chosenSeed); assert(screen.preview->starts.size() == starts.size()); for (size_t i = 0; i < starts.size(); ++i) @@ -963,7 +968,7 @@ struct CustomGameSetupHarness keyEvent(SDLK_UP); keyEvent(SDLK_RETURN); assert(!screen.chosenSeed && screen.previewPending); - screen.onTimer(screen.previewDue); + preview(); assert(screen.validMap); std::cout << "PASS landscape picker: " << shown.size() << " previews on " << picker.previewer.threadCount() From a564208dd039fcbb80b9f0a74b38b940d0862f79 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:39:40 -0400 Subject: [PATCH 098/136] Measure start metrics by colony index The fairness tournament's partial run found colonies 2 and 3 winning about twice as often as 0 and 1, pooled over two of four rotations, and left the cause open. tools/colony_start_metrics.py rolls every playable generator over a range of seeds with the study tool's quality output and averages each colony's measured start by its index, so a placement rule that favoured one index would show. Over 40 seeds of every playable generator at 256x256 with 4 colonies, 680 maps, the pooled means are flat: start score 0.673, 0.669, 0.672, 0.669 for colonies 0 to 3, nearest rival 84.3, 86.0, 82.6 and 84.5 tiles, room, wheat and wood distance and build sites alike. The skew is not in what colonies are given. The fairness document records the table and what it rules out, and notes the two older generators whose nearest-rival distance does depend on colony order. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 23 ++++++ tools/colony_start_metrics.py | 86 ++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100755 tools/colony_start_metrics.py diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index 1d7161a20..94d9e5b21 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -104,6 +104,29 @@ Symmetric arena (generator 15) is the baseline. Its colonies have exactly the sa construction, so its wins by start should be uniform, and its wins by team index show the engine's own team-index bias with no map effect at all. +## What the starts themselves say + +Before blaming the maps for a win-rate skew by colony index, ask whether the placement code hands +any index a better start. `tools/colony_start_metrics.py` rolls every playable generator over a +range of seeds with the study tool's `quality` output and averages each colony's measured start by +its index. On 2026-09-12, at 256x256 with 4 colonies over 40 seeds of every playable generator +(680 maps), the pooled means were flat to the last digit that matters: + +| Colony | Start score | Wheat distance | Wood distance | Room | Isolation | Nearest rival | Build sites | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 0 | 0.673 | 3.9 | 6.6 | 0.811 | 0.996 | 84.3 | 875 | +| 1 | 0.669 | 3.8 | 6.2 | 0.803 | 0.994 | 86.0 | 890 | +| 2 | 0.672 | 4.0 | 6.3 | 0.804 | 0.994 | 82.6 | 891 | +| 3 | 0.669 | 3.8 | 6.3 | 0.798 | 0.994 | 84.5 | 888 | + +So the skew a partial tournament run found (colonies 2 and 3 winning about twice as often as 0 +and 1, pooled over two of the four rotations) is not in what colonies are given: not in their +resources, their room, or how far their nearest rival stands. What remains is how the games +unfold from equal starts, which only full-rotation tournament games can separate from the team +index. Per generator the table is noisier, and two of the older generators do show an ordering +effect in nearest-rival distance (Islands' later colonies and Rugged archipelago's colony 0 sit +closer to a rival); the script prints the per-generator tables for that. + ## Adjudication A game ends when one colony has defeated every other, when total prestige reaches the lobby diff --git a/tools/colony_start_metrics.py b/tools/colony_start_metrics.py new file mode 100755 index 000000000..87df99cc1 --- /dev/null +++ b/tools/colony_start_metrics.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Start metrics by colony index, pooled over seeds and generators. + +Rolls every playable generator at one size and colony count over a range of seeds with +MapGeneratorStudy's `quality` output and averages each colony's measured start (score, wheat +and wood distance, room, isolation, nearest-rival distance, build sites) by its colony index. +A placement rule that favoured one index over another would show here; a flat table says the +starts colonies get do not depend on their index. + + python3 tools/colony_start_metrics.py [--seeds 1-40] [--size 256] [--colonies 4] + [--generators 9,11,...] [--study build/src/MapGeneratorStudy] +""" +import argparse +import statistics +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +COLUMNS = ["colony", "wheatDistance", "woodDistance", "catchmentTiles", "buildSites", + "resourceAmount", "rivalDistance", "rivalsWithinThreat", "meanFertility", "wheat", + "wood", "fertility", "depth", "room", "isolation", "total"] + + +def parse_range(text): + if "-" in text: + a, b = text.split("-", 1) + return list(range(int(a), int(b) + 1)) + return [int(v) for v in text.split(",")] + + +def playable_generators(study, profile): + out = subprocess.run([study, "--catalog"], capture_output=True, text=True, check=True).stdout + import json + catalog = json.loads(out) + return [g["method"] for g in catalog if not g.get("editorOnly")] + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--seeds", default="1-40") + ap.add_argument("--size", type=int, default=256) + ap.add_argument("--colonies", type=int, default=4) + ap.add_argument("--generators", help="comma-separated legacy ids; default: every playable generator") + ap.add_argument("--study", default=str(ROOT / "build" / "src" / "MapGeneratorStudy")) + ap.add_argument("--profile", default="glob2-colony-metrics") + args = ap.parse_args() + dec = args.size.bit_length() - 1 + if 1 << dec != args.size: + sys.exit("size must be a power of two") + generators = parse_range(args.generators) if args.generators else playable_generators(args.study, args.profile) + rows = [] + for gen in generators: + for seed in parse_range(args.seeds): + cmd = [args.study, str(gen), str(seed), args.profile, f"width={dec}", f"height={dec}", + f"teams={args.colonies}", "quality"] + out = subprocess.run(cmd, capture_output=True, text=True).stdout + for line in out.splitlines(): + if line.startswith("COLONY,"): + values = line.split(",")[1:] + rows.append((gen, dict(zip(COLUMNS, map(float, values))))) + if not rows: + sys.exit("no colonies scored; is the study tool built?") + + def mean(sub, key): + return statistics.mean(r[key] for _, r in sub) + + def table(sub, title): + maps = len(sub) // args.colonies + print(f"\n{title}: mean by colony index over {maps} maps") + print("colony total wheatD woodD room isol rivalD sites") + for c in range(args.colonies): + s = [(g, r) for g, r in sub if int(r["colony"]) == c] + print(f" {c} {mean(s, 'total'):.3f} {mean(s, 'wheatDistance'):5.1f} {mean(s, 'woodDistance'):5.1f}" + f" {mean(s, 'room'):.3f} {mean(s, 'isolation'):.3f} {mean(s, 'rivalDistance'):6.1f}" + f" {mean(s, 'buildSites'):6.1f}") + + table(rows, "All generators pooled") + for gen in generators: + sub = [(g, r) for g, r in rows if g == gen] + if sub: + table(sub, f"Generator {gen}") + + +if __name__ == "__main__": + main() From e2071ba27b844481391a86092b06cf2a24f64fe4 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 14:57:39 -0400 Subject: [PATCH 099/136] Record the full-rotation tournament result The smoke preset played in full: four generators, three 128x128 maps each, every rotation twice, 96 games. Pooled over all rotations there is no detectable engine team-index bias (28 / 21 / 23 / 24, p 0.80) and no colony-index skew that replicates (Contested commons 8 / 8 / 7 / 1, Watershed 7 / 9 / 4 / 4, Crater lakes 1 / 8 / 6 / 9); the partial run's "colonies 2 and 3 win twice as often" came from two rotations of unevenly dominated maps. What is real is per-map: on seven of nine asymmetric maps one start won seven or eight of eight games whichever team played it, with a different start each map, and the start scorer only partly predicts which. The fairness document records the numbers and what to do about them. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/map-generators/FAIRNESS_TOURNAMENT.md | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/map-generators/FAIRNESS_TOURNAMENT.md b/docs/map-generators/FAIRNESS_TOURNAMENT.md index 94d9e5b21..5e7046651 100644 --- a/docs/map-generators/FAIRNESS_TOURNAMENT.md +++ b/docs/map-generators/FAIRNESS_TOURNAMENT.md @@ -127,6 +127,33 @@ index. Per generator the table is noisier, and two of the older generators do sh effect in nearest-rival distance (Islands' later colonies and Rugged archipelago's colony 0 sit closer to a rival); the script prints the per-generator tables for that. +## What a full-rotation run says + +The `smoke` preset played in full on 2026-09-12 (Symmetric arena, Contested commons, Watershed +and Crater lakes; three 128x128 maps each; every rotation twice; 96 games, 82 decided by +elimination) separates the two questions the partial run could not: + +- **No engine team-index bias is detectable.** Pooled over all rotations, wins by team index were + 28 / 21 / 23 / 24 (p 0.80); on the Symmetric arena baseline 9 / 2 / 7 / 6 (p 0.21). +- **No colony-index skew replicates.** Pooled per generator, wins by the generator's colony index + were 8 / 8 / 7 / 1 for Contested commons, 7 / 9 / 4 / 4 for Watershed and 1 / 8 / 6 / 9 for + Crater lakes: no shared pattern, and nothing like colonies 2 and 3 winning twice as often. The + partial run's skew came from two rotations of unevenly dominated maps. +- **Individual maps are grossly unfair, and that is the real defect.** On seven of the nine + asymmetric maps one start won seven or eight of its eight games whichever team played it + (Contested commons: start 0, start 1 and start 2 on its three maps; Watershed and Crater lakes + one map each at 8 of 8). Position bias was 29 to 35 percentage points against a 14-point floor + for a fair map; Symmetric arena, identical starts by construction, sat at 0. +- **The start scorer only partly sees it.** Within-map rank correlation between start score and + win share was 0.68 for Crater lakes, 0.59 for Contested commons and -0.17 for Watershed; the + scorer's favourite won 71%, 62% and 17% of those generators' games. Whatever makes a start + dominate is only partly resources, room and distance. + +So the work ahead is per-map, not per-index: find what a dominant start has that the scorer does +not measure (expansion room in the direction of the commons or the delta, who is reachable first, +chokepoints), teach the scorer, and let the lobby's best-of-five reject the lopsided rolls. The +run's `summary.md`, `games.csv` and `colonies.csv` are the material for that. + ## Adjudication A game ends when one colony has defeated every other, when total prestige reaches the lobby From 8ad1401637e0250a857a97f77c25d4654dbe4b3c Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 15:08:20 -0400 Subject: [PATCH 100/136] Add the Everglades map generator A dense wetland, the same everywhere and across the wrap. Pools and long sloughs lie on a jittered lattice that tiles the torus, bigger where the land is wet and smaller where it is dry, and the grass between them starts thick with wood and wheat. Every tile is a few steps from water, so wood and wheat grow back at the engine's top rate; resources block movement, so the swamp grows shut unless workers keep cutting it. Nothing grows on sand, and every pool's beach is sand, so the banks between pools are the lanes that stay open, winding and narrow. Every colony starts in a clearing with a pond and an unscaled kit, ringed by a sand levee with gaps the swamp creeps through; a quarry stands on the side towards the map's centre. Nothing is symmetric on purpose: the landscape is one random field over the whole map and the clearings are nudged off their ring, so it reads as country, not as an arena. The generator bends rather than refuses: a clearing that would not fit shrinks, then loses its margin, then the ring widens; pools that would cover more than 60% of the map are scaled down; and after placement, openRoutes walks from colony 0 and opens the cheapest way through to any colony it cannot reach, clearing deposits and fording water. validateWorld rebuilds the design and requires every pond present, no deposit on any levee tile, and every colony reachable on foot from colony 0. Legacy id 19. Controls: pool spacing, pool size, clearing size, levee, sloughs and the five resource amounts. Strings in all 33 text tables; the framework reference documents the design; the registry contract test's injected-generator index moves up by one. The sources are in the tree's clang-format style and the golden table gains its seven rows. Verified: the golden table compares 133 rows with 0 failures (the 126 existing rows unchanged), the sweep seats Everglades on every seed at every cell (5/5 at 128 and 256 for 2 to 12 colonies, 3/3 at 512), MapGeneratorDefaultsTest, check_translations.py --strict and the headless CustomGameSetupHarness pass, and the maintainer played it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 16 + data/texts.br.txt | 16 + data/texts.ca.txt | 16 + data/texts.cz.txt | 16 + data/texts.de.txt | 16 + data/texts.dk.txt | 16 + data/texts.en.txt | 16 + data/texts.eo.txt | 16 + data/texts.es.txt | 16 + data/texts.eu.txt | 16 + data/texts.fa.txt | 16 + data/texts.fi.txt | 16 + data/texts.fr.txt | 16 + data/texts.gr.txt | 16 + data/texts.hu.txt | 16 + data/texts.id.txt | 16 + data/texts.it.txt | 16 + data/texts.ja.txt | 16 + data/texts.keys.txt | 8 + data/texts.ko.txt | 16 + data/texts.nl.txt | 16 + data/texts.pl.txt | 16 + data/texts.pt.txt | 16 + data/texts.ro.txt | 16 + data/texts.ru.txt | 16 + data/texts.si.txt | 16 + data/texts.sk.txt | 16 + data/texts.sr.txt | 16 + data/texts.sv.txt | 16 + data/texts.tr.txt | 16 + data/texts.uk.txt | 16 + data/texts.vi.txt | 16 + data/texts.zh-cn.txt | 16 + data/texts.zh-tw.txt | 16 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 52 +- src/SConscript | 1 + src/map/generator/core/GenerationRequest.h | 3 +- src/map/generator/core/GeneratorRegistry.cpp | 7 +- .../generators/EvergladesGenerator.cpp | 839 ++++++++++++++++++ .../generators/EvergladesGenerator.h | 11 + test/MapGeneratorDefaultsTest.cpp | 3 +- test/map-generator-golden.txt | 7 + 42 files changed, 1452 insertions(+), 7 deletions(-) create mode 100644 src/map/generator/generators/EvergladesGenerator.cpp create mode 100644 src/map/generator/generators/EvergladesGenerator.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index d152a7f3f..470cc23b2 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1898,6 +1898,22 @@ OpenGL غير متوفر في هذا الإصدار. بقع رملية [Tidal flats] السهول المدّية +[Everglades] +المستنقعات +[Pool spacing] +تباعد البرك +[Pool size] +حجم البرك +[Clearing size] +حجم الفسحة +[Levee] +السد الترابي +[Sloughs] +المستنقعات +[Everglades need at least one colony.] +تحتاج المستنقعات إلى مستعمرة واحدة على الأقل. +[Too many colonies for this map; use a bigger map or fewer colonies.] +عدد المستعمرات كبير جدًا لهذه الخريطة؛ استخدم خريطة أكبر أو مستعمرات أقل. [Sandbars] الحواجز الرملية [Tide pools] diff --git a/data/texts.br.txt b/data/texts.br.txt index f9179bbf1..fc2423616 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1896,6 +1896,22 @@ Muralhas de pedra Manchas de areia [Tidal flats] Planícies de maré +[Everglades] +Pântanos +[Pool spacing] +Espaçamento das poças +[Pool size] +Tamanho das poças +[Clearing size] +Tamanho da clareira +[Levee] +Dique +[Sloughs] +Geunioù +[Everglades need at least one colony.] +Os pântanos precisam de pelo menos uma colônia. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Re a drevadennoù evit ar gartenn-mañ; implijit ur gartenn vrasoc'h pe nebeutoc'h a drevadennoù. [Sandbars] Bancos de areia [Tide pools] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 36048e953..1d5cf1202 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1908,6 +1908,22 @@ Murs de pedra Clapes de sorra [Tidal flats] Planes de marea +[Everglades] +Aiguamolls +[Pool spacing] +Separació de les basses +[Pool size] +Mida de les basses +[Clearing size] +Mida de la clariana +[Levee] +Dic +[Sloughs] +Aiguamolls +[Everglades need at least one colony.] +Els aiguamolls necessiten almenys una colònia. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Massa colònies per a aquest mapa; useu un mapa més gran o menys colònies. [Sandbars] Bancs de sorra [Tide pools] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 2465fb5ea..82f30c42b 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1900,6 +1900,22 @@ Kamenné hradby Písčiny [Tidal flats] Přílivové pláně +[Everglades] +Mokřady +[Pool spacing] +Rozestup tůní +[Pool size] +Velikost tůní +[Clearing size] +Velikost mýtiny +[Levee] +Hráz +[Sloughs] +Slepá ramena +[Everglades need at least one colony.] +Mokřady potřebují alespoň jednu kolonii. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Příliš mnoho kolonií pro tuto mapu; použijte větší mapu nebo méně kolonií. [Sandbars] Písečné lavice [Tide pools] diff --git a/data/texts.de.txt b/data/texts.de.txt index a3a1efa56..1bc5d2bbf 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1900,6 +1900,22 @@ Steinmauern Sandflächen [Tidal flats] Wattenmeer +[Everglades] +Sumpfland +[Pool spacing] +Abstand der Tümpel +[Pool size] +Größe der Tümpel +[Clearing size] +Größe der Lichtung +[Levee] +Deich +[Sloughs] +Altwasser +[Everglades need at least one colony.] +Das Sumpfland braucht mindestens eine Kolonie. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Zu viele Kolonien für diese Karte; wähle eine größere Karte oder weniger Kolonien. [Sandbars] Sandbänke [Tide pools] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index 0182c36b8..a73afdfdd 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1976,6 +1976,22 @@ Stenmure Sandpletter [Tidal flats] Vadehav +[Everglades] +Sumpland +[Pool spacing] +Afstand mellem pøle +[Pool size] +Pølestørrelse +[Clearing size] +Lysningens størrelse +[Levee] +Dige +[Sloughs] +Sumpkanaler +[Everglades need at least one colony.] +Sumplandet kræver mindst én koloni. +[Too many colonies for this map; use a bigger map or fewer colonies.] +For mange kolonier til dette kort; brug et større kort eller færre kolonier. [Sandbars] Sandbanker [Tide pools] diff --git a/data/texts.en.txt b/data/texts.en.txt index e9e6b33f2..e9b86364f 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1888,6 +1888,22 @@ Stone walls Sand patches [Tidal flats] Tidal flats +[Everglades] +Everglades +[Pool spacing] +Pool spacing +[Pool size] +Pool size +[Clearing size] +Clearing size +[Levee] +Levee +[Sloughs] +Sloughs +[Everglades need at least one colony.] +Everglades need at least one colony. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Too many colonies for this map; use a bigger map or fewer colonies. [Sandbars] Sandbars [Tide pools] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index c3a886eb4..9dbb3dedb 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1898,6 +1898,22 @@ Urboŝtatoj bezonas almenaŭ unu kolonion. Sablaj makuloj [Tidal flats] Tajdaj ebenaĵoj +[Everglades] +Marĉoj +[Pool spacing] +Interspaco de flakoj +[Pool size] +Grandeco de flakoj +[Clearing size] +Grandeco de maldensejo +[Levee] +Digo +[Sloughs] +Marĉkanaloj +[Everglades need at least one colony.] +Marĉoj bezonas almenaŭ unu kolonion. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Tro da kolonioj por ĉi tiu mapo; uzu pli grandan mapon aŭ malpli da kolonioj. [Sandbars] Sablobenkoj [Tide pools] diff --git a/data/texts.es.txt b/data/texts.es.txt index 596a1e470..d59a64610 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1900,6 +1900,22 @@ Muros de piedra Parches de arena [Tidal flats] Marismas +[Everglades] +Pantanal +[Pool spacing] +Separación de las charcas +[Pool size] +Tamaño de las charcas +[Clearing size] +Tamaño del claro +[Levee] +Dique +[Sloughs] +Ciénagas +[Everglades need at least one colony.] +El pantanal necesita al menos una colonia. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Demasiadas colonias para este mapa; usa un mapa más grande o menos colonias. [Sandbars] Bancos de arena [Tide pools] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index b338198c9..cf7daec9d 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1910,6 +1910,22 @@ Harrizko hormak Harea-guneak [Tidal flats] Marea-lautadak +[Everglades] +Zingirak +[Pool spacing] +Putzuen arteko tartea +[Pool size] +Putzuen tamaina +[Clearing size] +Soilgunearen tamaina +[Levee] +Dikea +[Sloughs] +Zingirak +[Everglades need at least one colony.] +Zingirek gutxienez kolonia bat behar dute. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Kolonia gehiegi mapa honetarako; erabili mapa handiagoa edo kolonia gutxiago. [Sandbars] Hondar-bankuak [Tide pools] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index f70beb5ba..2aa73c0e1 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1898,6 +1898,22 @@ OpenGL در این بیلد موجود نیست. لکه‌های شنی [Tidal flats] پهنه‌های جزر و مدی +[Everglades] +باتلاق‌ها +[Pool spacing] +فاصلهٔ حوضچه‌ها +[Pool size] +اندازهٔ حوضچه‌ها +[Clearing size] +اندازهٔ محوطهٔ باز +[Levee] +خاکریز +[Sloughs] +مرداب‌ها +[Everglades need at least one colony.] +باتلاق‌ها دست‌کم به یک مستعمره نیاز دارند. +[Too many colonies for this map; use a bigger map or fewer colonies.] +تعداد مستعمره‌ها برای این نقشه زیاد است؛ از نقشهٔ بزرگ‌تر یا مستعمره‌های کمتر استفاده کنید. [Sandbars] پشته‌های شنی [Tide pools] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 44c63eedb..8b4112de6 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1900,6 +1900,22 @@ Kivimuurit Hiekkaläikät [Tidal flats] Vuorovesitasangot +[Everglades] +Suomaat +[Pool spacing] +Lammikoiden väli +[Pool size] +Lammikoiden koko +[Clearing size] +Aukion koko +[Levee] +Pato +[Sloughs] +Rimmet +[Everglades need at least one colony.] +Suomaat tarvitsevat vähintään yhden siirtokunnan. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Liikaa siirtokuntia tälle kartalle; käytä suurempaa karttaa tai vähemmän siirtokuntia. [Sandbars] Hiekkasärkät [Tide pools] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index ff8814922..80e36076b 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1910,6 +1910,22 @@ Murs de pierre Zones de sable [Tidal flats] Estran +[Everglades] +Marécages +[Pool spacing] +Espacement des mares +[Pool size] +Taille des mares +[Clearing size] +Taille de la clairière +[Levee] +Digue +[Sloughs] +Marigots +[Everglades need at least one colony.] +Les marécages ont besoin d'au moins une colonie. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Trop de colonies pour cette carte ; utilisez une carte plus grande ou moins de colonies. [Sandbars] Bancs de sable [Tide pools] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 92934d41f..226d1c23e 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1970,6 +1970,22 @@ Cortex Αμμώδεις εκτάσεις [Tidal flats] Παλιρροιακές πεδιάδες +[Everglades] +Βαλτότοποι +[Pool spacing] +Απόσταση λιμνών +[Pool size] +Μέγεθος λιμνών +[Clearing size] +Μέγεθος ξέφωτου +[Levee] +Ανάχωμα +[Sloughs] +Βαλτόνερα +[Everglades need at least one colony.] +Οι βαλτότοποι χρειάζονται τουλάχιστον μία αποικία. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Πάρα πολλές αποικίες για αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη ή λιγότερες αποικίες. [Sandbars] Αμμοσύρτεις [Tide pools] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 5e8572a42..0c0f8125d 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1900,6 +1900,22 @@ Kőfalak Homokfoltok [Tidal flats] Árapálysíkság +[Everglades] +Mocsárvidék +[Pool spacing] +Tavacskák távolsága +[Pool size] +Tavacskák mérete +[Clearing size] +Tisztás mérete +[Levee] +Gát +[Sloughs] +Holtágak +[Everglades need at least one colony.] +A mocsárvidékhez legalább egy kolónia kell. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Túl sok kolónia ehhez a térképhez; használj nagyobb térképet vagy kevesebb kolóniát. [Sandbars] Homokpadok [Tide pools] diff --git a/data/texts.id.txt b/data/texts.id.txt index c830f6ee7..3a8a6de32 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1896,6 +1896,22 @@ Tembok batu Petak pasir [Tidal flats] Dataran pasang surut +[Everglades] +Rawa-rawa +[Pool spacing] +Jarak antar kolam +[Pool size] +Ukuran kolam +[Clearing size] +Ukuran tanah lapang +[Levee] +Tanggul +[Sloughs] +Rawa-rawa +[Everglades need at least one colony.] +Rawa-rawa membutuhkan setidaknya satu koloni. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Terlalu banyak koloni untuk peta ini; gunakan peta lebih besar atau lebih sedikit koloni. [Sandbars] Gosong pasir [Tide pools] diff --git a/data/texts.it.txt b/data/texts.it.txt index d69794c9e..4ff5ea217 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1962,6 +1962,22 @@ Mura di pietra Chiazze di sabbia [Tidal flats] Piane di marea +[Everglades] +Paludi +[Pool spacing] +Distanza tra le pozze +[Pool size] +Dimensione delle pozze +[Clearing size] +Dimensione della radura +[Levee] +Argine +[Sloughs] +Lanche +[Everglades need at least one colony.] +Le paludi hanno bisogno di almeno una colonia. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Troppe colonie per questa mappa; usa una mappa più grande o meno colonie. [Sandbars] Banchi di sabbia [Tide pools] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 9d1612630..43bf364cc 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1896,6 +1896,22 @@ Globulation 2 のカーソルを表示します。 砂地 [Tidal flats] 干潟 +[Everglades] +湿地帯 +[Pool spacing] +池の間隔 +[Pool size] +池の大きさ +[Clearing size] +空き地の大きさ +[Levee] +堤防 +[Sloughs] +沼沢 +[Everglades need at least one colony.] +湿地帯には少なくとも一つのコロニーが必要です。 +[Too many colonies for this map; use a bigger map or fewer colonies.] +この地図にはコロニーが多すぎます。より大きな地図にするか、コロニーを減らしてください。 [Sandbars] 砂州 [Tide pools] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index b0aeaa83a..56f4ba0d2 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -942,6 +942,14 @@ [Stone walls] [Sand patches] [Tidal flats] +[Everglades] +[Pool spacing] +[Pool size] +[Clearing size] +[Levee] +[Sloughs] +[Everglades need at least one colony.] +[Too many colonies for this map; use a bigger map or fewer colonies.] [Sandbars] [Tide pools] [Lagoons] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index c9f63047a..c67be4345 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1896,6 +1896,22 @@ Globulation 2 커서를 표시합니다. 모래밭 [Tidal flats] 갯벌 +[Everglades] +습지대 +[Pool spacing] +웅덩이 간격 +[Pool size] +웅덩이 크기 +[Clearing size] +공터 크기 +[Levee] +제방 +[Sloughs] +늪지 +[Everglades need at least one colony.] +습지대에는 최소 한 개의 식민지가 필요합니다. +[Too many colonies for this map; use a bigger map or fewer colonies.] +이 지도에는 식민지가 너무 많습니다. 더 큰 지도를 사용하거나 식민지를 줄이세요. [Sandbars] 모래톱 [Tide pools] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 06c7451b3..4fa140090 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1924,6 +1924,22 @@ Stenen muren Zandvlakken [Tidal flats] Wadden +[Everglades] +Moerasland +[Pool spacing] +Afstand tussen poelen +[Pool size] +Grootte van de poelen +[Clearing size] +Grootte van de open plek +[Levee] +Dijk +[Sloughs] +Moerassloten +[Everglades need at least one colony.] +Het moerasland heeft minstens één kolonie nodig. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Te veel kolonies voor deze kaart; gebruik een grotere kaart of minder kolonies. [Sandbars] Zandbanken [Tide pools] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 476d28e38..7b40d29cd 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1900,6 +1900,22 @@ Kamienne mury Płaty piasku [Tidal flats] Równiny pływowe +[Everglades] +Mokradła +[Pool spacing] +Odstęp między rozlewiskami +[Pool size] +Rozmiar rozlewisk +[Clearing size] +Rozmiar polany +[Levee] +Grobla +[Sloughs] +Starorzecza +[Everglades need at least one colony.] +Mokradła potrzebują co najmniej jednej kolonii. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Za dużo kolonii jak na tę mapę; użyj większej mapy lub mniejszej liczby kolonii. [Sandbars] Łachy piaskowe [Tide pools] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 3456b2135..afca84285 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1906,6 +1906,22 @@ Muralhas de pedra Manchas de areia [Tidal flats] Planícies de maré +[Everglades] +Pântanos +[Pool spacing] +Espaçamento das poças +[Pool size] +Tamanho das poças +[Clearing size] +Tamanho da clareira +[Levee] +Dique +[Sloughs] +Charcos +[Everglades need at least one colony.] +Os pântanos precisam de pelo menos uma colónia. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Colónias a mais para este mapa; use um mapa maior ou menos colónias. [Sandbars] Bancos de areia [Tide pools] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 551c4df35..ca775267b 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1900,6 +1900,22 @@ Ziduri de piatră Petice de nisip [Tidal flats] Câmpii de maree +[Everglades] +Mlaștini +[Pool spacing] +Distanța dintre bălți +[Pool size] +Mărimea bălților +[Clearing size] +Mărimea poienii +[Levee] +Dig +[Sloughs] +Mlaștini +[Everglades need at least one colony.] +Mlaștinile au nevoie de cel puțin o colonie. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Prea multe colonii pentru această hartă; folosește o hartă mai mare sau mai puține colonii. [Sandbars] Bancuri de nisip [Tide pools] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 1b11568fe..9e5c6f01d 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1898,6 +1898,22 @@ OpenGL недоступен в этой сборке. Песчаные участки [Tidal flats] Приливные отмели +[Everglades] +Болота +[Pool spacing] +Расстояние между заводями +[Pool size] +Размер заводей +[Clearing size] +Размер поляны +[Levee] +Дамба +[Sloughs] +Старицы +[Everglades need at least one colony.] +Болотам нужна хотя бы одна колония. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Слишком много колоний для этой карты; используйте карту побольше или меньше колоний. [Sandbars] Песчаные банки [Tide pools] diff --git a/data/texts.si.txt b/data/texts.si.txt index ff2591d78..b1d316b7c 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1900,6 +1900,22 @@ Kamniti zidovi Peščene zaplate [Tidal flats] Plimske ravnice +[Everglades] +Močvirja +[Pool spacing] +Razmik med mlakami +[Pool size] +Velikost mlak +[Clearing size] +Velikost jase +[Levee] +Nasip +[Sloughs] +Mrtvice +[Everglades need at least one colony.] +Močvirja potrebujejo vsaj eno kolonijo. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Preveč kolonij za ta zemljevid; uporabite večji zemljevid ali manj kolonij. [Sandbars] Peščene sipine [Tide pools] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 108258a11..0e7f3f839 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1908,6 +1908,22 @@ Kamenné hradby Piesčiny [Tidal flats] Prílivové pláne +[Everglades] +Močiare +[Pool spacing] +Rozostup mlák +[Pool size] +Veľkosť mlák +[Clearing size] +Veľkosť čistiny +[Levee] +Hrádza +[Sloughs] +Mŕtve ramená +[Everglades need at least one colony.] +Močiare potrebujú aspoň jednu kolóniu. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Príliš veľa kolónií pre túto mapu; použite väčšiu mapu alebo menej kolónií. [Sandbars] Piesočné lavice [Tide pools] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index a368b2c0b..f7c8d3f56 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1900,6 +1900,22 @@ OpenGL није доступан у овој верзији. Пешчане површине [Tidal flats] Плимске равнице +[Everglades] +Мочваре +[Pool spacing] +Размак између бара +[Pool size] +Величина бара +[Clearing size] +Величина чистине +[Levee] +Насип +[Sloughs] +Мртваје +[Everglades need at least one colony.] +Мочварама треба бар једна колонија. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Превише колонија за ову мапу; користите већу мапу или мање колонија. [Sandbars] Пешчани спрудови [Tide pools] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 451f2766d..56f725957 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1910,6 +1910,22 @@ Stenmurar Sandfläckar [Tidal flats] Tidvattenslätter +[Everglades] +Sumpmarker +[Pool spacing] +Avstånd mellan pölar +[Pool size] +Pölarnas storlek +[Clearing size] +Gläntans storlek +[Levee] +Vall +[Sloughs] +Sumpkanaler +[Everglades need at least one colony.] +Sumpmarker kräver minst en koloni. +[Too many colonies for this map; use a bigger map or fewer colonies.] +För många kolonier för den här kartan; använd en större karta eller färre kolonier. [Sandbars] Sandbankar [Tide pools] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index d3e5049c3..6c3798358 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1908,6 +1908,22 @@ Taş duvarlar Kum lekeleri [Tidal flats] Gelgit düzlükleri +[Everglades] +Bataklıklar +[Pool spacing] +Gölcük aralığı +[Pool size] +Gölcük boyutu +[Clearing size] +Açıklık boyutu +[Levee] +Set +[Sloughs] +Bataklıklar +[Everglades need at least one colony.] +Bataklıklar için en az bir koloni gerekir. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Bu harita için çok fazla koloni var; daha büyük bir harita kullanın veya koloni sayısını azaltın. [Sandbars] Kum tepecikleri [Tide pools] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index baaee06d4..0af23ddd7 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1896,6 +1896,22 @@ OpenGL недоступний у цій збірці. Піщані ділянки [Tidal flats] Припливні мілини +[Everglades] +Болота +[Pool spacing] +Відстань між заводями +[Pool size] +Розмір заводей +[Clearing size] +Розмір галявини +[Levee] +Дамба +[Sloughs] +Стариці +[Everglades need at least one colony.] +Болотам потрібна хоча б одна колонія. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Забагато колоній для цієї мапи; використайте більшу мапу або менше колоній. [Sandbars] Піщані банки [Tide pools] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index afcc1f8e2..ceccfab1f 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1896,6 +1896,22 @@ Tường đá Bãi cát [Tidal flats] Bãi triều +[Everglades] +Đầm lầy +[Pool spacing] +Khoảng cách các ao +[Pool size] +Kích thước ao +[Clearing size] +Kích thước khoảng trống +[Levee] +Đê +[Sloughs] +Đầm lầy +[Everglades need at least one colony.] +Đầm lầy cần ít nhất một thuộc địa. +[Too many colonies for this map; use a bigger map or fewer colonies.] +Quá nhiều thuộc địa cho bản đồ này; hãy dùng bản đồ lớn hơn hoặc ít thuộc địa hơn. [Sandbars] Cồn cát [Tide pools] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index c3d275779..f159f67dc 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1896,6 +1896,22 @@ OpenGL 在此版本中不可用。 沙地 [Tidal flats] 潮间带 +[Everglades] +沼泽地 +[Pool spacing] +水塘间距 +[Pool size] +水塘大小 +[Clearing size] +空地大小 +[Levee] +堤坝 +[Sloughs] +沼泽水道 +[Everglades need at least one colony.] +沼泽地至少需要一个殖民地。 +[Too many colonies for this map; use a bigger map or fewer colonies.] +此地图的殖民地太多;请使用更大的地图或减少殖民地。 [Sandbars] 沙洲 [Tide pools] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 2e2d5ff7c..095ac556c 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1972,6 +1972,22 @@ OpenGL 在此版本中不可用。 沙地 [Tidal flats] 潮間帶 +[Everglades] +沼澤地 +[Pool spacing] +水塘間距 +[Pool size] +水塘大小 +[Clearing size] +空地大小 +[Levee] +堤壩 +[Sloughs] +沼澤水道 +[Everglades need at least one colony.] +沼澤地至少需要一個殖民地。 +[Too many colonies for this map; use a bigger map or fewer colonies.] +此地圖的殖民地太多;請使用更大的地圖或減少殖民地。 [Sandbars] 沙洲 [Tide pools] diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 2e16f0fbd..31a81d214 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -44,6 +44,7 @@ reused after a generator is retired. | `ring-world` | 16 | Ring world | Its own — see below | | `city-states` | 17 | City states | Its own — see below | | `tidal-flats` | 18 | Tidal flats | Its own — see below | +| `everglades` | 19 | Everglades | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -88,6 +89,7 @@ ambient layer but still leaves every colony a start. Maze keeps its explicit den | Ring world | The ambient scatter's wheat, wood, stone and fruit, and the shallows' algae; starter patches and island prizes are unscaled | Winding belt (on); Colonies on both coasts (on) | | City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard) and the sea's algae and island prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | | Tidal flats | Every island's ambient fields and outcrops and every island's prize; each home's kit is unscaled | Central island (on): off, the middle of the map is flats and there is no orchard | +| Everglades | The swamp's standing wood and wheat, its outcrops and groves, and the pools' algae; every home's kit is unscaled | None | `scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of @@ -159,8 +161,8 @@ fixed resource pass: states' walls; they are treated like terrain, never cleared and never looked past. It wraps each boot tile onto the map first, since the height-field generators' fallback site search can hand over one past the edge. - RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands, Ring world, City states and - Tidal flats call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount + RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands, Ring world, City states, + Tidal flats and Everglades call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount other than 100. Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane down every passage, and its `validateWorld` confirms every colony can still walk to every @@ -509,6 +511,52 @@ on its island's edge, and expansion means taking another island whole. present and every colony and the central island reachable on foot from colony 0, with water, buildings and every resource blocking. +## Everglades + +A dense wetland, the same everywhere and across the wrap. Pools and long sloughs lie on a jittered +lattice that tiles the torus, bigger where the land is wet and smaller where it is dry, and the +grass between them starts thick with wood and wheat. Every tile is a few steps from water, so wood +and wheat grow back at the engine's top rate; resources block movement, so the swamp grows shut +unless workers keep cutting it. Nothing grows on sand, and every pool's beach is sand, so the banks +between pools are the lanes that stay open, winding and narrow. Every colony starts in a clearing +with a pond and a kit, ringed by a sand levee with gaps the swamp creeps through; what lies beyond +is held only while it is kept cut. Nothing is symmetric on purpose: the landscape is one random +field over the whole map and the clearings are nudged off their ring, so it reads as country, not +as an arena. Fairness is statistical; the lobby keeps the best-scoring of several seeds. + +- **Layout.** One pool per cell of a `pool-spacing` lattice that divides the map's width and + height exactly, so the field wraps without a seam; each pool is jittered within its cell, its + radius `pool-size` scaled by a smooth wetness field (0.6 at the driest ground to 1.5 at the + wettest) and by a random factor, and `sloughs` percent of them are grown 1.4 times and + stretched 2.2 times along a random line. The engine wraps every pool in a two-tile beach, so + at the defaults (spacing 15, size 4) a 256 map is about 30% water, 34% beach and 36% grass; + that sits at the threshold where the beaches stop linking into one network, so some lanes + between neighbours stay open for ever and others close as the grass grows over. Closer or + bigger pools link every beach into a permanent maze; sparser or smaller ones close the map. Clearings of `clearing-size` radius (rough outlines) + sit near a ring at 58% of the half side, evenly spaced from a random start and each nudged by + up to 12% of a wedge and 8% of the radius, with a pond at the middle, a `kLeveeWidth` band of + sand round the outline of which `levee` percent stands (36 sectors, chosen at random), and no + pool water for two tiles beyond it so the gaps open onto grass. The generator bends rather than + refuses: a clearing that would not fit between its neighbours or inside the wrap at its fullest + reach shrinks until it does (down to a radius of 6; the pond shrinks with it and a clearing + under 9 has none, the swarm taking the middle), then the grass beyond the levee and the fit + margin go, then the ring widens to 76% of the half side; pools whose + expected coverage (mean pool area over cell area, overlaps discounted) would pass 60% of the map + are scaled down to that, and `validateRequest` refuses only when even the smallest clearings + cannot fit. +- **Resources.** Each home has an unscaled kit of 40 wheat and 30 wood beside its pond and a + quarry on the side towards the map's centre; the swarm stands just past the pond's beach on + the far side with the kit kept four tiles clear of it, so there is room to build. Off the clearings the + swamp starts with wood on 55% of the grass and wheat on 15%, in patches, scaled by the amounts, + with stone outcrops (which never grow) and fruit groves. Algae seeds the pools. +- **Routes opened, not hoped for.** Last of all, `openRoutes` walks from colony 0 and, for any + colony it cannot reach, opens the cheapest way through: deposits on it are cleared and water on + it becomes a sand ford (one sand corner per tile), so a boxed-in start is repaired rather than + failed. +- **Checked, not assumed.** `validateWorld` rebuilds the design and requires every clearing's pond + present, no deposit on any levee tile, and every colony reachable on foot from colony 0, with + water, buildings and every resource blocking. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort diff --git a/src/SConscript b/src/SConscript index 459104124..ead8241e6 100644 --- a/src/SConscript +++ b/src/SConscript @@ -184,6 +184,7 @@ map/generator/generators/ConcreteIslandsGenerator.cpp map/generator/generators/ContestedCommonsGenerator.cpp map/generator/generators/MazeGenerator.cpp map/generator/generators/CraterLakesGenerator.cpp +map/generator/generators/EvergladesGenerator.cpp map/generator/generators/FjordContinentGenerator.cpp map/generator/generators/IslandsGenerator.cpp map/generator/generators/IslesGenerator.cpp diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index 7b6190bc1..7e44d6b44 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -36,7 +36,8 @@ struct GenerationRequest eSYMMETRICARENA = 15, eRINGWORLD = 16, eCITYSTATES = 17, - eTIDALFLATS = 18 + eTIDALFLATS = 18, + eEVERGLADES = 19 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index e4b11beb9..2b59f7b5a 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -4,6 +4,7 @@ #include "ConcreteIslandsGenerator.h" #include "ContestedCommonsGenerator.h" #include "CraterLakesGenerator.h" +#include "EvergladesGenerator.h" #include "FjordContinentGenerator.h" #include "IslandsGenerator.h" #include "IslesGenerator.h" @@ -85,8 +86,8 @@ const GeneratorRegistry &GeneratorRegistry::builtins() {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), stoneHighlandsDefinition(), symmetricArenaDefinition(), ringWorldDefinition(), - cityStatesDefinition(), tidalFlatsDefinition(), ruggedArchipelagoDefinition(), - concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), - swampDefinition(), riverDefinition(), uniformDefinition()}); + cityStatesDefinition(), tidalFlatsDefinition(), evergladesDefinition(), + ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), + islandsDefinition(), swampDefinition(), riverDefinition(), uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/EvergladesGenerator.cpp b/src/map/generator/generators/EvergladesGenerator.cpp new file mode 100644 index 000000000..5e19b523c --- /dev/null +++ b/src/map/generator/generators/EvergladesGenerator.cpp @@ -0,0 +1,839 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "EvergladesGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "HeightMap.h" +#include "Resources.h" +#include "Settlements.h" +#include "Unit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Everglades: a dense wetland, the same everywhere and across the wrap. Pools and long sloughs lie +// on a jittered lattice that tiles the torus, bigger where the land is wet and smaller where it is +// dry, and the grass between them starts thick with wood and wheat. Every tile is a few steps from +// water, so wood and wheat grow back at the engine's top rate; resources block movement, so the +// swamp grows shut unless workers keep cutting it. Nothing grows on sand, and every pool's beach is +// sand, so the banks between pools are the lanes that stay open, winding and narrow. Every colony +// starts in a clearing with a pond and a kit, ringed by a sand levee with gaps the swamp creeps +// through; what lies beyond is held only while it is kept cut. +// +// Nothing here is symmetric on purpose: the landscape is one random field over the whole map and +// the clearings are nudged off their ring, so it reads as country, not as an arena. Fairness is +// statistical, and the lobby keeps the best-scoring of several seeds. The design is a pure +// function of the request, so validateWorld rebuilds it and checks the finished world. +namespace +{ + +constexpr double pi = 3.14159265358979323846; + +// The clearings sit near a ring at this share of the half side, evenly spaced from a random start, +// each nudged by up to these shares of a wedge and of the ring's radius. +constexpr double kHomeRing = 0.58; +// On a crowded map the ring widens up to this share of the half side before the request is refused. +constexpr double kHomeRingMax = 0.76; +constexpr double kHomeJitterTurn = 0.12; +constexpr double kHomeJitterRadius = 0.08; +// The clearings' outlines wobble by up to this share of their radius. A clearing that would not fit +// its ring shrinks down to this radius before the request is refused. +constexpr double kClearingRoughness = 0.3; +constexpr int kMinimumClearing = 6; +// A clearing smaller than this has no pond: the swamp's pools are a few steps away anyway. +constexpr int kPondClearing = 9; +// A pond up to this big sits in every clearing (smaller in a shrunken clearing); the levee is this +// wide, and no pool water lies within this distance beyond it, so the gaps open onto grass. +constexpr double kPondRadius = 3.0; +constexpr double kLeveeWidth = 2.0; +constexpr double kLeveeClearance = 2.0; +constexpr int kLeveeSectors = 36; +// Every home starts identical: wheat and wood beside its pond, a quarry, all unscaled. +constexpr int kHomeWheat = 40; +constexpr int kHomeWood = 30; +constexpr int kSwarmClearance = 2; +// The kit keeps this far from the swarm beyond the clearance, so there is room to build beside it. +constexpr int kBuildRoom = 4; +// The swamp's standing wood and wheat, as shares of the grass off the clearings, at 100. +constexpr int kWoodShare = 55; +constexpr int kWheatShare = 15; +// A slough is a pool this much bigger, stretched this much along a random line. +constexpr double kSloughGrowth = 1.4; +constexpr double kSloughStretch = 2.2; +// Pools scale from this at the driest ground to this at the wettest. +constexpr double kDryScale = 0.6; +constexpr double kWetScale = 1.5; +// The most of the map the pools may be expected to cover, before the beaches; pools that would +// cover more are scaled down to this, since beyond it the colonies can hardly reach each other. +constexpr double kMaximumCoverage = 0.6; +// Opening a route for a boxed-in colony: the cost of a step onto open ground, onto a deposit that +// will be cleared, onto stone or fruit, and onto water that becomes a ford. +constexpr int kStepOpen = 1, kStepDeposit = 3, kStepEternal = 8, kStepFord = 25; + +struct Torus +{ + int w, h; + int x(int v) const { return ((v % w) + w) % w; } + int y(int v) const { return ((v % h) + h) % h; } + int at(int px, int py) const { return y(py) * w + x(px); } + int offsetX(int from, int to) const + { + const int d = x(to - from); + return d > w / 2 ? d - w : d; + } + int offsetY(int from, int to) const + { + const int d = y(to - from); + return d > h / 2 ? d - h : d; + } +}; + +std::vector stepsFrom(const Torus &t, const std::vector &source, + const std::vector &open) +{ + std::vector dist(size_t(t.w) * t.h, -1); + std::vector queue; + queue.reserve(dist.size()); + for (size_t i = 0; i < dist.size(); ++i) + if (source[i]) + { + dist[i] = 0; + queue.push_back(int(i)); + } + for (size_t head = 0; head < queue.size(); ++head) + { + const int x = queue[head] % t.w, y = queue[head] / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int n = t.at(x + dx, y + dy); + if (dist[n] < 0 && open[n]) + { + dist[n] = dist[queue[head]] + 1; + queue.push_back(n); + } + } + } + return dist; +} + +struct Geometry +{ + int teams, half, clearing, buildRoom; + double spacing, poolRadius, sloughs, homeRing, levee, pond, clearance; + double reach, jitterTurn, jitterRadius; // a clearing's fullest extent; how far homes are nudged + double coverage; // the share of the map the pools are expected to cover +}; + +// The share of the map the pools cover, expected: the mean pool area (radius scaled by wetness, +// the random factor and the rough outline, stretched; sloughs grown and stretched further) over +// the cell area, with overlaps discounted. +double expectedCoverage(const Geometry &g) +{ + const double meanScale = (kDryScale + kWetScale) / 2; + const double radius2 = g.poolRadius * g.poolRadius * meanScale * meanScale * 1.1; + const double pool = pi * radius2 * 1.2; + const double slough = pool * kSloughGrowth * kSloughGrowth * kSloughStretch / 1.2; + const double nominal = ((1 - g.sloughs) * pool + g.sloughs * slough) / (g.spacing * g.spacing); + return 1 - std::exp(-nominal); +} + +// Whether every clearing, at its fullest reach, fits between its neighbours and inside the wrap +// with the homes nudged this far. +bool clearingsFit(const Geometry &g) +{ + const double nearest = + 2 * g.homeRing * (1 - g.jitterRadius) * std::sin(pi * (1 - 2 * g.jitterTurn) / g.teams); + const bool fitsRing = g.teams < 2 || nearest >= 2 * g.reach; + const bool fitsWrap = g.homeRing * (1 + g.jitterRadius) + g.reach <= g.half - 1; + return fitsRing && fitsWrap; +} + +Geometry geometryFor(const GenerationRequest &r) +{ + const EvergladesOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; + g.clearing = o.clearingSize; + g.spacing = o.poolSpacing; + g.poolRadius = o.poolSize; + g.sloughs = o.sloughs / 100.0; + g.homeRing = kHomeRing * g.half; + g.levee = o.levee / 100.0; + // Nudge the homes as far as the ring allows, down to not at all on a crowded ring; when even + // that does not fit, the clearings shrink until it does, then the grass beyond the levee and + // the margin go, then the ring widens. Only then is the request refused. + g.clearance = kLeveeClearance; + double margin = 2; + for (;;) + { + g.reach = g.clearing * (1 + kClearingRoughness) + kLeveeWidth + g.clearance + margin; + for (int step = 4; step >= 0; --step) + { + g.jitterTurn = kHomeJitterTurn * step / 4; + g.jitterRadius = kHomeJitterRadius * step / 4; + if (clearingsFit(g)) + break; + } + if (clearingsFit(g)) + break; + if (g.clearing > kMinimumClearing) + --g.clearing; + else if (g.clearance > 0 || margin > 0) + g.clearance = margin = 0; + else if (g.homeRing + 0.02 * g.half <= kHomeRingMax * g.half) + g.homeRing += 0.02 * g.half; + else + break; + } + g.pond = g.clearing >= kPondClearing ? std::clamp(g.clearing * 0.23, 2.0, kPondRadius) : 0.0; + g.buildRoom = std::clamp(g.clearing - 5, 0, kBuildRoom); + // Pools that would drown the map are scaled down to the wettest playable coverage. + g.coverage = expectedCoverage(g); + if (g.coverage > kMaximumCoverage) + { + const double wanted = -std::log(1 - kMaximumCoverage), now = -std::log(1 - g.coverage); + g.poolRadius *= std::sqrt(wanted / now); + g.coverage = expectedCoverage(g); + } + return g; +} + +// A pool: a rough disc stretched along a line through its centre. +struct Pool +{ + double x, y, aspect, spin, reach; + RadialShape shape; + bool holds(double dx, double dy) const + { + if (std::abs(dx) > reach || std::abs(dy) > reach) + return false; + const double u = (dx * std::cos(spin) + dy * std::sin(spin)) / aspect; + const double v = -dx * std::sin(spin) + dy * std::cos(spin); + return std::hypot(u, v) < shape.radiusAt(std::atan2(v, u)); + } +}; + +struct Home +{ + int x, y; + RadialShape outline, pond; + std::array standing; +}; + +struct Layout +{ + Torus t{1, 1}; + Geometry g{}; + int cx = 0, cy = 0; + std::vector pools; + std::vector homes; + std::vector water, sand; // per tile + std::vector clearingOf; // clearing tiles: the colony, else -1 + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.w * t.h, teams = g.teams; + L.cx = t.w / 2; + L.cy = t.h / 2; + const auto unit = [&](const char *stream) + { return context.bounded(stream, 2001) / 1000.0 - 1; }; + + // The homes: near a ring, evenly spaced from a random start, each nudged a little. + const double phase = context.bounded("glades-layout", 3600) / 3600.0 * 2 * pi; + const double wedge = 2 * pi / teams; + for (int k = 0; k < teams; ++k) + { + const double a = phase + wedge * (k + 0.5 + g.jitterTurn * unit("glades-layout")); + const double r = g.homeRing * (1 + g.jitterRadius * unit("glades-layout")); + Home home{t.x(L.cx + int(std::lround(r * std::cos(a)))), + t.y(L.cy + int(std::lround(r * std::sin(a)))), + RadialShape(g.clearing, kClearingRoughness, context, "glades-clearings"), + RadialShape(std::max(g.pond, 1.0), 0.3, context, "glades-ponds"), + {}}; + // The levee round the clearing: a share of its sectors stand, the rest are gaps. + home.standing.fill(1); + std::array sectors{}; + for (int s = 0; s < kLeveeSectors; ++s) + sectors[s] = s; + for (int i = kLeveeSectors - 1; i > 0; --i) + std::swap(sectors[i], sectors[context.bounded("glades-levee", i + 1)]); + const int open = int(std::lround((1 - g.levee) * kLeveeSectors)); + for (int i = 0; i < open; ++i) + home.standing[sectors[i]] = 0; + L.homes.push_back(home); + } + + // The pools: one per cell of a lattice that tiles the torus exactly, jittered, sized by a smooth + // wetness field so the country has wet reaches and dry ones, some grown and stretched into sloughs. + HeightMap wet(t.w, t.h, context.stream("glades-wet")); + wet.makePlain(12); + const int nx = std::max(1, int(std::lround(t.w / g.spacing))); + const int ny = std::max(1, int(std::lround(t.h / g.spacing))); + const double cellW = double(t.w) / nx, cellH = double(t.h) / ny; + for (int j = 0; j < ny; ++j) + for (int i = 0; i < nx; ++i) + { + const double px = (i + 0.5) * cellW + unit("glades-pools") * cellW / 3; + const double py = (j + 0.5) * cellH + unit("glades-pools") * cellH / 3; + const float wetness = + std::clamp(wet(unsigned(t.x(int(px))), unsigned(t.y(int(py)))), 0.0f, 1.0f); + const bool slough = context.bounded("glades-pools", 1000) < g.sloughs * 1000; + const double radius = + g.poolRadius * (0.7 + context.bounded("glades-pools", 601) / 1000.0) * + (kDryScale + (kWetScale - kDryScale) * wetness) * (slough ? kSloughGrowth : 1); + RadialShape shape(radius, 0.45, context, "glades-pools"); + const double aspect = + slough ? kSloughStretch : 1 + context.bounded("glades-pools", 401) / 1000.0; + const double spin = context.bounded("glades-pools", 3600) / 3600.0 * pi; + L.pools.push_back({px, py, aspect, spin, shape.maximumRadius() * aspect + 1, shape}); + } + + L.water.assign(n, 0); + L.sand.assign(n, 0); + L.clearingOf.assign(n, -1); + for (const Pool &p : L.pools) + { + const int r = int(std::ceil(p.reach)); + const int x0 = int(std::floor(p.x)), y0 = int(std::floor(p.y)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (p.holds(x0 + dx - p.x, y0 + dy - p.y)) + L.water[t.at(x0 + dx, y0 + dy)] = 1; + } + // The clearings, cut out of the swamp: grass with a pond, the levee, and grass just beyond it. + for (int k = 0; k < teams; ++k) + { + const Home &home = L.homes[k]; + const int r = int(std::ceil(home.outline.maximumRadius() + kLeveeWidth + g.clearance)) + 1; + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + { + const int i = t.at(home.x + dx, home.y + dy); + const double d = std::hypot(double(dx), double(dy)), + a = std::atan2(double(dy), double(dx)); + const double edge = home.outline.radiusAt(a); + if (d < edge) + { + L.clearingOf[i] = k; + L.water[i] = g.pond > 0 && d < home.pond.radiusAt(a); + L.sand[i] = 0; + } + else if (d < edge + kLeveeWidth) + { + const int sector = + int(std::fmod(a + 2 * pi, 2 * pi) / (2 * pi) * kLeveeSectors) % + kLeveeSectors; + L.water[i] = 0; + L.sand[i] = home.standing[sector]; + } + else if (d < edge + kLeveeWidth + g.clearance) + { + L.water[i] = 0; + } + } + } + return L; +} + +// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), +// so every land corner beside water becomes sand. +void layBeaches(std::vector &terrain, const Torus &t) +{ + const std::vector original(terrain); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[t.at(x + dx, y + dy)] == WATER; + if (shore) + terrain[i] = SAND; + } +} + +template +int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) +{ + std::vector queued(size_t(t.w) * t.h, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) + { + const int i = frontier[head], x = i % t.w, y = i / t.w; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) + { + const int m = t.at(x + step[0], y + step[1]); + if (!queued[m]) + { + queued[m] = 1; + frontier.push_back(m); + } + } + } + return placed; +} + +bool clearGround(const Map &map, int x, int y) +{ + return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; +} + +template +int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) +{ + int seed = -1, nearest = INT_MAX; + for (int dy = -within; dy <= within; ++dy) + for (int dx = -within; dx <= within; ++dx) + { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) + { + nearest = dx * dx + dy * dy; + seed = i; + } + } + return seed; +} + +// Every home's kit, identical and unscaled: wheat and wood beside the pond on the sides, a quarry +// on the near side towards the map's centre. The rest of the clearing starts clear. +void furnishHomes(Map &map, const Layout &L, GenerationContext &context) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + std::vector reserved(n, 0); + const int room = kSwarmClearance + L.g.buildRoom; + for (int team = 0; team < L.g.teams; ++team) + for (int dy = -room; dy < 4 + room; ++dy) + for (int dx = -room; dx < 4 + room; ++dx) + reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + for (int team = 0; team < L.g.teams; ++team) + { + const Home &home = L.homes[team]; + const auto eligible = [&](int i) + { return L.clearingOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + const double a = + std::atan2(double(t.offsetY(L.cy, home.y)), double(t.offsetX(L.cx, home.x))); + const auto at = [&](double along, double across) + { + return std::make_pair( + home.x + int(std::lround(along * std::cos(a) - across * std::sin(a))), + home.y + int(std::lround(along * std::sin(a) + across * std::cos(a)))); + }; + const double beside = L.g.pond * 1.3 + 3; + if (const auto [x, y] = at(0, -beside); true) + if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheat, eligible); + if (const auto [x, y] = at(0, beside); true) + if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) + growPatch(map, t, seed, WOOD, kHomeWood, eligible); + if (const auto [x, y] = at(-(L.g.clearing - 4), 0); true) + if (const int seed = seedNear(t, x, y, 8, eligible); seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + } +} + +// The swamp: standing wood and wheat in patches over the grass off the clearings, stone outcrops +// that never grow, and a few fruit groves. Everything will grow from here. +void stockSwamp(Map &map, const Layout &L, GenerationContext &context, const EvergladesOptions &o) +{ + const Torus &t = L.t; + const int n = t.w * t.h; + HeightMap patch(t.w, t.h, context.stream("glades-patch")); + patch.makePlain(10); + HeightMap split(t.w, t.h, context.stream("glades-split")); + split.makePlain(5); + const auto eligible = [&](int i) + { return L.clearingOf[i] < 0 && clearGround(map, i % t.w, i / t.w); }; + std::vector ground; + std::vector> byPatch; + for (int i = 0; i < n; ++i) + if (eligible(i)) + { + ground.push_back(i); + byPatch.push_back({-patch(i % t.w, i / t.w), i}); + } + if (ground.empty()) + return; + std::stable_sort(byPatch.begin(), byPatch.end()); + const int area = int(ground.size()); + const int wood = int(scaledCount(std::int64_t(area) * kWoodShare / 100, o.wood)); + const int wheat = int(scaledCount(std::int64_t(area) * kWheatShare / 100, o.wheat)); + const int total = std::min(area, wood + wheat); + std::vector chosen; + for (int k = 0; k < total; ++k) + chosen.push_back(byPatch[k].second); + std::stable_sort( + chosen.begin(), chosen.end(), [&](int p, int q) + { return split.uiLevel(p % t.w, p / t.w, 2048) < split.uiLevel(q % t.w, q / t.w, 2048); }); + const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wood + wheat)); + for (int k = 0; k < total; ++k) + map.setResource(chosen[k] % t.w, chosen[k] / t.w, k < wheatShare ? CORN : WOOD, 1); + const int outcrops = int(scaledCount(std::max(2, area / 1000), o.stone)); + for (int k = 0; k < outcrops; ++k) + for (int attempt = 0; attempt < 100; ++attempt) + { + const int at = ground[context.bounded("glades-stone", ground.size())]; + if (eligible(at)) + { + placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, + 1 + int(context.bounded("glades-stone", 2))); + break; + } + } + const int groves = int(scaledCount(std::max(3, area / 1500), o.fruit)); + for (int k = 0; k < groves; ++k) + for (int attempt = 0; attempt < 100; ++attempt) + { + const int at = ground[context.bounded("glades-fruit", ground.size())]; + if (eligible(at)) + { + placeResourceClump(map, context, {at % t.w, at / t.w}, + CHERRY + int(context.bounded("glades-fruit", 3)), 2); + break; + } + } +} + +// Algae in the pools. +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) +{ + std::vector water; + for (int i = 0; i < t.w * t.h; ++i) + if (map.isWater(i % t.w, i / t.w)) + water.emplace_back(i % t.w, i / t.w); + if (water.empty()) + return; + for (int clump = 0; clump < scaledCount(int(water.size()) / 40, algaePercent); ++clump) + placeResourceClump(map, context, water[context.bounded("glades-algae", water.size())], ALGA, + 1); +} + +void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t) +{ + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + { + const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); + if (map.isResource(x, y)) + map.setNoResource(x, y, 1); + } +} + +// Every colony must be able to walk to colony 0 at the start. Where the swamp or the sloughs box +// one in, the cheapest way through is opened: deposits on it are cleared and water on it becomes a +// sand ford. The map may look odd there; it does not fail. +void openRoutes(Game &game, const Layout &L, GenerationContext &context) +{ + Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (teams < 2) + return; + const auto openAt = [&](int i) + { + const int x = i % t.w, y = i / t.w; + return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + }; + const auto costAt = [&](int i) + { + const int x = i % t.w, y = i / t.w; + if (map.getBuilding(x, y) != NOGBID) + return -1; + if (map.isWater(x, y)) + return kStepFord; + if (map.isResource(x, y)) + { + const int type = map.getResource(x, y).type; + return type == STONE || (type >= CHERRY && type <= CHERRY + 2) ? kStepEternal + : kStepDeposit; + } + return kStepOpen; + }; + // The open tiles round a colony's swarm. + const auto doorstep = [&](int team) + { + std::vector tiles; + for (int dy = -1; dy <= 4; ++dy) + for (int dx = -1; dx <= 4; ++dx) + if (dy == -1 || dy == 4 || dx == -1 || dx == 4) + { + const int i = t.at(context.bootX[team] + dx, context.bootY[team] + dy); + if (openAt(i)) + tiles.push_back(i); + } + return tiles; + }; + static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + bool changed = false; + for (int team = 1; team < teams; ++team) + { + std::vector open(n), source(n, 0); + for (int i = 0; i < n; ++i) + open[i] = openAt(i); + for (int i : doorstep(0)) + source[i] = 1; + const std::vector walk = stepsFrom(t, source, open); + std::vector target(n, 0); + bool arrived = false; + for (int i : doorstep(team)) + { + target[i] = 1; + arrived = arrived || walk[i] >= 0; + } + if (arrived) + continue; + // The cheapest way from anything colony 0 can reach to this colony's doorstep. + std::vector cost(n, INT_MAX), from(n, -1); + std::priority_queue, std::vector>, std::greater<>> + heap; + for (int i = 0; i < n; ++i) + if (walk[i] >= 0) + { + cost[i] = 0; + heap.push({0, i}); + } + int reached = -1; + while (!heap.empty() && reached < 0) + { + const auto [c, i] = heap.top(); + heap.pop(); + if (c > cost[i]) + continue; + if (target[i]) + { + reached = i; + break; + } + const int x = i % t.w, y = i / t.w; + for (const auto &step : steps) + { + const int m = t.at(x + step[0], y + step[1]); + const int stepCost = costAt(m); + if (stepCost < 0 || c + stepCost >= cost[m]) + continue; + cost[m] = c + stepCost; + from[m] = i; + heap.push({cost[m], m}); + } + } + for (int i = reached; i >= 0 && cost[i] > 0; i = from[i]) + { + const int x = i % t.w, y = i / t.w; + if (map.isWater(x, y)) + { + // One sand corner turns this tile and its three other tiles into walkable shore. + map.setUMTerrain(x, y, SAND); + for (int dy = -1; dy <= 0; ++dy) + for (int dx = -1; dx <= 0; ++dx) + if (map.isResource(t.x(x + dx), t.y(y + dy))) + map.setNoResource(t.x(x + dx), t.y(y + dy), 1); + changed = true; + } + else if (map.isResource(x, y)) + { + map.setNoResource(x, y, 1); + } + } + if (changed) + map.rebuildTerrain(); + } +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "glades layout"; + const EvergladesOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(GRASS); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.w * t.h; + + context.stage = "glades terrain"; + std::vector terrain(n, GRASS); + for (int i = 0; i < n; ++i) + { + if (L.water[i]) + terrain[i] = WATER; + else if (L.sand[i]) + terrain[i] = SAND; + } + layBeaches(terrain, t); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); + map.rebuildTerrain(); + + context.stage = "glades colonies"; + for (int team = 0; team < teams; ++team) + { + const Home &home = L.homes[team]; + std::vector ground(n, 0); + for (int i = 0; i < n; ++i) + ground[i] = L.clearingOf[i] == team && map.isGrass(i % t.w, i / t.w); + // The swarm stands just past the pond's beach on the far side from the map's centre, leaving + // the rest of that side to build on; in a clearing too small for a pond it takes the middle. + const double a = + std::atan2(double(t.offsetY(L.cy, home.y)), double(t.offsetX(L.cx, home.x))); + const double back = L.g.pond > 0 ? L.g.pond * 1.3 + 4 : 0; + const MapGeneratorPoint anchor(home.x + int(std::lround(back * std::cos(a))) - 2, + home.y + int(std::lround(back * std::sin(a))) - 2); + if (!placeSettlement(game, context, team, ground, anchor, "glades-starts")) + return false; + } + + context.stage = "glades resources"; + furnishHomes(map, L, context); + stockSwamp(map, L, context, o); + seedAlgae(map, context, t, o.algae); + clearAroundSwarms(map, context, t); + guaranteeStartingResources(game, context, 24, 32, 0); + clearAroundSwarms(map, context, t); + context.stage = "glades routes"; + openRoutes(game, L, context); + return true; +} + +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 1) + return "Everglades need at least one colony."; + if (!clearingsFit(geometryFor(r))) + return "Too many colonies for this map; use a bigger map or fewer colonies."; + return ""; +} + +// Checked on the finished world: every clearing keeps its pond, no levee tile carries a deposit +// (nothing grows on sand, so nothing may start there), and colony 0 can walk to every other colony, +// with water, buildings and every resource blocking (openRoutes has already opened any way needed). +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (!L.failure.empty()) + return "The glades design could not be rebuilt: " + L.failure; + const Map &map = game.map; + const Torus &t = L.t; + const int n = t.w * t.h, teams = context.request.nbTeams; + if (map.getW() != t.w || map.getH() != t.h) + return "The glades design does not match the map size."; + for (int k = 0; k < teams && L.g.pond > 0; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(L.homes[k].x + dx), t.y(L.homes[k].y + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s clearing has lost its pond."; + } + for (int i = 0; i < n; ++i) + if (L.sand[i] && map.isResource(i % t.w, i / t.w)) + return "A deposit stands on the levee at (" + std::to_string(i % t.w) + ", " + + std::to_string(i / t.w) + ")."; + std::vector> workers(std::max(teams, 1)); + for (int i = 0; i < n; ++i) + { + const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); + if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) + workers[Unit::GIDtoTeam(gid)].push_back(i); + } + if (teams < 1 || workers[0].empty()) + return "Colony 0 has no workers to walk the glades."; + std::vector open(n, 0), source(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + open[i] = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + } + for (int i : workers[0]) + source[i] = 1; + const std::vector steps = stepsFrom(t, source, open); + for (int team = 1; team < teams; ++team) + { + bool arrived = false; + for (int i : workers[team]) + arrived = arrived || steps[i] >= 0; + if (!arrived) + return "Colony " + std::to_string(team) + + " cannot walk to colony 0 through the glades."; + } + return ""; +} +} // namespace + +EvergladesOptions::EvergladesOptions(const GenerationRequest &r) + : poolSpacing(r.option("pool-spacing")), poolSize(r.option("pool-size")), + sloughs(r.option("sloughs")), clearingSize(r.option("clearing-size")), + levee(r.option("levee")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition evergladesDefinition() +{ + return { + "everglades", + 19, + "Everglades", + 2, + false, + // Pool spacing and pool size in tiles; sloughs is the share of pools grown and stretched + // into sloughs; the clearings' radius in tiles; the levee is the share of each clearing's + // ring that is sand. + {{"pool-spacing", "Pool spacing", 8, 20, 1, 15, ControlGroup::Terrain}, + {"pool-size", "Pool size", 2, 7, 1, 4, ControlGroup::Terrain}, + {"sloughs", "Sloughs", 0, 50, 5, 15, ControlGroup::Terrain}, + {"clearing-size", "Clearing size", 12, 18, 1, 13, ControlGroup::Layout}, + {"levee", "Levee", 0, 100, 10, 70, ControlGroup::Layout}, + // The swamp's standing wood and wheat, its outcrops and groves, and the pools' algae; + // every home's kit is unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/EvergladesGenerator.h b/src/map/generator/generators/EvergladesGenerator.h new file mode 100644 index 000000000..344670a68 --- /dev/null +++ b/src/map/generator/generators/EvergladesGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct EvergladesOptions +{ + int poolSpacing, poolSize, sloughs, clearingSize, levee; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit EvergladesOptions(const GenerationRequest &r); +}; +GeneratorDefinition evergladesDefinition(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 238d254f9..6110b43f7 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -25,6 +25,7 @@ #include #include #include +#include GlobalContainer *globalContainer = nullptr; using D = GenerationRequest; @@ -282,7 +283,7 @@ class MapGeneratorDefaultsTest screen.descriptor.option("test-gap") == 5 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 18); + assert(registry.selectionIndex(101) == 19); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index a0b0a0dc0..e24ee6015 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -126,3 +126,10 @@ macos-arm64 18 3 8 8 4 2 ok 10102518421633888991 macos-arm64 18 3 8 8 4 3 ok 5379511258847874570 macos-arm64 18 3 8 8 8 1 ok 12075523445877924400 macos-arm64 18 3 9 9 4 1 ok 8538100221992578386 +macos-arm64 19 2 7 7 4 1 ok 2704824689328339712 +macos-arm64 19 2 8 8 2 1 ok 18374433395111261761 +macos-arm64 19 2 8 8 4 1 ok 9505660943407981793 +macos-arm64 19 2 8 8 4 2 ok 10374245496345104115 +macos-arm64 19 2 8 8 4 3 ok 3538515548796101002 +macos-arm64 19 2 8 8 8 1 ok 638962071964486508 +macos-arm64 19 2 9 9 4 1 ok 3483131854939269908 From b6f596d4b5c048e3de47df830a724577a0232527 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 15:47:59 -0400 Subject: [PATCH 101/136] Extract the designed generators' shared toolkit City states, Tidal flats, Everglades and Ring world each carried a private copy of the same helpers: the order-independent beach pass, the breadth-first kit patch, the nearest-eligible-seed search, the swarm clearance ring, the algae seeding, the island prize, the 0-1 road opener and the wedge-frame arithmetic. Everglades had even copied Torus and stepsFrom out of Grid.h. Those now live once under shared/: Sketch (TerrainSketch, layBeaches, raiseIslands, writeUndermap), Planting (clearGround, growPatch, seedNear, swarmSurroundings, clearAroundSwarms, seedAlgae, stockIslands), Roads (cheapestRoute, openRoad), Wedge (WedgeFrame, Blob) and Pipeline (settleColonies, reopenCrampedStarts, designMismatch, walkFromFirstColony). kPi is exported from Geometry.h. Watershed's connectColonies, Maze's and Stone highlands' validators and every generator's cramped-start tail use them too. City states' island gap rule differs from Ring world's by a constant; it is kept behind IslandPlacement::gapFromBothReaches until its revision moves. Every generator's output is unchanged: the golden table passes at every current revision (133 rows), as do the defaults test and the custom-setup harness. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- src/SConscript | 4 + .../generators/CityStatesGenerator.cpp | 407 +++--------------- .../generators/ConcreteIslandsGenerator.cpp | 13 +- .../generators/ContestedCommonsGenerator.cpp | 17 +- .../generators/EvergladesGenerator.cpp | 240 ++--------- .../generators/FjordContinentGenerator.cpp | 32 +- .../generator/generators/IslesGenerator.cpp | 11 +- .../generator/generators/MazeGenerator.cpp | 26 +- .../generators/RingWorldGenerator.cpp | 276 ++---------- .../generators/RuggedArchipelagoGenerator.cpp | 11 +- .../generators/ShatteredCoastGenerator.cpp | 11 +- .../generators/StoneHighlandsGenerator.cpp | 29 +- .../generators/SymmetricArenaGenerator.cpp | 4 +- .../generators/TidalFlatsGenerator.cpp | 223 +++------- .../generators/WatershedGenerator.cpp | 70 +-- src/map/generator/shared/Geometry.cpp | 6 +- src/map/generator/shared/Geometry.h | 1 + src/map/generator/shared/Pipeline.cpp | 36 ++ src/map/generator/shared/Pipeline.h | 82 ++++ src/map/generator/shared/Planting.cpp | 99 +++++ src/map/generator/shared/Planting.h | 101 +++++ src/map/generator/shared/Roads.cpp | 82 ++++ src/map/generator/shared/Roads.h | 26 ++ src/map/generator/shared/Sketch.cpp | 95 ++++ src/map/generator/shared/Sketch.h | 54 +++ src/map/generator/shared/Terrain.cpp | 15 +- src/map/generator/shared/Wedge.h | 81 ++++ 27 files changed, 930 insertions(+), 1122 deletions(-) create mode 100644 src/map/generator/shared/Pipeline.cpp create mode 100644 src/map/generator/shared/Pipeline.h create mode 100644 src/map/generator/shared/Planting.cpp create mode 100644 src/map/generator/shared/Planting.h create mode 100644 src/map/generator/shared/Roads.cpp create mode 100644 src/map/generator/shared/Roads.h create mode 100644 src/map/generator/shared/Sketch.cpp create mode 100644 src/map/generator/shared/Sketch.h create mode 100644 src/map/generator/shared/Wedge.h diff --git a/src/SConscript b/src/SConscript index ead8241e6..68380dd96 100644 --- a/src/SConscript +++ b/src/SConscript @@ -203,6 +203,10 @@ map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp map/generator/shared/Geometry.cpp map/generator/shared/Grid.cpp +map/generator/shared/Pipeline.cpp +map/generator/shared/Planting.cpp +map/generator/shared/Roads.cpp +map/generator/shared/Sketch.cpp map/generator/shared/Topology.cpp map/generator/shared/Settlements.cpp map/generator/shared/Regions.cpp diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 8062c59c4..4821cc3b2 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -6,9 +6,14 @@ #include "Grid.h" #include "Geometry.h" #include "HeightMap.h" +#include "Pipeline.h" +#include "Planting.h" #include "Resources.h" +#include "Roads.h" #include "Settlements.h" +#include "Sketch.h" #include "Unit.h" +#include "Wedge.h" #include #include #include @@ -45,8 +50,6 @@ using namespace MapGeneration; namespace { -constexpr double pi = 3.14159265358979323846; - // Sea kept between the homes' outer coast and the map's wrap, as a share of the half side. constexpr double kRimShare = 0.05; constexpr int kRimMinimum = 4; @@ -77,7 +80,6 @@ constexpr int kLakeSeaGap = 7; // unscaled, outside a clear ring round the swarm. constexpr int kHomeWheat = 40; constexpr int kHomeWood = 30; -constexpr int kSwarmClearance = 2; // Land kept between a lake and any coast, landing or other lake. constexpr int kLakeShore = 6; // The commons' heart: its central lake's radius as a share of the commons' radius, and how far @@ -120,7 +122,7 @@ struct Profile { double v = 0; for (int h = 0; h < count; ++h) - v += amplitude[h] * std::cos(2 * pi * (first + h) * u + phase[h]); + v += amplitude[h] * std::cos(2 * kPi * (first + h) * u + phase[h]); return v; } }; @@ -135,7 +137,7 @@ Profile rollProfile(GenerationContext &context, const char *stream, int first, i { p.amplitude[h] = (0.4 + 0.6 * context.bounded(stream, 1000) / 1000.0) / std::pow(double(first + h), falloff); - p.phase[h] = 2 * pi * context.bounded(stream, 3600) / 3600.0; + p.phase[h] = 2 * kPi * context.bounded(stream, 3600) / 3600.0; } double peak = 0; for (int i = 0; i < 720; ++i) @@ -165,10 +167,10 @@ struct Geometry // Arc of a home's inner coast beyond its channel, at the nominal radius. double innerArc() const { - return teams < 2 ? 2 * pi * innerRadius() : 2 * pi * innerRadius() / teams - strait; + return teams < 2 ? 2 * kPi * innerRadius() : 2 * kPi * innerRadius() / teams - strait; } // Half the arc a wedge spans at radius d, less half the channel. - double arcHalf(double d) const { return teams < 2 ? pi * d : pi * d / teams - strait / 2.0; } + double arcHalf(double d) const { return teams < 2 ? kPi * d : kPi * d / teams - strait / 2.0; } }; Geometry geometryFor(const GenerationRequest &r) @@ -199,17 +201,6 @@ double lakeOffset(const Geometry &g, double lakeRadius) return std::clamp(0.6 * g.depth(), low, std::max(low, high)); } -// A round feature in a wedge's frame: arc offset from the middle, radius from the centre. -struct Blob -{ - double s, r; - RadialShape shape; - bool holds(double ds, double dr) const - { - return std::hypot(ds, dr) < shape.radiusAt(std::atan2(dr, ds)); - } -}; - struct Home { double angle; // the wedge's middle, where its causeway and lake lie @@ -258,7 +249,7 @@ struct Coasts // begin only beyond the lake and its clearance. double lakeRadius, lakeReach, flatArc, bayArc; Coasts(const Geometry &g, GenerationContext &context) - : g(g), wedge(2 * pi / g.teams), inner(g.innerRadius()), depth(g.depth()), + : g(g), wedge(2 * kPi / g.teams), inner(g.innerRadius()), depth(g.depth()), coast(rollProfile(context, "city-coast", 1, 4, 1.2)), ripple(rollProfile(context, "city-coast", 6, 6, 0.6)), innerBays(rollProfile(context, "city-coast", 1, 3, 1.0)), @@ -293,7 +284,7 @@ struct Coasts double bendAt(double d) const { const double x = (d - inner) / std::max(1.0, depth); - return x <= 0 || x >= 1 ? 0.0 : bend * std::sin(pi * x); + return x <= 0 || x >= 1 ? 0.0 : bend * std::sin(kPi * x); } }; @@ -348,7 +339,7 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext // of the causeway's approach and of other features. f.mainLakeR = inner + lakeOffset(g, c.lakeRadius); f.lakes.push_back( - {0.0, f.mainLakeR, RadialShape(c.lakeRadius, 0.3, context, "city-home-lakes")}); + {0.0, f.mainLakeR, 1.0, 0.0, RadialShape(c.lakeRadius, 0.3, context, "city-home-lakes")}); const auto fits = [&](double s0, double r0, double reach, double gap, const std::vector &others) { @@ -374,7 +365,7 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext RadialShape shape(2.0 + context.bounded("city-home-lakes", 1000) / 1000.0 * 2.0, 0.3, context, "city-home-lakes"); if (fits(s0, r0, shape.maximumRadius(), kLakeSeaGap, f.lakes)) - f.lakes.push_back({s0, r0, shape}); + f.lakes.push_back({s0, r0, 1.0, 0.0, shape}); } // Riverside: a f.creek from the main lake towards one flank of the inner coast, with a ford. if (L.homeKind == Riverside) @@ -405,21 +396,21 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext std::vector avoid = f.lakes; avoid.insert(avoid.end(), f.sandBlobs.begin(), f.sandBlobs.end()); if (fits(s0, r0, shape.maximumRadius(), 5, avoid)) - f.sandBlobs.push_back({s0, r0, shape}); + f.sandBlobs.push_back({s0, r0, 1.0, 0.0, shape}); } // Sand on the commons: anywhere between the f.heart and the shore, off the landings. - const double commonsArea = pi * g.commonsRadius * g.commonsRadius; + const double commonsArea = kPi * g.commonsRadius * g.commonsRadius; const int wantedSand = int(std::lround(o.sand * commonsArea / 8192.0)); for (int attempt = 0; attempt < 400 && int(f.commonsSand.size()) < wantedSand; ++attempt) { - const double a = context.bounded("city-sand", 3600) / 3600.0 * 2 * pi; + const double a = context.bounded("city-sand", 3600) / 3600.0 * 2 * kPi; const double r0 = 0.45 * g.commonsRadius + context.bounded("city-sand", 1000) / 1000.0 * (0.55 * g.commonsRadius - 12); RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 5.0, 0.5, context, "city-sand"); const double reach = shape.maximumRadius(); - const double turn = std::fmod(a - L.phase + 4 * pi, 2 * pi); + const double turn = std::fmod(a - L.phase + 4 * kPi, 2 * kPi); const double u = turn / wedge - std::floor(turn / wedge); if (r0 * std::abs(u - 0.5) * wedge < reach + g.causeway + 4 && r0 > g.commonsRadius - kLanding - reach - 4) @@ -459,22 +450,20 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const const double roadHalf = g.causeway / 2.0; const double stripHalf = roadHalf + kShoulder; std::vector landingAlong(teams, -1.0), shoreAlong(teams, 1e9); + const WedgeFrame frame(t, L.phase, teams); for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) { const int i = y * t.w + x; - const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); - const double d = std::hypot(double(dx), double(dy)); - const double theta = std::atan2(double(dy), double(dx)); + // The wedge frame: which home, how far across it and how far along the arc from its middle, + // with the channels between homes bowed by this roll's bend. + WedgeFrame::Cell cell = frame.cell(x, y); + if (teams >= 2) + frame.bend(cell, coasts.bendAt(cell.d)); + const double d = cell.d, theta = cell.theta, u = cell.u, s = cell.s; + const int k = cell.k; L.radius[i] = d; L.angle[i] = theta; - // The wedge frame: which home, how far across it and how far along the arc from its middle. - double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); - if (teams >= 2) - turn = std::fmod(turn - coasts.bendAt(d) / std::max(1.0, d) + 4 * pi, 2 * pi); - const int k = std::min(teams - 1, int(turn / wedge)); - const double u = turn / wedge - k; - const double s = d * (u - 0.5) * wedge; const double toBoundary = teams < 2 ? 1e9 : d * std::min(u, 1 - u) * wedge; const double toLanding = d * std::abs(u - 0.5) * wedge; const double c = coasts.coastAt(u, d); @@ -657,8 +646,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const CityStatesOptions o(request); L.cx = t.w / 2; L.cy = t.h / 2; - L.phase = context.bounded("city-layout", 3600) / 3600.0 * 2 * pi; - const double wedge = 2 * pi / teams; + L.phase = context.bounded("city-layout", 3600) / 3600.0 * 2 * kPi; + const double wedge = 2 * kPi / teams; const double depth = g.depth(); // What this map is made of. @@ -697,27 +686,6 @@ std::vector heartTiles(const Map &map, const Layout &L) return heart; } -// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), -// so every land corner beside water becomes sand. Unlike Map::controlSand() this reads only the -// original terrain, so the result doesn't depend on scan order and water is never eaten away. -void layBeaches(std::vector &terrain, const Torus &t) -{ - const std::vector original(terrain); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - const int i = y * t.w + x; - if (original[i] == WATER) - continue; - bool shore = false; - for (int dy = -1; dy <= 1 && !shore; ++dy) - for (int dx = -1; dx <= 1 && !shore; ++dx) - shore = original[t.at(x + dx, y + dy)] == WATER; - if (shore) - terrain[i] = SAND; - } -} - // The ground a unit landing from the sea can reach without crossing solid grass: every land tile // with a sea vertex in the box its four corners touch, and every beach joined to those - a lake's // beach too, where it meets the sea's. Stone cannot stand on any of it. @@ -822,7 +790,7 @@ void carveValleys(std::vector &terrain, const Layout &L, Generati continue; const int x = at % t.w, y = at / t.w; const double stretch = 1.2 + context.bounded("city-valleys", 81) / 100.0; - const double turn = context.bounded("city-valleys", 3600) / 3600.0 * pi; + const double turn = context.bounded("city-valleys", 3600) / 3600.0 * kPi; const RadialShape shape((3 + context.bounded("city-valleys", 4)) * scale, 0.35, context, "city-valleys"); const double reach = shape.maximumRadius() * stretch; @@ -850,106 +818,6 @@ void carveValleys(std::vector &terrain, const Layout &L, Generati } } -struct Island -{ - int x, y; - std::vector tiles; -}; - -// Small islands out in the open sea, each well clear of every coast and of each other, so they are -// only ever reached by swimming. Counted per 128x128 of sea. -std::vector raiseIslands(std::vector &terrain, const Layout &L, - GenerationContext &context, int perStandardMap) -{ - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector islands; - if (perStandardMap <= 0) - return islands; - std::vector water(n), land(n); - std::vector sea; - for (int i = 0; i < n; ++i) - { - water[i] = terrain[i] == WATER; - land[i] = !water[i]; - if (water[i]) - sea.push_back(i); - } - if (sea.empty()) - return islands; - const std::vector offshore = stepsFrom(t, land, water); - const int wanted = int(std::lround(perStandardMap * double(sea.size()) / 16384.0)); - const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 1.0, 1.6); - for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 60; ++attempt) - { - const int at = sea[context.bounded("city-islands", sea.size())]; - const int x = at % t.w, y = at / t.w; - const RadialShape shape((4 + context.bounded("city-islands", 3)) * scale, 0.3, context, - "city-islands"); - const double reach = shape.maximumRadius(); - if (offshore[at] < reach + kLakeShore + 1) - continue; - bool open = true; - for (const Island &other : islands) - { - const double gap = 2 * reach + kLakeShore + 1; - const double dx = t.offsetX(x, other.x), dy = t.offsetY(y, other.y); - open = open && dx * dx + dy * dy >= gap * gap; - } - if (!open) - continue; - Island island{x, y, {}}; - const int r = int(std::ceil(reach)); - for (int dy = -r; dy <= r; ++dy) - for (int dx = -r; dx <= r; ++dx) - if (std::hypot(double(dx), double(dy)) < - shape.radiusAt(std::atan2(double(dy), double(dx)))) - { - const int i = t.at(x + dx, y + dy); - terrain[i] = GRASS; - island.tiles.push_back(i); - } - islands.push_back(std::move(island)); - } - return islands; -} - -// Grows a compact patch of one resource outward from a seed tile, breadth-first over the four -// cardinal neighbours, onto tiles the predicate allows. Returns how many tiles it placed. -template -int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) -{ - std::vector queued(size_t(t.w) * t.h, 0); - std::vector frontier{seed}; - queued[seed] = 1; - int placed = 0; - const auto &steps = kCardinalSteps; - for (size_t head = 0; head < frontier.size() && placed < count; ++head) - { - const int i = frontier[head], x = i % t.w, y = i / t.w; - if (!eligible(i) || !map.isResourceAllowed(x, y, type)) - continue; - map.setResource(x, y, type, 1); - ++placed; - for (const auto &step : steps) - { - const int n = t.at(x + step[0], y + step[1]); - if (!queued[n]) - { - queued[n] = 1; - frontier.push_back(n); - } - } - } - return placed; -} - -bool clearGround(const Map &map, int x, int y) -{ - return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID; -} - // Splits a set of tiles into wheat and wood by an unrelated noise field, so the two crops form // separate patches rather than rings sorted by fertility. void plantFields(Map &map, const Torus &t, std::vector tiles, int wheat, int wood, @@ -974,11 +842,8 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C { const Torus &t = L.t; const int n = t.w * t.h; - std::vector reserved(n, 0), water(n), dry(n); - for (int team = 0; team < L.g.teams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + const std::vector reserved = swarmSurroundings(t, context); + std::vector water(n), dry(n); for (int i = 0; i < n; ++i) { water[i] = map.isWater(i % t.w, i / t.w); @@ -1179,10 +1044,10 @@ void stockCommons(Map &map, const Layout &L, GenerationContext &context, const C : L.heartKind == Crag ? 0.6 * L.ringR : L.heartKind == ForestHeart ? L.lakeR + 3 : L.lakeR + kOrchardReach; - const double spin = context.bounded("city-fruit", 3600) / 3600.0 * 2 * pi; + const double spin = context.bounded("city-fruit", 3600) / 3600.0 * 2 * kPi; for (int f = 0; f < 3; ++f) { - const double a = spin + 2 * pi * f / 3; + const double a = spin + 2 * kPi * f / 3; const int ax = L.cx + int(std::lround(rho * std::cos(a))), ay = L.cy + int(std::lround(rho * std::sin(a))); int seed = -1, nearest = INT_MAX; @@ -1208,74 +1073,6 @@ void stockCommons(Map &map, const Layout &L, GenerationContext &context, const C map.setResource(i % t.w, i / t.w, WOOD, 1); } -// Each island carries one themed prize, as on Ring world. -void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) -{ - const int width = map.getW(); - for (const Island &island : islands) - { - std::vector grass; - for (int i : island.tiles) - if (map.isGrass(i % width, i / width)) - grass.emplace_back(i % width, i / width); - if (grass.empty()) - continue; - MapGeneratorPoint centre(island.x, island.y); - if (!map.isGrass(centre.x, centre.y)) - centre = grass[context.bounded("city-islands", grass.size())]; - switch (context.bounded("city-islands", 3)) - { - case 0: - placeResourceClump(map, context, centre, STONE, 2); - break; - case 1: - placeResourceClump(map, context, centre, - CHERRY + int(context.bounded("city-islands", 3)), 2); - break; - default: - placeResourceClump(map, context, centre, CORN, 2); - break; - } - } -} - -// Algae in the shallows along every coast, the strait's, the sea's and the lakes' alike. -void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) -{ - const int n = t.w * t.h; - std::vector water(n), dry(n); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - water[y * t.w + x] = map.isWater(x, y); - dry[y * t.w + x] = !map.isWater(x, y); - } - const std::vector offshore = stepsFrom(t, dry, water); - std::vector shallows; - for (int i = 0; i < n; ++i) - if (offshore[i] >= 2 && offshore[i] <= 4) - shallows.emplace_back(i % t.w, i / t.w); - if (shallows.empty()) - return; - for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) - placeResourceClump(map, context, shallows[context.bounded("city-algae", shallows.size())], - ALGA, 1); -} - -// Clears the ring the swarm's workers walk out through; a causeway's stone beside it stays. -void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t, - const std::vector &line) -{ - for (int team = 0; team < context.request.nbTeams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - { - const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); - if (map.isResource(x, y) && !line[y * t.w + x]) - map.setNoResource(x, y, 1); - } -} - // Causeways and their approaches hold nothing but the causeways' own stone lines. void clearRoads(Map &map, const Layout &L, const std::vector &line) { @@ -1285,63 +1082,6 @@ void clearRoads(Map &map, const Layout &L, const std::vector &lin map.setNoResource(i % t.w, i / t.w, 1); } -// The cheapest walk from any source tile to any goal tile (deposits cost one, open ground nothing; -// water, buildings and the stone lines are impassable), with only the deposits on it cleared. -bool openRoad(Map &map, const Layout &L, const std::vector &line, - const std::vector &sources, const std::vector &goal) -{ - const Torus &t = L.t; - const int n = t.w * t.h; - std::vector cost(n, INT_MAX), parent(n, -1); - std::vector done(n, 0); - std::deque queue; - for (int i : sources) - { - cost[i] = 0; - queue.push_back(i); - } - int reached = -1; - while (!queue.empty() && reached < 0) - { - const int i = queue.front(); - queue.pop_front(); - if (done[i]) - continue; - done[i] = 1; - if (goal[i]) - { - reached = i; - break; - } - const int x = i % t.w, y = i / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (!dx && !dy) - continue; - const int nx = t.x(x + dx), ny = t.y(y + dy), next = ny * t.w + nx; - if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID || line[next]) - continue; - const int nextCost = cost[i] + (map.isResource(nx, ny) ? 1 : 0); - if (nextCost < cost[next]) - { - cost[next] = nextCost; - parent[next] = i; - if (nextCost == cost[i]) - queue.push_front(next); - else - queue.push_back(next); - } - } - } - if (reached < 0) - return false; - for (int i = reached; i >= 0; i = parent[i]) - if (map.isResource(i % t.w, i / t.w)) - map.setNoResource(i % t.w, i / t.w, 1); - return true; -} - // Deposits may land anywhere, and a band of them could close a home's swarm off from its causeway // or a landing off from the heart. Keep both walks open, clearing only what stands on the cheapest // one. Almost always nothing is in the way. @@ -1351,23 +1091,17 @@ bool openRoads(Map &map, const Layout &L, const std::vector &line const Torus &t = L.t; const int n = t.w * t.h; const std::vector heart = heartTiles(map, L); + const auto workers = unitTilesByTeam(map, L.g.teams); for (int k = 0; k < L.g.teams; ++k) { - std::vector workers; - for (int i = 0; i < n; ++i) - { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == k) - workers.push_back(i); - } std::vector shore(n, 0); shore[L.homes[k].shore] = 1; - if (!openRoad(map, L, line, workers, shore)) + if (!openRoad(map, t, workers[k], shore, &line)) { context.detail = "colony " + std::to_string(k) + " has no way to its causeway"; return false; } - if (!openRoad(map, L, line, {L.homes[k].landing}, heart)) + if (!openRoad(map, t, {L.homes[k].landing}, heart, &line)) { context.detail = "causeway " + std::to_string(k) + " has no way into the commons"; return false; @@ -1395,7 +1129,7 @@ bool generate(Game &game, GenerationContext &context) const int n = t.w * t.h; context.stage = "city terrain"; - std::vector terrain(n, GRASS); + TerrainSketch terrain(n, GRASS); for (int i = 0; i < n; ++i) { if (L.region[i] == Sea || L.lake[i] || L.river[i]) @@ -1406,48 +1140,54 @@ bool generate(Game &game, GenerationContext &context) terrain[i] = SAND; } carveValleys(terrain, L, context, o.valleys); - const std::vector islands = raiseIslands(terrain, L, context, o.resourceIslands); + // Resource islands are counted per 128x128 of sea. + const std::vector islands = raiseIslands( + terrain, t, context, + {"city-islands", + int(std::lround(o.resourceIslands * double(countTiles(terrain, WATER)) / 16384.0)), 60, + kLakeShore + 1, /*gapFromBothReaches=*/false}); layBeaches(terrain, t); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); - map.rebuildTerrain(); + writeUndermap(map, terrain); const std::vector line = stoneTiles(map, L); for (int i = 0; i < n; ++i) if (line[i]) map.setResource(i % t.w, i / t.w, STONE, 1); context.stage = "city colonies"; - for (int team = 0; team < teams; ++team) + const auto home = [&](int team) { - const Home &h = L.homes[team]; - std::vector home(n, 0); + std::vector ground(n, 0); for (int i = 0; i < n; ++i) - home[i] = + ground[i] = L.homeOf[i] == team && !L.strip[i] && !L.clear[i] && map.isGrass(i % t.w, i / t.w); - // The swarm stands between the causeway's home end and the lake, a short walk from the lake's - // fields whatever the home's depth; placeSettlement measures from the footprint's top-left - // tile, so the anchor is offset to centre the 4x4 there. + return ground; + }; + // The swarm stands between the causeway's home end and the lake, a short walk from the lake's + // fields whatever the home's depth; placeSettlement measures from the footprint's top-left + // tile, so the anchor is offset to centre the 4x4 there. + const auto anchor = [&](int team) + { + const Home &h = L.homes[team]; const double lakeRadius = std::clamp(0.1 * L.g.depth(), 2.0, 6.0); const double rho = std::max(h.coast + L.g.strait + 8.0, h.coast + L.g.strait + lakeOffset(L.g, lakeRadius) - lakeRadius - 9.0); - const MapGeneratorPoint anchor(L.cx + int(std::lround(rho * std::cos(h.angle))) - 2, - L.cy + int(std::lround(rho * std::sin(h.angle))) - 2); - if (!placeSettlement(game, context, team, home, anchor, "city-starts")) - return false; - } + return MapGeneratorPoint(L.cx + int(std::lround(rho * std::cos(h.angle))) - 2, + L.cy + int(std::lround(rho * std::sin(h.angle))) - 2); + }; + if (!settleColonies(game, context, "city-starts", home, anchor)) + return false; context.stage = "city resources"; furnishHomes(map, L, context, o); stockCommons(map, L, context, o); - stockIslands(map, context, islands); - seedAlgae(map, context, t, o.algae); - clearAroundSwarms(map, context, t, line); + stockIslands(map, context, islands, "city-islands"); + seedAlgae(map, context, t, "city-algae", o.algae, AlgaeBand::shallows(2, 4)); + clearAroundSwarms(map, context, t, &line); // The kits already put wheat and wood a short walk from every swarm; this is only the backstop, // and the walls and the causeways' stone are designed and must never be cleared. guaranteeStartingResources(game, context, 24, 32, 0, &line); - clearAroundSwarms(map, context, t, line); + clearAroundSwarms(map, context, t, &line); clearRoads(map, L, line); context.stage = "city roads"; return openRoads(map, L, line, context); @@ -1474,13 +1214,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { GenerationContext replay(context.request); const Layout L = design(context.request, replay); - if (!L.failure.empty()) - return "The city design could not be rebuilt: " + L.failure; const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "city"); !mismatch.empty()) + return mismatch; const Torus &t = L.t; const int n = t.w * t.h, teams = context.request.nbTeams; - if (map.getW() != t.w || map.getH() != t.h) - return "The city design does not match the map size."; const auto where = [&](int i) { return "(" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")"; }; const auto walkable = [&](int i) @@ -1498,15 +1236,12 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (L.ford[i] && !walkable(i)) return "The ford at " + where(i) + " is blocked."; } - const auto workers = unitTilesByTeam(map, teams); - if (teams < 1 || workers[0].empty()) - return "Colony 0 has no workers to walk the map."; - std::vector open(n, 0), source(n, 0); - for (int i = 0; i < n; ++i) - open[i] = walkable(i); - const std::vector fromFirst = stepsFrom(t, tileMask(t, workers[0]), open); - if (const int cut = firstColonyCutOff(fromFirst, workers); cut >= 0) - return "Colony " + std::to_string(cut) + " cannot walk to colony 0."; + const ColonyWalk walk = walkFromFirstColony(map, teams, "the map", ""); + if (!walk.error.empty()) + return walk.error; + const std::vector> &workers = walk.workers; + const std::vector &fromFirst = walk.steps; + const std::vector open = walkableTiles(map); const std::vector heart = heartTiles(map, L); bool heartReached = false; for (int i = 0; i < n && !heartReached; ++i) diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.cpp b/src/map/generator/generators/ConcreteIslandsGenerator.cpp index 7f04e729c..bd5d26f59 100644 --- a/src/map/generator/generators/ConcreteIslandsGenerator.cpp +++ b/src/map/generator/generators/ConcreteIslandsGenerator.cpp @@ -8,6 +8,7 @@ #include "GlobalContainer.h" #include "HeightMap.h" #include "Map.h" +#include "Pipeline.h" #include "Regions.h" #include "Resources.h" #include "StartingPositions.h" @@ -178,15 +179,9 @@ static bool generate(Game &game, GenerationContext &context) {options.wheat, options.wood, options.stone})) return false; // A colony's own fields are its only starting wheat and wood, and a field or deposit grown well - // past its default size can also wall the colony in with nowhere left to build. At any amount - // other than the default, open up such a colony and then make sure each still has both crops - // within reach, in case the clearing took the nearest one along with the wall. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100 || options.fruit != 100) - { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32); - } + // past its default size can also wall the colony in with nowhere left to build. + reopenCrampedStarts(game, context, {options.wheat, options.wood, options.stone, options.algae, + options.fruit}); // Initialize final team info for (int i = 0; i < context.request.nbTeams; ++i) diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp index 421137eae..94611d188 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.cpp +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -5,6 +5,7 @@ #include "GenerationContext.h" #include "Geometry.h" #include "GlobalContainer.h" +#include "Grid.h" #include "Regions.h" #include "Resources.h" #include "Settlements.h" @@ -16,16 +17,6 @@ using namespace MapGeneration; namespace { -constexpr double pi = 3.14159265358979323846; -int wrapDelta(int a, int b, int size) -{ - int d = a - b; - if (d > size / 2) - d -= size; - else if (d < -size / 2) - d += size; - return d; -} void createJaggedIsland(Map &map, GenerationContext &context, std::vector &grid, int area, int x, int y, int radius, double roughness) { @@ -167,7 +158,7 @@ static void sizeIslands(Game &game, GenerationContext &context, L.areaNumber += 1; for (unsigned int b = 0; b < bridgeAngles.size() && options.moatBridges; ++b) { - double theta = bridgeAngles[b] * pi / 180.0; + double theta = bridgeAngles[b] * kPi / 180.0; for (int r = L.commonsRadius - 2; r <= L.commonsRadius + L.moatWidth + 2; ++r) { int cx = L.commonsCenter.x + (int)round(r * cos(theta)); @@ -228,9 +219,7 @@ static void paintTerrain(Game &game, GenerationContext &context, { if (L.grid[y * W + x] == L.commonsAreaNumber || L.grid[y * W + x] == L.bridgeAreaNumber) continue; - int dx = wrapDelta(x, L.commonsCenter.x, W); - int dy = wrapDelta(y, L.commonsCenter.y, H); - double r = sqrt((double)(dx * dx + dy * dy)); + double r = sqrt((double)Torus{W, H}.dist2(x, y, L.commonsCenter.x, L.commonsCenter.y)); if (r >= L.commonsRadius - 1 && r < L.commonsRadius + L.moatWidth) { game.map.setUMatPos(x, y, WATER, 1); diff --git a/src/map/generator/generators/EvergladesGenerator.cpp b/src/map/generator/generators/EvergladesGenerator.cpp index 5e19b523c..192746b76 100644 --- a/src/map/generator/generators/EvergladesGenerator.cpp +++ b/src/map/generator/generators/EvergladesGenerator.cpp @@ -4,9 +4,13 @@ #include "Game.h" #include "GenerationContext.h" #include "Geometry.h" +#include "Grid.h" #include "HeightMap.h" +#include "Pipeline.h" +#include "Planting.h" #include "Resources.h" #include "Settlements.h" +#include "Sketch.h" #include "Unit.h" #include #include @@ -36,8 +40,6 @@ using namespace MapGeneration; namespace { -constexpr double pi = 3.14159265358979323846; - // The clearings sit near a ring at this share of the half side, evenly spaced from a random start, // each nudged by up to these shares of a wedge and of the ring's radius. constexpr double kHomeRing = 0.58; @@ -60,7 +62,6 @@ constexpr int kLeveeSectors = 36; // Every home starts identical: wheat and wood beside its pond, a quarry, all unscaled. constexpr int kHomeWheat = 40; constexpr int kHomeWood = 30; -constexpr int kSwarmClearance = 2; // The kit keeps this far from the swarm beyond the clearance, so there is room to build beside it. constexpr int kBuildRoom = 4; // The swamp's standing wood and wheat, as shares of the grass off the clearings, at 100. @@ -79,53 +80,6 @@ constexpr double kMaximumCoverage = 0.6; // will be cleared, onto stone or fruit, and onto water that becomes a ford. constexpr int kStepOpen = 1, kStepDeposit = 3, kStepEternal = 8, kStepFord = 25; -struct Torus -{ - int w, h; - int x(int v) const { return ((v % w) + w) % w; } - int y(int v) const { return ((v % h) + h) % h; } - int at(int px, int py) const { return y(py) * w + x(px); } - int offsetX(int from, int to) const - { - const int d = x(to - from); - return d > w / 2 ? d - w : d; - } - int offsetY(int from, int to) const - { - const int d = y(to - from); - return d > h / 2 ? d - h : d; - } -}; - -std::vector stepsFrom(const Torus &t, const std::vector &source, - const std::vector &open) -{ - std::vector dist(size_t(t.w) * t.h, -1); - std::vector queue; - queue.reserve(dist.size()); - for (size_t i = 0; i < dist.size(); ++i) - if (source[i]) - { - dist[i] = 0; - queue.push_back(int(i)); - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % t.w, y = queue[head] / t.w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int n = t.at(x + dx, y + dy); - if (dist[n] < 0 && open[n]) - { - dist[n] = dist[queue[head]] + 1; - queue.push_back(n); - } - } - } - return dist; -} - struct Geometry { int teams, half, clearing, buildRoom; @@ -141,7 +95,7 @@ double expectedCoverage(const Geometry &g) { const double meanScale = (kDryScale + kWetScale) / 2; const double radius2 = g.poolRadius * g.poolRadius * meanScale * meanScale * 1.1; - const double pool = pi * radius2 * 1.2; + const double pool = kPi * radius2 * 1.2; const double slough = pool * kSloughGrowth * kSloughGrowth * kSloughStretch / 1.2; const double nominal = ((1 - g.sloughs) * pool + g.sloughs * slough) / (g.spacing * g.spacing); return 1 - std::exp(-nominal); @@ -152,7 +106,7 @@ double expectedCoverage(const Geometry &g) bool clearingsFit(const Geometry &g) { const double nearest = - 2 * g.homeRing * (1 - g.jitterRadius) * std::sin(pi * (1 - 2 * g.jitterTurn) / g.teams); + 2 * g.homeRing * (1 - g.jitterRadius) * std::sin(kPi * (1 - 2 * g.jitterTurn) / g.teams); const bool fitsRing = g.teams < 2 || nearest >= 2 * g.reach; const bool fitsWrap = g.homeRing * (1 + g.jitterRadius) + g.reach <= g.half - 1; return fitsRing && fitsWrap; @@ -257,8 +211,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { return context.bounded(stream, 2001) / 1000.0 - 1; }; // The homes: near a ring, evenly spaced from a random start, each nudged a little. - const double phase = context.bounded("glades-layout", 3600) / 3600.0 * 2 * pi; - const double wedge = 2 * pi / teams; + const double phase = context.bounded("glades-layout", 3600) / 3600.0 * 2 * kPi; + const double wedge = 2 * kPi / teams; for (int k = 0; k < teams; ++k) { const double a = phase + wedge * (k + 0.5 + g.jitterTurn * unit("glades-layout")); @@ -302,7 +256,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) RadialShape shape(radius, 0.45, context, "glades-pools"); const double aspect = slough ? kSloughStretch : 1 + context.bounded("glades-pools", 401) / 1000.0; - const double spin = context.bounded("glades-pools", 3600) / 3600.0 * pi; + const double spin = context.bounded("glades-pools", 3600) / 3600.0 * kPi; L.pools.push_back({px, py, aspect, spin, shape.maximumRadius() * aspect + 1, shape}); } @@ -339,7 +293,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) else if (d < edge + kLeveeWidth) { const int sector = - int(std::fmod(a + 2 * pi, 2 * pi) / (2 * pi) * kLeveeSectors) % + int(std::fmod(a + 2 * kPi, 2 * kPi) / (2 * kPi) * kLeveeSectors) % kLeveeSectors; L.water[i] = 0; L.sand[i] = home.standing[sector]; @@ -353,89 +307,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) return L; } -// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), -// so every land corner beside water becomes sand. -void layBeaches(std::vector &terrain, const Torus &t) -{ - const std::vector original(terrain); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - const int i = y * t.w + x; - if (original[i] == WATER) - continue; - bool shore = false; - for (int dy = -1; dy <= 1 && !shore; ++dy) - for (int dx = -1; dx <= 1 && !shore; ++dx) - shore = original[t.at(x + dx, y + dy)] == WATER; - if (shore) - terrain[i] = SAND; - } -} - -template -int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) -{ - std::vector queued(size_t(t.w) * t.h, 0); - std::vector frontier{seed}; - queued[seed] = 1; - int placed = 0; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - for (size_t head = 0; head < frontier.size() && placed < count; ++head) - { - const int i = frontier[head], x = i % t.w, y = i / t.w; - if (!eligible(i) || !map.isResourceAllowed(x, y, type)) - continue; - map.setResource(x, y, type, 1); - ++placed; - for (const auto &step : steps) - { - const int m = t.at(x + step[0], y + step[1]); - if (!queued[m]) - { - queued[m] = 1; - frontier.push_back(m); - } - } - } - return placed; -} - -bool clearGround(const Map &map, int x, int y) -{ - return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID; -} - -template -int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) -{ - int seed = -1, nearest = INT_MAX; - for (int dy = -within; dy <= within; ++dy) - for (int dx = -within; dx <= within; ++dx) - { - const int i = t.at(ax + dx, ay + dy); - if (eligible(i) && dx * dx + dy * dy < nearest) - { - nearest = dx * dx + dy * dy; - seed = i; - } - } - return seed; -} - // Every home's kit, identical and unscaled: wheat and wood beside the pond on the sides, a quarry // on the near side towards the map's centre. The rest of the clearing starts clear. void furnishHomes(Map &map, const Layout &L, GenerationContext &context) { const Torus &t = L.t; - const int n = t.w * t.h; - std::vector reserved(n, 0); - const int room = kSwarmClearance + L.g.buildRoom; - for (int team = 0; team < L.g.teams; ++team) - for (int dy = -room; dy < 4 + room; ++dy) - for (int dx = -room; dx < 4 + room; ++dx) - reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + const std::vector reserved = + swarmSurroundings(t, context, kSwarmClearance + L.g.buildRoom); for (int team = 0; team < L.g.teams; ++team) { const Home &home = L.homes[team]; @@ -524,32 +402,6 @@ void stockSwamp(Map &map, const Layout &L, GenerationContext &context, const Eve } } -// Algae in the pools. -void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) -{ - std::vector water; - for (int i = 0; i < t.w * t.h; ++i) - if (map.isWater(i % t.w, i / t.w)) - water.emplace_back(i % t.w, i / t.w); - if (water.empty()) - return; - for (int clump = 0; clump < scaledCount(int(water.size()) / 40, algaePercent); ++clump) - placeResourceClump(map, context, water[context.bounded("glades-algae", water.size())], ALGA, - 1); -} - -void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t) -{ - for (int team = 0; team < context.request.nbTeams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - { - const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); - if (map.isResource(x, y)) - map.setNoResource(x, y, 1); - } -} - // Every colony must be able to walk to colony 0 at the start. Where the swamp or the sloughs box // one in, the cheapest way through is opened: deposits on it are cleared and water on it becomes a // sand ford. The map may look odd there; it does not fail. @@ -594,7 +446,7 @@ void openRoutes(Game &game, const Layout &L, GenerationContext &context) } return tiles; }; - static const int steps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + const auto &steps = kCardinalSteps; bool changed = false; for (int team = 1; team < teams; ++team) { @@ -689,7 +541,7 @@ bool generate(Game &game, GenerationContext &context) const int n = t.w * t.h; context.stage = "glades terrain"; - std::vector terrain(n, GRASS); + TerrainSketch terrain(n, GRASS); for (int i = 0; i < n; ++i) { if (L.water[i]) @@ -698,33 +550,34 @@ bool generate(Game &game, GenerationContext &context) terrain[i] = SAND; } layBeaches(terrain, t); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); - map.rebuildTerrain(); + writeUndermap(map, terrain); context.stage = "glades colonies"; - for (int team = 0; team < teams; ++team) + const auto clearing = [&](int team) { - const Home &home = L.homes[team]; std::vector ground(n, 0); for (int i = 0; i < n; ++i) ground[i] = L.clearingOf[i] == team && map.isGrass(i % t.w, i / t.w); - // The swarm stands just past the pond's beach on the far side from the map's centre, leaving - // the rest of that side to build on; in a clearing too small for a pond it takes the middle. + return ground; + }; + // The swarm stands just past the pond's beach on the far side from the map's centre, leaving + // the rest of that side to build on; in a clearing too small for a pond it takes the middle. + const auto anchor = [&](int team) + { + const Home &home = L.homes[team]; const double a = std::atan2(double(t.offsetY(L.cy, home.y)), double(t.offsetX(L.cx, home.x))); const double back = L.g.pond > 0 ? L.g.pond * 1.3 + 4 : 0; - const MapGeneratorPoint anchor(home.x + int(std::lround(back * std::cos(a))) - 2, - home.y + int(std::lround(back * std::sin(a))) - 2); - if (!placeSettlement(game, context, team, ground, anchor, "glades-starts")) - return false; - } + return MapGeneratorPoint(home.x + int(std::lround(back * std::cos(a))) - 2, + home.y + int(std::lround(back * std::sin(a))) - 2); + }; + if (!settleColonies(game, context, "glades-starts", clearing, anchor)) + return false; context.stage = "glades resources"; furnishHomes(map, L, context); stockSwamp(map, L, context, o); - seedAlgae(map, context, t, o.algae); + seedAlgae(map, context, t, "glades-algae", o.algae, AlgaeBand::anyWater()); clearAroundSwarms(map, context, t); guaranteeStartingResources(game, context, 24, 32, 0); clearAroundSwarms(map, context, t); @@ -749,13 +602,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { GenerationContext replay(context.request); const Layout L = design(context.request, replay); - if (!L.failure.empty()) - return "The glades design could not be rebuilt: " + L.failure; const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "glades"); !mismatch.empty()) + return mismatch; const Torus &t = L.t; const int n = t.w * t.h, teams = context.request.nbTeams; - if (map.getW() != t.w || map.getH() != t.h) - return "The glades design does not match the map size."; for (int k = 0; k < teams && L.g.pond > 0; ++k) { bool pond = false; @@ -769,34 +620,7 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (L.sand[i] && map.isResource(i % t.w, i / t.w)) return "A deposit stands on the levee at (" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")."; - std::vector> workers(std::max(teams, 1)); - for (int i = 0; i < n; ++i) - { - const Uint16 gid = map.getGroundUnit(i % t.w, i / t.w); - if (gid != NOGUID && Unit::GIDtoTeam(gid) < teams) - workers[Unit::GIDtoTeam(gid)].push_back(i); - } - if (teams < 1 || workers[0].empty()) - return "Colony 0 has no workers to walk the glades."; - std::vector open(n, 0), source(n, 0); - for (int i = 0; i < n; ++i) - { - const int x = i % t.w, y = i / t.w; - open[i] = !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - } - for (int i : workers[0]) - source[i] = 1; - const std::vector steps = stepsFrom(t, source, open); - for (int team = 1; team < teams; ++team) - { - bool arrived = false; - for (int i : workers[team]) - arrived = arrived || steps[i] >= 0; - if (!arrived) - return "Colony " + std::to_string(team) + - " cannot walk to colony 0 through the glades."; - } - return ""; + return walkFromFirstColony(map, teams, "the glades", "through the glades").error; } } // namespace diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 31ae99043..4e5f55ca6 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -5,6 +5,7 @@ #include "GenerationContext.h" #include "Geometry.h" #include "GlobalContainer.h" +#include "Pipeline.h" #include "Regions.h" #include "Resources.h" #include "Settlements.h" @@ -16,10 +17,9 @@ using namespace MapGeneration; namespace { -constexpr double pi = 3.14159265358979323846; double randomAngle(GenerationContext &context) { - return context.bounded("layout", 3600) / 3600.0 * 2 * pi; + return context.bounded("layout", 3600) / 3600.0 * 2 * kPi; } void createJaggedIsland(Map &map, GenerationContext &context, std::vector &grid, int area, int x, int y, int radius, double roughness) @@ -120,7 +120,7 @@ FjordLayout computeLayout(Game &game, GenerationContext &context, std::vector teamTheta(nbTeams); { double baseRotation = randomAngle(context); - double slot = 2 * pi / nbTeams; + double slot = 2 * kPi / nbTeams; for (int i = 0; i < nbTeams; ++i) { double jitter = ((context.bounded("layout", 2001)) / 1000.0 - 1.0) * 0.2 * slot; @@ -169,9 +169,9 @@ std::vector> carveFjords(Game &game, GenerationCo int j = (k + 1) % layout.nbTeams; double ti = layout.teamTheta[i]; double tj = layout.teamTheta[j]; - double d = fmod(tj - ti, 2 * pi); + double d = fmod(tj - ti, 2 * kPi); if (d < 0) - d += 2 * pi; + d += 2 * kPi; double midTheta = ti + d / 2.0; double mouthR = layout.coast.radiusAt(midTheta) + 3.0; @@ -179,7 +179,7 @@ std::vector> carveFjords(Game &game, GenerationCo double tipU = layout.fjordInnerR * cos(midTheta), tipV = layout.fjordInnerR * sin(midTheta); double perpU = -sin(midTheta), perpV = cos(midTheta); - double gap = std::min(d, 2 * pi - d); + double gap = std::min(d, 2 * kPi - d); double amplitude = gap * mouthR * 0.17; double phase = randomAngle(context); double mouthWidth = options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; @@ -198,8 +198,8 @@ std::vector> carveFjords(Game &game, GenerationCo double t = double(s) / steps; double baseU = mouthU + t * (tipU - mouthU); double baseV = mouthV + t * (tipV - mouthV); - double envelope = sin(pi * t); - double lateral = amplitude * sin(2 * pi * t + phase) * envelope; + double envelope = sin(kPi * t); + double lateral = amplitude * sin(2 * kPi * t + phase) * envelope; double u = baseU + lateral * perpU; double v = baseV + lateral * perpV; double width = mouthWidth * (1 - t) + tipWidth * t; @@ -731,20 +731,10 @@ static bool generate(Game &game, GenerationContext &context) // sealing it before topping up whichever resource is still out of range - the same backstop // RuggedArchipelago and ShatteredCoast already rely on for the same class of problem. guaranteeStartingResources(game, context, 24, 32); - // The scatter and bank clumps above are sized by the resource amounts, and the ambient scatter - // covers up to two thirds of the continent's grass at the top of their range. That can leave a - // colony walled into its own clearing with nowhere to build, which the guarantee above doesn't - // address: a colony buried in wheat has wheat at its feet, so it counts as served. At any - // non-default amount, open such a colony back up and re-run the guarantee in case the clearing - // took its nearest crop with the wall. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100 || options.fruit != 100) - { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32); - } - + // covers up to two thirds of the continent's grass at the top of their range. + reopenCrampedStarts(game, context, {options.wheat, options.wood, options.stone, options.algae, + options.fruit}); return true; } diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index cde293179..d76aeba5b 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -8,6 +8,7 @@ #include "GlobalContainer.h" #include "HeightMap.h" #include "Map.h" +#include "Pipeline.h" #include "Regions.h" #include "Resources.h" #include "StartingPositions.h" @@ -259,14 +260,8 @@ static bool generate(Game &game, GenerationContext &context) return false; } // A colony's own fields are its only wheat and wood, and a field or deposit grown well past its - // default size can also wall the colony in with nowhere left to build. At any amount other than - // the default, open up such a colony and then make sure each still has both crops within reach, - // in case the clearing took the nearest one along with the wall. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || options.algae != 100) - { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32); - } + // default size can also wall the colony in with nowhere left to build. + reopenCrampedStarts(game, context, {options.wheat, options.wood, options.stone, options.algae}); // Initialize final team info for (int i = 0; i < context.request.nbTeams; ++i) diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 0db09367f..94d6749dc 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -3,6 +3,7 @@ #include "Game.h" #include "GenerationContext.h" #include "Grid.h" +#include "Pipeline.h" #include "Resources.h" #include "Settlements.h" #include "Unit.h" @@ -680,7 +681,7 @@ bool generate(Game &game, GenerationContext &context) map.setResource(nx, ny, STONE, 1); } - for (int team = 0; team < teams; ++team) + const auto chamber = [&](int team) { const int cell = homes[team], c = g.column(cell), r = g.row(cell); std::vector home(size_t(map.getW()) * map.getH(), 0); @@ -688,11 +689,13 @@ bool generate(Game &game, GenerationContext &context) for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) if (!map.isWater(x, y)) home[size_t(y) * map.getW() + x] = 1; - // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. - if (!placeSettlement(game, context, team, home, {g.centerX(cell) - 2, g.centerY(cell) - 2}, - "starts")) - return false; - } + return home; + }; + // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. + const auto centre = [&](int team) + { return MapGeneratorPoint(g.centerX(homes[team]) - 2, g.centerY(homes[team]) - 2); }; + if (!settleColonies(game, context, "starts", chamber, centre)) + return false; context.stage = "maze resources"; for (int home : homes) @@ -721,15 +724,8 @@ bool generate(Game &game, GenerationContext &context) // spines) block the flood; units don't, since they move. std::string validateWorld(const Game &game, const GenerationContext &context) { - const Map &map = game.map; - const int teams = context.request.nbTeams; - const Torus t(map); - const auto units = unitTilesByTeam(map, teams); - const std::vector reached = - stepsFrom(t, tileMask(t, teams > 0 ? units[0] : std::vector{}), walkableTiles(map)); - if (const int cut = firstColonyCutOff(reached, units, 0); cut >= 0) - return "Colony " + std::to_string(cut) + " cannot walk to colony 0 through the maze."; - return ""; + return walkFromFirstColony(game.map, context.request.nbTeams, "the maze", "through the maze") + .error; } } // namespace diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index 19493aff5..029aa6067 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -4,8 +4,11 @@ #include "GenerationContext.h" #include "Grid.h" #include "Geometry.h" +#include "Pipeline.h" +#include "Planting.h" #include "Resources.h" #include "Settlements.h" +#include "Sketch.h" #include "Unit.h" #include #include @@ -30,8 +33,6 @@ using namespace MapGeneration; namespace { -constexpr double pi = 3.14159265358979323846; - // Belt tiles along the ring each colony needs; validateRequest rejects crowding below this. constexpr int kBeltPerColony = 24; // A dry spine this many tiles either side of the centre line is never coast or lake, so the belt @@ -55,7 +56,6 @@ constexpr int kHomeShore = 6; constexpr int kHomeWheat = 16; constexpr int kHomeWood = 16; constexpr int kHomeStone = 10; -constexpr int kSwarmClearance = 2; constexpr int kUnreached = INT_MIN; @@ -82,14 +82,6 @@ Axes axesFor(int width, int height) return {width, height, width >= height}; } -int wrapSquare(int width, int height, int ax, int ay, int bx, int by) -{ - int dx = std::abs(ax - bx), dy = std::abs(ay - by); - dx = std::min(dx, width - dx); - dy = std::min(dy, height - dy); - return dx * dx + dy * dy; -} - // A smooth closed curve along the belt, one sample per tile. Its harmonics are whole numbers of // cycles per map length, so it meets itself exactly at the seam. Normalised to [-1, 1]; `slope` // receives its steepest step between neighbouring samples. @@ -100,14 +92,14 @@ std::vector closedCurve(int length, double falloff, std::mt19937 &rng, d for (int k = 0; k < harmonics; ++k) { amplitude[k] = (0.3 + 0.7 * unitDraw(rng)) / std::pow(k + 1.0, falloff); - phase[k] = 2 * pi * unitDraw(rng); + phase[k] = 2 * kPi * unitDraw(rng); } std::vector curve(length, 0.0); double peak = 0; for (int u = 0; u < length; ++u) { for (int k = 0; k < harmonics; ++k) - curve[u] += amplitude[k] * std::sin(2 * pi * (k + 1) * u / length + phase[k]); + curve[u] += amplitude[k] * std::sin(2 * kPi * (k + 1) * u / length + phase[k]); peak = std::max(peak, std::abs(curve[u])); } slope = 0; @@ -263,7 +255,7 @@ void carveLakes(std::vector &terrain, const Belt &belt, Generatio for (const Lake &other : lakes) { const double gap = reach + other.reach + kLakeGap; - clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; + clear = clear && Torus{width, height}.dist2(x, y, other.x, other.y) >= gap * gap; } if (!clear) continue; @@ -278,92 +270,6 @@ void carveLakes(std::vector &terrain, const Belt &belt, Generatio } } -struct Island -{ - int x, y; - double reach; - std::vector tiles; -}; - -// Small islands out in the open ocean, each well clear of the belt and of each other so they are -// only ever reached by swimming. The control counts islands per 128x128 of map. -std::vector raiseIslands(std::vector &terrain, int width, int height, - GenerationContext &context, int perStandardMap) -{ - std::vector islands; - if (perStandardMap <= 0) - return islands; - const size_t area = size_t(width) * height; - std::vector water(area), land(area); - std::vector sea; - for (size_t i = 0; i < area; ++i) - { - water[i] = terrain[i] == WATER; - land[i] = !water[i]; - if (water[i]) - sea.push_back(int(i)); - } - if (sea.empty()) - return islands; - const std::vector offshore = stepsFrom(Torus{width, height}, land, water); - const int wanted = std::max(1, int(std::lround(perStandardMap * double(area) / 16384.0))); - // Never smaller than on a 128-tile map: any smaller and the beach leaves no grass for a prize. - const double scale = std::clamp(std::sqrt(std::min(width, height) / 128.0), 1.0, 1.6); - for (int attempt = 0; int(islands.size()) < wanted && attempt < wanted * 40; ++attempt) - { - const int at = sea[context.bounded("islands", sea.size())]; - const int x = at % width, y = at / width; - const RadialShape shape((4 + context.bounded("islands", 3)) * scale, 0.3, context, - "islands"); - const double reach = shape.maximumRadius(); - if (offshore[at] < reach + kIslandMoat) - continue; - bool clear = true; - for (const Island &other : islands) - { - const double gap = reach + other.reach + kIslandMoat; - clear = clear && wrapSquare(width, height, x, y, other.x, other.y) >= gap * gap; - } - if (!clear) - continue; - Island island{x, y, reach, {}}; - const int r = int(std::ceil(reach)); - for (int dy = -r; dy <= r; ++dy) - for (int dx = -r; dx <= r; ++dx) - if (std::hypot(double(dx), double(dy)) < - shape.radiusAt(std::atan2(double(dy), double(dx)))) - { - const int i = (y + dy + height) % height * width + (x + dx + width) % width; - terrain[i] = GRASS; - island.tiles.push_back(i); - } - islands.push_back(std::move(island)); - } - return islands; -} - -// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), -// so every land corner beside water becomes sand. Unlike Map::controlSand() this reads only the -// original terrain, so the result doesn't depend on scan order and water is never eaten away. -void layBeaches(std::vector &terrain, int width, int height) -{ - const std::vector original(terrain); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - { - const size_t i = size_t(y) * width + x; - if (original[i] == WATER) - continue; - bool shore = false; - for (int dy = -1; dy <= 1 && !shore; ++dy) - for (int dx = -1; dx <= 1 && !shore; ++dx) - shore = original[size_t((y + dy + height) % height) * width + - (x + dx + width) % width] == WATER; - if (shore) - terrain[i] = SAND; - } -} - // A swarm anchored at (x, y) needs pure grass under its 4x4 footprint and dry, unoccupied ground in // the ring around it where its workers will stand. bool siteFits(const Map &map, int x, int y) @@ -435,7 +341,7 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, boo continue; bool crowded = false; for (const MapGeneratorPoint &other : placed) - crowded = crowded || wrapSquare(width, height, x, y, other.x, other.y) < + crowded = crowded || Torus{width, height}.dist2(x, y, other.x, other.y) < spacing * spacing; if (crowded) continue; @@ -478,63 +384,25 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, boo return true; } -// Grows a compact patch of one resource outward from a seed tile, breadth-first over the four -// cardinal neighbours, onto tiles the predicate allows. Returns how many tiles it placed. -template -int growPatch(Map &map, int seed, int type, int count, Eligible eligible) -{ - const int width = map.getW(), height = map.getH(); - std::vector queued(size_t(width) * height, 0); - std::vector frontier{seed}; - queued[seed] = 1; - int placed = 0; - const auto &steps = kCardinalSteps; - for (size_t head = 0; head < frontier.size() && placed < count; ++head) - { - const int i = frontier[head], x = i % width, y = i / width; - if (!eligible(i) || !map.isResourceAllowed(x, y, type)) - continue; - map.setResource(x, y, type, 1); - ++placed; - for (const auto &step : steps) - { - const int n = map.normalizeY(y + step[1]) * width + map.normalizeX(x + step[0]); - if (!queued[n]) - { - queued[n] = 1; - frontier.push_back(n); - } - } - } - return placed; -} - // Every home's starter kit: a wheat and a wood patch on the most fertile ground a short walk from // the swarm, and a little stone further inland. void furnishHomes(Game &game, GenerationContext &context) { Map &map = game.map; const int width = map.getW(), height = map.getH(), teams = context.request.nbTeams; + const Torus t{width, height}; const size_t area = size_t(width) * height; - std::vector water(area), dry(area), reserved(area, 0); + std::vector water(area), dry(area); for (int y = 0; y < height; ++y) for (int x = 0; x < width; ++x) { water[size_t(y) * width + x] = map.isWater(x, y); dry[size_t(y) * width + x] = !map.isWater(x, y); } - for (int team = 0; team < teams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - reserved[size_t(map.normalizeY(context.bootY[team] + dy)) * width + - map.normalizeX(context.bootX[team] + dx)] = 1; - const std::vector shore = stepsFrom(Torus{width, height}, water, dry); + const std::vector reserved = swarmSurroundings(t, context); + const std::vector shore = stepsFrom(t, water, dry); const auto eligible = [&](int i) - { - const int x = i % width, y = i / width; - return !reserved[i] && map.isGrass(x, y) && !map.isResource(x, y) && - map.getBuilding(x, y) == NOGBID && map.getGroundUnit(x, y) == NOGUID; - }; + { return !reserved[i] && clearGround(map, i % width, i / width); }; for (int team = 0; team < teams; ++team) { std::vector footprint(area, 0); @@ -551,8 +419,7 @@ void furnishHomes(Game &game, GenerationContext &context) { if (walk[i] < nearest || walk[i] > farthest || !eligible(i)) continue; - if (avoid >= 0 && wrapSquare(width, height, i % width, i / width, avoid % width, - avoid / width) < 36) + if (avoid >= 0 && t.dist2(i % width, i / width, avoid % width, avoid / width) < 36) continue; const bool better = best < 0 || (wet ? shore[i] < shore[best] : shore[i] > shore[best]) || @@ -564,85 +431,16 @@ void furnishHomes(Game &game, GenerationContext &context) }; const int wheat = pick(5, 9, true, -1); if (wheat >= 0) - growPatch(map, wheat, CORN, kHomeWheat, eligible); + growPatch(map, t, wheat, CORN, kHomeWheat, eligible); const int wood = pick(5, 9, true, wheat); if (wood >= 0) - growPatch(map, wood, WOOD, kHomeWood, eligible); + growPatch(map, t, wood, WOOD, kHomeWood, eligible); const int stone = pick(7, 12, false, -1); if (stone >= 0) - growPatch(map, stone, STONE, kHomeStone, eligible); + growPatch(map, t, stone, STONE, kHomeStone, eligible); } } -// Each island carries one themed prize, as on Fjord continent's outliers. -void stockIslands(Map &map, GenerationContext &context, const std::vector &islands) -{ - const int width = map.getW(); - for (const Island &island : islands) - { - std::vector grass; - for (int i : island.tiles) - if (map.isGrass(i % width, i / width)) - grass.emplace_back(i % width, i / width); - if (grass.empty()) - continue; - MapGeneratorPoint centre(island.x, island.y); - if (!map.isGrass(centre.x, centre.y)) - centre = grass[context.bounded("islands", grass.size())]; - switch (context.bounded("islands", 3)) - { - case 0: - placeResourceClump(map, context, centre, STONE, 2); - break; - case 1: - placeResourceClump(map, context, centre, CHERRY + int(context.bounded("islands", 3)), - 2); - break; - default: - placeResourceClump(map, context, centre, CORN, 2); - break; - } - } -} - -// Algae in the shallows along every coast, the ocean's and the lakes' alike: close enough to shore -// to regrow and to be harvested. -void seedAlgae(Map &map, GenerationContext &context, int algaePercent) -{ - const int width = map.getW(), height = map.getH(); - const size_t area = size_t(width) * height; - std::vector water(area), dry(area); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - { - water[size_t(y) * width + x] = map.isWater(x, y); - dry[size_t(y) * width + x] = !map.isWater(x, y); - } - const std::vector offshore = stepsFrom(Torus{width, height}, dry, water); - std::vector shallows; - for (int i = 0; i < int(area); ++i) - if (offshore[i] >= 2 && offshore[i] <= 5) - shallows.emplace_back(i % width, i / width); - if (shallows.empty()) - return; - for (int clump = 0; clump < scaledCount(int(shallows.size()) / 90, algaePercent); ++clump) - placeResourceClump(map, context, shallows[context.bounded("resources", shallows.size())], - ALGA, 1); -} - -void clearAroundSwarms(Map &map, const GenerationContext &context) -{ - for (int team = 0; team < context.request.nbTeams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - { - const int x = map.normalizeX(context.bootX[team] + dx); - const int y = map.normalizeY(context.bootY[team] + dy); - if (map.isResource(x, y)) - map.setNoResource(x, y, 1); - } -} - // Deposits may land anywhere, and stone is never cleared in play, so a band of them could wall // the belt off. This keeps the ring open: for each colony in order around the belt, the cheapest // walk forward to the next (resources cost one, open ground nothing; water and buildings are @@ -765,20 +563,23 @@ bool generate(Game &game, GenerationContext &context) } context.stage = "ring terrain"; + const Torus t{width, height}; const Belt belt = shapeBelt(axes, context, options); - std::vector terrain(size_t(width) * height, WATER); + TerrainSketch terrain(size_t(width) * height, WATER); for (int y = 0; y < height; ++y) for (int x = 0; x < width; ++x) if (belt.land(x, y)) terrain[size_t(y) * width + x] = GRASS; carveLakes(terrain, belt, context, options.lakeDensity); + // The control counts islands per 128x128 of map, never fewer than one when it is on at all. + const int wantedIslands = + options.resourceIslands > 0 + ? std::max(1, int(std::lround(options.resourceIslands * double(t.size()) / 16384.0))) + : 0; const std::vector islands = - raiseIslands(terrain, width, height, context, options.resourceIslands); - layBeaches(terrain, width, height); - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * width + x])); - map.rebuildTerrain(); + raiseIslands(terrain, t, context, {"islands", wantedIslands, 40, kIslandMoat}); + layBeaches(terrain, t); + writeUndermap(map, terrain); context.stage = "ring colonies"; if (!placeColonies(game, context, belt, options.bothCoasts)) @@ -786,7 +587,7 @@ bool generate(Game &game, GenerationContext &context) context.stage = "ring resources"; furnishHomes(game, context); - stockIslands(map, context, islands); + stockIslands(map, context, islands, "islands"); // The same ambient layer as Fjord continent: corn:wood 2:1, some stone, rare fruit. Algae is // seeded along the shallows below instead, since the shared band only scatters over land. scatterResources(game, context, @@ -794,22 +595,17 @@ bool generate(Game &game, GenerationContext &context) /*wood=*/int(scaledCount(12, options.wood)), /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, /*fruit=*/int(scaledCount(3, options.fruit))}); - seedAlgae(map, context, options.algae); - clearAroundSwarms(map, context); + seedAlgae(map, context, t, "resources", options.algae, AlgaeBand::shallows(2, 5)); + clearAroundSwarms(map, context, t); guaranteeStartingResources(game, context, 24, 32, 6); - clearAroundSwarms(map, context); - + clearAroundSwarms(map, context, t); // The scatter above is sized by the resource amounts, and at the top of their range it can wall - // a colony into its own clearing with nowhere to build — which the guarantee doesn't address, - // since a colony buried in wheat has wheat at its feet. At any non-default amount, open such a - // colony back up, then guarantee and clear again as above. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || - options.algae != 100 || options.fruit != 100) - { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32, 6); - clearAroundSwarms(map, context); - } + // a colony into its own clearing with nowhere to build; the reopened colony is cleared again. + if (reopenCrampedStarts(game, context, + {options.wheat, options.wood, options.stone, options.algae, + options.fruit}, + 24, 32, 6)) + clearAroundSwarms(map, context, t); context.stage = "ring road"; return openBeltRoad(game, context, axes); diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index fd4e0172f..3b1cbb9ea 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -8,6 +8,7 @@ #include "GlobalContainer.h" #include "HeightMap.h" #include "Map.h" +#include "Pipeline.h" #include "Regions.h" #include "Resources.h" #include "StartingPositions.h" @@ -264,14 +265,8 @@ static bool generate(Game &game, GenerationContext &context) // within comfortable range; islands that were already generous are left untouched. guaranteeStartingResources(game, context, 24, 32); // A deposit grown well past its default size can wall a colony into its own clearing with - // nowhere left to build on an island this small, which the guarantee above doesn't address: a - // colony buried in wood has wood at its feet. At any amount other than the default, open such a - // colony back up and guarantee the crops once more. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || options.algae != 100) - { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32); - } + // nowhere left to build on an island this small. + reopenCrampedStarts(game, context, {options.wheat, options.wood, options.stone, options.algae}); return true; } diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 1d7699499..9800ae844 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -8,6 +8,7 @@ #include "GlobalContainer.h" #include "HeightMap.h" #include "Map.h" +#include "Pipeline.h" #include "Regions.h" #include "Resources.h" #include "StartingPositions.h" @@ -760,14 +761,8 @@ static bool generate(Game &game, GenerationContext &context) // placeStarts() has already carved its own clearing, so there's nothing left to step on. guaranteeStartingResources(game, context, 24, 32); // A deposit grown well past its default size can wall a colony into its own clearing with - // nowhere left to build, which the guarantee above doesn't address: a colony buried in wheat - // has wheat at its feet. At any amount other than the default, open such a colony back up and - // guarantee the crops once more. At the defaults none of this runs. - if (options.wheat != 100 || options.wood != 100 || options.stone != 100 || options.algae != 100) - { - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32); - } + // nowhere left to build. + reopenCrampedStarts(game, context, {options.wheat, options.wood, options.stone, options.algae}); return true; } diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 07bcc3512..8d73c7ddb 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -4,6 +4,7 @@ #include "Game.h" #include "GenerationContext.h" #include "Grid.h" +#include "Pipeline.h" #include "Resources.h" #include "Settlements.h" #include "Topology.h" @@ -1158,7 +1159,7 @@ bool generate(Game &game, GenerationContext &context) context.stage = "highland colonies"; const std::vector pondDistance = chebyshevDistance(t, L.pond); - for (int team = 0; team < teams; ++team) + const auto valleyFloor = [&](int team) { const Home &h = L.homes[team]; std::vector home(n, 0); @@ -1170,10 +1171,13 @@ bool generate(Game &game, GenerationContext &context) pondDistance[i] >= 3) home[i] = 1; } - // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. - if (!placeSettlement(game, context, team, home, {h.x - 2, h.y - 2}, "highlands-starts")) - return false; - } + return home; + }; + // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. + const auto centre = [&](int team) + { return MapGeneratorPoint(L.homes[team].x - 2, L.homes[team].y - 2); }; + if (!settleColonies(game, context, "highlands-starts", valleyFloor, centre)) + return false; context.stage = "highland resources"; std::vector keepClear(n, 0), homeValley(L.valleys, 0); for (const Pass &p : L.passes) @@ -1223,13 +1227,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { GenerationContext replay(context.request); const Layout L = design(context.request, replay); - if (!L.failure.empty()) - return "The highland design could not be rebuilt: " + L.failure; const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "highland"); !mismatch.empty()) + return mismatch; const Torus &t = L.t; const int n = t.w * t.h, teams = context.request.nbTeams; - if (map.getW() != t.w || map.getH() != t.h) - return "The highland design does not match the map size."; for (int i = 0; i < n; ++i) if (L.ridge[i] && map.getResource(i % t.w, i / t.w).type != STONE) return "The ridge at (" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + @@ -1245,11 +1247,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) return "Pass " + std::to_string(k) + " at (" + std::to_string(p.x) + ", " + std::to_string(p.y) + ") is blocked."; } - const auto units = unitTilesByTeam(map, teams); - const std::vector reached = - stepsFrom(t, tileMask(t, teams > 0 ? units[0] : std::vector{}), walkableTiles(map)); - if (const int cut = firstColonyCutOff(reached, units, 0); cut >= 0) - return "Colony " + std::to_string(cut) + " cannot walk to colony 0 over the highlands."; + const ColonyWalk walk = walkFromFirstColony(map, teams, "the highlands", "over the highlands"); + if (!walk.error.empty()) + return walk.error; + const std::vector &reached = walk.steps; // Each pass must be on the colonies' walkable network and lead straight through: walking out // of its box, without going far, reaches both valleys it joins. for (size_t k = 0; k < L.passes.size(); ++k) diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index a968f58e0..08850972c 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -4,6 +4,7 @@ #include "BuildingType.h" #include "Game.h" #include "GenerationContext.h" +#include "Geometry.h" #include "Grid.h" #include "GlobalContainer.h" #include "HeightMap.h" @@ -20,6 +21,7 @@ #include #include #include +using MapGeneration::kPi; using MapGeneration::scaledCount; using MapGeneration::scaledShare; @@ -36,8 +38,6 @@ using MapGeneration::scaledShare; namespace { -constexpr double kPi = 3.14159265358979323846; - // Clear grass round every swarm; nothing is planted or dug inside it. constexpr double kHomeRadius = 6.5; // Each home's own pond, which keeps its starting farmland growing back. diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index 9e5ea6e91..a9cb9b9ac 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -6,9 +6,13 @@ #include "Grid.h" #include "Geometry.h" #include "HeightMap.h" +#include "Pipeline.h" +#include "Planting.h" #include "Resources.h" #include "Settlements.h" +#include "Sketch.h" #include "Unit.h" +#include "Wedge.h" #include #include #include @@ -36,8 +40,6 @@ using namespace MapGeneration; namespace { -constexpr double pi = 3.14159265358979323846; - // The home islands' centres lie this far out, as a share of the half side, and keep this much sand // from each other, from the map's wrap and from every other island. constexpr double kHomeRing = 0.58; @@ -45,7 +47,6 @@ constexpr int kIslandGap = 8; // Every home starts identical: wheat and wood beside its pond, a quarry, all unscaled. constexpr int kHomeWheat = 40; constexpr int kHomeWood = 30; -constexpr int kSwarmClearance = 2; // Neutral islands' radii as shares of the home islands' (never below a size that holds a pond and // a field), and the central island's as a share of the half side. constexpr double kNeutralLow = 0.45, kNeutralHigh = 0.6; @@ -78,7 +79,7 @@ Geometry geometryFor(const GenerationRequest &r) const double reach = 1 + g.amplitude; if (g.teams >= 2) radius = - std::min(radius, (2 * g.homeRing * std::sin(pi / g.teams) - kIslandGap) / (2 * reach)); + std::min(radius, (2 * g.homeRing * std::sin(kPi / g.teams) - kIslandGap) / (2 * reach)); radius = std::min(radius, (g.half - kIslandGap / 2.0 - g.homeRing) / reach); if (g.centralRadius > 0) radius = std::min(radius, (g.homeRing - g.centralRadius * 1.3 - kIslandGap) / reach); @@ -86,8 +87,8 @@ Geometry geometryFor(const GenerationRequest &r) return g; } -// A round feature in a wedge's frame: arc offset from the wedge's middle, radius from the centre, -// and what it is. Shapes are evaluated in the wedge's frame too, so every wedge gets the same. +// A feature in a wedge's frame (Blob) and what it is. Shapes are evaluated in the wedge's frame +// too, so every wedge gets the same. enum Kind { HomeIsland = 0, @@ -97,19 +98,11 @@ enum Kind Pool, Lagoon }; -struct Blob +struct Feature : Blob { Kind kind; - double s, r, reach, stretch, turn; - int prize; // neutral islands: 0 fruit, 1 stone, 2 wheat - RadialShape shape; - bool holds(double ds, double dr) const - { - // Rotate into the shape's own frame and undo its stretch. - const double c = std::cos(turn), sn = std::sin(turn); - const double px = (ds * c + dr * sn) / stretch, py = (-ds * sn + dr * c) * stretch; - return std::hypot(px, py) < shape.radiusAt(std::atan2(py, px)); - } + double reach; // the blob's fullest extent, stretched + int prize; // neutral islands: 0 fruit, 1 stone, 2 wheat }; struct Layout @@ -118,7 +111,7 @@ struct Layout Geometry g{}; int cx = 0, cy = 0; double phase = 0; - std::vector blobs; // in the wedge's frame + std::vector blobs; // in the wedge's frame std::vector homeX, homeY; // every home island's centre std::vector pondX, pondY; // and its pond's std::vector grass, water, island; // per tile; island: which home, else -1 below @@ -139,16 +132,19 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const TidalFlatsOptions o(request); L.cx = t.w / 2; L.cy = t.h / 2; - L.phase = context.bounded("flats-layout", 3600) / 3600.0 * 2 * pi; - const double wedge = 2 * pi / teams; - const double arcHalfAt = [&](double d) { return teams < 2 ? pi * d : pi * d / teams; }(1.0); + L.phase = context.bounded("flats-layout", 3600) / 3600.0 * 2 * kPi; + const double wedge = 2 * kPi / teams; + const double arcHalfAt = [&](double d) { return teams < 2 ? kPi * d : kPi * d / teams; }(1.0); // Everything in one wedge, then stamped into every wedge: the home island in the middle of the // ring, then neutral islands, sandbars, lagoons and pools that keep clear of each other, of the // central island and of the map's wrap. - std::vector &blobs = L.blobs; - blobs.push_back({HomeIsland, 0.0, g.homeRing, 0, 1.0, 0.0, -1, - RadialShape(g.homeRadius, g.amplitude, context, "flats-coast")}); + std::vector &blobs = L.blobs; + blobs.push_back({{0.0, g.homeRing, 1.0, 0.0, + RadialShape(g.homeRadius, g.amplitude, context, "flats-coast")}, + HomeIsland, + 0, + -1}); blobs.back().reach = blobs.back().shape.maximumRadius(); const auto fits = [&](double s0, double r0, double reach, double gap) { @@ -158,7 +154,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) return false; if (teams >= 2 && std::abs(s0) + reach + gap / 2 > arcHalfAt * r0) return false; - for (const Blob &b : blobs) + for (const Feature &b : blobs) { const double need = reach + b.reach + std::max(gap, b.kind == HomeIsland ? double(kIslandGap) : gap); @@ -176,12 +172,12 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const double s0 = (context.bounded(stream, 2001) / 1000.0 - 1) * arcHalfAt * r0; const double radius = low + context.bounded(stream, 1000) / 1000.0 * (high - low); const double stretch = stretched ? 1.2 + context.bounded(stream, 81) / 100.0 : 1.0; - const double turn = stretched ? context.bounded(stream, 3600) / 3600.0 * pi : 0.0; + const double turn = stretched ? context.bounded(stream, 3600) / 3600.0 * kPi : 0.0; RadialShape shape(radius, roughness, context, stream); const double reach = shape.maximumRadius() * stretch; if (!fits(s0, r0, reach, gap)) continue; - blobs.push_back({kind, s0, r0, reach, stretch, turn, placed % 2, shape}); + blobs.push_back({{s0, r0, stretch, turn, shape}, kind, reach, placed % 2}); ++placed; } }; @@ -217,13 +213,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const double pondRadius = std::clamp(0.2 * g.homeRadius, 3.0, 6.0); const RadialShape pond(pondRadius, 0.3, context, "flats-ponds"); const RadialShape oasis(2.5, 0.3, context, "flats-ponds"); + const WedgeFrame frame(t, L.phase, teams); for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) { const int i = y * t.w + x; - const int dx = t.offsetX(L.cx, x), dy = t.offsetY(L.cy, y); - const double d = std::hypot(double(dx), double(dy)); - const double theta = std::atan2(double(dy), double(dx)); + const WedgeFrame::Cell cell = frame.cell(x, y); + const double d = cell.d, theta = cell.theta; L.radius[i] = d; L.angle[i] = theta; if (g.centralRadius > 0 && d < central.radiusAt(theta)) @@ -234,11 +230,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.water[i] = 1; continue; } - const double turn = std::fmod(theta - L.phase + 4 * pi, 2 * pi); - const int k = std::min(teams - 1, int(turn / wedge)); - const double u = turn / wedge - k; - const double s = d * (u - 0.5) * wedge; - for (const Blob &b : blobs) + const int k = cell.k; + const double s = cell.s; + for (const Feature &b : blobs) { if (!b.holds(s - b.s, d - b.r)) continue; @@ -266,78 +260,6 @@ Layout design(const GenerationRequest &request, GenerationContext &context) return L; } -// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap corners), -// so every land corner beside water becomes sand. -void layBeaches(std::vector &terrain, const Torus &t) -{ - const std::vector original(terrain); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - const int i = y * t.w + x; - if (original[i] == WATER) - continue; - bool shore = false; - for (int dy = -1; dy <= 1 && !shore; ++dy) - for (int dx = -1; dx <= 1 && !shore; ++dx) - shore = original[t.at(x + dx, y + dy)] == WATER; - if (shore) - terrain[i] = SAND; - } -} - -template -int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) -{ - std::vector queued(size_t(t.w) * t.h, 0); - std::vector frontier{seed}; - queued[seed] = 1; - int placed = 0; - const auto &steps = kCardinalSteps; - for (size_t head = 0; head < frontier.size() && placed < count; ++head) - { - const int i = frontier[head], x = i % t.w, y = i / t.w; - if (!eligible(i) || !map.isResourceAllowed(x, y, type)) - continue; - map.setResource(x, y, type, 1); - ++placed; - for (const auto &step : steps) - { - const int m = t.at(x + step[0], y + step[1]); - if (!queued[m]) - { - queued[m] = 1; - frontier.push_back(m); - } - } - } - return placed; -} - -bool clearGround(const Map &map, int x, int y) -{ - return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && - map.getGroundUnit(x, y) == NOGUID; -} - -// The nearest eligible tile to a point, within a box. -template -int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) -{ - int seed = -1, nearest = INT_MAX; - for (int dy = -within; dy <= within; ++dy) - for (int dx = -within; dx <= within; ++dx) - { - const int i = t.at(ax + dx, ay + dy); - if (eligible(i) && dx * dx + dy * dy < nearest) - { - nearest = dx * dx + dy * dy; - seed = i; - } - } - return seed; -} - // Every home's kit, identical and unscaled: wheat and wood beside the pond on the side away from // the swarm, a quarry on the island's rim towards the centre of the map; then its own scaled // ambient farmland and outcrops. @@ -345,11 +267,7 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const T { const Torus &t = L.t; const int n = t.w * t.h; - std::vector reserved(n, 0); - for (int team = 0; team < L.g.teams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + const std::vector reserved = swarmSurroundings(t, context); const Fertility::Field fertility = Fertility::forMap(map, false); HeightMap split(t.w, t.h, context.stream("flats-home-split")); split.makePlain(6); @@ -484,11 +402,11 @@ void stockIslands(Map &map, const Layout &L, GenerationContext &context, const T const auto eligible = [&](int i) { return L.islandOf[i] == -2 && clearGround(map, i % t.w, i / t.w); }; const double rho = std::max(1.5, 0.22 * L.g.centralRadius) + 4; - const double spin = context.bounded("flats-prizes", 3600) / 3600.0 * 2 * pi; + const double spin = context.bounded("flats-prizes", 3600) / 3600.0 * 2 * kPi; if (scaledCount(1, o.fruit) > 0) for (int f = 0; f < 3; ++f) { - const double a = spin + 2 * pi * f / 3; + const double a = spin + 2 * kPi * f / 3; const int seed = seedNear(t, L.cx + int(std::lround(rho * std::cos(a))), L.cy + int(std::lround(rho * std::sin(a))), 8, eligible); if (seed >= 0) @@ -496,41 +414,14 @@ void stockIslands(Map &map, const Layout &L, GenerationContext &context, const T } if (scaledCount(1, o.stone) > 0) if (const int seed = seedNear( - t, L.cx + int(std::lround((L.g.centralRadius - 4) * std::cos(spin + pi / 3))), - L.cy + int(std::lround((L.g.centralRadius - 4) * std::sin(spin + pi / 3))), 8, + t, L.cx + int(std::lround((L.g.centralRadius - 4) * std::cos(spin + kPi / 3))), + L.cy + int(std::lround((L.g.centralRadius - 4) * std::sin(spin + kPi / 3))), 8, eligible); seed >= 0) placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); } } -// Algae in every pool and lagoon: water beside sand is exactly where it regrows. -void seedAlgae(Map &map, GenerationContext &context, const Torus &t, int algaePercent) -{ - const int n = t.w * t.h; - std::vector water; - for (int i = 0; i < n; ++i) - if (map.isWater(i % t.w, i / t.w)) - water.emplace_back(i % t.w, i / t.w); - if (water.empty()) - return; - for (int clump = 0; clump < scaledCount(int(water.size()) / 40, algaePercent); ++clump) - placeResourceClump(map, context, water[context.bounded("flats-algae", water.size())], ALGA, - 1); -} - -void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t) -{ - for (int team = 0; team < context.request.nbTeams; ++team) - for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) - for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) - { - const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); - if (map.isResource(x, y)) - map.setNoResource(x, y, 1); - } -} - bool generate(Game &game, GenerationContext &context) { context.stage = "flats layout"; @@ -550,7 +441,7 @@ bool generate(Game &game, GenerationContext &context) const int n = t.w * t.h; context.stage = "flats terrain"; - std::vector terrain(n, SAND); + TerrainSketch terrain(n, SAND); for (int i = 0; i < n; ++i) { if (L.grass[i]) @@ -559,32 +450,33 @@ bool generate(Game &game, GenerationContext &context) terrain[i] = WATER; } layBeaches(terrain, t); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - map.setUMTerrain(x, y, TerrainType(terrain[y * t.w + x])); - map.rebuildTerrain(); + writeUndermap(map, terrain); context.stage = "flats colonies"; - for (int team = 0; team < teams; ++team) + const auto island = [&](int team) { std::vector home(n, 0); for (int i = 0; i < n; ++i) home[i] = L.islandOf[i] == team && map.isGrass(i % t.w, i / t.w); - // The swarm stands on the far side of the pond from the map's centre, a short walk from the - // fields; placeSettlement measures from the footprint's top-left tile. + return home; + }; + // The swarm stands on the far side of the pond from the map's centre, a short walk from the + // fields; placeSettlement measures from the footprint's top-left tile. + const auto anchor = [&](int team) + { const double a = std::atan2(double(t.offsetY(L.cy, L.homeY[team])), double(t.offsetX(L.cx, L.homeX[team]))); const double back = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 6; - const MapGeneratorPoint anchor(L.pondX[team] + int(std::lround(back * std::cos(a))) - 2, - L.pondY[team] + int(std::lround(back * std::sin(a))) - 2); - if (!placeSettlement(game, context, team, home, anchor, "flats-starts")) - return false; - } + return MapGeneratorPoint(L.pondX[team] + int(std::lround(back * std::cos(a))) - 2, + L.pondY[team] + int(std::lround(back * std::sin(a))) - 2); + }; + if (!settleColonies(game, context, "flats-starts", island, anchor)) + return false; context.stage = "flats resources"; furnishHomes(map, L, context, o); stockIslands(map, L, context, o); - seedAlgae(map, context, t, o.algae); + seedAlgae(map, context, t, "flats-algae", o.algae, AlgaeBand::anyWater()); clearAroundSwarms(map, context, t); guaranteeStartingResources(game, context, 24, 32, 0); clearAroundSwarms(map, context, t); @@ -609,13 +501,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { GenerationContext replay(context.request); const Layout L = design(context.request, replay); - if (!L.failure.empty()) - return "The flats design could not be rebuilt: " + L.failure; const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "flats"); !mismatch.empty()) + return mismatch; const Torus &t = L.t; const int n = t.w * t.h, teams = context.request.nbTeams; - if (map.getW() != t.w || map.getH() != t.h) - return "The flats design does not match the map size."; for (int k = 0; k < teams; ++k) { bool pond = false; @@ -625,17 +515,14 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (!pond) return "Colony " + std::to_string(k) + "'s island has lost its pond."; } - const auto workers = unitTilesByTeam(map, teams); - if (teams < 1 || workers[0].empty()) - return "Colony 0 has no workers to walk the flats."; - const std::vector steps = stepsFrom(t, tileMask(t, workers[0]), walkableTiles(map)); - if (const int cut = firstColonyCutOff(steps, workers); cut >= 0) - return "Colony " + std::to_string(cut) + " cannot walk to colony 0 over the flats."; + const ColonyWalk walk = walkFromFirstColony(map, teams, "the flats", "over the flats"); + if (!walk.error.empty()) + return walk.error; if (L.g.centralRadius > 0) { bool arrived = false; for (int i = 0; i < n && !arrived; ++i) - arrived = L.islandOf[i] == -2 && steps[i] >= 0; + arrived = L.islandOf[i] == -2 && walk.steps[i] >= 0; if (!arrived) return "The central island cannot be reached over the flats."; } diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index 14bec5796..7227990b1 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -3,16 +3,18 @@ #include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" -#include "Topology.h" +#include "Geometry.h" #include "Grid.h" +#include "Pipeline.h" #include "Resources.h" +#include "Roads.h" #include "Settlements.h" +#include "Topology.h" #include "Unit.h" #include #include #include #include -#include #include #include #include @@ -37,7 +39,6 @@ using namespace MapGeneration; namespace { -constexpr double kPi = 3.14159265358979323846; constexpr const char *kLayoutStream = "watershed-layout"; constexpr const char *kRiverStream = "watershed-rivers"; constexpr const char *kStartStream = "watershed-starts"; @@ -1728,71 +1729,25 @@ void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, co } } -// Clears the fewest resource tiles that let every colony walk to colony 0: a 0-1 search where -// stepping onto a resource costs one and open ground nothing. +// Clears the fewest resource tiles that let every colony walk to colony 0. bool connectColonies(Game &game, int teams, std::string &detail) { Map &map = game.map; - const int w = map.getW(), h = map.getH(); + const Torus t(map); const auto workers = unitTilesByTeam(map, teams); for (int team = 1; team < teams; ++team) { - std::vector reach = - stepsFrom(Torus(map), tileMask(Torus(map), workers[0]), walkableTiles(map)); + const std::vector reach = stepsFrom(t, tileMask(t, workers[0]), walkableTiles(map)); bool connected = false; for (int p : workers[team]) connected = connected || reach[p] >= 0; if (connected) continue; - std::vector cost(size_t(w) * h, std::numeric_limits::max()), - from(size_t(w) * h, -1); - std::vector target(size_t(w) * h, 0); - for (int p : workers[team]) - target[p] = 1; - std::deque queue; - for (int p : workers[0]) - { - cost[p] = 0; - queue.push_back(p); - } - int reached = -1; - while (!queue.empty() && reached < 0) - { - const int p = queue.front(); - queue.pop_front(); - if (target[p]) - { - reached = p; - break; - } - const int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const int q = ny * w + nx; - if (map.isWater(nx, ny) || map.getBuilding(nx, ny) != NOGBID) - continue; - const int step = map.isResource(nx, ny) ? 1 : 0; - if (cost[p] + step < cost[q]) - { - cost[q] = cost[p] + step; - from[q] = p; - if (step) - queue.push_back(q); - else - queue.push_front(q); - } - } - } - if (reached < 0) + if (!openRoad(map, t, workers[0], tileMask(t, workers[team]))) { detail = "colony " + std::to_string(team) + " has no land route to colony 0"; return false; } - for (int p = reached; p >= 0; p = from[p]) - if (map.isResource(p % w, p / w)) - map.setNoResource(p % w, p / w, 1); } return true; } @@ -1836,7 +1791,7 @@ bool generate(Game &game, GenerationContext &context) return false; for (int team = 0; team < teams; ++team) game.addTeam(); - for (int team = 0; team < teams; ++team) + const auto floodplain = [&](int team) { std::vector home(size_t(w) * h, 0); for (int dy = -6; dy <= 9; ++dy) @@ -1846,9 +1801,10 @@ bool generate(Game &game, GenerationContext &context) y = wrapIndex(sites[team].y + dy, h); home[size_t(y) * w + x] = !tiles.water[size_t(y) * w + x]; } - if (!placeSettlement(game, context, team, home, sites[team], "starts")) - return false; - } + return home; + }; + if (!settleColonies(game, context, "starts", floodplain, [&](int team) { return sites[team]; })) + return false; context.stage = "watershed resources"; std::vector keepClear(size_t(w) * h, 0); diff --git a/src/map/generator/shared/Geometry.cpp b/src/map/generator/shared/Geometry.cpp index c283cf218..704950958 100644 --- a/src/map/generator/shared/Geometry.cpp +++ b/src/map/generator/shared/Geometry.cpp @@ -5,10 +5,6 @@ #include namespace MapGeneration { -namespace -{ -constexpr double pi = 3.14159265358979323846; -} ShapeTransform::ShapeTransform(ShapePoint c, double angle, double scale) : center(c), cosine(std::cos(angle)), sine(std::sin(angle)), stretch(scale) { @@ -40,7 +36,7 @@ RadialShape::RadialShape(double r, double roughness, GenerationContext &context, { const auto choices = std::uint32_t((amplitudeMaximum - 0.6) * 1000) + 1; amplitude[i] = roughness * falloff[i] * (0.6 + context.bounded(stream, choices) / 1000.0); - phase[i] = 2 * pi * context.bounded(stream, 65536) / 65536.0; + phase[i] = 2 * kPi * context.bounded(stream, 65536) / 65536.0; } } double RadialShape::radiusAt(double theta) const diff --git a/src/map/generator/shared/Geometry.h b/src/map/generator/shared/Geometry.h index 145c12c0a..d94ac04af 100644 --- a/src/map/generator/shared/Geometry.h +++ b/src/map/generator/shared/Geometry.h @@ -6,6 +6,7 @@ struct GenerationContext; namespace MapGeneration { +constexpr double kPi = 3.14159265358979323846; struct ShapePoint { double x, y; diff --git a/src/map/generator/shared/Pipeline.cpp b/src/map/generator/shared/Pipeline.cpp new file mode 100644 index 000000000..d68451fbc --- /dev/null +++ b/src/map/generator/shared/Pipeline.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Pipeline.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Resources.h" +namespace MapGeneration +{ +bool reopenCrampedStarts(Game &game, GenerationContext &context, const ResourceAmounts &amounts, + int wheatRange, int woodRange, int clearRadius, + const std::vector *protectedWalls) +{ + if (!amounts.scaled()) + return false; + openCrampedStarts(game, context, 16, 24, protectedWalls); + guaranteeStartingResources(game, context, wheatRange, woodRange, clearRadius, protectedWalls); + return true; +} + +ColonyWalk walkFromFirstColony(const Map &map, int teams, const std::string &ground, + const std::string &route) +{ + ColonyWalk walk; + walk.workers = unitTilesByTeam(map, teams); + if (teams < 1 || walk.workers[0].empty()) + { + walk.error = "Colony 0 has no workers to walk " + ground + "."; + return walk; + } + const Torus t(map); + walk.steps = stepsFrom(t, tileMask(t, walk.workers[0]), walkableTiles(map)); + if (const int cut = firstColonyCutOff(walk.steps, walk.workers); cut >= 0) + walk.error = "Colony " + std::to_string(cut) + " cannot walk to colony 0" + + (route.empty() ? "" : " " + route) + "."; + return walk; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Pipeline.h b/src/map/generator/shared/Pipeline.h new file mode 100644 index 000000000..b2168d7a8 --- /dev/null +++ b/src/map/generator/shared/Pipeline.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationContext.h" +#include "Grid.h" +#include "Regions.h" +#include "Settlements.h" +#include +#include +class Game; +class Map; +namespace MapGeneration +{ +// The stages a designed generator runs, as functions a generate() body calls in the order that +// suits its map. There is no superclass: a generator that needs a different order, or a +// different step, calls what it needs and writes the rest itself. +// +// The usual order is: design the layout as a pure function of the request; stamp it into a +// TerrainSketch, lay beaches and write it out (Sketch.h); settle the colonies; plant the kits and +// the ambient layers (Planting.h, Resources.h); clear round the swarms; guarantee every colony +// its crops; open any route the deposits closed (Roads.h); and have validateWorld rebuild the +// design and check the finished world against it. + +/// A swarm and its workers for every colony, each inside its own home mask and as near as the +/// mask allows to its anchor. Fails the candidate on the first colony that does not fit. +template +bool settleColonies(Game &game, GenerationContext &context, const char *stream, HomeMask homeMask, + Anchor anchor) +{ + for (int team = 0; team < context.request.nbTeams; ++team) + if (!placeSettlement(game, context, team, homeMask(team), anchor(team), stream)) + return false; + return true; +} + +/// The resource amounts a generator's ambient layers are scaled by; every one is 100 by default. +struct ResourceAmounts +{ + int wheat = 100, wood = 100, stone = 100, algae = 100, fruit = 100; + bool scaled() const + { + return wheat != 100 || wood != 100 || stone != 100 || algae != 100 || fruit != 100; + } +}; + +/// A resource amount well above the default can wall a colony into a pocket with nowhere to +/// build, which the wheat/wood guarantee doesn't address: a colony buried in wheat has wheat at +/// its feet. At any non-default amount this opens such a colony back up (openCrampedStarts) and +/// re-runs the guarantee in case the clearing took its nearest crop with the wall. At the +/// defaults nothing happens, and it returns whether anything ran. +bool reopenCrampedStarts(Game &, GenerationContext &, const ResourceAmounts &, int wheatRange = 24, + int woodRange = 32, int clearRadius = 0, + const std::vector *protectedWalls = nullptr); + +/// validateWorld's first check on a design rebuilt from the request: it rebuilt, and it is the +/// map's size. `L` needs a `failure` string and a `t` torus. +template +std::string designMismatch(const Layout &L, const Map &map, const char *name); + +/// Every colony's walk from colony 0's workers, with water, buildings and every resource +/// blocking (units don't, since they move): the check every validator makes. +struct ColonyWalk +{ + std::vector> workers; // each colony's unit tiles + std::vector steps; // from colony 0's workers + std::string error; +}; +/// `ground` names what the workers walk ("the flats"); `route` finishes the cut-off message +/// ("over the flats", or empty). +ColonyWalk walkFromFirstColony(const Map &, int teams, const std::string &ground, + const std::string &route); +} // namespace MapGeneration + +#include "Map.h" +template +std::string MapGeneration::designMismatch(const Layout &L, const Map &map, const char *name) +{ + if (!L.failure.empty()) + return std::string("The ") + name + " design could not be rebuilt: " + L.failure; + if (map.getW() != L.t.w || map.getH() != L.t.h) + return std::string("The ") + name + " design does not match the map size."; + return ""; +} diff --git a/src/map/generator/shared/Planting.cpp b/src/map/generator/shared/Planting.cpp new file mode 100644 index 000000000..0f1076894 --- /dev/null +++ b/src/map/generator/shared/Planting.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Planting.h" +#include "GenerationContext.h" +#include "Resources.h" +namespace MapGeneration +{ +bool clearGround(const Map &map, int x, int y) +{ + return map.isGrass(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && + map.getGroundUnit(x, y) == NOGUID; +} + +std::vector swarmSurroundings(const Torus &t, const GenerationContext &context, + int clearance) +{ + std::vector reserved(size_t(t.w) * t.h, 0); + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -clearance; dy < 4 + clearance; ++dy) + for (int dx = -clearance; dx < 4 + clearance; ++dx) + reserved[t.at(context.bootX[team] + dx, context.bootY[team] + dy)] = 1; + return reserved; +} + +void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus &t, + const std::vector *keep) +{ + for (int team = 0; team < context.request.nbTeams; ++team) + for (int dy = -kSwarmClearance; dy < 4 + kSwarmClearance; ++dy) + for (int dx = -kSwarmClearance; dx < 4 + kSwarmClearance; ++dx) + { + const int x = t.x(context.bootX[team] + dx), y = t.y(context.bootY[team] + dy); + if (map.isResource(x, y) && !(keep && (*keep)[y * t.w + x])) + map.setNoResource(x, y, 1); + } +} + +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char *stream, + int algaePercent, const AlgaeBand &band) +{ + const int n = t.w * t.h; + std::vector water; + if (band.nearestOffshore < 0) + { + for (int i = 0; i < n; ++i) + if (map.isWater(i % t.w, i / t.w)) + water.emplace_back(i % t.w, i / t.w); + } + else + { + std::vector wet(n), dry(n); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + wet[y * t.w + x] = map.isWater(x, y); + dry[y * t.w + x] = !map.isWater(x, y); + } + const std::vector offshore = stepsFrom(t, dry, wet); + for (int i = 0; i < n; ++i) + if (offshore[i] >= band.nearestOffshore && offshore[i] <= band.farthestOffshore) + water.emplace_back(i % t.w, i / t.w); + } + if (water.empty()) + return; + for (int clump = 0; clump < scaledCount(int(water.size()) / band.tilesPerClump, algaePercent); + ++clump) + placeResourceClump(map, context, water[context.bounded(stream, water.size())], ALGA, + band.clumpRadius); +} + +void stockIslands(Map &map, GenerationContext &context, const std::vector &islands, + const char *stream) +{ + const int width = map.getW(); + for (const Island &island : islands) + { + std::vector grass; + for (int i : island.tiles) + if (map.isGrass(i % width, i / width)) + grass.emplace_back(i % width, i / width); + if (grass.empty()) + continue; + MapGeneratorPoint centre(island.x, island.y); + if (!map.isGrass(centre.x, centre.y)) + centre = grass[context.bounded(stream, grass.size())]; + switch (context.bounded(stream, 3)) + { + case 0: + placeResourceClump(map, context, centre, STONE, 2); + break; + case 1: + placeResourceClump(map, context, centre, CHERRY + int(context.bounded(stream, 3)), 2); + break; + default: + placeResourceClump(map, context, centre, CORN, 2); + break; + } + } +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Planting.h b/src/map/generator/shared/Planting.h new file mode 100644 index 000000000..2a4f07dce --- /dev/null +++ b/src/map/generator/shared/Planting.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include "Map.h" +#include "Sketch.h" +#include +#include +struct GenerationContext; +namespace MapGeneration +{ +// Deposits placed by hand: starter kits, prizes, algae. These are the primitives a generator's +// own resource layout is built from; scatterResources (Resources.h) is the ambient layer that +// fills in between them. + +/// Clear ground round every swarm, where nothing is planted or dug: the ring the workers walk +/// out through. +constexpr int kSwarmClearance = 2; + +/// Grass with nothing on it: no deposit, no building, no unit. +bool clearGround(const Map &, int x, int y); + +/// Grows a compact patch of one resource outward from a seed tile, breadth-first over the four +/// cardinal neighbours, onto tiles the predicate allows. Returns how many tiles it placed. +template +int growPatch(Map &map, const Torus &t, int seed, int type, int count, Eligible eligible) +{ + std::vector queued(size_t(t.w) * t.h, 0); + std::vector frontier{seed}; + queued[seed] = 1; + int placed = 0; + const auto &steps = kCardinalSteps; + for (size_t head = 0; head < frontier.size() && placed < count; ++head) + { + const int i = frontier[head], x = i % t.w, y = i / t.w; + if (!eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++placed; + for (const auto &step : steps) + { + const int n = t.at(x + step[0], y + step[1]); + if (!queued[n]) + { + queued[n] = 1; + frontier.push_back(n); + } + } + } + return placed; +} + +/// The nearest eligible tile to a point, within a box; -1 if there is none. +template +int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) +{ + int seed = -1, nearest = INT_MAX; + for (int dy = -within; dy <= within; ++dy) + for (int dx = -within; dx <= within; ++dx) + { + const int i = t.at(ax + dx, ay + dy); + if (eligible(i) && dx * dx + dy * dy < nearest) + { + nearest = dx * dx + dy * dy; + seed = i; + } + } + return seed; +} + +/// A mask of the tiles within `clearance` of every colony's swarm footprint: the ground a kit +/// must leave alone. +std::vector swarmSurroundings(const Torus &, const GenerationContext &, + int clearance = kSwarmClearance); + +/// Clears every deposit within kSwarmClearance of a swarm's footprint, except tiles of `keep` +/// (a designed wall beside it, say). +void clearAroundSwarms(Map &, const GenerationContext &, const Torus &, + const std::vector *keep = nullptr); + +/// Where algae goes: any water, or only water this many steps offshore. +struct AlgaeBand +{ + int nearestOffshore = -1, farthestOffshore = -1; + /// One clump per this many eligible water tiles at an amount of 100, each this big. + int tilesPerClump = 40; + int clumpRadius = 1; + static AlgaeBand anyWater(int tilesPerClump = 40) { return {-1, -1, tilesPerClump, 1}; } + static AlgaeBand shallows(int nearest, int farthest, int tilesPerClump = 90) + { + return {nearest, farthest, tilesPerClump, 1}; + } +}; + +/// Algae clumps over the band, scaled by the amount. +void seedAlgae(Map &, GenerationContext &, const Torus &, const char *stream, int algaePercent, + const AlgaeBand &); + +/// Each island carries one themed prize at its middle: stone, a fruit or wheat, so finding one +/// feels like a distinct find rather than an interchangeable resource dump. +void stockIslands(Map &, GenerationContext &, const std::vector &, const char *stream); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Roads.cpp b/src/map/generator/shared/Roads.cpp new file mode 100644 index 000000000..655ad3963 --- /dev/null +++ b/src/map/generator/shared/Roads.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Roads.h" +#include "Map.h" +#include +#include +namespace MapGeneration +{ +std::vector cheapestRoute(const Torus &t, const std::vector &sources, + const std::vector &goal, + const std::vector &blocked, + const std::vector &costly) +{ + const int n = t.w * t.h; + std::vector cost(n, INT_MAX), parent(n, -1); + std::vector done(n, 0); + std::deque queue; + for (int i : sources) + { + cost[i] = 0; + queue.push_back(i); + } + int reached = -1; + while (!queue.empty() && reached < 0) + { + const int i = queue.front(); + queue.pop_front(); + if (done[i]) + continue; + done[i] = 1; + if (goal[i]) + { + reached = i; + break; + } + const int x = i % t.w, y = i / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (!dx && !dy) + continue; + const int next = t.at(x + dx, y + dy); + if (blocked[next]) + continue; + const int nextCost = cost[i] + (costly[next] ? 1 : 0); + if (nextCost < cost[next]) + { + cost[next] = nextCost; + parent[next] = i; + if (nextCost == cost[i]) + queue.push_front(next); + else + queue.push_back(next); + } + } + } + std::vector route; + for (int i = reached; i >= 0; i = parent[i]) + route.push_back(i); + return route; +} + +bool openRoad(Map &map, const Torus &t, const std::vector &sources, + const std::vector &goal, const std::vector *alsoBlocked) +{ + const int n = t.w * t.h; + std::vector blocked(n), costly(n); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + blocked[i] = map.isWater(x, y) || map.getBuilding(x, y) != NOGBID || + (alsoBlocked && (*alsoBlocked)[i]); + costly[i] = map.isResource(x, y); + } + const std::vector route = cheapestRoute(t, sources, goal, blocked, costly); + if (route.empty()) + return false; + for (int i : route) + if (map.isResource(i % t.w, i / t.w)) + map.setNoResource(i % t.w, i / t.w, 1); + return true; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Roads.h b/src/map/generator/shared/Roads.h new file mode 100644 index 000000000..d2f810621 --- /dev/null +++ b/src/map/generator/shared/Roads.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include +class Map; +namespace MapGeneration +{ +/// The cheapest walk from any source tile to any goal tile, eight-connected on the torus: +/// stepping onto a `costly` tile costs one, any other open tile nothing, and `blocked` tiles +/// are impassable. Returns the tiles of the walk from the goal it reached back to its source, or +/// nothing when no goal can be reached. A 0-1 breadth-first search, so ties go to the walk found +/// first in neighbour order. +std::vector cheapestRoute(const Torus &, const std::vector &sources, + const std::vector &goal, + const std::vector &blocked, + const std::vector &costly); + +/// Deposits may land anywhere, and a band of them could close a colony off from where it must +/// be able to walk. This keeps one way open: the cheapest walk from the sources to the goal +/// (deposits cost one, open ground nothing; water, buildings and `alsoBlocked` are impassable), +/// with only the deposits on it cleared. Almost always nothing is in the way and nothing is +/// cleared. False when no walk exists at all. +bool openRoad(Map &, const Torus &, const std::vector &sources, + const std::vector &goal, + const std::vector *alsoBlocked = nullptr); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Sketch.cpp b/src/map/generator/shared/Sketch.cpp new file mode 100644 index 000000000..5d2c66345 --- /dev/null +++ b/src/map/generator/shared/Sketch.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Sketch.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Map.h" +#include +#include +namespace MapGeneration +{ +void layBeaches(TerrainSketch &terrain, const Torus &t) +{ + const TerrainSketch original(terrain); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (original[i] == WATER) + continue; + bool shore = false; + for (int dy = -1; dy <= 1 && !shore; ++dy) + for (int dx = -1; dx <= 1 && !shore; ++dx) + shore = original[t.at(x + dx, y + dy)] == WATER; + if (shore) + terrain[i] = SAND; + } +} + +void writeUndermap(Map &map, const TerrainSketch &terrain) +{ + const int w = map.getW(), h = map.getH(); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + map.setUMTerrain(x, y, TerrainType(terrain[size_t(y) * w + x])); + map.rebuildTerrain(); +} + +int countTiles(const TerrainSketch &terrain, TerrainType type) +{ + return int(std::count(terrain.begin(), terrain.end(), (unsigned char)type)); +} + +std::vector raiseIslands(TerrainSketch &terrain, const Torus &t, GenerationContext &context, + const IslandPlacement &placement) +{ + std::vector islands; + const size_t area = size_t(t.w) * t.h; + std::vector water(area), land(area); + std::vector sea; + for (size_t i = 0; i < area; ++i) + { + water[i] = terrain[i] == WATER; + land[i] = !water[i]; + if (water[i]) + sea.push_back(int(i)); + } + if (placement.wanted <= 0 || sea.empty()) + return islands; + const std::vector offshore = stepsFrom(t, land, water); + const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 1.0, 1.6); + for (int attempt = 0; int(islands.size()) < placement.wanted && + attempt < placement.wanted * placement.attemptsPerIsland; + ++attempt) + { + const int at = sea[context.bounded(placement.stream, sea.size())]; + const int x = at % t.w, y = at / t.w; + const RadialShape shape((4 + context.bounded(placement.stream, 3)) * scale, 0.3, context, + placement.stream); + const double reach = shape.maximumRadius(); + if (offshore[at] < reach + placement.moat) + continue; + bool clear = true; + for (const Island &other : islands) + { + const double gap = + (placement.gapFromBothReaches ? reach + other.reach : 2 * reach) + placement.moat; + clear = clear && t.dist2(x, y, other.x, other.y) >= gap * gap; + } + if (!clear) + continue; + Island island{x, y, reach, {}}; + const int r = int(std::ceil(reach)); + for (int dy = -r; dy <= r; ++dy) + for (int dx = -r; dx <= r; ++dx) + if (std::hypot(double(dx), double(dy)) < + shape.radiusAt(std::atan2(double(dy), double(dx)))) + { + const int i = t.at(x + dx, y + dy); + terrain[i] = GRASS; + island.tiles.push_back(i); + } + islands.push_back(std::move(island)); + } + return islands; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Sketch.h b/src/map/generator/shared/Sketch.h new file mode 100644 index 000000000..882cfdf17 --- /dev/null +++ b/src/map/generator/shared/Sketch.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include "TerrainType.h" +#include +class Map; +struct GenerationContext; +namespace MapGeneration +{ +/// A map's undermap as one TerrainType per tile, designed in memory before anything is written +/// to the Map: the buffer a designed generator stamps its terrain into, lays beaches on, carves +/// lakes and raises islands in, and finally writes out with writeUndermap. +using TerrainSketch = std::vector; + +/// Grass may never touch water (Map::regenerateMap reads each tile from its four undermap +/// corners), so every land tile beside water becomes sand. Unlike Map::controlSand() this reads +/// only the original terrain, so the result doesn't depend on scan order and water is never +/// eaten away. +void layBeaches(TerrainSketch &, const Torus &); + +/// Writes the sketch to the map's undermap and rebuilds the tiles from it. +void writeUndermap(Map &, const TerrainSketch &); + +int countTiles(const TerrainSketch &, TerrainType); + +/// A small island raised out in open water: its centre, how far its outline can reach, and +/// every tile it covers. +struct Island +{ + int x, y; + double reach; + std::vector tiles; +}; + +struct IslandPlacement +{ + const char *stream; + /// How many islands to raise; each costs at most attemptsPerIsland candidate draws. + int wanted; + int attemptsPerIsland; + /// Water kept between an island and any coast, and between two islands. + double moat; + /// Two islands keep the sum of their reaches plus the moat apart. Off, they keep twice the + /// new island's reach plus the moat: City states' original rule, kept until its revision + /// moves, since the difference changes which candidates it accepts. + bool gapFromBothReaches = true; +}; + +/// Small islands out in the open water, each well clear of every coast and of each other, so +/// they are only ever reached by swimming. Islands never shrink below their size on a 128-tile +/// map: any smaller and the beach leaves no grass for a prize. +std::vector raiseIslands(TerrainSketch &, const Torus &, GenerationContext &, + const IslandPlacement &); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index 61571c761..a09d11d38 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -9,6 +9,7 @@ #include "GlobalContainer.h" #include "HeightMap.h" #include "Map.h" +#include "Pipeline.h" #include "Regions.h" #include "Resources.h" #include "StartingPositions.h" @@ -37,16 +38,12 @@ void MapGeneration::readResourceControls(HeightFieldOptions &options, void MapGeneration::openStartsBuriedByAmounts(Game &game, GenerationContext &context, const HeightFieldOptions &options) { - if (options.wheat == 100 && options.wood == 100 && options.stone == 100 && options.algae == 100) - return; // The resource bands are painted from map-wide noise levels with no awareness of where any team - // starts, so an amount well above the default widens them until they can wall a colony in with - // nowhere left to build — which the wheat/wood guarantee inside generateHeightField doesn't - // address, since a colony buried in wheat has wheat at its feet. placeStarts has already carved - // out the swarm's own rectangle by now, so nothing needs to be kept clear for it, and the - // guarantee runs once more in case the clearing took the colony's nearest crop with the wall. - openCrampedStarts(game, context); - guaranteeStartingResources(game, context, 24, 32); + // starts, so an amount well above the default widens them until they can wall a colony in. + // placeStarts has already carved out the swarm's own rectangle by now, so nothing needs to be + // kept clear for it. + reopenCrampedStarts(game, context, + {options.wheat, options.wood, options.stone, options.algae}); } bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, const HeightFieldOptions &options, diff --git a/src/map/generator/shared/Wedge.h b/src/map/generator/shared/Wedge.h new file mode 100644 index 000000000..669d76f59 --- /dev/null +++ b/src/map/generator/shared/Wedge.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Geometry.h" +#include "Grid.h" +#include +#include +namespace MapGeneration +{ +/// The wedge frame: the map divided into one equal wedge per colony round its centre, turned by +/// a random phase. A feature designed in the frame - arc offset across the wedge and radius out +/// from the centre - is stamped into every wedge alike, so whatever the roll, every colony gets +/// the same neighbourhood turned round the centre and the layout is fair for any colony count. +struct WedgeFrame +{ + Torus t; + int cx, cy; // the centre tile + double phase; // where wedge 0 begins, in radians + int teams; + double wedge; // each wedge's angle + + WedgeFrame(const Torus &torus, double phase, int teams) + : t(torus), cx(torus.w / 2), cy(torus.h / 2), phase(phase), teams(teams), + wedge(2 * kPi / teams) + { + } + + /// A tile seen from the frame. + struct Cell + { + int dx, dy; // offset from the centre, the short way round + double d, theta; // distance and angle from the centre + double turn; // angle past the phase, in [0, 2 pi) + int k; // which wedge + double u; // how far round the wedge, 0 at one edge and 1 at the other + double s; // arc offset from the wedge's middle, in tiles + }; + Cell cell(int x, int y) const + { + Cell c; + c.dx = t.offsetX(cx, x); + c.dy = t.offsetY(cy, y); + c.d = std::hypot(double(c.dx), double(c.dy)); + c.theta = std::atan2(double(c.dy), double(c.dx)); + c.turn = std::fmod(c.theta - phase + 4 * kPi, 2 * kPi); + place(c); + return c; + } + /// Bows the wedge boundaries sideways by `arc` tiles at the cell's radius. + void bend(Cell &c, double arc) const + { + c.turn = std::fmod(c.turn - arc / std::max(1.0, c.d) + 4 * kPi, 2 * kPi); + place(c); + } + + private: + void place(Cell &c) const + { + c.k = std::min(teams - 1, int(c.turn / wedge)); + c.u = c.turn / wedge - c.k; + c.s = c.d * (c.u - 0.5) * wedge; + } +}; + +/// A round feature in a wedge's frame: arc offset from the wedge's middle, radius from the +/// centre, a rough outline, stretched and turned. Shapes are evaluated in the wedge's frame too, +/// so every wedge gets the same. +struct Blob +{ + double s, r; + double stretch, turn; + RadialShape shape; + double reach() const { return shape.maximumRadius() * stretch; } + bool holds(double ds, double dr) const + { + // Rotate into the shape's own frame and undo its stretch. + const double c = std::cos(turn), sn = std::sin(turn); + const double px = (ds * c + dr * sn) / stretch, py = (-ds * sn + dr * c) * stretch; + return std::hypot(px, py) < shape.radiusAt(std::atan2(py, px)); + } +}; +} // namespace MapGeneration From acb72180fa1e18e0fa487a3198b979110ca6cf3c Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 15:54:12 -0400 Subject: [PATCH 102/136] Route every flood through Grid and consolidate the lattice noise The shared layer carried its own breadth-first floods in Resources (components, the resource-respecting and terrain-only reaches, the workers' room), StartingPositions (distance to a deposit), StartQuality (the workers' walk) and Distances (the legacy point flood), and Tidal flats and Symmetric arena each had another. floodFrom in Grid.h now does all of them, with a step limit and the visit order for the callers that need it; components come from Topology's connectedRegions. groundUnitTiles asks the engine's own passability rule once per map. Watershed's PeriodicNoise, Stone highlands' periodicNoise/fractalNoise/ percentile and Ring world's torusNoise move verbatim into shared/LatticeNoise, documented as three flavours to converge with a revision bump. The Fisher-Yates shuffle five generators wrote inline is GenerationContext:: shuffle; Contested commons' and Fjord's jagged island is Geometry's stampRoughDisc. Six shared headers gain their SPDX line, four legacy generators include their own header before `using namespace`, and Rugged archipelago loses four dead stores of the legacy resource amounts. Tidal flats keeps planting each neutral island in flood order from its first tile: the engine draws every deposit's amount from the gameplay RNG in call order, so the order is part of the map. Golden table: 133 rows, 0 failures; defaults test and custom-setup harness pass. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- src/SConscript | 1 + src/map/generator/core/GenerationContext.h | 7 + .../generators/ConcreteIslandsGenerator.cpp | 2 +- .../generators/ContestedCommonsGenerator.cpp | 4 +- .../generators/EvergladesGenerator.cpp | 3 +- .../generators/FjordContinentGenerator.cpp | 5 +- .../generator/generators/IslesGenerator.cpp | 2 +- .../generator/generators/MazeGenerator.cpp | 9 +- .../generators/RingWorldGenerator.cpp | 41 +---- .../generators/RuggedArchipelagoGenerator.cpp | 6 +- .../generators/ShatteredCoastGenerator.cpp | 2 +- .../generators/StoneHighlandsGenerator.cpp | 72 +-------- .../generators/SymmetricArenaGenerator.cpp | 65 ++------ .../generators/TidalFlatsGenerator.cpp | 35 ++--- .../generators/WatershedGenerator.cpp | 35 +---- src/map/generator/shared/Distances.cpp | 81 ++-------- src/map/generator/shared/Distances.h | 1 + src/map/generator/shared/Geometry.cpp | 7 + src/map/generator/shared/Geometry.h | 5 + src/map/generator/shared/Grid.cpp | 28 +++- src/map/generator/shared/Grid.h | 21 ++- src/map/generator/shared/LatticeNoise.cpp | 129 ++++++++++++++++ src/map/generator/shared/LatticeNoise.h | 41 +++++ src/map/generator/shared/Noise.h | 1 + src/map/generator/shared/Regions.h | 1 + src/map/generator/shared/Resources.cpp | 146 ++++-------------- src/map/generator/shared/Resources.h | 1 + src/map/generator/shared/StartQuality.cpp | 46 +----- .../generator/shared/StartingPositions.cpp | 55 ++----- src/map/generator/shared/StartingPositions.h | 1 + src/map/generator/shared/Terrain.h | 1 + 31 files changed, 340 insertions(+), 514 deletions(-) create mode 100644 src/map/generator/shared/LatticeNoise.cpp create mode 100644 src/map/generator/shared/LatticeNoise.h diff --git a/src/SConscript b/src/SConscript index 68380dd96..c16e0fffc 100644 --- a/src/SConscript +++ b/src/SConscript @@ -203,6 +203,7 @@ map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp map/generator/shared/Geometry.cpp map/generator/shared/Grid.cpp +map/generator/shared/LatticeNoise.cpp map/generator/shared/Pipeline.cpp map/generator/shared/Planting.cpp map/generator/shared/Roads.cpp diff --git a/src/map/generator/core/GenerationContext.h b/src/map/generator/core/GenerationContext.h index 1b4b1c074..4f01b1a51 100644 --- a/src/map/generator/core/GenerationContext.h +++ b/src/map/generator/core/GenerationContext.h @@ -5,6 +5,7 @@ #include #include #include +#include class Game; struct GenerationContext { @@ -15,6 +16,12 @@ struct GenerationContext explicit GenerationContext(const GenerationRequest &r) : request(r) {} std::mt19937 &stream(const std::string &name); std::uint32_t bounded(const std::string &name, std::uint32_t bound); + /// Shuffles a range, one bounded draw per element from the back forward. + template void shuffle(It first, It last, const std::string &name) + { + for (auto n = last - first; n > 1; --n) + std::swap(first[n - 1], first[bounded(name, std::uint32_t(n))]); + } static std::uint32_t deriveSeed(std::uint32_t seed, const std::string &name); static std::uint32_t randomSeed(); diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.cpp b/src/map/generator/generators/ConcreteIslandsGenerator.cpp index bd5d26f59..bda1c02a8 100644 --- a/src/map/generator/generators/ConcreteIslandsGenerator.cpp +++ b/src/map/generator/generators/ConcreteIslandsGenerator.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière // Copyright (C) 2008 Bradley Arsenault +#include "ConcreteIslandsGenerator.h" #include "Distances.h" #include "Game.h" #include "GenerationContext.h" @@ -18,7 +19,6 @@ #include #include using namespace MapGeneration; -#include "ConcreteIslandsGenerator.h" static bool generate(Game &game, GenerationContext &context) { context.stage = "layout"; diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp index 94611d188..7c8550ec4 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.cpp +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -20,9 +20,7 @@ namespace void createJaggedIsland(Map &map, GenerationContext &context, std::vector &grid, int area, int x, int y, int radius, double roughness) { - RadialShape shape(radius, roughness, context, "coast", 1.4); - stampShape(grid, map.getW(), map.getH(), area, ShapeTransform({double(x), double(y)}, 0), shape, - true); + stampRoughDisc(grid, map.getW(), map.getH(), area, x, y, radius, roughness, context, "coast"); } // What the stages of a roll hand each other: the working area grid and the next free area // number, where the colonies and the commons were seeded, and how big everything came out. diff --git a/src/map/generator/generators/EvergladesGenerator.cpp b/src/map/generator/generators/EvergladesGenerator.cpp index 192746b76..58436af49 100644 --- a/src/map/generator/generators/EvergladesGenerator.cpp +++ b/src/map/generator/generators/EvergladesGenerator.cpp @@ -227,8 +227,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) std::array sectors{}; for (int s = 0; s < kLeveeSectors; ++s) sectors[s] = s; - for (int i = kLeveeSectors - 1; i > 0; --i) - std::swap(sectors[i], sectors[context.bounded("glades-levee", i + 1)]); + context.shuffle(sectors.begin(), sectors.end(), "glades-levee"); const int open = int(std::lround((1 - g.levee) * kLeveeSectors)); for (int i = 0; i < open; ++i) home.standing[sectors[i]] = 0; diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 4e5f55ca6..0fcf658e8 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -24,9 +24,8 @@ double randomAngle(GenerationContext &context) void createJaggedIsland(Map &map, GenerationContext &context, std::vector &grid, int area, int x, int y, int radius, double roughness) { - RadialShape shape(radius, roughness, context, "outliers", 1.4); - stampShape(grid, map.getW(), map.getH(), area, ShapeTransform({double(x), double(y)}, 0), shape, - true); + stampRoughDisc(grid, map.getW(), map.getH(), area, x, y, radius, roughness, context, + "outliers"); } bool placeBankClump(Map &map, GenerationContext &context, diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index d76aeba5b..2d24853de 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière // Copyright (C) 2008 Bradley Arsenault +#include "IslesGenerator.h" #include "Distances.h" #include "Game.h" #include "GenerationContext.h" @@ -17,7 +18,6 @@ #include #include using namespace MapGeneration; -#include "IslesGenerator.h" // What the stages of a roll hand each other: the area grid and the next free area number, each // colony's seed point, weight and area, the spacing the dispersion found, the height field the // islands and bridges are raised in, the last distance field, and the bridges' tiles and area. diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 94d6749dc..52105cfcb 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -244,8 +244,7 @@ std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int const int r = mirrorY ? (g.rows - g.row(home)) % g.rows : g.row(home); home = ((r + offsetY) % g.rows) * g.columns + (c + offsetX) % g.columns; } - for (size_t i = homes.size(); i > 1; --i) - std::swap(homes[i - 1], homes[context.bounded("maze", i)]); + context.shuffle(homes.begin(), homes.end(), "maze"); return homes; } @@ -301,8 +300,7 @@ void addLoops(const MazeGrid &g, GenerationContext &context, if (!open[g.edgeId(cell, d)] && !isHome[g.neighbour(cell, d)]) closed.push_back(g.edgeId(cell, d)); } - for (size_t i = closed.size(); i > 1; --i) - std::swap(closed[i - 1], closed[context.bounded("maze", i)]); + context.shuffle(closed.begin(), closed.end(), "maze"); const size_t extra = std::min(closed.size(), size_t(freeCells * loopiness / 100)); for (size_t i = 0; i < extra; ++i) open[closed[i]] = 1; @@ -706,8 +704,7 @@ bool generate(Game &game, GenerationContext &context) // Without treasure the same fruit is scattered along the passages' shores instead. if (o.fruit > 0 && o.treasure) { - for (size_t i = deadEnds.size(); i > 1; --i) - std::swap(deadEnds[i - 1], deadEnds[context.bounded("resources", i)]); + context.shuffle(deadEnds.begin(), deadEnds.end(), "resources"); const int firstType = context.bounded("resources", 3); for (size_t i = 0; i < deadEnds.size(); ++i) placeTreasure(map, g, deadEnds[i], exitOf[deadEnds[i]], half, o.fruit, diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index 029aa6067..584f79731 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -4,6 +4,7 @@ #include "GenerationContext.h" #include "Grid.h" #include "Geometry.h" +#include "LatticeNoise.h" #include "Pipeline.h" #include "Planting.h" #include "Resources.h" @@ -112,46 +113,6 @@ std::vector closedCurve(int length, double falloff, std::mt19937 &rng, d return curve; } -// Value noise whose lattice cells divide the map exactly (both sides are powers of two), so it -// tiles the torus with no seam in either direction. Normalised to [-1, 1]. -std::vector torusNoise(int width, int height, std::mt19937 &rng) -{ - static const std::pair octaves[] = {{32, 0.4}, {16, 0.3}, {8, 0.2}, {4, 0.1}}; - std::vector field(size_t(width) * height, 0.0); - const auto ease = [](double t) { return t * t * (3 - 2 * t); }; - for (const auto &octave : octaves) - { - const int cell = std::min({octave.first, width, height}); - const int columns = width / cell, rows = height / cell; - std::vector lattice(size_t(columns) * rows); - for (double &value : lattice) - value = 2 * unitDraw(rng) - 1; - for (int y = 0; y < height; ++y) - { - const int r0 = y / cell, r1 = (r0 + 1) % rows; - const double ty = ease(double(y % cell) / cell); - for (int x = 0; x < width; ++x) - { - const int c0 = x / cell, c1 = (c0 + 1) % columns; - const double tx = ease(double(x % cell) / cell); - const double top = - lattice[r0 * columns + c0] * (1 - tx) + lattice[r0 * columns + c1] * tx; - const double bottom = - lattice[r1 * columns + c0] * (1 - tx) + lattice[r1 * columns + c1] * tx; - field[size_t(y) * width + x] += octave.second * (top * (1 - ty) + bottom * ty); - } - } - } - double peak = 0; - for (double value : field) - peak = std::max(peak, std::abs(value)); - std::vector result(field.size(), 0.0f); - if (peak > 0) - for (size_t i = 0; i < field.size(); ++i) - result[i] = float(field[i] / peak); - return result; -} - struct Belt { Axes axes; diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index 3b1cbb9ea..b81fdb945 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière // Copyright (C) 2008 Bradley Arsenault +#include "RuggedArchipelagoGenerator.h" #include "Distances.h" #include "Game.h" #include "GenerationContext.h" @@ -17,7 +18,6 @@ #include #include using namespace MapGeneration; -#include "RuggedArchipelagoGenerator.h" // The eight directions the growth passes below draw from: a pair of opposite neighbours at one // or two tiles, in the order the draw has always indexed them. @@ -188,7 +188,6 @@ static void resources(Game &game, GenerationContext &context, for (d = 0; d < islandsSize; d++) if (!map.isGrass(bootX[s], bootY[s] - d)) break; - amount = context.request.resourceAmounts[WOOD]; amount = d - smoothResources - 2; if (amount < 1) amount = 1; @@ -202,7 +201,6 @@ static void resources(Game &game, GenerationContext &context, for (d = 0; d < islandsSize; d++) if (!map.isGrass(bootX[s] - d, bootY[s])) break; - amount = context.request.resourceAmounts[CORN]; amount = d - smoothResources - 0; if (amount < 1) amount = 1; @@ -225,7 +223,6 @@ static void resources(Game &game, GenerationContext &context, for (d = 0; d < islandsSize; d++) if (!map.isGrass(bootX[s] + d, bootY[s] + d)) break; - amount = context.request.resourceAmounts[smallestResource]; amount = d - smoothResources - 3; if (amount < 1) amount = 1; @@ -238,7 +235,6 @@ static void resources(Game &game, GenerationContext &context, for (d = 0; d < 2 * islandsSize; d++) if (map.isWater(bootX[s] + d, bootY[s])) break; - amount = context.request.resourceAmounts[ALGA]; amount = smoothResources; p = d + smoothResources - 1 + amount / 2; if (amount > 0) diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 9800ae844..71a4f2626 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière // Copyright (C) 2008 Bradley Arsenault +#include "ShatteredCoastGenerator.h" #include "Distances.h" #include "Game.h" #include "GenerationContext.h" @@ -18,7 +19,6 @@ #include #include using namespace MapGeneration; -#include "ShatteredCoastGenerator.h" static void simulateRandomMap(GenerationContext &context, int smooth, double baseWater, double baseSand, double baseGrass, double *finalWater, diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 8d73c7ddb..a420809e2 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -4,6 +4,7 @@ #include "Game.h" #include "GenerationContext.h" #include "Grid.h" +#include "LatticeNoise.h" #include "Pipeline.h" #include "Resources.h" #include "Settlements.h" @@ -77,71 +78,6 @@ constexpr int kFruitGrove = 6; constexpr int kAreaPerColony = 1024; constexpr int kFar = INT_MAX / 4; -template -void shuffle(std::vector &values, GenerationContext &context, const char *stream) -{ - for (size_t i = values.size(); i > 1; --i) - std::swap(values[i - 1], values[context.bounded(stream, std::uint32_t(i))]); -} - -// Smooth value noise that tiles the map's torus exactly: a lattice of random values about -// `period` tiles apart (a whole number of lattice cells spans the map), blended with a -// smoothstep. Integer arithmetic throughout. Values are 0..65535. -std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng) -{ - const int gw = std::max(1, (w + period / 2) / std::max(1, period)); - const int gh = std::max(1, (h + period / 2) / std::max(1, period)); - std::vector lattice(size_t(gw) * gh); - for (int &v : lattice) - v = int(rng() >> 16); - const auto smooth = [](int t) - { return int(std::int64_t(t) * t * (3 * 1024 - 2 * t) / (1024 * 1024)); }; - std::vector field(size_t(w) * h); - for (int y = 0; y < h; ++y) - { - const std::int64_t fy = std::int64_t(y) * gh * 1024 / h; - const int y0 = int(fy >> 10), y1 = (y0 + 1) % gh, ty = smooth(int(fy & 1023)); - for (int x = 0; x < w; ++x) - { - const std::int64_t fx = std::int64_t(x) * gw * 1024 / w; - const int x0 = int(fx >> 10), x1 = (x0 + 1) % gw, tx = smooth(int(fx & 1023)); - const int a = lattice[size_t(y0) * gw + x0], b = lattice[size_t(y0) * gw + x1]; - const int c = lattice[size_t(y1) * gw + x0], d = lattice[size_t(y1) * gw + x1]; - const int top = a + (b - a) * tx / 1024, bottom = c + (d - c) * tx / 1024; - field[size_t(y) * w + x] = top + (bottom - top) * ty / 1024; - } - } - return field; -} - -// Octaves at period, period/2, ... weighted 2:1 per step. -std::vector fractalNoise(int w, int h, int period, int octaves, std::mt19937 &rng) -{ - std::vector sum(size_t(w) * h, 0); - int total = 0; - for (int octave = 0; octave < octaves; ++octave) - { - const int weight = 1 << (octaves - 1 - octave); - const std::vector layer = periodicNoise(w, h, std::max(2, period >> octave), rng); - for (size_t i = 0; i < sum.size(); ++i) - sum[i] += layer[i] * weight; - total += weight; - } - for (int &v : sum) - v /= total; - return sum; -} - -// The value below which `percent` of the given samples fall. -int percentile(std::vector samples, int percent) -{ - if (samples.empty()) - return 0; - const size_t k = std::min(samples.size() - 1, samples.size() * size_t(percent) / 100); - std::nth_element(samples.begin(), samples.begin() + k, samples.end()); - return samples[k]; -} - // Chebyshev steps from every tile to the nearest source tile; kFar with no sources. std::vector chebyshevDistance(const Torus &t, const std::vector &source) { @@ -838,7 +774,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (const auto &e : exclusive) if (int(e.second.size()) >= kMinimumBoundary) edges.push_back(e.first); - shuffle(edges, context, "highlands-passes"); + context.shuffle(edges.begin(), edges.end(), "highlands-passes"); for (const auto &e : edges) { if (sets.find(e.first) == sets.find(e.second)) @@ -858,7 +794,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) std::vector> fallback; for (const auto &e : anyPair) fallback.push_back(e.first); - shuffle(fallback, context, "highlands-passes"); + context.shuffle(fallback.begin(), fallback.end(), "highlands-passes"); for (const auto &e : fallback) if (sets.find(e.first) != sets.find(e.second) && carvePass(L, context, e.first, e.second, anyPair[e], alongRidge, o.passWidth)) @@ -1113,7 +1049,7 @@ void plantFruit(Map &map, GenerationContext &context, const Layout &L, valleys.push_back(v); if (valleys.empty()) return; - shuffle(valleys, context, "highlands-fruit"); + context.shuffle(valleys.begin(), valleys.end(), "highlands-fruit"); const int groves = std::max(1, int(std::int64_t(fruit) * n / 16384)); const int firstType = int(context.bounded("highlands-fruit", 3)); for (int g = 0; g < groves; ++g) diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index 08850972c..76c379deb 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -9,6 +9,7 @@ #include "GlobalContainer.h" #include "HeightMap.h" #include "Resources.h" +#include "Topology.h" #include "Unit.h" #include #include @@ -489,30 +490,16 @@ bool carvePaths(const Arena &a, const Layout &l, Terrain &t) // a matter of geometry alone, so every symmetry maps groups onto groups of the same size. std::vector> groupsOf(const std::vector &mask, int w, int h) { + const std::vector label = + MapGeneration::connectedRegions(mask, w, h, true, MapGeneration::GridNeighbors::Eight); std::vector> groups; - std::vector seen(mask.size(), 0); - for (size_t first = 0; first < mask.size(); ++first) - { - if (!mask[first] || seen[first]) - continue; - std::vector group{int(first)}; - seen[first] = 1; - for (size_t head = 0; head < group.size(); ++head) + for (size_t i = 0; i < label.size(); ++i) + if (label[i] >= 0) { - const int x = group[head] % w, y = group[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); - if (mask[j] && !seen[j]) - { - seen[j] = 1; - group.push_back(int(j)); - } - } + if (label[i] >= int(groups.size())) + groups.resize(label[i] + 1); + groups[label[i]].push_back(int(i)); } - groups.push_back(std::move(group)); - } return groups; } @@ -1033,7 +1020,6 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { const Map &map = game.map; const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; - const size_t n = size_t(w) * h; const Arena a = arenaFor(context.request); const Symmetry &s = a.symmetry; if (s.order() != teams) @@ -1111,39 +1097,14 @@ std::string validateWorld(const Game &game, const GenerationContext &context) static const char *const targets[] = {"wheat", "wood", "cherries", "oranges", "prunes", "the orchard"}; - std::vector walkable(n); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - walkable[size_t(y) * w + x] = - !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + const std::vector walkable = MapGeneration::walkableTiles(map); + const MapGeneration::Torus torus(map); + const auto workers = MapGeneration::unitTilesByTeam(map, teams); std::array first{}; for (int team = 0; team < teams; ++team) { - std::vector dist(n, -1), queue; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) - { - dist[size_t(y) * w + x] = 0; - queue.push_back(y * w + x); - } - } - for (size_t head = 0; head < queue.size(); ++head) - { - const int x = queue[head] % w, y = queue[head] / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const size_t j = size_t(wrap(y + dy, h)) * w + wrap(x + dx, w); - if (dist[j] < 0 && walkable[j]) - { - dist[j] = dist[size_t(queue[head])] + 1; - queue.push_back(int(j)); - } - } - } + const std::vector dist = MapGeneration::stepsFrom( + torus, MapGeneration::tileMask(torus, workers[team]), walkable); std::array steps; steps.fill(-1); const auto reach = [&](int slot, int d) diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index a9cb9b9ac..6ff099850 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -11,6 +11,7 @@ #include "Resources.h" #include "Settlements.h" #include "Sketch.h" +#include "Topology.h" #include "Unit.h" #include "Wedge.h" #include @@ -342,28 +343,20 @@ void stockIslands(Map &map, const Layout &L, GenerationContext &context, const T { const Torus &t = L.t; const int n = t.w * t.h; - // Every neutral island is its own component of grass; each gets its prize at its middle. - std::vector seen(n, 0); - for (int start = 0; start < n; ++start) + // Every neutral island is its own component of grass; each gets its prize at its middle. The + // engine draws each deposit's amount from the gameplay RNG as it is placed, so an island's + // tiles are planted in the order a flood from its first tile reaches them. + std::vector neutral(n, 0); + for (int i = 0; i < n; ++i) + neutral[i] = L.islandOf[i] == -3 && L.prizeOf[i] >= 0; + const std::vector label = connectedRegions(neutral, t.w, t.h, true, GridNeighbors::Eight); + std::vector firstTile; + for (int i = 0; i < n; ++i) + if (label[i] >= int(firstTile.size())) + firstTile.push_back(i); + for (const int start : firstTile) { - if (seen[start] || L.islandOf[start] != -3 || L.prizeOf[start] < 0) - continue; - std::vector tiles{start}; - seen[start] = 1; - for (size_t head = 0; head < tiles.size(); ++head) - { - const int i = tiles[head]; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int m = t.at(i % t.w + dx, i / t.w + dy); - if (!seen[m] && L.islandOf[m] == -3 && L.prizeOf[m] >= 0) - { - seen[m] = 1; - tiles.push_back(m); - } - } - } + const std::vector tiles = floodFrom(t, tileMask(t, {start}), neutral).visited; double sx = 0, sy = 0; for (int i : tiles) { diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index 7227990b1..6fbdbb4e3 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -5,6 +5,7 @@ #include "GenerationContext.h" #include "Geometry.h" #include "Grid.h" +#include "LatticeNoise.h" #include "Pipeline.h" #include "Resources.h" #include "Roads.h" @@ -161,37 +162,6 @@ Ripple makeRipple(GenerationContext &context, double base, double amplitude, int return ripple; } -// Smooth value noise on a lattice that tiles the map, so it wraps like the map does. -class PeriodicNoise -{ - public: - PeriodicNoise(int width, int height, double cell, std::mt19937 &random) - : width(width), height(height), columns(std::max(2, int(std::lround(width / cell)))), - rows(std::max(2, int(std::lround(height / cell)))), lattice(size_t(columns) * rows) - { - for (double &value : lattice) - value = random() / 4294967296.0; - } - double at(double x, double y) const - { - const double fx = x * columns / width, fy = y * rows / height; - const double x0 = std::floor(fx), y0 = std::floor(fy); - double tx = fx - x0, ty = fy - y0; - tx = tx * tx * (3 - 2 * tx); - ty = ty * ty * (3 - 2 * ty); - const int ix = wrapIndex(int(x0), columns), iy = wrapIndex(int(y0), rows); - const int jx = (ix + 1) % columns, jy = (iy + 1) % rows; - const double top = lattice[iy * columns + ix] * (1 - tx) + lattice[iy * columns + jx] * tx; - const double bottom = - lattice[jy * columns + ix] * (1 - tx) + lattice[jy * columns + jx] * tx; - return top * (1 - ty) + bottom * ty; - } - - private: - int width, height, columns, rows; - std::vector lattice; -}; - // The canonical frame: u runs along the coast, v across it with the sea at large v. The map is // the frame turned so the sea lies along one of its sides: orientations 0 and 1 put it south or // north, 2 and 3 east or west. @@ -1648,8 +1618,7 @@ void placeFruit(Game &game, GenerationContext &context, const Layout &layout, co Map &map = game.map; const int w = t.w, h = t.h; std::vector junctions = layout.junctions; - for (size_t i = junctions.size(); i > 1; --i) - std::swap(junctions[i - 1], junctions[context.bounded("resources", i)]); + context.shuffle(junctions.begin(), junctions.end(), "resources"); const int groves = int(scaledCount(std::max(3, w * h / 6000), fruitPercent)); int type = int(context.bounded("resources", 3)), placed = 0; for (size_t j = 0; j < junctions.size() && placed < groves; ++j) diff --git a/src/map/generator/shared/Distances.cpp b/src/map/generator/shared/Distances.cpp index 7476db193..f93595919 100644 --- a/src/map/generator/shared/Distances.cpp +++ b/src/map/generator/shared/Distances.cpp @@ -3,6 +3,7 @@ // Copyright (C) 2008 Bradley Arsenault #include "Distances.h" #include "GenerationContext.h" +#include "Grid.h" #include "HeightMap.h" #include "Map.h" #include "Regions.h" @@ -45,73 +46,19 @@ void adjustHeightmapFromPerlinNoise(Map &map, GenerationContext &context, std::v void computeDistances(Map &map, std::vector &sources, std::vector &obstacles, std::vector &heightmap) { - // Every cell is enqueued at most once - the `side == 0` guard below stops a second push - // once it's been visited - so a flood over the whole map never needs more than w*h queue - // slots. A flat, preallocated FIFO fills that bound with sequential writes; std::queue - // (std::deque-backed) instead grows by separately heap-allocated blocks, turning most - // pushes and pops into a pointer chase to a freshly touched cache line. - const size_t mapSize = size_t(map.getW()) * map.getH(); - std::vector places(mapSize); - size_t head = 0, tail = 0; - heightmap.clear(); - heightmap.resize(mapSize, 0); - for (unsigned int i = 0; i < sources.size(); ++i) - { - // Callers can list a tile more than once (Isles' land-bridge lines cross each other). Queueing - // a repeat takes a slot the w*h bound above never counted, so each one overran `places`, and - // the flood then read tile indices back from past its end. A repeated source adds nothing - // to the flood, so skip it. - const int index = sources[i].y * map.getW() + sources[i].x; - if (heightmap[index] == 1) - continue; - heightmap[index] = 1; - places[tail++] = index; - } - for (unsigned int i = 0; i < obstacles.size(); ++i) - { - heightmap[obstacles[i].y * map.getW() + obstacles[i].x] = -1; - } - - Uint32 wDec = map.wDec; - Uint32 hMask = map.hMask; - Uint32 wMask = map.wMask; - while (head < tail) - { - int deltaAddrG = places[head++]; - - size_t y = deltaAddrG >> wDec; // Calculate the coordinates of - size_t x = deltaAddrG & wMask; // the current field and of the - - size_t yu = ((y - 1) & hMask); // fields next to it. - size_t yd = ((y + 1) & hMask); // We live on a torus! If we are on - size_t xl = ((x - 1) & wMask); // the "last line" of the map, the - size_t xr = ((x + 1) & wMask); // next line is the line 0 again. - - int g = heightmap[(y << wDec) | x] + 1; - - size_t deltaAddrC[8]; - int *addr; - int side; - - deltaAddrC[0] = (yu << wDec) | xl; // Calculate the positions of the - deltaAddrC[1] = (yu << wDec) | x; // 8 fields next to us from their - deltaAddrC[2] = (yu << wDec) | xr; // coordinates. - deltaAddrC[3] = (y << wDec) | xr; - deltaAddrC[4] = (yd << wDec) | xr; - deltaAddrC[5] = (yd << wDec) | x; - deltaAddrC[6] = (yd << wDec) | xl; - deltaAddrC[7] = (y << wDec) | xl; - for (int ci = 0; ci < 8; ci++) // Check for each of this fields if we - { // can improve its gradient value - addr = &heightmap[deltaAddrC[ci]]; - side = *addr; - if (side == 0) - { - *addr = g; - places[tail++] = deltaAddrC[ci]; - } - } - } + // Callers can list a tile more than once (Isles' land-bridge lines cross each other); a mask + // takes it once. Sources and obstacles never overlap in any caller. + const Torus t(map); + std::vector source(size_t(t.size()), 0), open(size_t(t.size()), 1); + for (const MapGeneratorPoint &p : sources) + source[t.at(p.x, p.y)] = 1; + for (const MapGeneratorPoint &p : obstacles) + open[t.at(p.x, p.y)] = 0; + const std::vector steps = stepsFrom(t, source, open); + // The historical encoding: a source reads 1, each ring one more, obstacles -1, unreached 0. + heightmap.assign(steps.size(), 0); + for (size_t i = 0; i < steps.size(); ++i) + heightmap[i] = !open[i] ? -1 : steps[i] < 0 ? 0 : steps[i] + 1; } int computeAverageDistance(Map &map, std::vector &grid, int areaN, diff --git a/src/map/generator/shared/Distances.h b/src/map/generator/shared/Distances.h index aa88ea9f6..001ba7e26 100644 --- a/src/map/generator/shared/Distances.h +++ b/src/map/generator/shared/Distances.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "Regions.h" namespace MapGeneration diff --git a/src/map/generator/shared/Geometry.cpp b/src/map/generator/shared/Geometry.cpp index 704950958..d292e1b37 100644 --- a/src/map/generator/shared/Geometry.cpp +++ b/src/map/generator/shared/Geometry.cpp @@ -74,4 +74,11 @@ void stampShape(std::vector &labels, int width, int height, int label, labels[size_t(y) * width + x] = label; } } +void stampRoughDisc(std::vector &labels, int width, int height, int label, int x, int y, + double radius, double roughness, GenerationContext &context, + const std::string &stream, double amplitudeMaximum) +{ + RadialShape shape(radius, roughness, context, stream, amplitudeMaximum); + stampShape(labels, width, height, label, ShapeTransform({double(x), double(y)}, 0), shape, true); +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Geometry.h b/src/map/generator/shared/Geometry.h index d94ac04af..98f09f71c 100644 --- a/src/map/generator/shared/Geometry.h +++ b/src/map/generator/shared/Geometry.h @@ -39,4 +39,9 @@ class RadialShape // Wrapping visits each tile once using its nearest image relative to the shape center. void stampShape(std::vector &labels, int width, int height, int label, const ShapeTransform &, const RadialShape &, bool wrap); +// A rough disc round a tile: a RadialShape of the given radius and roughness drawn from `stream`, +// stamped wrapping. Jagged islands are made of these. +void stampRoughDisc(std::vector &labels, int width, int height, int label, int x, int y, + double radius, double roughness, GenerationContext &, const std::string &stream, + double amplitudeMaximum = 1.4); } // namespace MapGeneration diff --git a/src/map/generator/shared/Grid.cpp b/src/map/generator/shared/Grid.cpp index 0828cb2c9..7244d773f 100644 --- a/src/map/generator/shared/Grid.cpp +++ b/src/map/generator/shared/Grid.cpp @@ -7,11 +7,12 @@ namespace MapGeneration { Torus::Torus(const Map &map) : w(map.getW()), h(map.getH()) {} -std::vector stepsFrom(const Torus &t, const std::vector &source, - const std::vector &open) +Flood floodFrom(const Torus &t, const std::vector &source, + const std::vector &open, int limit) { - std::vector dist(size_t(t.size()), -1); - std::vector queue; + Flood flood; + std::vector &dist = flood.steps, &queue = flood.visited; + dist.assign(size_t(t.size()), -1); queue.reserve(dist.size()); for (size_t i = 0; i < dist.size(); ++i) if (source[i]) @@ -21,6 +22,8 @@ std::vector stepsFrom(const Torus &t, const std::vector &sou } for (size_t head = 0; head < queue.size(); ++head) { + if (dist[queue[head]] >= limit) + continue; const int x = queue[head] % t.w, y = queue[head] / t.w; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) @@ -33,7 +36,13 @@ std::vector stepsFrom(const Torus &t, const std::vector &sou } } } - return dist; + return flood; +} + +std::vector stepsFrom(const Torus &t, const std::vector &source, + const std::vector &open) +{ + return floodFrom(t, source, open).steps; } std::vector stepsFrom(const Torus &t, const std::vector &source) @@ -75,6 +84,15 @@ std::vector walkableTiles(const Map &map) return open; } +std::vector groundUnitTiles(const Map &map) +{ + std::vector hard(size_t(map.getW()) * map.getH(), 0); + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + hard[size_t(y) * map.getW() + x] = map.isHardSpaceForGroundUnit(x, y, false, 0); + return hard; +} + int firstColonyCutOff(const std::vector &steps, const std::vector> &units, int first) { diff --git a/src/map/generator/shared/Grid.h b/src/map/generator/shared/Grid.h index 7dd01ab54..0c82c0f00 100644 --- a/src/map/generator/shared/Grid.h +++ b/src/map/generator/shared/Grid.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include +#include #include #include #include @@ -45,9 +46,20 @@ struct Torus /// The four cardinal steps, in the order the generators have always listed them. constexpr int kCardinalSteps[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; -/// Breadth-first steps from every source tile, eight-connected across the wrap, through open -/// tiles only: 0 at a source, -1 where nothing was reached. Every step costs one, so the field -/// is the same whatever order the tiles were visited in. +/// A breadth-first flood, eight-connected across the wrap, through open tiles only. +struct Flood +{ + /// Steps from the nearest source: 0 at a source, -1 where nothing was reached. Every step + /// costs one, so the field is the same whatever order the tiles were visited in. + std::vector steps; + /// Every tile reached, in the order it was reached. + std::vector visited; +}; +/// Floods from every source tile. A source is reached whether or not it is open. A tile +/// `limit` steps out is reached but not stepped on from. +Flood floodFrom(const Torus &, const std::vector &source, + const std::vector &open, int limit = INT_MAX); +/// The steps of floodFrom. std::vector stepsFrom(const Torus &, const std::vector &source, const std::vector &open); /// The same flood with every tile open. @@ -60,6 +72,9 @@ std::vector> unitTilesByTeam(const Map &, int teams); /// The tiles a ground unit can stand on and walk through: land carrying no deposit and no /// building. std::vector walkableTiles(const Map &); +/// The same question put to the engine's own rule, Map::isHardSpaceForGroundUnit, for a unit +/// that cannot swim and belongs to no team. +std::vector groundUnitTiles(const Map &); /// The lowest colony from `first` on none of whose units the flood reached, or -1 when every /// one was: the check every validator makes that colonies can walk to colony 0. int firstColonyCutOff(const std::vector &steps, const std::vector> &units, diff --git a/src/map/generator/shared/LatticeNoise.cpp b/src/map/generator/shared/LatticeNoise.cpp new file mode 100644 index 000000000..f5d74d857 --- /dev/null +++ b/src/map/generator/shared/LatticeNoise.cpp @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "LatticeNoise.h" +#include +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +int wrapIndex(int value, int period) +{ + return ((value % period) + period) % period; +} +} // namespace + +PeriodicNoise::PeriodicNoise(int width, int height, double cell, std::mt19937 &random) + : width(width), height(height), columns(std::max(2, int(std::lround(width / cell)))), + rows(std::max(2, int(std::lround(height / cell)))), lattice(size_t(columns) * rows) +{ + for (double &value : lattice) + value = random() / 4294967296.0; +} + +double PeriodicNoise::at(double x, double y) const +{ + const double fx = x * columns / width, fy = y * rows / height; + const double x0 = std::floor(fx), y0 = std::floor(fy); + double tx = fx - x0, ty = fy - y0; + tx = tx * tx * (3 - 2 * tx); + ty = ty * ty * (3 - 2 * ty); + const int ix = wrapIndex(int(x0), columns), iy = wrapIndex(int(y0), rows); + const int jx = (ix + 1) % columns, jy = (iy + 1) % rows; + const double top = lattice[iy * columns + ix] * (1 - tx) + lattice[iy * columns + jx] * tx; + const double bottom = lattice[jy * columns + ix] * (1 - tx) + lattice[jy * columns + jx] * tx; + return top * (1 - ty) + bottom * ty; +} + +std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng) +{ + const int gw = std::max(1, (w + period / 2) / std::max(1, period)); + const int gh = std::max(1, (h + period / 2) / std::max(1, period)); + std::vector lattice(size_t(gw) * gh); + for (int &v : lattice) + v = int(rng() >> 16); + const auto smooth = [](int t) + { return int(std::int64_t(t) * t * (3 * 1024 - 2 * t) / (1024 * 1024)); }; + std::vector field(size_t(w) * h); + for (int y = 0; y < h; ++y) + { + const std::int64_t fy = std::int64_t(y) * gh * 1024 / h; + const int y0 = int(fy >> 10), y1 = (y0 + 1) % gh, ty = smooth(int(fy & 1023)); + for (int x = 0; x < w; ++x) + { + const std::int64_t fx = std::int64_t(x) * gw * 1024 / w; + const int x0 = int(fx >> 10), x1 = (x0 + 1) % gw, tx = smooth(int(fx & 1023)); + const int a = lattice[size_t(y0) * gw + x0], b = lattice[size_t(y0) * gw + x1]; + const int c = lattice[size_t(y1) * gw + x0], d = lattice[size_t(y1) * gw + x1]; + const int top = a + (b - a) * tx / 1024, bottom = c + (d - c) * tx / 1024; + field[size_t(y) * w + x] = top + (bottom - top) * ty / 1024; + } + } + return field; +} + +std::vector fractalNoise(int w, int h, int period, int octaves, std::mt19937 &rng) +{ + std::vector sum(size_t(w) * h, 0); + int total = 0; + for (int octave = 0; octave < octaves; ++octave) + { + const int weight = 1 << (octaves - 1 - octave); + const std::vector layer = periodicNoise(w, h, std::max(2, period >> octave), rng); + for (size_t i = 0; i < sum.size(); ++i) + sum[i] += layer[i] * weight; + total += weight; + } + for (int &v : sum) + v /= total; + return sum; +} + +std::vector torusNoise(int width, int height, std::mt19937 &rng) +{ + static const std::pair octaves[] = {{32, 0.4}, {16, 0.3}, {8, 0.2}, {4, 0.1}}; + std::vector field(size_t(width) * height, 0.0); + const auto ease = [](double t) { return t * t * (3 - 2 * t); }; + for (const auto &octave : octaves) + { + const int cell = std::min({octave.first, width, height}); + const int columns = width / cell, rows = height / cell; + std::vector lattice(size_t(columns) * rows); + for (double &value : lattice) + value = 2 * (rng() / 4294967296.0) - 1; + for (int y = 0; y < height; ++y) + { + const int r0 = y / cell, r1 = (r0 + 1) % rows; + const double ty = ease(double(y % cell) / cell); + for (int x = 0; x < width; ++x) + { + const int c0 = x / cell, c1 = (c0 + 1) % columns; + const double tx = ease(double(x % cell) / cell); + const double top = + lattice[r0 * columns + c0] * (1 - tx) + lattice[r0 * columns + c1] * tx; + const double bottom = + lattice[r1 * columns + c0] * (1 - tx) + lattice[r1 * columns + c1] * tx; + field[size_t(y) * width + x] += octave.second * (top * (1 - ty) + bottom * ty); + } + } + } + double peak = 0; + for (double value : field) + peak = std::max(peak, std::abs(value)); + std::vector result(field.size(), 0.0f); + if (peak > 0) + for (size_t i = 0; i < field.size(); ++i) + result[i] = float(field[i] / peak); + return result; +} + +int percentile(std::vector samples, int percent) +{ + if (samples.empty()) + return 0; + const size_t k = std::min(samples.size() - 1, samples.size() * size_t(percent) / 100); + std::nth_element(samples.begin(), samples.begin() + k, samples.end()); + return samples[k]; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/LatticeNoise.h b/src/map/generator/shared/LatticeNoise.h new file mode 100644 index 000000000..e26b27e9c --- /dev/null +++ b/src/map/generator/shared/LatticeNoise.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +#include +namespace MapGeneration +{ +// Smooth value noise on a lattice that tiles the map's torus exactly, so a field wraps like the +// map does and the seam can't be seen. HeightMap (Perlin noise faded across the wrap) is the +// other option; these are cheaper and exactly periodic. +// +// Three flavours are in use, each with its own arithmetic, and a generator's output depends on +// which it draws from. They should converge on one once a revision bump is acceptable; until +// then, a new generator should take PeriodicNoise, which samples at any continuous point. + +/// A lattice of random values about `cell` tiles apart (a whole number of cells spans the map), +/// blended with a smoothstep and sampled at any point. Values are in [0, 1). +class PeriodicNoise +{ + public: + PeriodicNoise(int width, int height, double cell, std::mt19937 &random); + double at(double x, double y) const; + + private: + int width, height, columns, rows; + std::vector lattice; +}; + +/// The same idea in integer arithmetic, one value per tile: a lattice about `period` tiles +/// apart, smoothstep-blended. Values are 0..65535. +std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng); + +/// periodicNoise at period, period/2, ... weighted 2:1 per octave, normalised back to 0..65535. +std::vector fractalNoise(int w, int h, int period, int octaves, std::mt19937 &rng); + +/// Four fixed octaves (32, 16, 8 and 4 tiles, or the map if smaller) whose cells divide the map +/// exactly, normalised to [-1, 1] by the field's own peak. +std::vector torusNoise(int width, int height, std::mt19937 &rng); + +/// The value below which `percent` of the samples fall; 0 with no samples. +int percentile(std::vector samples, int percent); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Noise.h b/src/map/generator/shared/Noise.h index b6f2a7232..e861f8c86 100644 --- a/src/map/generator/shared/Noise.h +++ b/src/map/generator/shared/Noise.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include diff --git a/src/map/generator/shared/Regions.h b/src/map/generator/shared/Regions.h index 5605a2f58..5d39d6ddb 100644 --- a/src/map/generator/shared/Regions.h +++ b/src/map/generator/shared/Regions.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include class Game; diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 207cf90fd..0f2481bd3 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -7,11 +7,13 @@ #include "Game.h" #include "GenerationContext.h" #include "GenerationResult.h" +#include "Grid.h" #include "HeightMap.h" #include "Map.h" #include "Regions.h" #include "StartingPositions.h" #include "Terrain.h" +#include "Topology.h" #include "Unit.h" #include #include @@ -134,42 +136,14 @@ namespace std::vector computeComponents(const Map &map, bool water, int &numComponents) { const int w = map.getW(), h = map.getH(); - std::vector component(size_t(w) * h, -1); - // Bounded by the component[np] < 0 guard below to at most w*h enqueues per component, and - // every tile belongs to exactly one component - a flat preallocated FIFO reused across - // components, the same fix as floodReach below and computeDistances in Distances.cpp. - std::vector queue(size_t(w) * h); - int nextId = 0; + std::vector kind(size_t(w) * h); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) - { - const int start = y * w + x; - if (component[start] != -1 || map.isWater(x, y) != water) - continue; - size_t qHead = 0, qTail = 0; - component[start] = nextId; - queue[qTail++] = start; - while (qHead < qTail) - { - const int p = queue[qHead++]; - const int px = p % w, py = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (!dx && !dy) - continue; - const int nx = map.normalizeX(px + dx), ny = map.normalizeY(py + dy); - const int np = ny * w + nx; - if (component[np] == -1 && map.isWater(nx, ny) == water) - { - component[np] = nextId; - queue[qTail++] = np; - } - } - } - ++nextId; - } - numComponents = nextId; + kind[size_t(y) * w + x] = map.isWater(x, y) == water; + const std::vector component = connectedRegions(kind, w, h, true, GridNeighbors::Eight); + numComponents = 0; + for (int c : component) + numComponents = std::max(numComponents, c + 1); return component; } @@ -416,21 +390,14 @@ struct ReachResult ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int closeRange, int clearRadius) { - const int w = map.getW(), h = map.getH(); + const Torus t(map); ReachResult r; - r.dist.assign(size_t(w) * h, -1); - // Bounded by the r.dist[np] < 0 guard below to at most w*h enqueues - a flat preallocated - // FIFO instead of std::queue's std::deque, which grows by separately heap-allocated - // blocks (same fix as computeDistances in Distances.cpp). - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - int start = bootY * w + bootX; - r.dist[start] = 0; - q[qTail++] = start; - while (qHead < qTail) + const Flood flood = + floodFrom(t, tileMask(t, {bootY * t.w + bootX}), groundUnitTiles(map), exploreLimit); + r.dist = flood.steps; + for (int p : flood.visited) { - int p = q[qHead++]; - int x = p % w, y = p / w; + const int x = p % t.w, y = p / t.w; if (map.getUMTerrain(x, y) == GRASS && r.dist[p] >= clearRadius && r.dist[p] <= exploreLimit) (r.dist[p] <= closeRange ? r.closeGrass : r.farGrass) @@ -440,19 +407,11 @@ ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int clo { if (dx == 0 && dy == 0) continue; - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int np = ny * w + nx; - int resType = map.getResource(nx, ny).type; + const int resType = map.getResource(t.x(x + dx), t.y(y + dy)).type; if (resType == CORN && r.wheatDist < 0) r.wheatDist = r.dist[p] + 1; if (resType == WOOD && r.woodDist < 0) r.woodDist = r.dist[p] + 1; - if (r.dist[np] < 0 && r.dist[p] < exploreLimit && - map.isHardSpaceForGroundUnit(nx, ny, false, 0)) - { - r.dist[np] = r.dist[p] + 1; - q[qTail++] = np; - } } } return r; @@ -466,35 +425,11 @@ ReachResult floodReach(Map &map, int bootX, int bootY, int exploreLimit, int clo std::vector terrainOnlyReach(Map &map, int bootX, int bootY, int limit, const std::vector *protectedWalls) { - const int w = map.getW(), h = map.getH(); - std::vector dist(size_t(w) * h, -1); - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - int start = bootY * w + bootX; - dist[start] = 0; - q[qTail++] = start; - while (qHead < qTail) - { - int p = q[qHead++]; - if (dist[p] >= limit) - continue; - int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (dx == 0 && dy == 0) - continue; - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int np = ny * w + nx; - if (dist[np] < 0 && !map.isWater(nx, ny) && - !(protectedWalls && (*protectedWalls)[np])) - { - dist[np] = dist[p] + 1; - q[qTail++] = np; - } - } - } - return dist; + const Torus t(map); + std::vector open(size_t(t.size())); + for (int i = 0; i < t.size(); ++i) + open[i] = !map.isWater(i % t.w, i / t.w) && !(protectedWalls && (*protectedWalls)[i]); + return floodFrom(t, tileMask(t, {bootY * t.w + bootX}), open, limit).steps; } // Clears the resource tiles directly responsible for a team's cramped pocket: exactly the @@ -617,43 +552,14 @@ struct WorkerReach }; WorkerReach reachFromWorkers(Map &map, int team, int range) { - const int w = map.getW(), h = map.getH(); + const Torus t(map); + const Flood flood = floodFrom(t, tileMask(t, unitTilesByTeam(map, team + 1)[team]), + groundUnitTiles(map), range); WorkerReach r; - r.dist.assign(size_t(w) * h, -1); - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid != NOGUID && Unit::GIDtoTeam(gid) == team) - { - r.dist[size_t(y) * w + x] = 0; - q[qTail++] = y * w + x; - } - } - while (qHead < qTail) - { - const int p = q[qHead++]; - const int x = p % w, y = p / w; - if (map.isFreeForBuilding(x, y, 4, 4)) + r.dist = flood.steps; + for (int p : flood.visited) + if (map.isFreeForBuilding(p % t.w, p / t.w, 4, 4)) ++r.sites; - if (r.dist[p] >= range) - continue; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (dx == 0 && dy == 0) - continue; - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy), - np = ny * w + nx; - if (r.dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) - { - r.dist[np] = r.dist[p] + 1; - q[qTail++] = np; - } - } - } return r; } } // namespace diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h index 4aebd650d..3178994fc 100644 --- a/src/map/generator/shared/Resources.h +++ b/src/map/generator/shared/Resources.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "Regions.h" #include diff --git a/src/map/generator/shared/StartQuality.cpp b/src/map/generator/shared/StartQuality.cpp index 696cd322b..d5316ad6b 100644 --- a/src/map/generator/shared/StartQuality.cpp +++ b/src/map/generator/shared/StartQuality.cpp @@ -2,6 +2,7 @@ #include "StartQuality.h" #include "FertilityField.h" #include "Game.h" +#include "Grid.h" #include "Map.h" #include "Unit.h" #include @@ -21,37 +22,8 @@ double clampUnit(double v) /// Workers, not the boot tile: the swarm occupies the boot tile and nobody walks out of it. std::vector walkFromWorkers(const Map &map, const std::vector &workers) { - const int w = map.getW(), h = map.getH(); - std::vector dist(size_t(w) * h, -1); - // Bounded by the dist[np] < 0 guard below to at most w*h enqueues - a flat preallocated FIFO - // instead of std::queue's std::deque, which grows by separately heap-allocated blocks. - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - for (int p : workers) - if (dist[p] < 0) - { - dist[p] = 0; - q[qTail++] = p; - } - while (qHead < qTail) - { - const int p = q[qHead++]; - const int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (!dx && !dy) - continue; - const int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - const int np = ny * w + nx; - if (dist[np] < 0 && map.isHardSpaceForGroundUnit(nx, ny, false, 0)) - { - dist[np] = dist[p] + 1; - q[qTail++] = np; - } - } - } - return dist; + const Torus t(map); + return stepsFrom(t, tileMask(t, workers), groundUnitTiles(map)); } } // namespace @@ -65,17 +37,7 @@ StartQualityReport scoreStarts(Game &game, int requestedTeams, const StartQualit if (nbTeams <= 0) return report; - std::vector> workers(nbTeams); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const Uint16 gid = map.getGroundUnit(x, y); - if (gid == NOGUID) - continue; - const int team = Unit::GIDtoTeam(gid); - if (team >= 0 && team < nbTeams) - workers[team].push_back(y * w + x); - } + const std::vector> workers = unitTilesByTeam(map, nbTeams); for (const auto &team : workers) if (team.empty()) return report; // nothing walked out of this colony; there is nothing to score diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp index f797af897..1aa6c4a56 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/StartingPositions.cpp @@ -7,6 +7,7 @@ #include "GenerationContext.h" #include "GeneratorDefinition.h" #include "GlobalContainer.h" +#include "Grid.h" #include "HeightMap.h" #include "Map.h" #include "Regions.h" @@ -309,12 +310,7 @@ namespace // branchy, multi-array accessor call at every visited tile into a single sequential array read. std::vector buildHardSpaceGrid(Map &map) { - const int w = map.getW(), h = map.getH(); - std::vector hard(size_t(w) * h); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - hard[size_t(y) * w + x] = map.isHardSpaceForGroundUnit(x, y, false, 0) ? 1 : 0; - return hard; + return groundUnitTiles(map); } // Walking distance from every tile to the nearest deposit of one resource. Workers stand @@ -322,52 +318,29 @@ std::vector buildHardSpaceGrid(Map &map) // walkable tiles touching one. One flood answers the question for every tile on the map, which // is what makes scoring a few hundred candidate sites cheap enough to do exhaustively. Distances // on any map this engine supports fit comfortably in 16 bits, halving the footprint of an array -// this flood (and every scoreAsBuilt call after it) touches over and over. +// every scoreAsBuilt call after it touches over and over. std::vector distanceToResource(Map &map, const std::vector &hard, int resourceType) { - const int w = map.getW(), h = map.getH(); - std::vector dist(size_t(w) * h, -1); - // Every cell is enqueued at most once (the dist[np] < 0 guard below), so this never needs - // more than w*h slots - a flat preallocated FIFO instead of std::queue's - // std::deque-backed, block-by-block growth (see computeDistances for the same fix). - std::vector q(size_t(w) * h); - size_t qHead = 0, qTail = 0; - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) + const Torus t(map); + std::vector beside(size_t(t.size()), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) { if (map.getResource(x, y).type != resourceType) continue; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) { - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int np = ny * w + nx; - if (dist[np] < 0 && hard[np]) - { - dist[np] = 0; - q[qTail++] = np; - } + const int np = t.at(x + dx, y + dy); + if (hard[np]) + beside[np] = 1; } } - while (qHead < qTail) - { - int p = q[qHead++]; - int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (dx == 0 && dy == 0) - continue; - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int np = ny * w + nx; - if (dist[np] < 0 && hard[np]) - { - dist[np] = dist[p] + 1; - q[qTail++] = np; - } - } - } + const std::vector steps = stepsFrom(t, beside, hard); + std::vector dist(steps.size()); + for (size_t i = 0; i < steps.size(); ++i) + dist[i] = std::int16_t(steps[i]); return dist; } } // namespace diff --git a/src/map/generator/shared/StartingPositions.h b/src/map/generator/shared/StartingPositions.h index 618f241e6..67c7a0801 100644 --- a/src/map/generator/shared/StartingPositions.h +++ b/src/map/generator/shared/StartingPositions.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "Regions.h" namespace MapGeneration diff --git a/src/map/generator/shared/Terrain.h b/src/map/generator/shared/Terrain.h index 38913e44e..b25b5ee16 100644 --- a/src/map/generator/shared/Terrain.h +++ b/src/map/generator/shared/Terrain.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "GenerationRequest.h" #include "HeightMap.h" From b5aae89b204f1d244f74c4452614330aeed7b84d Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 15:58:27 -0400 Subject: [PATCH 103/136] Quarantine the legacy toolkit and split the height-field pipeline Regions, Distances and StartingPositions are the 2008 area-grid toolkit: integer area numbers, MapGeneratorPoint, in-out vector arguments. Only the six older generators build on them, and nothing in them composes with the tile masks and Torus the designed generators use. They move to shared/legacy/ with a note on what they are, and chooseBalancedStarts, the one modern piece among them, becomes shared/BalancedStarts. generateHeightField was a 340-line function doing terrain classification, resource bands, start choice with a legacy fallback, the reachability guarantee and fruit in one go, so no height-field stage could be reused elsewhere. It is now heightFieldTiling, classifyHeightField, paintHeightFieldTerrain, paintHeightFieldResources, chooseHeightFieldStarts and plantHeightFieldGroves, each callable on its own, with generateHeightField composing them in the same order. HeightFieldOptions:: fromRequest replaces the eight-field positional aggregate init the four height-field generators each wrote out. Golden table: 133 rows, 0 failures; defaults test and custom-setup harness pass. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- SConstruct | 3 +- src/SConscript | 7 +- .../generators/CraterLakesGenerator.cpp | 4 +- .../generator/generators/IslandsGenerator.cpp | 4 +- .../generator/generators/RiverGenerator.cpp | 4 +- .../generator/generators/SwampGenerator.cpp | 5 +- src/map/generator/shared/BalancedStarts.cpp | 249 +++++++++++ src/map/generator/shared/BalancedStarts.h | 13 + src/map/generator/shared/Terrain.cpp | 416 +++++++++--------- src/map/generator/shared/Terrain.h | 52 ++- .../shared/{ => legacy}/Distances.cpp | 0 .../generator/shared/{ => legacy}/Distances.h | 2 + .../generator/shared/{ => legacy}/Regions.cpp | 0 .../generator/shared/{ => legacy}/Regions.h | 4 + .../shared/{ => legacy}/StartingPositions.cpp | 237 ---------- .../shared/{ => legacy}/StartingPositions.h | 12 +- 16 files changed, 554 insertions(+), 458 deletions(-) create mode 100644 src/map/generator/shared/BalancedStarts.cpp create mode 100644 src/map/generator/shared/BalancedStarts.h rename src/map/generator/shared/{ => legacy}/Distances.cpp (100%) rename src/map/generator/shared/{ => legacy}/Distances.h (78%) rename src/map/generator/shared/{ => legacy}/Regions.cpp (100%) rename src/map/generator/shared/{ => legacy}/Regions.h (83%) rename src/map/generator/shared/{ => legacy}/StartingPositions.cpp (61%) rename src/map/generator/shared/{ => legacy}/StartingPositions.h (68%) diff --git a/SConstruct b/SConstruct index f6603dfcf..be09d4c95 100644 --- a/SConstruct +++ b/SConstruct @@ -334,7 +334,8 @@ def main(): '#src/gui', '#src/map', '#src/map/edit', '#src/map/generator', '#src/map/generator/core', '#src/map/generator/generators', - '#src/map/generator/shared', '#src/map/generator/compatibility', + '#src/map/generator/shared', '#src/map/generator/shared/legacy', + '#src/map/generator/compatibility', '#src/map/gradient', '#src/map/io', '#src/map/pathfind', '#src/net', '#src/net/irc', '#src/net/message', '#src/sgsl', diff --git a/src/SConscript b/src/SConscript index c16e0fffc..ac00999ef 100644 --- a/src/SConscript +++ b/src/SConscript @@ -198,9 +198,10 @@ map/generator/generators/SymmetricArenaGenerator.cpp map/generator/generators/TidalFlatsGenerator.cpp map/generator/generators/UniformGenerator.cpp map/generator/generators/WatershedGenerator.cpp -map/generator/shared/Distances.cpp +map/generator/shared/legacy/Distances.cpp map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp +map/generator/shared/BalancedStarts.cpp map/generator/shared/Geometry.cpp map/generator/shared/Grid.cpp map/generator/shared/LatticeNoise.cpp @@ -210,9 +211,9 @@ map/generator/shared/Roads.cpp map/generator/shared/Sketch.cpp map/generator/shared/Topology.cpp map/generator/shared/Settlements.cpp -map/generator/shared/Regions.cpp +map/generator/shared/legacy/Regions.cpp map/generator/shared/Resources.cpp -map/generator/shared/StartingPositions.cpp +map/generator/shared/legacy/StartingPositions.cpp map/generator/shared/StartQuality.cpp map/generator/shared/Terrain.cpp map/gradient/MapGradientArea.cpp diff --git a/src/map/generator/generators/CraterLakesGenerator.cpp b/src/map/generator/generators/CraterLakesGenerator.cpp index 74e33d1e8..fb25b380d 100644 --- a/src/map/generator/generators/CraterLakesGenerator.cpp +++ b/src/map/generator/generators/CraterLakesGenerator.cpp @@ -9,9 +9,7 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const CraterLakesOptions options(context.request); - HeightFieldOptions terrain{options.water, options.sand, options.grass, options.desert, - options.smoothing, options.fruit, options.repeat, false}; - readResourceControls(terrain, context.request); + const HeightFieldOptions terrain = HeightFieldOptions::fromRequest(context.request, false); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) { diff --git a/src/map/generator/generators/IslandsGenerator.cpp b/src/map/generator/generators/IslandsGenerator.cpp index 3292ee4e2..7c314c353 100644 --- a/src/map/generator/generators/IslandsGenerator.cpp +++ b/src/map/generator/generators/IslandsGenerator.cpp @@ -9,9 +9,7 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const IslandsOptions options(context.request); - HeightFieldOptions terrain{options.water, options.sand, options.grass, options.desert, - options.smoothing, options.fruit, options.repeat, false}; - readResourceControls(terrain, context.request); + const HeightFieldOptions terrain = HeightFieldOptions::fromRequest(context.request, false); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) { diff --git a/src/map/generator/generators/RiverGenerator.cpp b/src/map/generator/generators/RiverGenerator.cpp index 80dda134f..6abd95f71 100644 --- a/src/map/generator/generators/RiverGenerator.cpp +++ b/src/map/generator/generators/RiverGenerator.cpp @@ -9,9 +9,7 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { const RiverOptions options(context.request); - HeightFieldOptions terrain{options.water, options.sand, options.grass, options.desert, - options.smoothing, options.fruit, options.repeat, false}; - readResourceControls(terrain, context.request); + const HeightFieldOptions terrain = HeightFieldOptions::fromRequest(context.request, false); if (!generateHeightField( game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) { hm.makeRiver(options.river_width * (w + h) / 2 / 100, smoothing, options.winding); })) diff --git a/src/map/generator/generators/SwampGenerator.cpp b/src/map/generator/generators/SwampGenerator.cpp index 368cc77a3..53e51adf7 100644 --- a/src/map/generator/generators/SwampGenerator.cpp +++ b/src/map/generator/generators/SwampGenerator.cpp @@ -8,10 +8,7 @@ using namespace MapGeneration; static bool generate(Game &game, GenerationContext &context) { - const SwampOptions options(context.request); - HeightFieldOptions terrain{ - options.water, 0, options.grass, 0, options.smoothing, options.fruit, options.repeat, true}; - readResourceControls(terrain, context.request); + const HeightFieldOptions terrain = HeightFieldOptions::fromRequest(context.request, true); if (!generateHeightField(game, context, terrain, [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) { hm.makeSwamp(smoothing); })) diff --git a/src/map/generator/shared/BalancedStarts.cpp b/src/map/generator/shared/BalancedStarts.cpp new file mode 100644 index 000000000..fbbd42cab --- /dev/null +++ b/src/map/generator/shared/BalancedStarts.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "BalancedStarts.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GlobalContainer.h" +#include "Grid.h" +#include "Map.h" +#include +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +// isHardSpaceForGroundUnit(x, y, false, 0) is a pure function of terrain/resource state for +// every call in this file: canSwim and the team mask are always the same two constants, no +// forbidden-area bit survives a `& 0`, and nothing places a building before chooseBalancedStarts +// runs (placeStarts(), the only thing that does, runs after boot tiles are already chosen). So +// the answer never changes across the two distanceToResource() floods and the up-to-900 calls +// to scoreAsBuilt() below; computing it once into a flat byte per tile turns what used to be a +// branchy, multi-array accessor call at every visited tile into a single sequential array read. +std::vector buildHardSpaceGrid(Map &map) +{ + return groundUnitTiles(map); +} + +// Walking distance from every tile to the nearest deposit of one resource. Workers stand +// beside a deposit rather than on it (a resource tile is not walkable), so the sources are the +// walkable tiles touching one. One flood answers the question for every tile on the map, which +// is what makes scoring a few hundred candidate sites cheap enough to do exhaustively. Distances +// on any map this engine supports fit comfortably in 16 bits, halving the footprint of an array +// every scoreAsBuilt call after it touches over and over. +std::vector distanceToResource(Map &map, const std::vector &hard, + int resourceType) +{ + const Torus t(map); + std::vector beside(size_t(t.size()), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + if (map.getResource(x, y).type != resourceType) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int np = t.at(x + dx, y + dy); + if (hard[np]) + beside[np] = 1; + } + } + const std::vector steps = stepsFrom(t, beside, hard); + std::vector dist(steps.size()); + for (size_t i = 0; i < steps.size(); ++i) + dist[i] = std::int16_t(steps[i]); + return dist; +} +} // namespace + +bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSquare) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const int nbTeams = context.request.nbTeams; + if (nbTeams <= 0 || minDistSquare <= 0) + return false; + const int typeNum = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + const BuildingType *swarm = globalContainer->buildingsTypes.get(typeNum); + if (!swarm) + return false; + + const std::vector hard = buildHardSpaceGrid(map); + const std::vector woodDist = distanceToResource(map, hard, WOOD); + const std::vector wheatDist = distanceToResource(map, hard, CORN); + + // A site is worth exactly what its *worse* resource costs to reach: a colony next to wood + // but a long walk from wheat is not a good start, however good the wood is. + // A colony changes its own surroundings the moment it is built: placeStarts() clears a + // five by seven box of resources to make room, the swarm itself becomes four by four tiles + // of obstacle, and the workers appear on the row above it rather than on the boot tile. A + // site scored against the bare map is therefore scored on deposits it is about to destroy + // and paths it is about to block, which is how four sites picked as exactly equal can + // finish unequal. Score what the colony will actually live with instead. The offsets below + // mirror placeStarts(); they are what it does, not a guess at it. + std::vector visited(size_t(w) * h, 0); + std::uint16_t visitStamp = 0; + // scoreAsBuilt runs its own small flood per candidate site - up to 900 times per call to + // this function. A fresh std::queue per call means a fresh std::deque allocation (and its + // block-by-block growth) 900 times over; every one of those floods visits each tile at most + // once (the visitStamp guard below), so one pair of w*h-sized buffers, reused across every + // call and just reset to empty (an O(1) index reset, not a reallocation), is always enough. + std::vector queueTile(size_t(w) * h), queueDist(size_t(w) * h); + auto scoreAsBuilt = [&](int bx, int by, int limit) -> int + { + auto cleared = [&](int tx, int ty) + { + int ox = map.normalizeX(tx - bx), oy = map.normalizeY(ty - by); + if (ox >= w / 2) + ox -= w; + if (oy >= h / 2) + oy -= h; + return ox >= 0 && ox <= 4 && oy >= -2 && oy <= 4; + }; + auto blocked = [&](int tx, int ty) + { + int ox = map.normalizeX(tx - bx), oy = map.normalizeY(ty - by); + if (ox >= w / 2) + ox -= w; + if (oy >= h / 2) + oy -= h; + return ox >= 0 && ox < swarm->width && oy >= 0 && oy < swarm->height; + }; + ++visitStamp; + size_t qHead = 0, qTail = 0; + auto push = [&](int tx, int ty, int d) + { + int nx = map.normalizeX(tx), ny = map.normalizeY(ty); + int np = ny * w + nx; + if (visited[np] == visitStamp) + return; + visited[np] = visitStamp; + queueTile[qTail] = np; + queueDist[qTail] = d; + ++qTail; + }; + // Workers spawn on the row above the swarm, so that is where a gathering trip starts. + for (int i = 0; i < std::max(1, context.request.nbWorkers); ++i) + push(bx + (i % 4), by - 1 - (i / 4), 0); + int wood = -1, wheat = -1; + while (qHead < qTail && (wood < 0 || wheat < 0)) + { + int p = queueTile[qHead]; + int d = queueDist[qHead]; + ++qHead; + if (d >= limit) + continue; + int x = p % w, y = p / w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dy == 0) + continue; + int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); + int type = map.getResource(nx, ny).type; + if (!cleared(nx, ny)) + { + if (type == WOOD && wood < 0) + wood = d + 1; + if (type == CORN && wheat < 0) + wheat = d + 1; + } + if (!blocked(nx, ny) && hard[ny * w + nx]) + push(nx, ny, d + 1); + } + } + if (wood < 0 || wheat < 0) + return -1; + return std::max(wood, wheat); + }; + + std::vector> sites; // (score, tile index) + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + if (!map.isFreeForBuilding(x, y, swarm->width, swarm->height)) + continue; + const int p = y * w + x; + if (woodDist[p] < 0 || wheatDist[p] < 0) + continue; + // The bare-map distance can only understate what the built colony will walk, so it + // is a sound cheap filter: it shortlists sites worth the exact simulation above. + sites.push_back({std::max(woodDist[p], wheatDist[p]), p}); + } + if ((int)sites.size() < nbTeams) + return false; + std::sort(sites.begin(), sites.end()); + // Large maps can offer tens of thousands of legal sites. Thinning by stride keeps the + // sample spread across the whole score range instead of crowding one end of it, and keeps + // the search below a bounded cost regardless of map size. + const size_t cap = 900; + if (sites.size() > cap) + { + std::vector> thinned; + thinned.reserve(cap); + for (size_t i = 0; i < cap; ++i) + thinned.push_back(sites[i * sites.size() / cap]); + sites.swap(thinned); + } + + // Re-score the shortlist as it will actually be built, and re-sort on the honest number. + { + std::vector> exact; + exact.reserve(sites.size()); + for (const auto &s : sites) + { + const int built = scoreAsBuilt(s.second % w, s.second / w, 32); + if (built >= 0) + exact.push_back({built, s.second}); + } + if ((int)exact.size() < nbTeams) + return false; + std::sort(exact.begin(), exact.end()); + sites.swap(exact); + } + + // Sites are sorted by score, so any set of colonies drawn from a short window of this list + // is a set whose colonies are closely matched. Find the narrowest window that still holds + // nbTeams mutually distant sites; scanning windows from the low-score end means ties are + // settled in favour of the set that is not just equal but good. + std::vector best; + int bestSpread = -1; + for (size_t i = 0; i < sites.size(); ++i) + { + if (bestSpread == 0) + break; + std::vector picked; + for (size_t j = i; j < sites.size(); ++j) + { + if (bestSpread >= 0 && sites[j].first - sites[i].first >= bestSpread) + break; // this window is already no better than what we hold + const int px = sites[j].second % w, py = sites[j].second / w; + bool farEnough = true; + for (int q : picked) + if (map.warpDistSquare(px, py, q % w, q / w) < minDistSquare) + { + farEnough = false; + break; + } + if (!farEnough) + continue; + picked.push_back(sites[j].second); + if ((int)picked.size() == nbTeams) + { + bestSpread = sites[j].first - sites[i].first; + best = picked; + break; + } + } + } + if ((int)best.size() != nbTeams) + return false; + for (int team = 0; team < nbTeams; ++team) + { + context.bootX[team] = best[team] % w; + context.bootY[team] = best[team] / w; + } + return true; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/BalancedStarts.h b/src/map/generator/shared/BalancedStarts.h new file mode 100644 index 000000000..006a92439 --- /dev/null +++ b/src/map/generator/shared/BalancedStarts.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +class Game; +struct GenerationContext; +namespace MapGeneration +{ +// Chooses each colony's boot tile so that every colony's walk to its primary resources is as +// nearly equal as the finished map allows, rather than handing the best land to whoever is +// picked first. Requires resources to already be on the map. Returns false (leaving bootX/bootY +// untouched) when no set of legal, mutually distant sites can reach both resources, so a caller +// can fall back to a terrain-only search. +bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSquare); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index a09d11d38..9306707e9 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -2,22 +2,21 @@ // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière // Copyright (C) 2008 Bradley Arsenault #include "Terrain.h" -#include "Distances.h" +#include "BalancedStarts.h" #include "Game.h" #include "GenerationContext.h" #include "GeneratorDefinition.h" -#include "GlobalContainer.h" #include "HeightMap.h" #include "Map.h" #include "Pipeline.h" -#include "Regions.h" #include "Resources.h" -#include "StartingPositions.h" -#include "Unit.h" #include +#include #include -using namespace MapGeneration; -std::vector MapGeneration::heightFieldResourceControls() +#include +namespace MapGeneration +{ +std::vector heightFieldResourceControls() { return {GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), @@ -26,17 +25,23 @@ std::vector MapGeneration::heightFieldResourceControls() GeneratorControl::toggle("hilltop-stone", "Stone on hilltops", false, ControlGroup::Resources)}; } -void MapGeneration::readResourceControls(HeightFieldOptions &options, - const GenerationRequest &request) + +HeightFieldOptions HeightFieldOptions::fromRequest(const GenerationRequest &r, bool swamp) { - options.wheat = request.option("wheat-amount"); - options.wood = request.option("wood-amount"); - options.stone = request.option("stone-amount"); - options.algae = request.option("algae-amount"); - options.hilltopStone = request.option("hilltop-stone") != 0; + const auto weight = [&](const char *id) { return r.options.count(id) ? r.option(id) : 0; }; + HeightFieldOptions options{r.option("water"), weight("sand"), r.option("grass"), + weight("desert"), r.option("smoothing"), r.option("fruit"), + r.option("repeat"), swamp}; + options.wheat = r.option("wheat-amount"); + options.wood = r.option("wood-amount"); + options.stone = r.option("stone-amount"); + options.algae = r.option("algae-amount"); + options.hilltopStone = r.option("hilltop-stone") != 0; + return options; } -void MapGeneration::openStartsBuriedByAmounts(Game &game, GenerationContext &context, - const HeightFieldOptions &options) + +void openStartsBuriedByAmounts(Game &game, GenerationContext &context, + const HeightFieldOptions &options) { // The resource bands are painted from map-wide noise levels with no awareness of where any team // starts, so an amount well above the default widens them until they can wall a colony in. @@ -45,41 +50,34 @@ void MapGeneration::openStartsBuriedByAmounts(Game &game, GenerationContext &con reopenCrampedStarts(game, context, {options.wheat, options.wood, options.stone, options.algae}); } -bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, - const HeightFieldOptions &options, - const HeightFieldBuilder &build) + +HeightFieldTiling heightFieldTiling(int w, int h, int repeat) { - Map &map = game.map; - const int w = map.getW(), h = map.getH(); + /// respect symmetry-requirements + unsigned int wPower2Divider = 0, hPower2Divider = 0; + for (int i = 0; i < repeat; i++) + if ((w >> wPower2Divider) > (h >> hPower2Divider)) + wPower2Divider++; + else + hPower2Divider++; + HeightFieldTiling tiling; + tiling.wRepeat = 1 << wPower2Divider; + tiling.hRepeat = 1 << hPower2Divider; + tiling.w = (unsigned int)(w / tiling.wRepeat); + tiling.h = (unsigned int)(h / tiling.hRepeat); + return tiling; +} - /// all under waterLevel is water, under sandLevel is beach, under grassLevel is grass and above - /// grasslevel is desert - float waterLevel, sandLevel, grassLevel, wheatLevel, woodLevel, algaeLevel, stoneLevel, - stoneFloorLevel; - /// to influence the roughness - float smoothingFactor = (float)(options.smoothing + 4) * 3; +HeightFieldLevels classifyHeightField(HeightMap &hm, const HeightFieldTiling &tiling, + const HeightFieldOptions &options) +{ + const unsigned int wHeightMap = tiling.w, hHeightMap = tiling.h; /// the proportions requested through the gui can directly be translated into tile counts of the /// undermap. unsigned int waterTiles, sandTiles, grassTiles, wheatWoodTiles, algaeTiles; /// grass + sand + water + desert as from the gui unsigned int totalGSWFromUI = options.water + options.sand + options.grass + options.desert + options.fruit; - /// respect symmetry-requirements - unsigned int wPower2Divider = 0, hPower2Divider = 0; - int power2Divider = options.repeat; - for (int i = 0; i < power2Divider; i++) - if ((w >> wPower2Divider) > (h >> hPower2Divider)) - wPower2Divider++; - else - hPower2Divider++; - int wRepeat = 1 << wPower2Divider; - int hRepeat = 1 << hPower2Divider; - unsigned int wHeightMap = (unsigned int)(w / wRepeat); - unsigned int hHeightMap = (unsigned int)(h / hRepeat); - /// lets generate a patch of perlin noise. That's a smooth mapping R^2 to ]0;1[ - HeightMap hm(wHeightMap, hHeightMap, context.stream("heightmap")); - /// 1 to avoid division by zero, - build(hm, wHeightMap, hHeightMap, smoothingFactor); if (options.swamp) { waterTiles = options.water * wHeightMap * hHeightMap / (1u + options.water + options.grass); @@ -121,56 +119,57 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, } unsigned int accumulatedHistogram = 0; int i = 0; - waterLevel = 0; - sandLevel = 0; - grassLevel = 0; - wheatLevel = 0; - woodLevel = 0; - stoneLevel = 0; - stoneFloorLevel = 0; - algaeLevel = 0; - while ((waterLevel == 0) && (i < 2048)) + HeightFieldLevels levels; + while ((levels.water == 0) && (i < 2048)) { accumulatedHistogram += histogram[i++]; - if (algaeLevel == 0 && accumulatedHistogram >= algaeTiles) - algaeLevel = (float)(i - 1) / 2048.0; + if (levels.algae == 0 && accumulatedHistogram >= algaeTiles) + levels.algae = (float)(i - 1) / 2048.0; if (accumulatedHistogram >= waterTiles) - waterLevel = (float)(i - 1) / 2048.0; + levels.water = (float)(i - 1) / 2048.0; } - while ((sandLevel == 0) && (i < 2048)) + while ((levels.sand == 0) && (i < 2048)) { accumulatedHistogram += histogram[i++]; if (accumulatedHistogram >= waterTiles + sandTiles) - sandLevel = (float)(i - 1) / 2048.0; + levels.sand = (float)(i - 1) / 2048.0; } - while ((grassLevel == 0) && (i < 2048)) + while ((levels.grass == 0) && (i < 2048)) { accumulatedHistogram += histogram[i++]; - if (wheatLevel == 0 && accumulatedHistogram >= wheatTop) - wheatLevel = (float)(i - 1) / 2048.0; - if (woodLevel == 0 && accumulatedHistogram >= woodTop) - woodLevel = (float)(i - 1) / 2048.0; - if (stoneFloorLevel == 0 && accumulatedHistogram >= stoneFloor) - stoneFloorLevel = (float)(i - 1) / 2048.0; - if (stoneLevel == 0 && accumulatedHistogram >= stoneTop) - stoneLevel = (float)(i - 1) / 2048.0; + if (levels.wheat == 0 && accumulatedHistogram >= wheatTop) + levels.wheat = (float)(i - 1) / 2048.0; + if (levels.wood == 0 && accumulatedHistogram >= woodTop) + levels.wood = (float)(i - 1) / 2048.0; + if (levels.stoneFloor == 0 && accumulatedHistogram >= stoneFloor) + levels.stoneFloor = (float)(i - 1) / 2048.0; + if (levels.stone == 0 && accumulatedHistogram >= stoneTop) + levels.stone = (float)(i - 1) / 2048.0; if (accumulatedHistogram >= landTop) - grassLevel = (float)(i - 1) / 2048.0; + levels.grass = (float)(i - 1) / 2048.0; } + return levels; +} + +void paintHeightFieldTerrain(Map &map, HeightMap &hm, const HeightFieldTiling &tiling, + const HeightFieldLevels &levels) +{ + const int w = map.getW(); + const unsigned int wHeightMap = tiling.w, hHeightMap = tiling.h; for (unsigned y = 0; y < hHeightMap; y++) for (unsigned x = 0; x < wHeightMap; x++) { int tmpUndermap; - if (hm(y * wHeightMap + x) < waterLevel) + if (hm(y * wHeightMap + x) < levels.water) tmpUndermap = WATER; - else if (hm(y * wHeightMap + x) < sandLevel) + else if (hm(y * wHeightMap + x) < levels.sand) tmpUndermap = SAND; - else if (hm(y * wHeightMap + x) < grassLevel) + else if (hm(y * wHeightMap + x) < levels.grass) tmpUndermap = GRASS; else tmpUndermap = SAND; - for (int yRepeat = 0; yRepeat < hRepeat; yRepeat++) - for (int xRepeat = 0; xRepeat < wRepeat; xRepeat++) + for (int yRepeat = 0; yRepeat < tiling.hRepeat; yRepeat++) + for (int xRepeat = 0; xRepeat < tiling.wRepeat; xRepeat++) map.setUMTerrain( (xRepeat * wHeightMap + x + (yRepeat * hHeightMap + y) * w) % w, (xRepeat * wHeightMap + x + (yRepeat * hHeightMap + y) * w) / w, @@ -178,8 +177,12 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, } map.controlSand(); map.rebuildTerrain(); - context.stage = "resources"; - // now to add primary resources for current map generator +} + +void paintHeightFieldResources(Map &map, HeightMap &hm, const HeightFieldTiling &tiling, + const HeightFieldLevels &levels, const HeightFieldOptions &options) +{ + const unsigned int wHeightMap = tiling.w, hHeightMap = tiling.h; for (unsigned y = 0; y < hHeightMap; y++) { for (unsigned x = 0; x < wHeightMap; x++) @@ -187,9 +190,9 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, int tmpResource = NO_RES; const float level = hm(x + wHeightMap * y); const bool stoneBand = options.hilltopStone - ? level >= stoneFloorLevel && level < stoneLevel - : level < stoneLevel; - if (level < algaeLevel) + ? level >= levels.stoneFloor && level < levels.stone + : level < levels.stone; + if (level < levels.algae) { if (options.algae > 0) tmpResource = ALGA; @@ -206,18 +209,18 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, else if (hm((x + wHeightMap / 2) % wHeightMap + wHeightMap * y) < hm((x + wHeightMap / 2 + 1) % wHeightMap + wHeightMap * y)) { - if (level < wheatLevel) + if (level < levels.wheat) tmpResource = CORN; } - else if (level < woodLevel) + else if (level < levels.wood) { tmpResource = WOOD; } if (tmpResource != NO_RES) { - for (int yRepeat = 0; yRepeat < hRepeat; yRepeat++) + for (int yRepeat = 0; yRepeat < tiling.hRepeat; yRepeat++) { - for (int xRepeat = 0; xRepeat < wRepeat; xRepeat++) + for (int xRepeat = 0; xRepeat < tiling.wRepeat; xRepeat++) { map.setResource(xRepeat * wHeightMap + x, yRepeat * hHeightMap + y, tmpResource, 1); @@ -226,15 +229,17 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, } } } +} +bool chooseHeightFieldStarts(Game &game, GenerationContext &context) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); // Choosing where the colonies go *after* the resources exist is what makes a fair choice - // possible at all: the search below scores a site by how far its workers must actually walk - // to wood and wheat, which is unknowable while the map is still bare. Nothing in between - // reads a boot position or draws from a random stream, so the two passes simply swapped - // order. The legacy search — largest grass rectangle first, and everyone after it takes - // what is left — stays as a fallback for maps where no set of sites can reach both - // resources at all. - context.stage = "starting locations"; + // possible at all: the search scores a site by how far its workers must actually walk to wood + // and wheat, which is unknowable while the map is still bare. The legacy search — largest grass + // rectangle first, and everyone after it takes what is left — stays as a fallback for maps + // where no set of sites can reach both resources at all. int nbTeams = context.request.nbTeams; int minDistSquare = (int)((double)w * h / (double)nbTeams / 5); if (minDistSquare <= 0) @@ -243,72 +248,150 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, } int *bootX = context.bootX.data(); int *bootY = context.bootY.data(); - if (!chooseBalancedStarts(game, context, minDistSquare)) + if (chooseBalancedStarts(game, context, minDistSquare)) + return true; + // TODO: First pass to find the number of available places. + for (int team = 0; team < nbTeams; team++) { - // TODO: First pass to find the number of available places. - for (int team = 0; team < nbTeams; team++) + int maxSurface = 0; + int maxX = 0; + int maxY = 0; + for (int y = 0; y < h; y++) { - int maxSurface = 0; - int maxX = 0; - int maxY = 0; - for (int y = 0; y < h; y++) + int width = 0; + int startX = 0; + for (int x = 0; x < w; x++) { - int width = 0; - int startX = 0; - for (int x = 0; x < w; x++) + int a = map.getUMTerrain(x, y); + if (a == GRASS) + width++; + else { - int a = map.getUMTerrain(x, y); - if (a == GRASS) - width++; - else + if (width > 7) { - if (width > 7) - { - int centerX = ((x + startX) >> 1); - int top, bot; - for (top = 0; top < h; top++) - if (map.getUMTerrain(centerX, y - top) != GRASS) - break; - for (bot = 0; bot < h; bot++) - if (map.getUMTerrain(centerX, y + bot) != GRASS) - break; - int height = top + bot - 1; - int surface = height * width; - assert(surface > 0); - - int centerY = y + ((bot - top) >> 1); - bool farEnough = true; - for (int ti = 0; ti < team; ti++) - if (map.warpDistSquare(centerX, centerY, bootX[ti], bootY[ti]) < - minDistSquare) - { - farEnough = false; - break; - } + int centerX = ((x + startX) >> 1); + int top, bot; + for (top = 0; top < h; top++) + if (map.getUMTerrain(centerX, y - top) != GRASS) + break; + for (bot = 0; bot < h; bot++) + if (map.getUMTerrain(centerX, y + bot) != GRASS) + break; + int height = top + bot - 1; + int surface = height * width; + assert(surface > 0); - if (surface > maxSurface && farEnough) + int centerY = y + ((bot - top) >> 1); + bool farEnough = true; + for (int ti = 0; ti < team; ti++) + if (map.warpDistSquare(centerX, centerY, bootX[ti], bootY[ti]) < + minDistSquare) { - maxSurface = surface; - maxX = centerX; - maxY = centerY; + farEnough = false; + break; } + + if (surface > maxSurface && farEnough) + { + maxSurface = surface; + maxX = centerX; + maxY = centerY; } - width = 0; - startX = x; } + width = 0; + startX = x; } } + } + + if (maxSurface <= 0) + { + return false; + } + assert(maxSurface); + bootX[team] = maxX; + bootY[team] = maxY; + } + return true; +} - if (maxSurface <= 0) +bool plantHeightFieldGroves(Map &map, GenerationContext &context, const HeightFieldTiling &tiling, + int count) +{ + const unsigned int wHeightMap = tiling.w, hHeightMap = tiling.h; + // TODO: count of groves does not scale with mapsize, so it has to be adjusted higher on + // bigger maps now. + for (int q1 = 0; q1 < count; q1++) // counting groves + { + // choose fruit + int fruit; + switch (context.stream("resources")() % 3) + { + case 0: + fruit = CHERRY; + break; + case 1: + fruit = ORANGE; + break; + case 2: + default: + fruit = PRUNE; + break; + } + // choose coordinate where there is grass but no resource yet + int x, y; + int attempts = 0; + do + { + if (++attempts > int(wHeightMap * hHeightMap * 4)) { + context.detail = "No free grass for fruit"; return false; } - assert(maxSurface); - bootX[team] = maxX; - bootY[team] = maxY; + x = (context.stream("resources")() % wHeightMap); + y = (context.stream("resources")() % hHeightMap); + } while (map.getUMTerrain(x, y) != GRASS || map.isResource(x, y)); + // choose size of grove (tree count) + int grovesize = (context.stream("resources")() % 10) + 1; + for (int i = 0; i < grovesize; i++) + { + for (int yRepeat = 0; yRepeat < tiling.hRepeat; yRepeat++) + for (int xRepeat = 0; xRepeat < tiling.wRepeat; xRepeat++) + map.setResource(xRepeat * wHeightMap + x, yRepeat * hHeightMap + y, fruit, 1); + // find a valid neighbor of actual coordinate + for (int iTry = 0; iTry < 100; iTry++) + { + int xNew = x + context.stream("resources")() % 3 - 1; + int yNew = y + context.stream("resources")() % 3 - 1; + if (map.getUMTerrain(xNew, yNew) == GRASS && !map.isResource(xNew, yNew)) + { + x = xNew; + y = yNew; + break; + } + } } } + return true; +} +bool generateHeightField(Game &game, GenerationContext &context, const HeightFieldOptions &options, + const HeightFieldBuilder &build) +{ + Map &map = game.map; + /// to influence the roughness + const float smoothingFactor = (float)(options.smoothing + 4) * 3; + const HeightFieldTiling tiling = heightFieldTiling(map.getW(), map.getH(), options.repeat); + /// lets generate a patch of perlin noise. That's a smooth mapping R^2 to ]0;1[ + HeightMap hm(tiling.w, tiling.h, context.stream("heightmap")); + build(hm, tiling.w, tiling.h, smoothingFactor); + const HeightFieldLevels levels = classifyHeightField(hm, tiling, options); + paintHeightFieldTerrain(map, hm, tiling, levels); + context.stage = "resources"; + paintHeightFieldResources(map, hm, tiling, levels, options); + context.stage = "starting locations"; + if (!chooseHeightFieldStarts(game, context)) + return false; // Fairness guarantee: the bands above are painted from map-wide noise levels with no // awareness of where any team actually starts, so a team's assigned tile can land in a // stretch of grass the noise field never happens to touch — or a straight-line-nearby @@ -320,65 +403,6 @@ bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, // (StartingPositions.cpp's setNoResource calls), so skip that band or a clump placed here // would just be wiped a moment later. guaranteeStartingResources(game, context, 24, 32, /*clearRadius=*/6); - - // TODO: count of groves(=options.fruit) does not scale with mapsize. - // so it has to be adjusted higher on bigger maps now. - - // fruit-placement: - if (options.fruit > 0) - { - for (int q1 = 0; q1 < options.fruit; q1++) // counting groves - { - // choose fruit - int fruit; - switch (context.stream("resources")() % 3) - { - case 0: - fruit = CHERRY; - break; - case 1: - fruit = ORANGE; - break; - case 2: - default: - fruit = PRUNE; - break; - } - // choose coordinate where there is grass but no resource yet - int x, y; - int attempts = 0; - do - { - if (++attempts > int(wHeightMap * hHeightMap * 4)) - { - context.detail = "No free grass for fruit"; - return false; - } - x = (context.stream("resources")() % wHeightMap); - y = (context.stream("resources")() % hHeightMap); - } while (map.getUMTerrain(x, y) != GRASS || map.isResource(x, y)); - // choose size of grove (tree count) - int grovesize = (context.stream("resources")() % 10) + 1; - for (int i = 0; i < grovesize; i++) - { - for (int yRepeat = 0; yRepeat < hRepeat; yRepeat++) - for (int xRepeat = 0; xRepeat < wRepeat; xRepeat++) - map.setResource(xRepeat * wHeightMap + x, yRepeat * hHeightMap + y, fruit, - 1); - // find a valid neighbor of actual coordinate - for (int iTry = 0; iTry < 100; iTry++) - { - int xNew = x + context.stream("resources")() % 3 - 1; - int yNew = y + context.stream("resources")() % 3 - 1; - if (map.getUMTerrain(xNew, yNew) == GRASS && !map.isResource(xNew, yNew)) - { - x = xNew; - y = yNew; - break; - } - } - } - } - } - return true; + return options.fruit <= 0 || plantHeightFieldGroves(map, context, tiling, options.fruit); } +} // namespace MapGeneration diff --git a/src/map/generator/shared/Terrain.h b/src/map/generator/shared/Terrain.h index b25b5ee16..37e3a9fae 100644 --- a/src/map/generator/shared/Terrain.h +++ b/src/map/generator/shared/Terrain.h @@ -5,9 +5,16 @@ #include #include class Game; +class Map; struct GenerationContext; namespace MapGeneration { +// The height-field generators (Swamp, River, Islands, Crater lakes): one Perlin height field +// shapes the terrain and paints the resource bands from the same values, then the colonies take +// the most balanced sites on it. generateHeightField runs the stages below in order; each is +// also callable on its own, so a generator can take the field's terrain and do its own resources, +// or its starts, or its groves. + struct HeightFieldOptions { int water, sand, grass, desert, smoothing, fruit, repeat; @@ -16,15 +23,56 @@ struct HeightFieldOptions int wheat = 100, wood = 100, stone = 100, algae = 100; // Stone on the highest grass rather than beside the shore. bool hilltopStone = false; + /// The shared terrain and resource controls of a request. A swamp has no sand or desert + /// weight and takes its water against its grass alone. + static HeightFieldOptions fromRequest(const GenerationRequest &, bool swamp); }; -// The resource controls every height-field generator shares, and reading them from a request. +// The resource controls every height-field generator shares. std::vector heightFieldResourceControls(); -void readResourceControls(HeightFieldOptions &, const GenerationRequest &); + +/// How the field tiles the map: `repeat` halvings shared out between the axes, so a field of +/// w by h is stamped wRepeat by hRepeat times. +struct HeightFieldTiling +{ + int wRepeat = 1, hRepeat = 1; + unsigned w = 0, h = 0; +}; +HeightFieldTiling heightFieldTiling(int mapWidth, int mapHeight, int repeat); + +/// The field's terrain and resource bands as thresholds on its values, from the shares the +/// options ask for: below water is water, below sand beach, below grass grass and above it +/// desert; algae is the lowest of the water, stone a band just above the beach (or the highest +/// grass), and wheat and wood the farmland above. +struct HeightFieldLevels +{ + float water = 0, sand = 0, grass = 0, wheat = 0, wood = 0, algae = 0, stone = 0, stoneFloor = 0; +}; +HeightFieldLevels classifyHeightField(HeightMap &, const HeightFieldTiling &, + const HeightFieldOptions &); + +/// Writes the field's terrain to the undermap, tiled, and runs the engine's sand control. +void paintHeightFieldTerrain(Map &, HeightMap &, const HeightFieldTiling &, + const HeightFieldLevels &); +/// Paints algae, stone, wheat and wood as bands of the field; wheat and wood alternate along the +/// field's own slope so the two come out in smooth areas rather than rings. +void paintHeightFieldResources(Map &, HeightMap &, const HeightFieldTiling &, + const HeightFieldLevels &, const HeightFieldOptions &); +/// Every colony's boot tile: chooseBalancedStarts on the finished resources, falling back to +/// the oldest search (the widest grass patch each) where no balanced set exists. False when +/// the map has no room at all. +bool chooseHeightFieldStarts(Game &, GenerationContext &); +/// `count` fruit groves of a random kind each, grown tile by tile over free grass; false with a +/// diagnostic when no grass is free. +bool plantHeightFieldGroves(Map &, GenerationContext &, const HeightFieldTiling &, int count); + // Call after placeStarts: at any non-default resource amount, open up a colony that the widened // bands walled in (openCrampedStarts) and re-run the wheat/wood guarantee in case the clearing // took its nearest crop too. Nothing happens at the default amounts. void openStartsBuriedByAmounts(Game &, GenerationContext &, const HeightFieldOptions &); + using HeightFieldBuilder = std::function; +/// The whole pipeline: build the field, classify and paint its terrain and resources, choose +/// the starts, guarantee their crops and plant the groves. The caller then places the colonies. bool generateHeightField(Game &, GenerationContext &, const HeightFieldOptions &, const HeightFieldBuilder &); } // namespace MapGeneration diff --git a/src/map/generator/shared/Distances.cpp b/src/map/generator/shared/legacy/Distances.cpp similarity index 100% rename from src/map/generator/shared/Distances.cpp rename to src/map/generator/shared/legacy/Distances.cpp diff --git a/src/map/generator/shared/Distances.h b/src/map/generator/shared/legacy/Distances.h similarity index 78% rename from src/map/generator/shared/Distances.h rename to src/map/generator/shared/legacy/Distances.h index 001ba7e26..243406bfb 100644 --- a/src/map/generator/shared/Distances.h +++ b/src/map/generator/shared/legacy/Distances.h @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +// Height and distance fields over the legacy area grid (Regions.h). computeDistances is +// Grid.h's stepsFrom in the historical encoding; new code should call stepsFrom directly. #include "Regions.h" namespace MapGeneration { diff --git a/src/map/generator/shared/Regions.cpp b/src/map/generator/shared/legacy/Regions.cpp similarity index 100% rename from src/map/generator/shared/Regions.cpp rename to src/map/generator/shared/legacy/Regions.cpp diff --git a/src/map/generator/shared/Regions.h b/src/map/generator/shared/legacy/Regions.h similarity index 83% rename from src/map/generator/shared/Regions.h rename to src/map/generator/shared/legacy/Regions.h index 5d39d6ddb..35e3976aa 100644 --- a/src/map/generator/shared/Regions.h +++ b/src/map/generator/shared/legacy/Regions.h @@ -1,5 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +// The area-grid toolkit the point-dispersion generators (Concrete islands, Isles, Contested +// commons) and the older resource passes are built on: a grid of integer area numbers, points +// as MapGeneratorPoint, and in-out vector arguments. New generators should design on the tile +// masks and Torus of shared/Grid.h instead; nothing here composes with them without conversion. #include class Game; class Map; diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/legacy/StartingPositions.cpp similarity index 61% rename from src/map/generator/shared/StartingPositions.cpp rename to src/map/generator/shared/legacy/StartingPositions.cpp index 1aa6c4a56..6f67e04c6 100644 --- a/src/map/generator/shared/StartingPositions.cpp +++ b/src/map/generator/shared/legacy/StartingPositions.cpp @@ -7,7 +7,6 @@ #include "GenerationContext.h" #include "GeneratorDefinition.h" #include "GlobalContainer.h" -#include "Grid.h" #include "HeightMap.h" #include "Map.h" #include "Regions.h" @@ -299,242 +298,6 @@ Building *addBuilding(Game &game, int x, int y, int team, int type, int level, namespace MapGeneration { -namespace -{ -// isHardSpaceForGroundUnit(x, y, false, 0) is a pure function of terrain/resource state for -// every call in this file: canSwim and the team mask are always the same two constants, no -// forbidden-area bit survives a `& 0`, and nothing places a building before chooseBalancedStarts -// runs (placeStarts(), the only thing that does, runs after boot tiles are already chosen). So -// the answer never changes across the two distanceToResource() floods and the up-to-900 calls -// to scoreAsBuilt() below; computing it once into a flat byte per tile turns what used to be a -// branchy, multi-array accessor call at every visited tile into a single sequential array read. -std::vector buildHardSpaceGrid(Map &map) -{ - return groundUnitTiles(map); -} - -// Walking distance from every tile to the nearest deposit of one resource. Workers stand -// beside a deposit rather than on it (a resource tile is not walkable), so the sources are the -// walkable tiles touching one. One flood answers the question for every tile on the map, which -// is what makes scoring a few hundred candidate sites cheap enough to do exhaustively. Distances -// on any map this engine supports fit comfortably in 16 bits, halving the footprint of an array -// every scoreAsBuilt call after it touches over and over. -std::vector distanceToResource(Map &map, const std::vector &hard, - int resourceType) -{ - const Torus t(map); - std::vector beside(size_t(t.size()), 0); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - if (map.getResource(x, y).type != resourceType) - continue; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int np = t.at(x + dx, y + dy); - if (hard[np]) - beside[np] = 1; - } - } - const std::vector steps = stepsFrom(t, beside, hard); - std::vector dist(steps.size()); - for (size_t i = 0; i < steps.size(); ++i) - dist[i] = std::int16_t(steps[i]); - return dist; -} -} // namespace - -bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSquare) -{ - Map &map = game.map; - const int w = map.getW(), h = map.getH(); - const int nbTeams = context.request.nbTeams; - if (nbTeams <= 0 || minDistSquare <= 0) - return false; - const int typeNum = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); - const BuildingType *swarm = globalContainer->buildingsTypes.get(typeNum); - if (!swarm) - return false; - - const std::vector hard = buildHardSpaceGrid(map); - const std::vector woodDist = distanceToResource(map, hard, WOOD); - const std::vector wheatDist = distanceToResource(map, hard, CORN); - - // A site is worth exactly what its *worse* resource costs to reach: a colony next to wood - // but a long walk from wheat is not a good start, however good the wood is. - // A colony changes its own surroundings the moment it is built: placeStarts() clears a - // five by seven box of resources to make room, the swarm itself becomes four by four tiles - // of obstacle, and the workers appear on the row above it rather than on the boot tile. A - // site scored against the bare map is therefore scored on deposits it is about to destroy - // and paths it is about to block, which is how four sites picked as exactly equal can - // finish unequal. Score what the colony will actually live with instead. The offsets below - // mirror placeStarts(); they are what it does, not a guess at it. - std::vector visited(size_t(w) * h, 0); - std::uint16_t visitStamp = 0; - // scoreAsBuilt runs its own small flood per candidate site - up to 900 times per call to - // this function. A fresh std::queue per call means a fresh std::deque allocation (and its - // block-by-block growth) 900 times over; every one of those floods visits each tile at most - // once (the visitStamp guard below), so one pair of w*h-sized buffers, reused across every - // call and just reset to empty (an O(1) index reset, not a reallocation), is always enough. - std::vector queueTile(size_t(w) * h), queueDist(size_t(w) * h); - auto scoreAsBuilt = [&](int bx, int by, int limit) -> int - { - auto cleared = [&](int tx, int ty) - { - int ox = map.normalizeX(tx - bx), oy = map.normalizeY(ty - by); - if (ox >= w / 2) - ox -= w; - if (oy >= h / 2) - oy -= h; - return ox >= 0 && ox <= 4 && oy >= -2 && oy <= 4; - }; - auto blocked = [&](int tx, int ty) - { - int ox = map.normalizeX(tx - bx), oy = map.normalizeY(ty - by); - if (ox >= w / 2) - ox -= w; - if (oy >= h / 2) - oy -= h; - return ox >= 0 && ox < swarm->width && oy >= 0 && oy < swarm->height; - }; - ++visitStamp; - size_t qHead = 0, qTail = 0; - auto push = [&](int tx, int ty, int d) - { - int nx = map.normalizeX(tx), ny = map.normalizeY(ty); - int np = ny * w + nx; - if (visited[np] == visitStamp) - return; - visited[np] = visitStamp; - queueTile[qTail] = np; - queueDist[qTail] = d; - ++qTail; - }; - // Workers spawn on the row above the swarm, so that is where a gathering trip starts. - for (int i = 0; i < std::max(1, context.request.nbWorkers); ++i) - push(bx + (i % 4), by - 1 - (i / 4), 0); - int wood = -1, wheat = -1; - while (qHead < qTail && (wood < 0 || wheat < 0)) - { - int p = queueTile[qHead]; - int d = queueDist[qHead]; - ++qHead; - if (d >= limit) - continue; - int x = p % w, y = p / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (dx == 0 && dy == 0) - continue; - int nx = map.normalizeX(x + dx), ny = map.normalizeY(y + dy); - int type = map.getResource(nx, ny).type; - if (!cleared(nx, ny)) - { - if (type == WOOD && wood < 0) - wood = d + 1; - if (type == CORN && wheat < 0) - wheat = d + 1; - } - if (!blocked(nx, ny) && hard[ny * w + nx]) - push(nx, ny, d + 1); - } - } - if (wood < 0 || wheat < 0) - return -1; - return std::max(wood, wheat); - }; - - std::vector> sites; // (score, tile index) - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - if (!map.isFreeForBuilding(x, y, swarm->width, swarm->height)) - continue; - const int p = y * w + x; - if (woodDist[p] < 0 || wheatDist[p] < 0) - continue; - // The bare-map distance can only understate what the built colony will walk, so it - // is a sound cheap filter: it shortlists sites worth the exact simulation above. - sites.push_back({std::max(woodDist[p], wheatDist[p]), p}); - } - if ((int)sites.size() < nbTeams) - return false; - std::sort(sites.begin(), sites.end()); - // Large maps can offer tens of thousands of legal sites. Thinning by stride keeps the - // sample spread across the whole score range instead of crowding one end of it, and keeps - // the search below a bounded cost regardless of map size. - const size_t cap = 900; - if (sites.size() > cap) - { - std::vector> thinned; - thinned.reserve(cap); - for (size_t i = 0; i < cap; ++i) - thinned.push_back(sites[i * sites.size() / cap]); - sites.swap(thinned); - } - - // Re-score the shortlist as it will actually be built, and re-sort on the honest number. - { - std::vector> exact; - exact.reserve(sites.size()); - for (const auto &s : sites) - { - const int built = scoreAsBuilt(s.second % w, s.second / w, 32); - if (built >= 0) - exact.push_back({built, s.second}); - } - if ((int)exact.size() < nbTeams) - return false; - std::sort(exact.begin(), exact.end()); - sites.swap(exact); - } - - // Sites are sorted by score, so any set of colonies drawn from a short window of this list - // is a set whose colonies are closely matched. Find the narrowest window that still holds - // nbTeams mutually distant sites; scanning windows from the low-score end means ties are - // settled in favour of the set that is not just equal but good. - std::vector best; - int bestSpread = -1; - for (size_t i = 0; i < sites.size(); ++i) - { - if (bestSpread == 0) - break; - std::vector picked; - for (size_t j = i; j < sites.size(); ++j) - { - if (bestSpread >= 0 && sites[j].first - sites[i].first >= bestSpread) - break; // this window is already no better than what we hold - const int px = sites[j].second % w, py = sites[j].second / w; - bool farEnough = true; - for (int q : picked) - if (map.warpDistSquare(px, py, q % w, q / w) < minDistSquare) - { - farEnough = false; - break; - } - if (!farEnough) - continue; - picked.push_back(sites[j].second); - if ((int)picked.size() == nbTeams) - { - bestSpread = sites[j].first - sites[i].first; - best = picked; - break; - } - } - } - if ((int)best.size() != nbTeams) - return false; - for (int team = 0; team < nbTeams; ++team) - { - context.bootX[team] = best[team] % w; - context.bootY[team] = best[team] / w; - } - return true; -} - bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSize) { for (int s = 0; s < context.request.nbTeams; s++) diff --git a/src/map/generator/shared/StartingPositions.h b/src/map/generator/shared/legacy/StartingPositions.h similarity index 68% rename from src/map/generator/shared/StartingPositions.h rename to src/map/generator/shared/legacy/StartingPositions.h index 67c7a0801..92f85f595 100644 --- a/src/map/generator/shared/StartingPositions.h +++ b/src/map/generator/shared/legacy/StartingPositions.h @@ -1,5 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +// The older colony placers: divideUpPlayerLands for the point-dispersion generators and the +// boot-tile placers of the height-field and old random generators. A designed generator +// places its colonies with shared/Settlements.h; the balanced site search the height-field +// generators use is shared/BalancedStarts.h. #include "Regions.h" namespace MapGeneration { @@ -21,12 +25,8 @@ Building *addBuilding(Game &game, int x, int y, int team, int type, int level, namespace MapGeneration { -// Chooses each colony's boot tile so that every colony's walk to its primary resources is as -// nearly equal as the finished map allows, rather than handing the best land to whoever is -// picked first. Requires resources to already be on the map. Returns false (leaving bootX/bootY -// untouched) when no set of legal, mutually distant sites can reach both resources, so a caller -// can fall back to a terrain-only search. -bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSquare); +/// Builds every colony's swarm and workers on its chosen boot tile, clearing a small rectangle of +/// grass for them first. bool placeStarts(Game &, GenerationContext &); bool placeArchipelagoStarts(Game &, GenerationContext &, int islandSize); } // namespace MapGeneration From 8c0904dcc27332f7f9e470454e126d0451ddb6aa Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 16:08:03 -0400 Subject: [PATCH 104/136] Test the shared generator toolkit and document it module by module test/MapGeneratorToolkitChecks.h checks every module under shared/ on a map built by hand, wired into MapGeneratorDefaultsTest: floods and their limits, beaches and islands, patches and algae bands, the cheapest route, settlements and the colony walk, the crop guarantee through a wall and around a protected one, a buried colony's room, balanced starts, per-landmass scatter, lattice noise, the wedge frame and the context's shuffle. Until now guaranteeStartingResources, openCrampedStarts, chooseBalancedStarts, scoreStarts and the scatter bands were covered only through golden fingerprints. The framework doc now lists the toolkit one module per row and describes the designed-generator pipeline the nine modern generators share; the adding guide names shared/legacy for what it is and replaces "generator- specific routines remain local" with "extract on the second use". Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/map-generators/ADDING_A_GENERATOR.md | 11 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 101 ++- docs/map-generators/README.md | 6 +- test/MapGeneratorDefaultsTest.cpp | 2 + test/MapGeneratorToolkitChecks.h | 632 ++++++++++++++++++ 5 files changed, 718 insertions(+), 34 deletions(-) create mode 100644 test/MapGeneratorToolkitChecks.h diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index 1afbb610c..f35065cb8 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -5,10 +5,13 @@ A generator is a registered module, not a new branch in a central dispatch switc ## Module boundaries - **`core/`** owns requests, control metadata, registration, seed streams, validation and results. It does not implement terrain recipes. -- **`generators/`** contains one header/source pair per generator. The header defines its named options; the source owns its controls and ordered generation steps. Generator-specific terrain and resource routines remain local to that source. -- **`shared/`** contains height fields, instance-owned noise, terrain classification, region geometry, distance fields, resource filling and starting-position helpers. These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. +- **`generators/`** contains one header/source pair per generator. The header defines its named options; the source owns its controls, its design (the layout as a pure function of the request) and the order it calls the shared stages in. A generator source holds only what is particular to its map. +- **`shared/`** is the toolkit every generator is built from: the torus grid and its floods (`Grid`), shapes (`Geometry`, `Wedge`), the terrain sketch (`Sketch`), noise (`LatticeNoise`, `HeightMap`), deposits (`Planting`, `Resources`), routes (`Roads`), colonies (`Settlements`, `BalancedStarts`), the pipeline stages (`Pipeline`), the height-field stages (`Terrain`) and the start scoring (`StartQuality`). These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) lists what each gives you. +- **`shared/legacy/`** is the older area-grid toolkit (`Regions`, `Distances`, `StartingPositions`) the point-dispersion and height-field generators still build on. It does not compose with the tile masks the rest of `shared/` uses; do not start a new generator on it. - **`compatibility/`** owns the historical descriptor codec and overloaded-field conversion. New generators do not add fields to that descriptor. `MapGenerator.h` is only a small compatibility facade for older callers. +Extract on the second use. When a routine you are about to write already exists in another generator, move it into `shared/` and call it from both rather than copying it: every helper that was once copied between generators (the beach pass, the kit patch, the swarm clearance, the algae seeding, the road opener, the wedge frame) is now a shared module, and the next copy is a regression. A routine that genuinely only makes sense for one map stays in that generator. + `Map` retains general terrain-editing operations, including `rebuildTerrain()` for bulk undermap edits. `Game` retains team, unit and building mutation APIs. Neither dispatches generation algorithms. ## Register a module @@ -16,7 +19,7 @@ A generator is a registered module, not a new branch in a central dispatch switc 1. Add `ExampleGenerator.h/.cpp` under `src/map/generator/generators/`. Use a named options struct that reads its scalar values from the request by stable option ID. 2. Return a `GeneratorDefinition` from `exampleDefinition()`. Provide a stable string ID, unique nonnegative numeric ID, translation key, revision, editor-only availability, control list and callback. Numeric IDs are identifiers, never list positions; gaps are supported. `hasStartingColonies` defaults to true. Uniform alone disables normal starting-colony validation in the shipped catalog. 3. Add that definition to the single list in `GeneratorRegistry::builtins()` and its source to `src/SConscript`. Add the display name and control labels to the translation tables using the existing translation workflow. -4. Write the callback as `bool generate(Game&, GenerationContext&)`. Read the immutable request, build into the supplied fresh game, set `context.stage` before operations that may fail, and return false with a useful `context.detail` if the layout cannot be placed. +4. Write the callback as `bool generate(Game&, GenerationContext&)`. Read the immutable request, build into the supplied fresh game, set `context.stage` before operations that may fail, and return false with a useful `context.detail` if the layout cannot be placed. The usual shape is a `design()` that computes the whole layout from the request and the context's streams without touching the map, a `generate()` that stamps it through the shared stages, and a `validateWorld` that calls `design()` again and checks the finished map against it (`designMismatch`, `walkFromFirstColony` in `Pipeline.h`). Each `GeneratorControl` defines `id`, translation label, minimum, maximum, step, default, group, power-of-two display formatting, whether the value participates in terrain weighting, and its `kind`. The all-zero-weight check reads this metadata, not generator-specific option names. IDs must be unique within a generator and cannot shadow shared controls. For irregular integer domains, supply a sorted, unique `allowedValues` list such as `{4, 8, 16}` with matching minimum and maximum. These are literal stored values; `powerOfTwo` instead formats stored exponents. Use `values()`, `indexOf()`, `valueAt()` and `displayValue()` in consumers, never selection-index arithmetic. Use `GeneratorControl::set()` for UI normalization. The service rejects malformed requests instead of silently changing their settings. @@ -34,7 +37,7 @@ Switch modes with `GenerationHistory::select()`. It remembers options per stable ## Generation, randomness and failure -Choose the generation sequence appropriate to the map's character. Modern height-field modes classify terrain, choose starting locations, add resources, then place colonies. Legacy modes place colonies before their final resource pass. Concrete Islands and Isles deliberately interleave region division, resources and starts. Shared helpers do not impose a universal pipeline. +The stages are shared functions; call them in the order the map needs. A designed generator usually runs: `design()`; stamp the layout into a `TerrainSketch`, `layBeaches` and `writeUndermap` (`Sketch.h`); `settleColonies` (`Pipeline.h`); the kits and ambient layers (`growPatch`, `seedNear`, `scatterResources`, `seedAlgae`, `stockIslands`); `clearAroundSwarms`; `guaranteeStartingResources`; `openRoad` for any walk the deposits closed; then `reopenCrampedStarts` at non-default amounts. The height-field modes run `generateHeightField`'s stages (`Terrain.h`) then `placeStarts`; the legacy point-dispersion modes place colonies before their final resource pass and interleave region division, resources and starts. Nothing forces one order, but a generator that needs a different one should say why in its header comment. Use `context.stream("name")` for a per-attempt `std::mt19937` stream, or `context.bounded("name", count)` for a rejection-sampled bounded choice. Seed derivation is defined in `GenerationContext.cpp`: unsigned FNV-1a mixed with the root seed, followed by a fixed integer avalanche. Do not use `std::hash`, wall-clock reseeding, `rand()`, `srand()` or the gameplay RNG in generator algorithms. Noise tables and stamp caches belong to each height-map instance. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 31a81d214..6682ed23c 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -5,25 +5,66 @@ alongside its metadata in a `GeneratorDefinition` (id, stable legacy numeric id, revision, its `GeneratorControl`s, and optional `validateRequest`/`validateWorld` callbacks). `GeneratorRegistry::builtins()` holds the fixed list; `GenerationService` drives the actual lifecycle (validate the request, sample candidate seeds, run `generate`, score the result, -validate the finished world). There is no generator superclass, pipeline dispatcher or terrain -repair policy — a generator is free to build its terrain and resources however it likes, and -leans on the shared modules below only where doing so is actually less work than not. - -## Shared building blocks - -| Concern | Lives in | What it gives a generator | -|---|---|---| -| Discrete/stepped/power-of-two option domains, one shared UI and catalog export | `GeneratorControls` | Declares options once; normalization, display and validation are centralized | -| Cross-control and dimension constraints | `GeneratorDefinition::validateRequest` | A pure check run before generation; no silent settings rewrites | -| Jagged islands, stretched/rotated continents, coastline shape | `shared/Geometry` (`ShapeTransform`, `RadialShape`, `stampShape`) | An invertible shape transform and a seeded radial shape with a per-angle `radiusAt()` query — checking a specific candidate against its own angle is exact, where a single global "furthest possible" bound is only ever a conservative overestimate | -| Distances through corridors, connected components, resource-role adjacency | `shared/Topology`, `shared/Distances` | Graph distances and grid-component labelling with explicit wrapping/neighbor policy | -| Torus grid arithmetic, breadth-first steps, walkable tiles, units by team and the colony reachability check (every torus generator and validator) | `shared/Grid` | One `Torus` with wrap and offset arithmetic, one eight-connected flood, one walkable mask and one "each colony reaches colony 0" check, in place of the copies each generator carried | -| Whole-region or local point dispersion (Contested Commons, Concrete Islands, Isles) | `shared/Regions` | Paired weight/point shuffling, a bounded local search and an opt-in whole-region search that scores every tile from its two nearest weighted distances, kept current as points move, under an explicit pass budget rather than silently treating an unfinished search as converged | -| Settlements restricted to a home island or room | `shared/Settlements` | Whole-footprint mask, nearest legal anchor, exact worker count, per-colony diagnostics | -| Generator-specific connectivity guarantees | `GeneratorDefinition::validateWorld` | Runs after structural checks, against the actual finished terrain/movement mask | - -Geometry, Topology, Distances and Regions operate on plain dimensions and grids, independent of -`Game`. Settlements needs `Game` because placing buildings and units requires its mutation APIs. +validate the finished world). There is no generator superclass or pipeline dispatcher: a +generator is a function that calls the shared stages below in whatever order its map needs, and +a designed generator's body is a short sequence of them. What a generator keeps to itself is its +design — the layout as a pure function of the request — and the few routines that only make sense +for its map. + +## The shared toolkit + +One header per concern under `src/map/generator/shared/`. Everything down to Roads operates on +plain dimensions, masks and a `Map`, independent of `Game`; Settlements, BalancedStarts, Pipeline +and Terrain take a `Game` because placing buildings and units needs its mutation APIs. + +| Module | What it gives a generator | +|---|---| +| `Grid` | `Torus` wrap and offset arithmetic; `floodFrom`/`stepsFrom`, the one eight-connected breadth-first flood (with a step limit and the visit order for callers that need it); `walkableTiles` and `groundUnitTiles` passability masks; `unitTilesByTeam` and `firstColonyCutOff` | +| `Topology` | `connectedRegions` component labelling with explicit wrap and neighbour policy; `regionAdjacency` and `graphDistances` over sparse labels | +| `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid | +| `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; `Blob`, a stretched, turned rough disc in that frame | +| `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `writeUndermap` | +| `LatticeNoise` | Value noise that tiles the torus exactly, in three flavours (`PeriodicNoise` sampled anywhere; `periodicNoise`/`fractalNoise` integer fields; `torusNoise`), plus `percentile` | +| `HeightMap`, `Noise` | Perlin noise faded across the wrap, and the stamped height fields the height-field generators shape | +| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band), `stockIslands` | +| `Resources` | The ambient layer and its fairness guards: `scaledCount`/`scaledShare`, `placeResourceClump`, `setScaledResource`, `scatterResources`, `guaranteeStartingResources`, `openCrampedStarts` | +| `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared | +| `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics | +| `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | +| `Pipeline` | The stages round the others: `settleColonies`, `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | +| `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | +| `StartQuality` | `scoreStarts`, the finished map's colony quality and fairness the service ranks candidates by | +| `GenerationContext` | Named `std::mt19937` streams, `bounded` draws and `shuffle` | +| `legacy/Regions`, `legacy/Distances`, `legacy/StartingPositions` | The older area-grid toolkit: point dispersion (`splitUpPoints`, `splitUpArea`, `divideUpArea`), the legacy distance encoding, `divideUpPlayerLands` and the boot-tile placers `placeStarts`/`placeArchipelagoStarts`. Concrete islands, Isles, Contested commons and the height-field generators still build on it; nothing new should | + +Controls and validation live in `core/`: `GeneratorControls` declares discrete, stepped and +power-of-two option domains once for the UI and the catalog; `GeneratorDefinition::validateRequest` +is a pure check run before generation, and `validateWorld` runs after the structural checks +against the finished terrain. + +## The designed generator + +Nine generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, +City states, Tidal flats, Everglades) follow one shape, and the four newest of them are little +more than a sequence of shared stages: + +1. `design(request, context)` computes the whole layout from the request and the context's + named streams without touching the map, and returns it with a `failure` string when the + request leaves no room. +2. `generate` stamps the layout into a `TerrainSketch`, calls `layBeaches` and `writeUndermap`, + then `settleColonies` with a home mask and an anchor per colony. +3. The kits go down with `growPatch` from `seedNear` seeds, then the ambient layers + (`scatterResources` or the generator's own, `seedAlgae`, `stockIslands`), then + `clearAroundSwarms`, `guaranteeStartingResources` and `clearAroundSwarms` again. +4. `openRoad` (or the generator's own cheapest-walk variant) keeps every walk the map promises + open, clearing only the deposits in the way; `reopenCrampedStarts` runs at non-default amounts. +5. `validateWorld` calls `design` again on a fresh context, checks it with `designMismatch`, + walks every colony from colony 0 with `walkFromFirstColony`, and then checks whatever the + design promised: ponds present, walls standing, fords open, symmetry exact. + +A generator with a different order calls the same stages in its own order; a generator with a +different need (Ring world's belt-wide road, Everglades' fords, Symmetric arena's orbit +stamping) writes that one piece itself and says why in its header comment. ## The generator catalog @@ -33,7 +74,7 @@ reused after a generator is retired. | id | legacy id | Display name | Family | |---|---|---|---| -| `contested-commons` | 9 | Contested commons | Point dispersion (`shared/Regions`) | +| `contested-commons` | 9 | Contested commons | Point dispersion (`shared/legacy/Regions`) | | `maze` | 11 | Maze | Its own — see below | | `fjord-continent` | 12 | Fjord continent | Its own — see below | | `shattered-coast` | 7 | Old random | Iterative water/sand/grass balancer, own resource search | @@ -54,10 +95,11 @@ reused after a generator is retired. | `uniform` | 0 | uniform terrain | Editor-only; one terrain type, unstructured | Swamp, River, Islands and Crater Lakes ("the height-field generators") shape their terrain and -paint their resource bands from the same Perlin noise field via `generateHeightField`, and use -`chooseBalancedStarts` (below) for colony placement. Contested Commons, Concrete Islands and -Isles build on the older Voronoi-style point-dispersion machinery in `shared/Regions` and -`shared/Distances`; Contested Commons additionally paints solid, sharply-bordered resource zones +paint their resource bands from the same Perlin noise field via `generateHeightField`'s stages +(`shared/Terrain`), and use `chooseBalancedStarts` (below) for colony placement. Contested +Commons, Concrete Islands and Isles build on the older Voronoi-style point-dispersion machinery in +`shared/legacy/Regions` and `shared/legacy/Distances`; Contested Commons additionally paints solid, +sharply-bordered resource zones as a deliberate design choice rather than a noise scatter. Shattered Coast and Rugged Archipelago predate the shared resource/placement machinery and still place resources relative to each boot tile with their own compass-direction search, rather than through @@ -189,7 +231,7 @@ evaluate a candidate's growth potential without needing a finished map. Two further shared pieces score and choose where colonies actually start, beyond a generator's own terrain and resources: -- `shared/StartingPositions::chooseBalancedStarts` (used by the four height-field generators) +- `shared/BalancedStarts::chooseBalancedStarts` (used by the four height-field generators) scores every legal site by its *worse* primary resource — a colony beside wood but far from wheat is not a good start — using the finished, as-built state: the swarm's clearing already cleared, its footprint impassable, the flood starting from where workers actually stand. It @@ -586,8 +628,13 @@ as an arena. Fairness is statistical; the lobby keeps the best-scoring of severa footprints, exact worker counts, seed repeatability and RNG stream isolation, the lobby/editor UI's own control-editing behavior, and the shared toolkit's own guarantees: the point dispersion ends at a mutual best response checked against a brute-force score, - and the distance flood matches a Chebyshev oracle on the torus, with obstacles and repeated - sources. + the distance flood matches a Chebyshev oracle on the torus, with obstacles and repeated + sources, and `test/MapGeneratorToolkitChecks.h` checks every module of `shared/` on a map + built by hand — floods and their limits, beaches and islands, patches and algae bands, the + cheapest route, settlements and the colony walk, the crop guarantee through and around a + wall, a buried colony's room, balanced starts, per-landmass scatter, lattice noise, the + wedge frame and the context's shuffle — so a change to a module fails there before it shows + up as a changed golden fingerprint downstream. - `test/MapGeneratorGoldenTest.cpp` builds to `MapGeneratorGoldenTest` and keeps the revision rule honest. `test/map-generator-golden.txt` records, per platform, the fingerprint (terrain, resources and colony starts) every registered generator produces for three seeds at 256, one diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index b17452ecc..5a110e0e7 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -14,6 +14,6 @@ some start positions in real games; see [Map fairness tournament](FAIRNESS_TOURN Study results and screenshots belong in ignored `artifacts/`, not in this directory. Record investigation findings and validation results in the relevant pull request. -See [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) for how the shared modules, generator -catalog, resource placement and colony fairness currently work, and -[Adding a generator](ADDING_A_GENERATOR.md) for the registry and module interfaces. +See [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) for the shared toolkit a generator is +built from, the designed-generator pipeline, the generator catalog, resource placement and colony +fairness, and [Adding a generator](ADDING_A_GENERATOR.md) for the registry and module interfaces. diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 6110b43f7..4dfbfb375 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -8,6 +8,7 @@ #include "IntBuildingType.h" #include "LegacyGenerationDescriptor.h" #include "MapGeneratorFrameworkChecks.h" +#include "MapGeneratorToolkitChecks.h" #include "NewMapScreen.h" #include "Race.h" #include "Resources.h" @@ -86,6 +87,7 @@ class MapGeneratorDefaultsTest { globalsInit(); frameworkChecks(); + ToolkitChecks::toolkitChecks(); GenerationService service; for (int method : GeneratorRegistry::builtins().methods()) { diff --git a/test/MapGeneratorToolkitChecks.h b/test/MapGeneratorToolkitChecks.h new file mode 100644 index 000000000..6a4a46743 --- /dev/null +++ b/test/MapGeneratorToolkitChecks.h @@ -0,0 +1,632 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +// Contracts of the shared generator toolkit under src/map/generator/shared, each checked on a +// small map built by hand rather than through a generator, so a change to one module fails +// here before it shows up as a changed golden fingerprint somewhere downstream. Needs the +// globals loaded: building and resource types. +#include "BalancedStarts.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "LatticeNoise.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "StartQuality.h" +#include "Wedge.h" +#include +#include +#include +#include +#include +#include +#include + +namespace ToolkitChecks +{ +using namespace MapGeneration; + +inline void grassMap(Game &game, int wDec, int hDec) +{ + game.map.setSize(wDec, hDec); + game.map.setGame(&game); + game.map.makeHomogenMap(GRASS); +} + +inline int countResource(const Map &map, int type) +{ + int count = 0; + for (int y = 0; y < map.getH(); ++y) + for (int x = 0; x < map.getW(); ++x) + count += map.getResource(x, y).type == type; + return count; +} + +// floodFrom: steps match the Chebyshev oracle on an open torus, a limit stops the flood there, +// tiles are visited in nondecreasing order, and an obstacle ring holds it. +inline void floodChecks() +{ + const Torus t(16, 8); + std::vector source(t.size(), 0), open(t.size(), 1); + source[t.at(3, 2)] = 1; + const Flood limited = floodFrom(t, source, open, 3); + assert(limited.steps[t.at(3, 2)] == 0 && limited.steps[t.at(6, 2)] == 3); + assert(limited.steps[t.at(7, 2)] == -1 && limited.steps[t.at(0, 7)] == 3); + assert(limited.visited.front() == t.at(3, 2)); + for (size_t i = 1; i < limited.visited.size(); ++i) + assert(limited.steps[limited.visited[i - 1]] <= limited.steps[limited.visited[i]]); + const std::vector steps = stepsFrom(t, source, open); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + assert(steps[t.at(x, y)] == t.chebyshev(3, 2, x, y)); + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (dx || dy) + open[t.at(3 + dx, 2 + dy)] = 0; + const std::vector held = stepsFrom(t, source, open); + assert(held[t.at(3, 2)] == 0 && held[t.at(4, 2)] == -1 && held[t.at(9, 5)] == -1); + assert(std::count(held.begin(), held.end(), 0) == 1); +} + +// Sketch: every land tile touching water becomes beach and nothing else changes; islands raise +// out at sea, clear of the coast and of each other, and the same seed raises the same islands. +inline void sketchChecks() +{ + const Torus t(64, 64); + TerrainSketch sketch(t.size(), GRASS); + for (int y = 10; y < 14; ++y) + for (int x = 10; x < 14; ++x) + sketch[t.at(x, y)] = WATER; + const TerrainSketch before = sketch; + layBeaches(sketch, t); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = t.at(x, y); + if (before[i] == WATER) + { + assert(sketch[i] == WATER); + continue; + } + bool shore = false; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + shore = shore || before[t.at(x + dx, y + dy)] == WATER; + assert(sketch[i] == (shore ? SAND : GRASS)); + } + assert(countTiles(sketch, WATER) == 16 && countTiles(sketch, SAND) == 20); + + TerrainSketch sea(t.size(), WATER); + for (int y = 0; y < 4; ++y) + for (int x = 0; x < 4; ++x) + sea[t.at(x, y)] = GRASS; + GenerationRequest request; + request.seed = 11; + GenerationContext context(request), again(request); + const IslandPlacement placement{"islands", 2, 40, 7.0}; + TerrainSketch sameSea = sea; + const std::vector islands = raiseIslands(sea, t, context, placement); + const std::vector same = raiseIslands(sameSea, t, again, placement); + assert(!islands.empty() && islands.size() <= 2 && islands.size() == same.size()); + assert(sea == sameSea); + int raised = 0; + for (const Island &island : islands) + { + assert(!island.tiles.empty() && island.reach > 0); + for (int i : island.tiles) + assert(sea[i] == GRASS); + assert(std::find(island.tiles.begin(), island.tiles.end(), t.at(island.x, island.y)) != + island.tiles.end()); + // Clear of the corner's land: more than the moat plus the outline's reach away. + assert(t.chebyshev(island.x, island.y, 2, 2) > island.reach + 7 - 2); + raised += int(island.tiles.size()); + } + for (size_t a = 0; a < islands.size(); ++a) + for (size_t b = a + 1; b < islands.size(); ++b) + { + const double gap = islands[a].reach + islands[b].reach + 7; + assert(t.dist2(islands[a].x, islands[a].y, islands[b].x, islands[b].y) >= gap * gap); + } + assert(countTiles(sea, GRASS) == 16 + raised); + TerrainSketch none(t.size(), WATER); + assert(raiseIslands(none, t, context, placement).empty()); // no coast to keep clear of +} + +// Planting: patches grow to size and stay four-connected, seedNear finds the nearest eligible +// tile, algae lands on water only and in the band asked for, and the swarm clearance is exact. +inline void plantingChecks() +{ + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus t(map); + assert(clearGround(map, 5, 5)); + const auto anywhere = [](int) { return true; }; + assert(growPatch(map, t, t.at(20, 20), CORN, 12, anywhere) == 12); + assert(countResource(map, CORN) == 12 && !clearGround(map, 20, 20)); + std::vector corn(t.size(), 0), seed(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + corn[i] = map.getResource(i % t.w, i / t.w).type == CORN; + seed[t.at(20, 20)] = 1; + // Four-connected: a cardinal flood over the patch from its seed reaches every tile of it. + std::vector reached{t.at(20, 20)}; + std::vector seen(t.size(), 0); + seen[t.at(20, 20)] = 1; + for (size_t head = 0; head < reached.size(); ++head) + for (const auto &step : kCardinalSteps) + { + const int n = t.at(reached[head] % t.w + step[0], reached[head] / t.w + step[1]); + if (corn[n] && !seen[n]) + { + seen[n] = 1; + reached.push_back(n); + } + } + assert(reached.size() == 12); + const auto clear = [&](int i) { return clearGround(map, i % t.w, i / t.w); }; + const int near = seedNear(t, 20, 20, 6, clear); + assert(near >= 0 && clear(near)); + for (int dy = -6; dy <= 6; ++dy) + for (int dx = -6; dx <= 6; ++dx) + if (clear(t.at(20 + dx, 20 + dy))) + assert(dx * dx + dy * dy >= t.dist2(20, 20, near % t.w, near / t.w)); + assert(seedNear(t, 20, 20, 2, [](int) { return false; }) == -1); + // Only eligible tiles are planted, and the count stops where eligibility does: a 2 by 4 box. + const auto box = [&](int i) + { return i % t.w >= 40 && i % t.w < 42 && i / t.w >= 40 && i / t.w < 44; }; + assert(growPatch(map, t, t.at(40, 40), WOOD, 50, box) == 8); + assert(countResource(map, WOOD) == 8); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + if (map.getResource(x, y).type == WOOD) + assert(box(t.at(x, y))); + + GenerationRequest request; + request.seed = 5; + request.nbTeams = 1; + GenerationContext context(request); + // A pond: algae anywhere on its water, then only in its shallows. + for (int y = 40; y < 52; ++y) + for (int x = 8; x < 20; ++x) + map.setUMTerrain(x, y, WATER); + map.controlSand(); + map.rebuildTerrain(); + seedAlgae(map, context, t, "algae", 100, AlgaeBand::anyWater(1)); + const int anywhereAlgae = countResource(map, ALGA); + assert(anywhereAlgae > 0); + std::vector wet(t.size()), dry(t.size()); + for (int i = 0; i < t.size(); ++i) + { + wet[i] = map.isWater(i % t.w, i / t.w); + dry[i] = !wet[i]; + if (map.getResource(i % t.w, i / t.w).type == ALGA) + { + assert(wet[i]); + map.setNoResource(i % t.w, i / t.w, 1); + } + } + const std::vector offshore = stepsFrom(t, dry, wet); + // The band constrains the clumps' centres, so single-tile clumps show it exactly. + seedAlgae(map, context, t, "algae", 100, AlgaeBand{2, 3, 1, 0}); + assert(countResource(map, ALGA) > 0); + for (int i = 0; i < t.size(); ++i) + if (map.getResource(i % t.w, i / t.w).type == ALGA) + assert(offshore[i] >= 2 && offshore[i] <= 3); + assert(scaledCount(40, 0) == 0); + map.setNoResource(8, 40, 1); + seedAlgae(map, context, t, "algae", 0, AlgaeBand::anyWater(1)); + + // The swarm clearance: the 4x4 footprint and kSwarmClearance tiles round it. + context.bootX[0] = 30; + context.bootY[0] = 10; + const std::vector reserved = swarmSurroundings(t, context); + assert(std::count(reserved.begin(), reserved.end(), 1) == 8 * 8); + assert(reserved[t.at(28, 8)] && reserved[t.at(35, 15)] && !reserved[t.at(27, 8)] && + !reserved[t.at(36, 15)]); + for (int dy = -3; dy < 7; ++dy) + for (int dx = -3; dx < 7; ++dx) + map.setResource(30 + dx, 10 + dy, STONE, 1); + std::vector keep(t.size(), 0); + keep[t.at(30, 10)] = 1; + clearAroundSwarms(map, context, t, &keep); + for (int dy = -3; dy < 7; ++dy) + for (int dx = -3; dx < 7; ++dx) + { + const bool inside = dx >= -2 && dx < 6 && dy >= -2 && dy < 6; + const bool kept = dx == 0 && dy == 0; + assert((map.getResource(30 + dx, 10 + dy).type == STONE) == (!inside || kept)); + } +} + +// Roads: the cheapest route crosses the fewest costly tiles, never a blocked one, and is empty +// when there is no way; openRoad clears exactly the deposits on it. +inline void roadChecks() +{ + // Column 15 is blocked, so the way round the wrap is shut; column 7 costs a deposit to cross and + // column 9 is shut but for one gate at (9, 8). + const Torus t(16, 16); + std::vector goal(t.size(), 0), blocked(t.size(), 0), costly(t.size(), 0); + goal[t.at(12, 8)] = 1; + for (int y = 0; y < t.h; ++y) + { + costly[t.at(7, y)] = 1; + blocked[t.at(15, y)] = 1; + if (y != 8) + blocked[t.at(9, y)] = 1; + } + const std::vector route = cheapestRoute(t, {t.at(2, 8)}, goal, blocked, costly); + assert(!route.empty() && route.front() == t.at(12, 8) && route.back() == t.at(2, 8)); + int crossings = 0; + bool gate = false; + for (int i : route) + { + assert(!blocked[i]); + crossings += costly[i]; + gate = gate || i == t.at(9, 8); + } + assert(crossings == 1 && gate); + blocked[t.at(9, 8)] = 1; + assert(cheapestRoute(t, {t.at(2, 8)}, goal, blocked, costly).empty()); + + // On the map, stone columns either side of the source: any walk to the goal crosses one of + // them, and openRoad clears exactly one tile of it. + Game game(nullptr); + grassMap(game, 4, 4); + Map &map = game.map; + for (int y = 0; y < map.getH(); ++y) + { + map.setResource(7, y, STONE, 1); + map.setResource(15, y, STONE, 1); + } + assert(countResource(map, STONE) == 32); + assert(openRoad(map, Torus(map), {t.at(2, 8)}, goal)); + assert(countResource(map, STONE) == 31); + std::vector wall(t.size(), 0); + for (int y = 0; y < t.h; ++y) + wall[t.at(9, y)] = wall[t.at(15, y)] = 1; + assert(!openRoad(map, Torus(map), {t.at(2, 8)}, goal, &wall)); + assert(countResource(map, STONE) == 31); +} + +// Settlements and the pipeline stages round them: settleColonies puts every colony down, +// walkFromFirstColony reports a cut-off colony by number, the guarantee reaches wheat and wood +// through a resource wall but never through a protected one, and openCrampedStarts gives a +// buried colony its building room back. +inline void settlementChecks() +{ + { + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus t(map); + GenerationRequest request; + request.seed = 3; + request.nbTeams = 2; + request.nbWorkers = 3; + GenerationContext context(request); + game.addTeam(); + game.addTeam(); + const std::vector everywhere(t.size(), 1); + assert(settleColonies( + game, context, "starts", [&](int) { return everywhere; }, + [](int team) { return MapGeneratorPoint(team ? 44 : 12, 20); })); + assert(context.bootX[0] == 12 && context.bootY[0] == 20 && context.bootX[1] == 44); + ColonyWalk walk = walkFromFirstColony(map, 2, "the map", ""); + assert(walk.error.empty() && walk.workers[1].size() == 3 && walk.steps[t.at(12, 19)] >= 0); + for (int i : walk.workers[1]) + assert(walk.steps[i] > 0); + assert(!reopenCrampedStarts(game, context, {})); + // Scoring the finished colonies. + growPatch(map, t, t.at(12, 26), CORN, 10, [](int) { return true; }); + growPatch(map, t, t.at(44, 26), CORN, 10, [](int) { return true; }); + growPatch(map, t, t.at(8, 14), WOOD, 10, [](int) { return true; }); + growPatch(map, t, t.at(40, 14), WOOD, 10, [](int) { return true; }); + const StartQualityReport report = scoreStarts(game, 2); + assert(report.measured && report.colonies.size() == 2); + for (const ColonyQuality &colony : report.colonies) + assert(colony.wheatDistance > 0 && colony.woodDistance > 0 && colony.total > 0 && + colony.total <= 1 && colony.buildSites > 0); + assert(report.worst <= report.best && report.score <= report.worst + 1e-12 && + report.fairness > 0 && report.fairness <= 1); + // Two water walls, one across the wrap, cut colony 1 off from colony 0. + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + if ((x >= 28 && x < 32) || x < 4) + map.setUMTerrain(x, y, WATER); + map.rebuildTerrain(); + walk = walkFromFirstColony(map, 2, "the map", "over the wall"); + assert(walk.error == "Colony 1 cannot walk to colony 0 over the wall."); + assert(walkFromFirstColony(map, 0, "the map", "").error == + "Colony 0 has no workers to walk the map."); + } + { + // A colony on bare grass: the guarantee gives it wheat and wood within range. + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus t(map); + GenerationRequest request; + request.seed = 9; + request.nbTeams = 1; + GenerationContext context(request); + context.bootX[0] = 20; + context.bootY[0] = 20; + guaranteeStartingResources(game, context, 24, 32); + const auto nearest = [&](int type) + { + const std::vector steps = + stepsFrom(t, tileMask(t, {t.at(20, 20)}), groundUnitTiles(map)); + int best = -1; + for (int i = 0; i < t.size(); ++i) + if (map.getResource(i % t.w, i / t.w).type == type) + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int d = steps[t.at(i % t.w + dx, i / t.w + dy)]; + if (d >= 0 && (best < 0 || d + 1 < best)) + best = d + 1; + } + return best; + }; + assert(nearest(CORN) > 0 && nearest(CORN) <= 24 && nearest(WOOD) > 0 && + nearest(WOOD) <= 32); + // Walled into a pocket by stone with crops outside: the wall is cleared, not added to. + Game walled(nullptr); + grassMap(walled, 6, 6); + Map &pocket = walled.map; + std::vector ring(t.size(), 0); + for (int dy = -3; dy <= 3; ++dy) + for (int dx = -3; dx <= 3; ++dx) + if (std::max(std::abs(dx), std::abs(dy)) == 3) + { + pocket.setResource(20 + dx, 20 + dy, STONE, 1); + ring[t.at(20 + dx, 20 + dy)] = 1; + } + pocket.setResource(28, 20, CORN, 1); + pocket.setResource(12, 20, WOOD, 1); + guaranteeStartingResources(walled, context, 24, 32); + assert(countResource(pocket, STONE) < 24 && countResource(pocket, CORN) == 1 && + countResource(pocket, WOOD) == 1); + // The same pocket with the ring protected: the stone stays and crops are placed inside. + Game keep(nullptr); + grassMap(keep, 6, 6); + Map &designed = keep.map; + for (int i = 0; i < t.size(); ++i) + if (ring[i]) + designed.setResource(i % t.w, i / t.w, STONE, 1); + designed.setResource(28, 20, CORN, 1); + designed.setResource(12, 20, WOOD, 1); + guaranteeStartingResources(keep, context, 24, 32, 0, &ring); + assert(countResource(designed, STONE) == 24 && countResource(designed, CORN) > 1 && + countResource(designed, WOOD) > 1); + } + { + // A colony buried in wood: openCrampedStarts clears rings until it can build again. + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus t(map); + GenerationRequest request; + request.seed = 21; + request.nbTeams = 1; + request.nbWorkers = 2; + GenerationContext context(request); + game.addTeam(); + const std::vector everywhere(t.size(), 1); + assert(placeSettlement(game, context, 0, everywhere, {30, 30}, "starts")); + for (int dy = -12; dy <= 12; ++dy) + for (int dx = -12; dx <= 12; ++dx) + if (clearGround(map, t.x(30 + dx), t.y(30 + dy))) + map.setResource(t.x(30 + dx), t.y(30 + dy), WOOD, 1); + const auto sites = [&] + { + const Flood flood = floodFrom(t, tileMask(t, unitTilesByTeam(map, 1)[0]), + groundUnitTiles(map), 24); + int count = 0; + for (int i : flood.visited) + count += map.isFreeForBuilding(i % t.w, i / t.w, 4, 4); + return count; + }; + assert(sites() < 16); + const int buried = countResource(map, WOOD); + openCrampedStarts(game, context); + assert(sites() >= 16 && countResource(map, WOOD) < buried && countResource(map, WOOD) > 0); + assert(reopenCrampedStarts(game, context, {100, 150, 100, 100, 100})); + } +} + +// chooseBalancedStarts picks legal, mutually distant sites that can reach both crops, the same +// ones every time for the same map. +inline void balancedStartChecks() +{ + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus t(map); + const auto anywhere = [](int) { return true; }; + growPatch(map, t, t.at(16, 16), CORN, 12, anywhere); + growPatch(map, t, t.at(22, 16), WOOD, 12, anywhere); + growPatch(map, t, t.at(48, 48), CORN, 12, anywhere); + growPatch(map, t, t.at(42, 48), WOOD, 12, anywhere); + GenerationRequest request; + request.seed = 4; + request.nbTeams = 2; + request.nbWorkers = 4; + GenerationContext context(request), again(request); + assert(chooseBalancedStarts(game, context, 20 * 20)); + assert(chooseBalancedStarts(game, again, 20 * 20)); + for (int team = 0; team < 2; ++team) + { + assert(context.bootX[team] == again.bootX[team] && context.bootY[team] == again.bootY[team]); + assert(map.isFreeForBuilding(context.bootX[team], context.bootY[team], 4, 4)); + } + assert(t.dist2(context.bootX[0], context.bootY[0], context.bootX[1], context.bootY[1]) >= + 20 * 20); + // No wood anywhere: no balanced set exists, and the boot tiles are left alone. + Game bare(nullptr); + grassMap(bare, 6, 6); + growPatch(bare.map, t, t.at(16, 16), CORN, 12, anywhere); + GenerationContext untouched(request); + untouched.bootX[0] = untouched.bootY[0] = 7; + assert(!chooseBalancedStarts(bare, untouched, 20 * 20)); + assert(untouched.bootX[0] == 7 && untouched.bootY[0] == 7); +} + +// scatterResources shares each band out between landmasses: two islands both get farmland and +// stone, and nothing lands on water. +inline void scatterChecks() +{ + Game game(nullptr); + game.map.setSize(6, 6); + game.map.setGame(&game); + game.map.makeHomogenMap(WATER); + for (int y = 20; y < 40; ++y) + for (int x = 0; x < 64; ++x) + if ((x >= 4 && x < 24) || (x >= 36 && x < 56)) + game.map.setUMTerrain(x, y, GRASS); + game.map.controlSand(); + game.map.rebuildTerrain(); + GenerationRequest request; + request.seed = 13; + GenerationContext context(request); + scatterResources(game, context, {40, 20, 30, 0, 0}); + int corn[2] = {0, 0}, wood[2] = {0, 0}, stone[2] = {0, 0}; + for (int y = 0; y < game.map.getH(); ++y) + for (int x = 0; x < game.map.getW(); ++x) + { + const int type = game.map.getResource(x, y).type; + if (type == NO_RES_TYPE) + continue; + assert(!game.map.isWater(x, y)); + const int island = x < 30 ? 0 : 1; + corn[island] += type == CORN; + wood[island] += type == WOOD; + stone[island] += type == STONE; + } + for (int island = 0; island < 2; ++island) + assert(corn[island] > 0 && wood[island] > 0 && stone[island] > 0); + assert(corn[0] + corn[1] > wood[0] + wood[1]); +} + +// Lattice noise tiles the torus, stays in range and is deterministic; percentile is what it says. +inline void noiseChecks() +{ + std::mt19937 rng(77), same(77); + const PeriodicNoise noise(64, 32, 8, rng), twin(64, 32, 8, same); + for (int y = 0; y < 32; y += 3) + for (int x = 0; x < 64; x += 5) + { + const double v = noise.at(x + 0.5, y + 0.5); + assert(v >= 0 && v < 1 && v == twin.at(x + 0.5, y + 0.5)); + assert(std::abs(noise.at(x + 64.5, y + 0.5) - v) < 1e-9); + assert(std::abs(noise.at(x + 0.5, y - 31.5) - v) < 1e-9); + } + const std::vector field = periodicNoise(64, 32, 16, rng); + const std::vector fractal = fractalNoise(64, 32, 16, 3, rng); + assert(field.size() == 64 * 32 && fractal.size() == field.size()); + for (int v : field) + assert(v >= 0 && v <= 65535); + for (int v : fractal) + assert(v >= 0 && v <= 65535); + const std::vector torus = torusNoise(64, 32, rng); + float peak = 0; + for (float v : torus) + { + assert(v >= -1 && v <= 1); + peak = std::max(peak, std::abs(v)); + } + assert(peak == 1.0f); + assert(percentile({5, 1, 4, 2, 3}, 40) == 3 && percentile({5, 1, 4, 2, 3}, 0) == 1); + assert(percentile({5, 1, 4, 2, 3}, 100) == 5 && percentile({}, 50) == 0); +} + +// The wedge frame divides the map into equal wedges round the centre, with the arc offset zero +// on each wedge's middle; a blob holds what lies inside its outline. +inline void wedgeChecks() +{ + const Torus t(64, 64); + const WedgeFrame frame(t, 0.0, 4); + assert(frame.cx == 32 && frame.cy == 32 && std::abs(frame.wedge - kPi / 2) < 1e-12); + int perWedge[4] = {0, 0, 0, 0}; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const WedgeFrame::Cell cell = frame.cell(x, y); + assert(cell.k >= 0 && cell.k < 4 && cell.u >= 0 && cell.u < 1); + assert(std::abs(cell.s - cell.d * (cell.u - 0.5) * frame.wedge) < 1e-9); + assert(std::abs(cell.d - std::hypot(double(cell.dx), double(cell.dy))) < 1e-12); + ++perWedge[cell.k]; + } + for (int k = 0; k < 4; ++k) + assert(perWedge[k] > 64 * 64 / 5); + const WedgeFrame::Cell middle = frame.cell(32 + 7, 32 + 7); // straight down wedge 0's middle + assert(middle.k == 0 && std::abs(middle.s) < 1e-9 && std::abs(middle.u - 0.5) < 1e-12); + WedgeFrame::Cell bent = middle; + frame.bend(bent, 2.0); + assert(bent.s < middle.s - 1.5 && bent.d == middle.d); + GenerationRequest request; + request.seed = 2; + GenerationContext context(request); + const Blob round{0, 10, 1.0, 0.0, RadialShape(3, 0, context, "blob")}; + assert(round.holds(0, 0) && round.holds(2.5, 0) && !round.holds(0, 3.5) && round.reach() == 3); + const Blob stretched{0, 10, 2.0, 0.0, RadialShape(3, 0, context, "blob")}; + assert(stretched.holds(5, 0) && !stretched.holds(0, 2) && stretched.reach() == 6); +} + +// GenerationContext::shuffle is a permutation drawn from its stream: deterministic per seed and +// name, independent of the other streams. +inline void shuffleChecks() +{ + GenerationRequest request; + request.seed = 6; + GenerationContext context(request), again(request); + std::vector order(20), same(20); + for (int i = 0; i < 20; ++i) + order[i] = same[i] = i; + context.bounded("other", 1000); + context.shuffle(order.begin(), order.end(), "deal"); + again.shuffle(same.begin(), same.end(), "deal"); + assert(order == same && order != std::vector({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19})); + std::vector sorted = order; + std::sort(sorted.begin(), sorted.end()); + for (int i = 0; i < 20; ++i) + assert(sorted[i] == i); + struct Layout + { + Torus t{1, 1}; + std::string failure; + }; + Game game(nullptr); + game.map.setSize(5, 5); + Layout rebuilt; + rebuilt.t = {32, 32}; + assert(designMismatch(rebuilt, game.map, "test").empty()); + rebuilt.t = {16, 32}; + assert(designMismatch(rebuilt, game.map, "test") == + "The test design does not match the map size."); + rebuilt.failure = "no room"; + assert(designMismatch(rebuilt, game.map, "test") == + "The test design could not be rebuilt: no room"); +} + +inline void toolkitChecks() +{ + floodChecks(); + sketchChecks(); + plantingChecks(); + roadChecks(); + settlementChecks(); + balancedStartChecks(); + scatterChecks(); + noiseChecks(); + wedgeChecks(); + shuffleChecks(); + puts("PASS shared toolkit: floods, sketch, planting, roads, settlements, balanced starts, " + "scatter, lattice noise, wedge frame, shuffle"); +} +} // namespace ToolkitChecks From 8051b17bc83beb2d1e6014d0bfa0904bbc5eb796 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 16:40:18 -0400 Subject: [PATCH 105/136] Converge the noise, roads, kits and island rule; bump three revisions With bit-for-bit reproduction no longer required where the algorithm is the same, the leftovers from the extraction close: - LatticeNoise is one implementation, PeriodicNoise, with periodicNoise, fractalNoise and torusNoise sampled from it. Stone highlands' integer arithmetic goes, so its revision moves to 2; Ring world's field came out identical. - raiseIslands has one spacing rule, the sum of both islands' reaches plus the moat. City states used twice the new island's reach; its revision moves to 5. - Tidal flats plants each neutral island in tile order rather than in the order a flood visited it; revision 4. - cheapestWalk in Roads.h is the weighted Dijkstra Everglades' ford-opening route and Symmetric arena's causeway paths were each writing; both are byte-identical on it. - plantKit in Planting.h plants a home's wheat, wood and stone from three seeds, replacing the same nine lines in City states, Tidal flats and Everglades. - GenerationRequest::Method names only the ids the legacy descriptor knows; GeneratorRegistry::idOf looks the rest up by string id, and the two tests that named modern ids use it. The golden table's rows for the three bumped generators are regenerated; the other 112 rows are unchanged. Defaults test and custom-setup harness pass. The docs describe the converged modules and outline a generator built from the stages. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/map-generators/ADDING_A_GENERATOR.md | 42 ++++++++++++- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 13 ++-- src/map/generator/core/GenerationRequest.h | 15 ++--- src/map/generator/core/GeneratorRegistry.cpp | 7 +++ src/map/generator/core/GeneratorRegistry.h | 3 + .../generators/CityStatesGenerator.cpp | 49 +++++---------- .../generators/EvergladesGenerator.cpp | 62 +++++-------------- .../generators/StoneHighlandsGenerator.cpp | 2 +- .../generators/SymmetricArenaGenerator.cpp | 50 +++++---------- .../generators/TidalFlatsGenerator.cpp | 40 ++++++------ src/map/generator/shared/LatticeNoise.cpp | 43 ++----------- src/map/generator/shared/LatticeNoise.h | 16 ++--- src/map/generator/shared/Planting.h | 31 ++++++++++ src/map/generator/shared/Roads.h | 61 ++++++++++++++++++ src/map/generator/shared/Sketch.cpp | 3 +- src/map/generator/shared/Sketch.h | 4 -- test/CustomGameSetupHarness.cpp | 9 +-- test/MapGeneratorDefaultsTest.cpp | 8 +-- test/map-generator-golden.txt | 42 ++++++------- 19 files changed, 259 insertions(+), 241 deletions(-) diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index f35065cb8..9e3caaf27 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -17,7 +17,7 @@ Extract on the second use. When a routine you are about to write already exists ## Register a module 1. Add `ExampleGenerator.h/.cpp` under `src/map/generator/generators/`. Use a named options struct that reads its scalar values from the request by stable option ID. -2. Return a `GeneratorDefinition` from `exampleDefinition()`. Provide a stable string ID, unique nonnegative numeric ID, translation key, revision, editor-only availability, control list and callback. Numeric IDs are identifiers, never list positions; gaps are supported. `hasStartingColonies` defaults to true. Uniform alone disables normal starting-colony validation in the shipped catalog. +2. Return a `GeneratorDefinition` from `exampleDefinition()`. Provide a stable string ID, unique nonnegative numeric ID, translation key, revision, editor-only availability, control list and callback. Numeric IDs are identifiers, never list positions; gaps are supported, and the id lives only in the definition (`GeneratorRegistry::idOf` looks one up by string; the `Method` enum names only the ids the legacy descriptor knows). `hasStartingColonies` defaults to true. Uniform alone disables normal starting-colony validation in the shipped catalog. 3. Add that definition to the single list in `GeneratorRegistry::builtins()` and its source to `src/SConscript`. Add the display name and control labels to the translation tables using the existing translation workflow. 4. Write the callback as `bool generate(Game&, GenerationContext&)`. Read the immutable request, build into the supplied fresh game, set `context.stage` before operations that may fail, and return false with a useful `context.detail` if the layout cannot be placed. The usual shape is a `design()` that computes the whole layout from the request and the context's streams without touching the map, a `generate()` that stamps it through the shared stages, and a `validateWorld` that calls `design()` again and checks the finished map against it (`designMismatch`, `walkFromFirstColony` in `Pipeline.h`). @@ -35,6 +35,46 @@ Shared settings are width and height exponents, colony count, starting workers a Switch modes with `GenerationHistory::select()`. It remembers options per stable numeric ID and carries the shared controls between modes. Do not add UI-specific defaults or range tables. +## A generator from the stages + +The shape most generators take, as an outline (the real ones add their own design): + +```cpp +struct Layout { Torus t{1, 1}; std::vector water; std::vector homeX, homeY; + std::string failure; }; +Layout design(const GenerationRequest &r, GenerationContext &c); // pure: request and streams only + +bool generate(Game &game, GenerationContext &context) +{ + const Layout L = design(context.request, context); + if (!L.failure.empty()) { context.detail = L.failure; return false; } + for (int i = 0; i < context.request.nbTeams; ++i) game.addTeam(); + TerrainSketch terrain(L.t.size(), GRASS); // stamp the design + for (int i = 0; i < L.t.size(); ++i) if (L.water[i]) terrain[i] = WATER; + layBeaches(terrain, L.t); + writeUndermap(game.map, terrain); + if (!settleColonies(game, context, "starts", homeMaskFor(L), anchorFor(L))) return false; + for (int team = 0; team < context.request.nbTeams; ++team) + plantKit(game.map, L.t, context, kitFor(L, team), eligibleFor(L, team)); + scatterResources(game, context, {24, 12, 10, 0, 3}); + seedAlgae(game.map, context, L.t, "algae", options.algae, AlgaeBand::shallows(2, 4)); + clearAroundSwarms(game.map, context, L.t); + guaranteeStartingResources(game, context, 24, 32); + reopenCrampedStarts(game, context, {options.wheat, options.wood, options.stone, options.algae}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (const std::string m = designMismatch(L, game.map, "example"); !m.empty()) return m; + return walkFromFirstColony(game.map, context.request.nbTeams, "the map", "").error; +} +``` + +Everything named here is in `shared/`; `homeMaskFor`, `anchorFor`, `kitFor` and `eligibleFor` are the generator's own lambdas over its layout. + ## Generation, randomness and failure The stages are shared functions; call them in the order the map needs. A designed generator usually runs: `design()`; stamp the layout into a `TerrainSketch`, `layBeaches` and `writeUndermap` (`Sketch.h`); `settleColonies` (`Pipeline.h`); the kits and ambient layers (`growPatch`, `seedNear`, `scatterResources`, `seedAlgae`, `stockIslands`); `clearAroundSwarms`; `guaranteeStartingResources`; `openRoad` for any walk the deposits closed; then `reopenCrampedStarts` at non-default amounts. The height-field modes run `generateHeightField`'s stages (`Terrain.h`) then `placeStarts`; the legacy point-dispersion modes place colonies before their final resource pass and interleave region division, resources and starts. Nothing forces one order, but a generator that needs a different one should say why in its header comment. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 6682ed23c..e4188aead 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -24,11 +24,11 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid | | `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; `Blob`, a stretched, turned rough disc in that frame | | `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `writeUndermap` | -| `LatticeNoise` | Value noise that tiles the torus exactly, in three flavours (`PeriodicNoise` sampled anywhere; `periodicNoise`/`fractalNoise` integer fields; `torusNoise`), plus `percentile` | +| `LatticeNoise` | `PeriodicNoise`, value noise that tiles the torus exactly and samples anywhere, with `periodicNoise`/`fractalNoise` (integer fields per tile, octaves) and `torusNoise` (four octaves in [-1, 1]) sampled from it, plus `percentile` | | `HeightMap`, `Noise` | Perlin noise faded across the wrap, and the stamped height fields the height-field generators shape | -| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band), `stockIslands` | +| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band), `stockIslands` | | `Resources` | The ambient layer and its fairness guards: `scaledCount`/`scaledShare`, `placeResourceClump`, `setScaledResource`, `scatterResources`, `guaranteeStartingResources`, `openCrampedStarts` | -| `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared | +| `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared; `cheapestWalk`, the same search by any step cost (Everglades' fords, Symmetric arena's causeway routes) | | `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | | `Pipeline` | The stages round the others: `settleColonies`, `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | @@ -53,7 +53,8 @@ more than a sequence of shared stages: request leaves no room. 2. `generate` stamps the layout into a `TerrainSketch`, calls `layBeaches` and `writeUndermap`, then `settleColonies` with a home mask and an anchor per colony. -3. The kits go down with `growPatch` from `seedNear` seeds, then the ambient layers +3. The kits go down with `plantKit` (three seeds, each grown from the nearest eligible + tile), then the ambient layers (`scatterResources` or the generator's own, `seedAlgae`, `stockIslands`), then `clearAroundSwarms`, `guaranteeStartingResources` and `clearAroundSwarms` again. 4. `openRoad` (or the generator's own cheapest-walk variant) keeps every walk the map promises @@ -63,8 +64,8 @@ more than a sequence of shared stages: design promised: ponds present, walls standing, fords open, symmetry exact. A generator with a different order calls the same stages in its own order; a generator with a -different need (Ring world's belt-wide road, Everglades' fords, Symmetric arena's orbit -stamping) writes that one piece itself and says why in its header comment. +different need (Ring world's belt-wide road, Symmetric arena's orbit stamping) writes that one +piece itself and says why in its header comment. ## The generator catalog diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index 7e44d6b44..3f00e8e17 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -10,6 +10,9 @@ class GeneratorRegistry; struct GenerationRequest { + /// The numeric ids the fixed legacy descriptor (compatibility/) knows by name. Every other + /// generator's id lives only in its GeneratorDefinition; look one up by its string id with + /// GeneratorRegistry::idOf. enum Method : int { /// No terrain (terrain undefined) @@ -27,17 +30,7 @@ struct GenerationRequest eCONCRETEISLANDS = 5, eISLES = 6, eOLDRANDOM = 7, - eOLDISLANDS = 8, - eCONTESTEDCOMMONS = 9, - eMAZE = 11, - eFJORDCONTINENT = 12, - eWATERSHED = 13, - eSTONEHIGHLANDS = 14, - eSYMMETRICARENA = 15, - eRINGWORLD = 16, - eCITYSTATES = 17, - eTIDALFLATS = 18, - eEVERGLADES = 19 + eOLDISLANDS = 8 }; using Control = GeneratorControl; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 2b59f7b5a..c9d40ee9d 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -63,6 +63,13 @@ const GeneratorDefinition &GeneratorRegistry::at(int id) const return *d; throw std::invalid_argument("Unknown generator ID"); } +int GeneratorRegistry::idOf(const std::string &id) const +{ + for (const auto &d : definitions) + if (id == d.id) + return d.legacyId; + throw std::invalid_argument("Unknown generator: " + id); +} std::vector GeneratorRegistry::methods(bool editor) const { std::vector result; diff --git a/src/map/generator/core/GeneratorRegistry.h b/src/map/generator/core/GeneratorRegistry.h index e3d22ac2a..4e66c3175 100644 --- a/src/map/generator/core/GeneratorRegistry.h +++ b/src/map/generator/core/GeneratorRegistry.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "GeneratorDefinition.h" +#include class GeneratorRegistry { std::vector definitions; @@ -9,6 +10,8 @@ class GeneratorRegistry explicit GeneratorRegistry(std::vector definitions); const GeneratorDefinition *find(int id) const; const GeneratorDefinition &at(int id) const; + /// The numeric id of the generator with this string id; throws for an unknown one. + int idOf(const std::string &id) const; std::vector methods(bool editor = true) const; int selectionIndex(int id, bool editor = true) const; static const GeneratorRegistry &builtins(); diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 4821cc3b2..9392f88bf 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -867,41 +867,20 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C // creek, and the quarry behind the lake. const double reach = std::clamp(0.1 * L.g.depth(), 2.0, 6.0) * 1.3 + 3; const double flank = L.creekSide != 0 ? -L.creekSide : -1.0; - const auto seedNear = [&](double along, double across, int within) + const auto at = [&](double along, double across, int within) { - const int ax = - h.lakeX + int(std::lround(along * std::cos(h.angle) - across * std::sin(h.angle))); - const int ay = - h.lakeY + int(std::lround(along * std::sin(h.angle) + across * std::cos(h.angle))); - int seed = -1, nearest = INT_MAX; - for (int dy = -within; dy <= within; ++dy) - for (int dx = -within; dx <= within; ++dx) - { - const int i = t.at(ax + dx, ay + dy); - if (eligible(i) && dx * dx + dy * dy < nearest) - { - nearest = dx * dx + dy * dy; - seed = i; - } - } - return seed; + return KitSeed{ + h.lakeX + int(std::lround(along * std::cos(h.angle) - across * std::sin(h.angle))), + h.lakeY + int(std::lround(along * std::sin(h.angle) + across * std::cos(h.angle))), + within}; }; - if (L.creekSide == 0) - { - if (const int seed = seedNear(-0.3 * reach, -reach, 14); seed >= 0) - growPatch(map, t, seed, CORN, kHomeWheat, eligible); - if (const int seed = seedNear(-0.3 * reach, reach, 14); seed >= 0) - growPatch(map, t, seed, WOOD, kHomeWood, eligible); - } - else - { - if (const int seed = seedNear(-0.5 * reach, flank * reach, 14); seed >= 0) - growPatch(map, t, seed, CORN, kHomeWheat, eligible); - if (const int seed = seedNear(0.6 * reach, flank * (reach + 1), 14); seed >= 0) - growPatch(map, t, seed, WOOD, kHomeWood, eligible); - } - if (const int seed = seedNear(reach + 8, 0, 12); seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + const Kit kit = L.creekSide == 0 + ? Kit{at(-0.3 * reach, -reach, 14), at(-0.3 * reach, reach, 14), + at(reach + 8, 0, 12), kHomeWheat, kHomeWood, 2} + : Kit{at(-0.5 * reach, flank * reach, 14), + at(0.6 * reach, flank * (reach + 1), 14), at(reach + 8, 0, 12), + kHomeWheat, kHomeWood, 2}; + plantKit(map, t, context, kit, eligible); // Ambient farmland on the home's fertile ground, in patches, then outcrops and a grove. std::vector ground; std::vector> farm; @@ -1145,7 +1124,7 @@ bool generate(Game &game, GenerationContext &context) terrain, t, context, {"city-islands", int(std::lround(o.resourceIslands * double(countTiles(terrain, WATER)) / 16384.0)), 60, - kLakeShore + 1, /*gapFromBothReaches=*/false}); + kLakeShore + 1}); layBeaches(terrain, t); writeUndermap(map, terrain); const std::vector line = stoneTiles(map, L); @@ -1314,7 +1293,7 @@ GeneratorDefinition cityStatesDefinition() "city-states", 17, "City states", - 4, + 5, false, // The commons' radius as a share of half the shorter side, the strait's width as a // share of the shorter side, the causeway's road in tiles; valleys per 128x128 of diff --git a/src/map/generator/generators/EvergladesGenerator.cpp b/src/map/generator/generators/EvergladesGenerator.cpp index 58436af49..4bc3aeaa5 100644 --- a/src/map/generator/generators/EvergladesGenerator.cpp +++ b/src/map/generator/generators/EvergladesGenerator.cpp @@ -9,6 +9,7 @@ #include "Pipeline.h" #include "Planting.h" #include "Resources.h" +#include "Roads.h" #include "Settlements.h" #include "Sketch.h" #include "Unit.h" @@ -18,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -320,22 +320,17 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context) { return L.clearingOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; const double a = std::atan2(double(t.offsetY(L.cy, home.y)), double(t.offsetX(L.cx, home.x))); - const auto at = [&](double along, double across) + const auto at = [&](double along, double across, int within) { - return std::make_pair( - home.x + int(std::lround(along * std::cos(a) - across * std::sin(a))), - home.y + int(std::lround(along * std::sin(a) + across * std::cos(a)))); + return KitSeed{home.x + int(std::lround(along * std::cos(a) - across * std::sin(a))), + home.y + int(std::lround(along * std::sin(a) + across * std::cos(a))), + within}; }; const double beside = L.g.pond * 1.3 + 3; - if (const auto [x, y] = at(0, -beside); true) - if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) - growPatch(map, t, seed, CORN, kHomeWheat, eligible); - if (const auto [x, y] = at(0, beside); true) - if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) - growPatch(map, t, seed, WOOD, kHomeWood, eligible); - if (const auto [x, y] = at(-(L.g.clearing - 4), 0); true) - if (const int seed = seedNear(t, x, y, 8, eligible); seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + plantKit(map, t, context, + {at(0, -beside, 10), at(0, beside, 10), at(-(L.g.clearing - 4), 0, 8), kHomeWheat, + kHomeWood, 2}, + eligible); } } @@ -445,7 +440,6 @@ void openRoutes(Game &game, const Layout &L, GenerationContext &context) } return tiles; }; - const auto &steps = kCardinalSteps; bool changed = false; for (int team = 1; team < teams; ++team) { @@ -465,40 +459,14 @@ void openRoutes(Game &game, const Layout &L, GenerationContext &context) if (arrived) continue; // The cheapest way from anything colony 0 can reach to this colony's doorstep. - std::vector cost(n, INT_MAX), from(n, -1); - std::priority_queue, std::vector>, std::greater<>> - heap; + std::vector reachable; for (int i = 0; i < n; ++i) if (walk[i] >= 0) - { - cost[i] = 0; - heap.push({0, i}); - } - int reached = -1; - while (!heap.empty() && reached < 0) - { - const auto [c, i] = heap.top(); - heap.pop(); - if (c > cost[i]) - continue; - if (target[i]) - { - reached = i; - break; - } - const int x = i % t.w, y = i / t.w; - for (const auto &step : steps) - { - const int m = t.at(x + step[0], y + step[1]); - const int stepCost = costAt(m); - if (stepCost < 0 || c + stepCost >= cost[m]) - continue; - cost[m] = c + stepCost; - from[m] = i; - heap.push({cost[m], m}); - } - } - for (int i = reached; i >= 0 && cost[i] > 0; i = from[i]) + reachable.push_back(i); + const std::vector route = + cheapestWalk(t, GridNeighbors::Cardinal, reachable, target, + [&](int, int to, int, int) { return costAt(to); }); + for (int i : route) { const int x = i % t.w, y = i / t.w; if (map.isWater(x, y)) diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index a420809e2..4c633888b 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -1246,7 +1246,7 @@ GeneratorDefinition stoneHighlandsDefinition() return {"stone-highlands", 14, "Stone highlands", - 1, + 2, false, {// Average spacing between valley centres, in tiles. {"valley-size", "Valley size", 20, 44, 4, 32, ControlGroup::Layout}, diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index 76c379deb..465a0eac2 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -9,6 +9,7 @@ #include "GlobalContainer.h" #include "HeightMap.h" #include "Resources.h" +#include "Roads.h" #include "Topology.h" #include "Unit.h" #include @@ -425,48 +426,27 @@ bool carvePaths(const Arena &a, const Layout &l, Terrain &t) constexpr int kStraight = 2, kDiagonal = 3, kFord = 60; const int w = a.width, h = a.height; const size_t n = size_t(w) * h; + const MapGeneration::Torus torus(w, h); std::vector route(n, 0); for (double angle : l.gates) { const double reach = a.centre + a.moat + 3.5; const int target = wrap(int(std::lround(h / 2.0 + reach * std::sin(angle))), h) * w + wrap(int(std::lround(w / 2.0 + reach * std::cos(angle))), w); - const int start = l.homeV * w + l.homeU; - std::vector cost(n, INT_MAX), parent(n, -1); - using Entry = std::pair; - std::priority_queue, std::greater> queue; - queue.push({0, start}); - cost[size_t(start)] = 0; - while (!queue.empty()) - { - const auto [c, i] = queue.top(); - queue.pop(); - if (c != cost[size_t(i)]) - continue; - if (i == target) - break; - const int u = i % w, v = i / w; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - if (!dx && !dy) - continue; - const int j = wrap(v + dy, h) * w + wrap(u + dx, w); - const bool water = t.undermap[size_t(j)] == WATER; - if (water && (t.moat[size_t(j)] || t.ponds[size_t(j)])) - continue; - const int next = c + (dx && dy ? kDiagonal : kStraight) + (water ? kFord : 0); - if (next < cost[size_t(j)]) - { - cost[size_t(j)] = next; - parent[size_t(j)] = i; - queue.push({next, j}); - } - } - } - if (cost[size_t(target)] == INT_MAX) + std::vector goal(n, 0); + goal[size_t(target)] = 1; + const std::vector walk = MapGeneration::cheapestWalk( + torus, MapGeneration::GridNeighbors::Eight, {l.homeV * w + l.homeU}, goal, + [&](int, int j, int dx, int dy) + { + const bool water = t.undermap[size_t(j)] == WATER; + if (water && (t.moat[size_t(j)] || t.ponds[size_t(j)])) + return -1; + return (dx && dy ? kDiagonal : kStraight) + (water ? kFord : 0); + }); + if (walk.empty()) return false; - for (int i = target; i != -1; i = parent[size_t(i)]) + for (int i : walk) route[size_t(i)] = 1; } t.paths = stamp(a.symmetry, route, true); diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index 6ff099850..20006b9fa 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -279,22 +279,18 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const T { return L.islandOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; const double a = std::atan2(double(t.offsetY(L.cy, L.pondY[team])), double(t.offsetX(L.cx, L.pondX[team]))); - const auto at = [&](double along, double across) + const auto at = [&](double along, double across, int within) { - return std::make_pair( + return KitSeed{ L.pondX[team] + int(std::lround(along * std::cos(a) - across * std::sin(a))), - L.pondY[team] + int(std::lround(along * std::sin(a) + across * std::cos(a)))); + L.pondY[team] + int(std::lround(along * std::sin(a) + across * std::cos(a))), + within}; }; // Wheat and wood on the two sides of the pond, the quarry towards the map's centre. - if (const auto [x, y] = at(0, -pondRadius); true) - if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) - growPatch(map, t, seed, CORN, kHomeWheat, eligible); - if (const auto [x, y] = at(0, pondRadius); true) - if (const int seed = seedNear(t, x, y, 12, eligible); seed >= 0) - growPatch(map, t, seed, WOOD, kHomeWood, eligible); - if (const auto [x, y] = at(-(pondRadius + 4), 0); true) - if (const int seed = seedNear(t, x, y, 10, eligible); seed >= 0) - placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + plantKit(map, t, context, + {at(0, -pondRadius, 12), at(0, pondRadius, 12), at(-(pondRadius + 4), 0, 10), + kHomeWheat, kHomeWood, 2}, + eligible); // Ambient farmland on the island's fertile ground, split two to one. std::vector ground; std::vector> farm; @@ -343,20 +339,22 @@ void stockIslands(Map &map, const Layout &L, GenerationContext &context, const T { const Torus &t = L.t; const int n = t.w * t.h; - // Every neutral island is its own component of grass; each gets its prize at its middle. The - // engine draws each deposit's amount from the gameplay RNG as it is placed, so an island's - // tiles are planted in the order a flood from its first tile reaches them. + // Every neutral island is its own component of grass; each gets its prize at its middle. std::vector neutral(n, 0); for (int i = 0; i < n; ++i) neutral[i] = L.islandOf[i] == -3 && L.prizeOf[i] >= 0; const std::vector label = connectedRegions(neutral, t.w, t.h, true, GridNeighbors::Eight); - std::vector firstTile; + std::vector> islands; for (int i = 0; i < n; ++i) - if (label[i] >= int(firstTile.size())) - firstTile.push_back(i); - for (const int start : firstTile) + if (label[i] >= 0) + { + if (label[i] >= int(islands.size())) + islands.resize(label[i] + 1); + islands[label[i]].push_back(i); + } + for (const std::vector &tiles : islands) { - const std::vector tiles = floodFrom(t, tileMask(t, {start}), neutral).visited; + const int start = tiles.front(); double sx = 0, sy = 0; for (int i : tiles) { @@ -539,7 +537,7 @@ GeneratorDefinition tidalFlatsDefinition() "tidal-flats", 18, "Tidal flats", - 3, + 4, false, // The home islands' radius as a share of the half side; extra islands, sandbars and // lagoons per colony; tide pools per 128x128 of flats. diff --git a/src/map/generator/shared/LatticeNoise.cpp b/src/map/generator/shared/LatticeNoise.cpp index f5d74d857..3eb0ca572 100644 --- a/src/map/generator/shared/LatticeNoise.cpp +++ b/src/map/generator/shared/LatticeNoise.cpp @@ -2,7 +2,6 @@ #include "LatticeNoise.h" #include #include -#include #include namespace MapGeneration { @@ -38,28 +37,11 @@ double PeriodicNoise::at(double x, double y) const std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng) { - const int gw = std::max(1, (w + period / 2) / std::max(1, period)); - const int gh = std::max(1, (h + period / 2) / std::max(1, period)); - std::vector lattice(size_t(gw) * gh); - for (int &v : lattice) - v = int(rng() >> 16); - const auto smooth = [](int t) - { return int(std::int64_t(t) * t * (3 * 1024 - 2 * t) / (1024 * 1024)); }; + const PeriodicNoise noise(w, h, period, rng); std::vector field(size_t(w) * h); for (int y = 0; y < h; ++y) - { - const std::int64_t fy = std::int64_t(y) * gh * 1024 / h; - const int y0 = int(fy >> 10), y1 = (y0 + 1) % gh, ty = smooth(int(fy & 1023)); for (int x = 0; x < w; ++x) - { - const std::int64_t fx = std::int64_t(x) * gw * 1024 / w; - const int x0 = int(fx >> 10), x1 = (x0 + 1) % gw, tx = smooth(int(fx & 1023)); - const int a = lattice[size_t(y0) * gw + x0], b = lattice[size_t(y0) * gw + x1]; - const int c = lattice[size_t(y1) * gw + x0], d = lattice[size_t(y1) * gw + x1]; - const int top = a + (b - a) * tx / 1024, bottom = c + (d - c) * tx / 1024; - field[size_t(y) * w + x] = top + (bottom - top) * ty / 1024; - } - } + field[size_t(y) * w + x] = std::min(65535, int(noise.at(x, y) * 65536.0)); return field; } @@ -84,29 +66,12 @@ std::vector torusNoise(int width, int height, std::mt19937 &rng) { static const std::pair octaves[] = {{32, 0.4}, {16, 0.3}, {8, 0.2}, {4, 0.1}}; std::vector field(size_t(width) * height, 0.0); - const auto ease = [](double t) { return t * t * (3 - 2 * t); }; for (const auto &octave : octaves) { - const int cell = std::min({octave.first, width, height}); - const int columns = width / cell, rows = height / cell; - std::vector lattice(size_t(columns) * rows); - for (double &value : lattice) - value = 2 * (rng() / 4294967296.0) - 1; + const PeriodicNoise noise(width, height, std::min({octave.first, width, height}), rng); for (int y = 0; y < height; ++y) - { - const int r0 = y / cell, r1 = (r0 + 1) % rows; - const double ty = ease(double(y % cell) / cell); for (int x = 0; x < width; ++x) - { - const int c0 = x / cell, c1 = (c0 + 1) % columns; - const double tx = ease(double(x % cell) / cell); - const double top = - lattice[r0 * columns + c0] * (1 - tx) + lattice[r0 * columns + c1] * tx; - const double bottom = - lattice[r1 * columns + c0] * (1 - tx) + lattice[r1 * columns + c1] * tx; - field[size_t(y) * width + x] += octave.second * (top * (1 - ty) + bottom * ty); - } - } + field[size_t(y) * width + x] += octave.second * (2 * noise.at(x, y) - 1); } double peak = 0; for (double value : field) diff --git a/src/map/generator/shared/LatticeNoise.h b/src/map/generator/shared/LatticeNoise.h index e26b27e9c..f1265ed11 100644 --- a/src/map/generator/shared/LatticeNoise.h +++ b/src/map/generator/shared/LatticeNoise.h @@ -6,14 +6,11 @@ namespace MapGeneration { // Smooth value noise on a lattice that tiles the map's torus exactly, so a field wraps like the // map does and the seam can't be seen. HeightMap (Perlin noise faded across the wrap) is the -// other option; these are cheaper and exactly periodic. -// -// Three flavours are in use, each with its own arithmetic, and a generator's output depends on -// which it draws from. They should converge on one once a revision bump is acceptable; until -// then, a new generator should take PeriodicNoise, which samples at any continuous point. +// other option; this is cheaper and exactly periodic. /// A lattice of random values about `cell` tiles apart (a whole number of cells spans the map), -/// blended with a smoothstep and sampled at any point. Values are in [0, 1). +/// blended with a smoothstep and sampled at any point. Values are in [0, 1). One draw from the +/// stream per lattice cell, row-major. class PeriodicNoise { public: @@ -25,15 +22,14 @@ class PeriodicNoise std::vector lattice; }; -/// The same idea in integer arithmetic, one value per tile: a lattice about `period` tiles -/// apart, smoothstep-blended. Values are 0..65535. +/// PeriodicNoise sampled once per tile, as integers 0..65535. std::vector periodicNoise(int w, int h, int period, std::mt19937 &rng); /// periodicNoise at period, period/2, ... weighted 2:1 per octave, normalised back to 0..65535. std::vector fractalNoise(int w, int h, int period, int octaves, std::mt19937 &rng); -/// Four fixed octaves (32, 16, 8 and 4 tiles, or the map if smaller) whose cells divide the map -/// exactly, normalised to [-1, 1] by the field's own peak. +/// Four octaves (32, 16, 8 and 4 tiles, or the map if smaller) sampled once per tile and +/// normalised to [-1, 1] by the field's own peak: a coastline's worth of detail. std::vector torusNoise(int width, int height, std::mt19937 &rng); /// The value below which `percent` of the samples fall; 0 with no samples. diff --git a/src/map/generator/shared/Planting.h b/src/map/generator/shared/Planting.h index 2a4f07dce..9b3314589 100644 --- a/src/map/generator/shared/Planting.h +++ b/src/map/generator/shared/Planting.h @@ -2,6 +2,7 @@ #pragma once #include "Grid.h" #include "Map.h" +#include "Resources.h" #include "Sketch.h" #include #include @@ -67,6 +68,36 @@ int seedNear(const Torus &t, int ax, int ay, int within, Eligible eligible) return seed; } +/// Where a kit's three deposits go: each grows from the nearest eligible tile to its point, +/// searched `within` tiles of it. +struct KitSeed +{ + int x, y, within; +}; +struct Kit +{ + KitSeed wheat, wood, stone; + int wheatTiles, woodTiles; // patch sizes + int stoneRadius; // a clump +}; +/// A home's starter kit: a wheat patch, a wood patch and a stone clump, each from the nearest +/// eligible tile to its seed, in that order. A deposit whose seed finds no eligible tile is left +/// out, since the guarantee that follows tops a colony up. +template +void plantKit(Map &map, const Torus &t, GenerationContext &context, const Kit &kit, + Eligible eligible) +{ + if (const int seed = seedNear(t, kit.wheat.x, kit.wheat.y, kit.wheat.within, eligible); + seed >= 0) + growPatch(map, t, seed, CORN, kit.wheatTiles, eligible); + if (const int seed = seedNear(t, kit.wood.x, kit.wood.y, kit.wood.within, eligible); seed >= 0) + growPatch(map, t, seed, WOOD, kit.woodTiles, eligible); + if (const int seed = seedNear(t, kit.stone.x, kit.stone.y, kit.stone.within, eligible); + seed >= 0) + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), STONE, + kit.stoneRadius); +} + /// A mask of the tiles within `clearance` of every colony's swarm footprint: the ground a kit /// must leave alone. std::vector swarmSurroundings(const Torus &, const GenerationContext &, diff --git a/src/map/generator/shared/Roads.h b/src/map/generator/shared/Roads.h index d2f810621..bb8b836e3 100644 --- a/src/map/generator/shared/Roads.h +++ b/src/map/generator/shared/Roads.h @@ -1,10 +1,71 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "Grid.h" +#include "Topology.h" +#include +#include +#include +#include #include class Map; namespace MapGeneration { +/// The cheapest walk on the torus by any step cost: from any source tile (at cost 0) to any +/// goal tile, where `stepCost(from, to, dx, dy)` is the cost of stepping from one tile onto its +/// neighbour, or -1 when that step cannot be taken. Returns the walk from the goal reached back +/// to its source, or nothing. Dijkstra, so ties go to the tile queued first. +template +std::vector cheapestWalk(const Torus &t, GridNeighbors neighbours, + const std::vector &sources, + const std::vector &goal, StepCost stepCost) +{ + const int n = t.w * t.h; + std::vector cost(n, INT_MAX), from(n, -1); + using Entry = std::pair; + std::priority_queue, std::greater> heap; + for (int i : sources) + { + cost[i] = 0; + heap.push({0, i}); + } + int reached = -1; + while (!heap.empty() && reached < 0) + { + const auto [c, i] = heap.top(); + heap.pop(); + if (c > cost[i]) + continue; + if (goal[i]) + { + reached = i; + break; + } + const int x = i % t.w, y = i / t.w; + const auto consider = [&](int dx, int dy) + { + const int m = t.at(x + dx, y + dy); + const int step = stepCost(i, m, dx, dy); + if (step < 0 || c + step >= cost[m]) + return; + cost[m] = c + step; + from[m] = i; + heap.push({cost[m], m}); + }; + if (neighbours == GridNeighbors::Cardinal) + for (const auto &step : kCardinalSteps) + consider(step[0], step[1]); + else + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (dx || dy) + consider(dx, dy); + } + std::vector route; + for (int i = reached; i >= 0; i = from[i]) + route.push_back(i); + return route; +} + /// The cheapest walk from any source tile to any goal tile, eight-connected on the torus: /// stepping onto a `costly` tile costs one, any other open tile nothing, and `blocked` tiles /// are impassable. Returns the tiles of the walk from the goal it reached back to its source, or diff --git a/src/map/generator/shared/Sketch.cpp b/src/map/generator/shared/Sketch.cpp index 5d2c66345..01a20dc83 100644 --- a/src/map/generator/shared/Sketch.cpp +++ b/src/map/generator/shared/Sketch.cpp @@ -71,8 +71,7 @@ std::vector raiseIslands(TerrainSketch &terrain, const Torus &t, Generat bool clear = true; for (const Island &other : islands) { - const double gap = - (placement.gapFromBothReaches ? reach + other.reach : 2 * reach) + placement.moat; + const double gap = reach + other.reach + placement.moat; clear = clear && t.dist2(x, y, other.x, other.y) >= gap * gap; } if (!clear) diff --git a/src/map/generator/shared/Sketch.h b/src/map/generator/shared/Sketch.h index 882cfdf17..7fd66edce 100644 --- a/src/map/generator/shared/Sketch.h +++ b/src/map/generator/shared/Sketch.h @@ -40,10 +40,6 @@ struct IslandPlacement int attemptsPerIsland; /// Water kept between an island and any coast, and between two islands. double moat; - /// Two islands keep the sum of their reaches plus the moat apart. Off, they keep twice the - /// new island's reach plus the moat: City states' original rule, kept until its revision - /// moves, since the difference changes which candidates it accepts. - bool gapFromBothReaches = true; }; /// Small islands out in the open water, each well clear of every coast and of each other, so diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index 8b1f3a825..84f9a6974 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +#include "GeneratorRegistry.h" #include "AIImplementation.h" #include "AINames.h" #include "CustomGameScreen.h" @@ -125,7 +126,7 @@ struct CustomGameSetupHarness return loaded && reloaded.setup.generator.method == method && reloaded.setup.generator.options == draft.setup.generator.options; }; - const int fjord = GenerationRequest::eFJORDCONTINENT, ring = GenerationRequest::eRINGWORLD; + const int fjord = GeneratorRegistry::builtins().idOf("fjord-continent"), ring = GeneratorRegistry::builtins().idOf("ring-world"); // Fjord's lake size shares the legacy field that held the old generators' "use the // default" sentinel 50, and used to reload as 30. assert(roundTrip(fjord, {{"lake-size", 50}})); @@ -731,7 +732,7 @@ struct CustomGameSetupHarness capture("shattered-coast-controls"); // Switches are checkboxes: a click, or Space or Return on the focused row, flips one, and // either edit invalidates the preview like any other generator control. - landscape(GenerationRequest::eFJORDCONTINENT); + landscape(GeneratorRegistry::builtins().idOf("fjord-continent")); screen.expanded[0] = screen.expanded[1] = screen.expanded[2] = true; preview(); assert(screen.validMap); @@ -764,7 +765,7 @@ struct CustomGameSetupHarness capture("map-1000"); // Rectangular terrain and markers must use the same cropped preview area. for (auto dimensions : {std::pair{9, 7}, std::pair{7, 9}, std::pair{9, 6}, std::pair{6, 9}}) { - screen.setup.generatorHistory.select(screen.setup.generator, GenerationRequest::eCONTESTEDCOMMONS); + screen.setup.generatorHistory.select(screen.setup.generator, GeneratorRegistry::builtins().idOf("contested-commons")); screen.setup.generator.wDec = dimensions.first; screen.setup.generator.hDec = dimensions.second; screen.setup.setCapacity(4); @@ -827,7 +828,7 @@ struct CustomGameSetupHarness // playing exactly the map that was shown. { screen.setup.generatorHistory.select(screen.setup.generator, - GenerationRequest::eCONTESTEDCOMMONS); + GeneratorRegistry::builtins().idOf("contested-commons")); screen.setup.generator.wDec = screen.setup.generator.hDec = 8; screen.setup.setCapacity(4); screen.invalidate(); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 4dfbfb375..a47de1f34 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -155,7 +155,7 @@ class MapGeneratorDefaultsTest for (std::uint32_t seed = 1; seed <= 3; ++seed) { D crowded; - crowded.setMethodDefaults(D::eCONTESTEDCOMMONS); + crowded.setMethodDefaults(GeneratorRegistry::builtins().idOf("contested-commons")); crowded.wDec = crowded.hDec = dims; crowded.nbTeams = teams; crowded.seed = seed; @@ -379,8 +379,8 @@ class MapGeneratorDefaultsTest s.dispatchInit(); CustomGameSetup lobby; D commons; - commons.setMethodDefaults(D::eCONTESTEDCOMMONS); - assert(GeneratorRegistry::builtins().methods(false).front() == D::eCONTESTEDCOMMONS); + commons.setMethodDefaults(GeneratorRegistry::builtins().idOf("contested-commons")); + assert(GeneratorRegistry::builtins().methods(false).front() == GeneratorRegistry::builtins().idOf("contested-commons")); assert(s.methods->getSelectionIndex() == 0); sameControls(s.descriptor, commons); sameControls(lobby.generator, commons); @@ -418,7 +418,7 @@ class MapGeneratorDefaultsTest assert(decoded.setData(encoded.getData(), encoded.getDataLength())); sameControls(fromLegacyDescriptor(decoded, 0), expected); } - if (output && ((m >= 4 && m <= 8) || m == D::eFJORDCONTINENT)) + if (output && ((m >= 4 && m <= 8) || m == GeneratorRegistry::builtins().idOf("fjord-continent"))) { s.gfx->drawFilledRect(0, 0, 640, 480, GAGCore::Color(34, 55, 42)); for (auto *w : s.widgets) diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index e24ee6015..30e3cf790 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -91,13 +91,13 @@ macos-arm64 13 1 8 8 4 2 ok 13659661778465349112 macos-arm64 13 1 8 8 4 3 ok 13134881442491555389 macos-arm64 13 1 8 8 8 1 ok 16484172863350979963 macos-arm64 13 1 9 9 4 1 ok 9971144189844359399 -macos-arm64 14 1 7 7 4 1 ok 8352155152496438549 -macos-arm64 14 1 8 8 2 1 ok 2194436084179284477 -macos-arm64 14 1 8 8 4 1 ok 2921791490199882621 -macos-arm64 14 1 8 8 4 2 ok 2778819455373451420 -macos-arm64 14 1 8 8 4 3 ok 10572830601036884107 -macos-arm64 14 1 8 8 8 1 ok 8166217628013975417 -macos-arm64 14 1 9 9 4 1 ok 10894948886516768346 +macos-arm64 14 2 7 7 4 1 ok 17202733506137778788 +macos-arm64 14 2 8 8 2 1 ok 16088282592338295379 +macos-arm64 14 2 8 8 4 1 ok 11925006621409592431 +macos-arm64 14 2 8 8 4 2 ok 414349330464996680 +macos-arm64 14 2 8 8 4 3 ok 760189253045661613 +macos-arm64 14 2 8 8 8 1 ok 3322640947906419646 +macos-arm64 14 2 9 9 4 1 ok 1552916736750568752 macos-arm64 15 1 7 7 4 1 ok 18061507907613243474 macos-arm64 15 1 8 8 2 1 ok 14205129108449246087 macos-arm64 15 1 8 8 4 1 ok 1822623745155517541 @@ -112,20 +112,20 @@ macos-arm64 16 1 8 8 4 2 ok 10075510292789336147 macos-arm64 16 1 8 8 4 3 ok 4396075304858481954 macos-arm64 16 1 8 8 8 1 ok 699982766957804005 macos-arm64 16 1 9 9 4 1 ok 12471630520837885284 -macos-arm64 17 4 7 7 4 1 ok 4983285384533269693 -macos-arm64 17 4 8 8 2 1 ok 15410170018519406689 -macos-arm64 17 4 8 8 4 1 ok 1496908417910164065 -macos-arm64 17 4 8 8 4 2 ok 11861326676775640834 -macos-arm64 17 4 8 8 4 3 ok 17308555237519110064 -macos-arm64 17 4 8 8 8 1 ok 7621305293371525278 -macos-arm64 17 4 9 9 4 1 ok 11272053320214136168 -macos-arm64 18 3 7 7 4 1 ok 2131984367767490485 -macos-arm64 18 3 8 8 2 1 ok 16443395861378427243 -macos-arm64 18 3 8 8 4 1 ok 9678987861891776728 -macos-arm64 18 3 8 8 4 2 ok 10102518421633888991 -macos-arm64 18 3 8 8 4 3 ok 5379511258847874570 -macos-arm64 18 3 8 8 8 1 ok 12075523445877924400 -macos-arm64 18 3 9 9 4 1 ok 8538100221992578386 +macos-arm64 17 5 7 7 4 1 ok 4983285384533269693 +macos-arm64 17 5 8 8 2 1 ok 15410170018519406689 +macos-arm64 17 5 8 8 4 1 ok 1496908417910164065 +macos-arm64 17 5 8 8 4 2 ok 11861326676775640834 +macos-arm64 17 5 8 8 4 3 ok 17308555237519110064 +macos-arm64 17 5 8 8 8 1 ok 7621305293371525278 +macos-arm64 17 5 9 9 4 1 ok 11272053320214136168 +macos-arm64 18 4 7 7 4 1 ok 2131984367767490485 +macos-arm64 18 4 8 8 2 1 ok 3078796380374205351 +macos-arm64 18 4 8 8 4 1 ok 7736833362775075996 +macos-arm64 18 4 8 8 4 2 ok 3733351741626345607 +macos-arm64 18 4 8 8 4 3 ok 4515656875628782478 +macos-arm64 18 4 8 8 8 1 ok 12075523445877924400 +macos-arm64 18 4 9 9 4 1 ok 10761736745352955182 macos-arm64 19 2 7 7 4 1 ok 2704824689328339712 macos-arm64 19 2 8 8 2 1 ok 18374433395111261761 macos-arm64 19 2 8 8 4 1 ok 9505660943407981793 From 745098856e3849682401deba0545199d3dad8842 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 18:00:09 -0400 Subject: [PATCH 106/136] Add the Spider web map generator and its drawing primitives An orb web of land spun over open water. Spokes run from a hub at the centre of the map out to a frame thread, capture threads cross between them sagging towards the hub, and every colony starts on a pad where its spoke meets the frame. Threads are wide enough to build on, but every ground route follows them: the knots are the places to hold, the hub's orchard of all three fruits is the prize, and the water between is crossed only once colonies can swim. Dew drops, islets in the web's cells, carry a fruit grove or stone each. The web is fair by rotation. With the spiral on, the capture threads are a spiral with one arm per colony that climbs one ring spacing per wedge, so it maps onto itself under a turn of one wedge; every choice about a thread (torn, sag, bow, stone at its knot) is a stateless roll of a key its images share, dew drops are chosen in one wedge and turned round the centre, and the threads' farmland is ranked and split by lattice noise sampled in the wedge frame. Few colonies get more spokes so the web never reads as a star, many get fewer so the spokes never crowd the hub; pads shrink from 13 to 7 tiles on a crowded ring before the request is refused, and small maps squeeze the capture threads. After the crop guarantee, a road is opened from colony 0 onto the hub and to every colony, since a small hub can be stocked shut. validateWorld rebuilds the design and requires every spoke intact and every colony and the hub reachable on foot. Legacy id 20. Controls: spokes per colony, ring spacing, thread width, sag, torn strands, hub size, dew drops, spiral, and the five resource amounts. Strings in all text tables. New shared primitives, extracted where the same code already existed: - shared/Drawing: strokePath (thick paths with a half width per point, through the wrap), bezierPath, polarPoint, forEachTileInShape and fillShape (a RadialShape at any centre and heading). - Planting: KitFrame, plantFields (farmland dealt into wheat and wood by any split key) and scatterClumps, now used by Tidal flats, City states and Everglades in place of their own copies. - Pipeline: secureStartingCrops, the clear/guarantee/clear tail of Tidal flats, Everglades, City states, Ring world and Spider web. The four refactored generators are byte-identical: their golden rows are unchanged and their revisions stay. The toolkit checks gain strokes, shape fills, fields, kit frames and clumps; the framework reference documents the modules and the generator. Verified: MapGeneratorGoldenTest compares 140 rows with 0 failures (the 133 existing rows unchanged, 7 new), --sweep has 0 failing combinations across every generator, a Spider web stress run generated 1662 of 1742 rolls over 64-512 maps, 1-12 colonies and every control extreme (the 80 misses are the up-front refusal of 6+ colonies on 64x64), with median start fairness 0.965; MapGeneratorDefaultsTest and check_translations.py --strict pass, and the maintainer approved the rendered results. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 20 + data/texts.br.txt | 20 + data/texts.ca.txt | 20 + data/texts.cz.txt | 20 + data/texts.de.txt | 20 + data/texts.dk.txt | 20 + data/texts.en.txt | 20 + data/texts.eo.txt | 20 + data/texts.es.txt | 20 + data/texts.eu.txt | 20 + data/texts.fa.txt | 20 + data/texts.fi.txt | 20 + data/texts.fr.txt | 20 + data/texts.gr.txt | 20 + data/texts.hu.txt | 20 + data/texts.id.txt | 20 + data/texts.it.txt | 20 + data/texts.ja.txt | 20 + data/texts.keys.txt | 10 + data/texts.ko.txt | 20 + data/texts.nl.txt | 20 + data/texts.pl.txt | 20 + data/texts.pt.txt | 20 + data/texts.ro.txt | 20 + data/texts.ru.txt | 20 + data/texts.si.txt | 20 + data/texts.sk.txt | 20 + data/texts.sr.txt | 20 + data/texts.sv.txt | 20 + data/texts.tr.txt | 20 + data/texts.uk.txt | 20 + data/texts.vi.txt | 20 + data/texts.zh-cn.txt | 20 + data/texts.zh-tw.txt | 20 + docs/map-generators/ADDING_A_GENERATOR.md | 4 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 59 +- src/SConscript | 2 + src/map/generator/core/GeneratorRegistry.cpp | 3 +- .../generators/CityStatesGenerator.cpp | 79 +-- .../generators/EvergladesGenerator.cpp | 66 +- .../generators/RingWorldGenerator.cpp | 4 +- .../generators/SpiderWebGenerator.cpp | 642 ++++++++++++++++++ .../generator/generators/SpiderWebGenerator.h | 12 + .../generators/TidalFlatsGenerator.cpp | 51 +- src/map/generator/shared/Drawing.cpp | 59 ++ src/map/generator/shared/Drawing.h | 62 ++ src/map/generator/shared/Pipeline.cpp | 9 + src/map/generator/shared/Pipeline.h | 8 + src/map/generator/shared/Planting.h | 60 ++ test/MapGeneratorDefaultsTest.cpp | 2 +- test/MapGeneratorToolkitChecks.h | 83 ++- test/map-generator-golden.txt | 7 + 52 files changed, 1725 insertions(+), 157 deletions(-) create mode 100644 src/map/generator/generators/SpiderWebGenerator.cpp create mode 100644 src/map/generator/generators/SpiderWebGenerator.h create mode 100644 src/map/generator/shared/Drawing.cpp create mode 100644 src/map/generator/shared/Drawing.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 470cc23b2..2055dba10 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1914,6 +1914,26 @@ OpenGL غير متوفر في هذا الإصدار. تحتاج المستنقعات إلى مستعمرة واحدة على الأقل. [Too many colonies for this map; use a bigger map or fewer colonies.] عدد المستعمرات كبير جدًا لهذه الخريطة؛ استخدم خريطة أكبر أو مستعمرات أقل. +[Spider web] +شبكة العنكبوت +[Spokes per colony] +أشعة لكل مستعمرة +[Ring spacing] +تباعد الحلقات +[Thread width] +عرض الخيوط +[Sag] +الارتخاء +[Torn strands] +خيوط ممزقة +[Hub size] +حجم المركز +[Dew drops] +قطرات الندى +[Spiral] +حلزوني +[Spider webs need at least one colony.] +تحتاج شبكة العنكبوت إلى مستعمرة واحدة على الأقل. [Sandbars] الحواجز الرملية [Tide pools] diff --git a/data/texts.br.txt b/data/texts.br.txt index fc2423616..e95ce3aae 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1912,6 +1912,26 @@ Geunioù Os pântanos precisam de pelo menos uma colônia. [Too many colonies for this map; use a bigger map or fewer colonies.] Re a drevadennoù evit ar gartenn-mañ; implijit ur gartenn vrasoc'h pe nebeutoc'h a drevadennoù. +[Spider web] +Gwiad-kevnid +[Spokes per colony] +Skinoù dre drevadenn +[Ring spacing] +Esaouenn etre ar c'helc'hioù +[Thread width] +Ledander an neudennoù +[Sag] +Distegnadur +[Torn strands] +Neudennoù freget +[Hub size] +Ment ar c'hreiz +[Dew drops] +Takennoù glizh +[Spiral] +Troellenn +[Spider webs need at least one colony.] +Ezhomm en deus ur gwiad-kevnid eus un drevadenn da vihanañ. [Sandbars] Bancos de areia [Tide pools] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 1d5cf1202..98dfa5fec 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1924,6 +1924,26 @@ Aiguamolls Els aiguamolls necessiten almenys una colònia. [Too many colonies for this map; use a bigger map or fewer colonies.] Massa colònies per a aquest mapa; useu un mapa més gran o menys colònies. +[Spider web] +Teranyina +[Spokes per colony] +Radis per colònia +[Ring spacing] +Separació dels anells +[Thread width] +Amplada dels fils +[Sag] +Caiguda +[Torn strands] +Fils trencats +[Hub size] +Mida del centre +[Dew drops] +Gotes de rosada +[Spiral] +Espiral +[Spider webs need at least one colony.] +Les teranyines necessiten almenys una colònia. [Sandbars] Bancs de sorra [Tide pools] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 82f30c42b..e2cec9a1c 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1916,6 +1916,26 @@ Slepá ramena Mokřady potřebují alespoň jednu kolonii. [Too many colonies for this map; use a bigger map or fewer colonies.] Příliš mnoho kolonií pro tuto mapu; použijte větší mapu nebo méně kolonií. +[Spider web] +Pavučina +[Spokes per colony] +Paprsků na kolonii +[Ring spacing] +Rozestup kruhů +[Thread width] +Šířka vláken +[Sag] +Průvěs +[Torn strands] +Přetržená vlákna +[Hub size] +Velikost středu +[Dew drops] +Kapky rosy +[Spiral] +Spirála +[Spider webs need at least one colony.] +Pavučina potřebuje alespoň jednu kolonii. [Sandbars] Písečné lavice [Tide pools] diff --git a/data/texts.de.txt b/data/texts.de.txt index 1bc5d2bbf..4f413d366 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1916,6 +1916,26 @@ Altwasser Das Sumpfland braucht mindestens eine Kolonie. [Too many colonies for this map; use a bigger map or fewer colonies.] Zu viele Kolonien für diese Karte; wähle eine größere Karte oder weniger Kolonien. +[Spider web] +Spinnennetz +[Spokes per colony] +Speichen pro Kolonie +[Ring spacing] +Ringabstand +[Thread width] +Fadenbreite +[Sag] +Durchhang +[Torn strands] +Gerissene Fäden +[Hub size] +Nabengröße +[Dew drops] +Tautropfen +[Spiral] +Spirale +[Spider webs need at least one colony.] +Spinnennetze brauchen mindestens eine Kolonie. [Sandbars] Sandbänke [Tide pools] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index a73afdfdd..d120f5e63 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1992,6 +1992,26 @@ Sumpkanaler Sumplandet kræver mindst én koloni. [Too many colonies for this map; use a bigger map or fewer colonies.] For mange kolonier til dette kort; brug et større kort eller færre kolonier. +[Spider web] +Spindelvæv +[Spokes per colony] +Eger pr. koloni +[Ring spacing] +Afstand mellem ringe +[Thread width] +Trådbredde +[Sag] +Nedhæng +[Torn strands] +Revne tråde +[Hub size] +Navstørrelse +[Dew drops] +Dugdråber +[Spiral] +Spiral +[Spider webs need at least one colony.] +Spindelvæv kræver mindst én koloni. [Sandbars] Sandbanker [Tide pools] diff --git a/data/texts.en.txt b/data/texts.en.txt index e9b86364f..eafa77dc5 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1904,6 +1904,26 @@ Sloughs Everglades need at least one colony. [Too many colonies for this map; use a bigger map or fewer colonies.] Too many colonies for this map; use a bigger map or fewer colonies. +[Spider webs need at least one colony.] +Spider webs need at least one colony. +[Spiral] +Spiral +[Dew drops] +Dew drops +[Hub size] +Hub size +[Torn strands] +Torn strands +[Sag] +Sag +[Thread width] +Thread width +[Ring spacing] +Ring spacing +[Spokes per colony] +Spokes per colony +[Spider web] +Spider web [Sandbars] Sandbars [Tide pools] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 9dbb3dedb..4a7bc7270 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1914,6 +1914,26 @@ Marĉkanaloj Marĉoj bezonas almenaŭ unu kolonion. [Too many colonies for this map; use a bigger map or fewer colonies.] Tro da kolonioj por ĉi tiu mapo; uzu pli grandan mapon aŭ malpli da kolonioj. +[Spider web] +Araneaĵo +[Spokes per colony] +Radioj po kolonio +[Ring spacing] +Interspaco de ringoj +[Thread width] +Larĝo de fadenoj +[Sag] +Pendiĝo +[Torn strands] +Ŝiritaj fadenoj +[Hub size] +Grando de la centro +[Dew drops] +Rosoguttoj +[Spiral] +Spiralo +[Spider webs need at least one colony.] +Araneaĵo bezonas almenaŭ unu kolonion. [Sandbars] Sablobenkoj [Tide pools] diff --git a/data/texts.es.txt b/data/texts.es.txt index d59a64610..c95d8fbe6 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1916,6 +1916,26 @@ Ciénagas El pantanal necesita al menos una colonia. [Too many colonies for this map; use a bigger map or fewer colonies.] Demasiadas colonias para este mapa; usa un mapa más grande o menos colonias. +[Spider web] +Telaraña +[Spokes per colony] +Radios por colonia +[Ring spacing] +Separación de anillos +[Thread width] +Ancho de los hilos +[Sag] +Caída +[Torn strands] +Hilos rotos +[Hub size] +Tamaño del centro +[Dew drops] +Gotas de rocío +[Spiral] +Espiral +[Spider webs need at least one colony.] +Las telarañas necesitan al menos una colonia. [Sandbars] Bancos de arena [Tide pools] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index cf7daec9d..1c05169b0 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1926,6 +1926,26 @@ Zingirak Zingirek gutxienez kolonia bat behar dute. [Too many colonies for this map; use a bigger map or fewer colonies.] Kolonia gehiegi mapa honetarako; erabili mapa handiagoa edo kolonia gutxiago. +[Spider web] +Armiarma-sarea +[Spokes per colony] +Erradioak koloniako +[Ring spacing] +Eraztunen arteko tartea +[Thread width] +Harien zabalera +[Sag] +Makurdura +[Torn strands] +Hari hautsiak +[Hub size] +Erdiko gunearen tamaina +[Dew drops] +Ihintz-tantak +[Spiral] +Espirala +[Spider webs need at least one colony.] +Armiarma-sareak gutxienez kolonia bat behar du. [Sandbars] Hondar-bankuak [Tide pools] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 2aa73c0e1..fc744f4e0 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1914,6 +1914,26 @@ OpenGL در این بیلد موجود نیست. باتلاق‌ها دست‌کم به یک مستعمره نیاز دارند. [Too many colonies for this map; use a bigger map or fewer colonies.] تعداد مستعمره‌ها برای این نقشه زیاد است؛ از نقشهٔ بزرگ‌تر یا مستعمره‌های کمتر استفاده کنید. +[Spider web] +تار عنکبوت +[Spokes per colony] +پره برای هر مستعمره +[Ring spacing] +فاصله حلقه‌ها +[Thread width] +پهنای رشته‌ها +[Sag] +افتادگی +[Torn strands] +رشته‌های پاره +[Hub size] +اندازه مرکز +[Dew drops] +قطره‌های شبنم +[Spiral] +مارپیچ +[Spider webs need at least one colony.] +تار عنکبوت دست‌کم به یک مستعمره نیاز دارد. [Sandbars] پشته‌های شنی [Tide pools] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 8b4112de6..17662bf2a 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1916,6 +1916,26 @@ Rimmet Suomaat tarvitsevat vähintään yhden siirtokunnan. [Too many colonies for this map; use a bigger map or fewer colonies.] Liikaa siirtokuntia tälle kartalle; käytä suurempaa karttaa tai vähemmän siirtokuntia. +[Spider web] +Hämähäkinseitti +[Spokes per colony] +Puolia siirtokuntaa kohden +[Ring spacing] +Renkaiden väli +[Thread width] +Lankojen leveys +[Sag] +Riippuma +[Torn strands] +Katkenneet langat +[Hub size] +Keskustan koko +[Dew drops] +Kastepisarat +[Spiral] +Spiraali +[Spider webs need at least one colony.] +Hämähäkinseitti tarvitsee vähintään yhden siirtokunnan. [Sandbars] Hiekkasärkät [Tide pools] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 80e36076b..bd2a9bbc8 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1926,6 +1926,26 @@ Marigots Les marécages ont besoin d'au moins une colonie. [Too many colonies for this map; use a bigger map or fewer colonies.] Trop de colonies pour cette carte ; utilisez une carte plus grande ou moins de colonies. +[Spider web] +Toile d'araignée +[Spokes per colony] +Rayons par colonie +[Ring spacing] +Espacement des anneaux +[Thread width] +Largeur des fils +[Sag] +Affaissement +[Torn strands] +Fils rompus +[Hub size] +Taille du centre +[Dew drops] +Gouttes de rosée +[Spiral] +Spirale +[Spider webs need at least one colony.] +Une toile d'araignée nécessite au moins une colonie. [Sandbars] Bancs de sable [Tide pools] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 226d1c23e..2af8d70b2 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1986,6 +1986,26 @@ Cortex Οι βαλτότοποι χρειάζονται τουλάχιστον μία αποικία. [Too many colonies for this map; use a bigger map or fewer colonies.] Πάρα πολλές αποικίες για αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη ή λιγότερες αποικίες. +[Spider web] +Ιστός αράχνης +[Spokes per colony] +Ακτίνες ανά αποικία +[Ring spacing] +Απόσταση δακτυλίων +[Thread width] +Πλάτος νημάτων +[Sag] +Κρέμασμα +[Torn strands] +Σπασμένα νήματα +[Hub size] +Μέγεθος κέντρου +[Dew drops] +Σταγόνες δροσιάς +[Spiral] +Σπιράλ +[Spider webs need at least one colony.] +Ο ιστός αράχνης χρειάζεται τουλάχιστον μία αποικία. [Sandbars] Αμμοσύρτεις [Tide pools] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 0c0f8125d..63b3c4e1e 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1916,6 +1916,26 @@ Holtágak A mocsárvidékhez legalább egy kolónia kell. [Too many colonies for this map; use a bigger map or fewer colonies.] Túl sok kolónia ehhez a térképhez; használj nagyobb térképet vagy kevesebb kolóniát. +[Spider web] +Pókháló +[Spokes per colony] +Küllők kolóniánként +[Ring spacing] +Gyűrűk távolsága +[Thread width] +Szálszélesség +[Sag] +Belógás +[Torn strands] +Szakadt szálak +[Hub size] +Középpont mérete +[Dew drops] +Harmatcseppek +[Spiral] +Spirál +[Spider webs need at least one colony.] +A pókhálóhoz legalább egy kolónia kell. [Sandbars] Homokpadok [Tide pools] diff --git a/data/texts.id.txt b/data/texts.id.txt index 3a8a6de32..ff2f79502 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1912,6 +1912,26 @@ Rawa-rawa Rawa-rawa membutuhkan setidaknya satu koloni. [Too many colonies for this map; use a bigger map or fewer colonies.] Terlalu banyak koloni untuk peta ini; gunakan peta lebih besar atau lebih sedikit koloni. +[Spider web] +Jaring laba-laba +[Spokes per colony] +Jari-jari per koloni +[Ring spacing] +Jarak cincin +[Thread width] +Lebar benang +[Sag] +Kelenturan +[Torn strands] +Benang putus +[Hub size] +Ukuran pusat +[Dew drops] +Tetes embun +[Spiral] +Spiral +[Spider webs need at least one colony.] +Jaring laba-laba memerlukan setidaknya satu koloni. [Sandbars] Gosong pasir [Tide pools] diff --git a/data/texts.it.txt b/data/texts.it.txt index 4ff5ea217..bdb728e47 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1978,6 +1978,26 @@ Lanche Le paludi hanno bisogno di almeno una colonia. [Too many colonies for this map; use a bigger map or fewer colonies.] Troppe colonie per questa mappa; usa una mappa più grande o meno colonie. +[Spider web] +Ragnatela +[Spokes per colony] +Raggi per colonia +[Ring spacing] +Distanza tra gli anelli +[Thread width] +Larghezza dei fili +[Sag] +Cedimento +[Torn strands] +Fili spezzati +[Hub size] +Dimensione del centro +[Dew drops] +Gocce di rugiada +[Spiral] +Spirale +[Spider webs need at least one colony.] +Le ragnatele richiedono almeno una colonia. [Sandbars] Banchi di sabbia [Tide pools] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 43bf364cc..688c73511 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1912,6 +1912,26 @@ Globulation 2 のカーソルを表示します。 湿地帯には少なくとも一つのコロニーが必要です。 [Too many colonies for this map; use a bigger map or fewer colonies.] この地図にはコロニーが多すぎます。より大きな地図にするか、コロニーを減らしてください。 +[Spider web] +クモの巣 +[Spokes per colony] +コロニーごとの放射糸 +[Ring spacing] +輪の間隔 +[Thread width] +糸の幅 +[Sag] +たるみ +[Torn strands] +切れた糸 +[Hub size] +中心の大きさ +[Dew drops] +露のしずく +[Spiral] +らせん +[Spider webs need at least one colony.] +クモの巣には少なくとも1つのコロニーが必要です。 [Sandbars] 砂州 [Tide pools] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 56f4ba0d2..f2dcb88e8 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -950,6 +950,16 @@ [Sloughs] [Everglades need at least one colony.] [Too many colonies for this map; use a bigger map or fewer colonies.] +[Spider webs need at least one colony.] +[Spiral] +[Dew drops] +[Hub size] +[Torn strands] +[Sag] +[Thread width] +[Ring spacing] +[Spokes per colony] +[Spider web] [Sandbars] [Tide pools] [Lagoons] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index c67be4345..6d0a07b39 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1912,6 +1912,26 @@ Globulation 2 커서를 표시합니다. 습지대에는 최소 한 개의 식민지가 필요합니다. [Too many colonies for this map; use a bigger map or fewer colonies.] 이 지도에는 식민지가 너무 많습니다. 더 큰 지도를 사용하거나 식민지를 줄이세요. +[Spider web] +거미줄 +[Spokes per colony] +식민지당 방사줄 +[Ring spacing] +고리 간격 +[Thread width] +실 너비 +[Sag] +처짐 +[Torn strands] +끊어진 실 +[Hub size] +중심 크기 +[Dew drops] +이슬방울 +[Spiral] +나선 +[Spider webs need at least one colony.] +거미줄에는 식민지가 하나 이상 필요합니다. [Sandbars] 모래톱 [Tide pools] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 4fa140090..46c14fc40 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1940,6 +1940,26 @@ Moerassloten Het moerasland heeft minstens één kolonie nodig. [Too many colonies for this map; use a bigger map or fewer colonies.] Te veel kolonies voor deze kaart; gebruik een grotere kaart of minder kolonies. +[Spider web] +Spinnenweb +[Spokes per colony] +Spaken per kolonie +[Ring spacing] +Ringafstand +[Thread width] +Draadbreedte +[Sag] +Doorhang +[Torn strands] +Gescheurde draden +[Hub size] +Grootte van het midden +[Dew drops] +Dauwdruppels +[Spiral] +Spiraal +[Spider webs need at least one colony.] +Een spinnenweb heeft minstens één kolonie nodig. [Sandbars] Zandbanken [Tide pools] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 7b40d29cd..bae4ced58 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1916,6 +1916,26 @@ Starorzecza Mokradła potrzebują co najmniej jednej kolonii. [Too many colonies for this map; use a bigger map or fewer colonies.] Za dużo kolonii jak na tę mapę; użyj większej mapy lub mniejszej liczby kolonii. +[Spider web] +Pajęczyna +[Spokes per colony] +Promienie na kolonię +[Ring spacing] +Odstęp pierścieni +[Thread width] +Szerokość nici +[Sag] +Zwis +[Torn strands] +Zerwane nici +[Hub size] +Rozmiar środka +[Dew drops] +Krople rosy +[Spiral] +Spirala +[Spider webs need at least one colony.] +Pajęczyna wymaga co najmniej jednej kolonii. [Sandbars] Łachy piaskowe [Tide pools] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index afca84285..cd42487fe 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1922,6 +1922,26 @@ Charcos Os pântanos precisam de pelo menos uma colónia. [Too many colonies for this map; use a bigger map or fewer colonies.] Colónias a mais para este mapa; use um mapa maior ou menos colónias. +[Spider web] +Teia de aranha +[Spokes per colony] +Raios por colónia +[Ring spacing] +Espaçamento dos anéis +[Thread width] +Largura dos fios +[Sag] +Flacidez +[Torn strands] +Fios rompidos +[Hub size] +Tamanho do centro +[Dew drops] +Gotas de orvalho +[Spiral] +Espiral +[Spider webs need at least one colony.] +As teias de aranha precisam de pelo menos uma colónia. [Sandbars] Bancos de areia [Tide pools] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index ca775267b..280aa7853 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1916,6 +1916,26 @@ Mlaștini Mlaștinile au nevoie de cel puțin o colonie. [Too many colonies for this map; use a bigger map or fewer colonies.] Prea multe colonii pentru această hartă; folosește o hartă mai mare sau mai puține colonii. +[Spider web] +Pânză de păianjen +[Spokes per colony] +Raze per colonie +[Ring spacing] +Distanța dintre inele +[Thread width] +Lățimea firelor +[Sag] +Lăsare +[Torn strands] +Fire rupte +[Hub size] +Mărimea centrului +[Dew drops] +Picături de rouă +[Spiral] +Spirală +[Spider webs need at least one colony.] +Pânza de păianjen are nevoie de cel puțin o colonie. [Sandbars] Bancuri de nisip [Tide pools] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 9e5c6f01d..f40916b5f 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1914,6 +1914,26 @@ OpenGL недоступен в этой сборке. Болотам нужна хотя бы одна колония. [Too many colonies for this map; use a bigger map or fewer colonies.] Слишком много колоний для этой карты; используйте карту побольше или меньше колоний. +[Spider web] +Паутина +[Spokes per colony] +Радиусов на колонию +[Ring spacing] +Шаг колец +[Thread width] +Ширина нитей +[Sag] +Провисание +[Torn strands] +Порванные нити +[Hub size] +Размер центра +[Dew drops] +Капли росы +[Spiral] +Спираль +[Spider webs need at least one colony.] +Для паутины нужна хотя бы одна колония. [Sandbars] Песчаные банки [Tide pools] diff --git a/data/texts.si.txt b/data/texts.si.txt index b1d316b7c..0e3425c70 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1916,6 +1916,26 @@ Mrtvice Močvirja potrebujejo vsaj eno kolonijo. [Too many colonies for this map; use a bigger map or fewer colonies.] Preveč kolonij za ta zemljevid; uporabite večji zemljevid ali manj kolonij. +[Spider web] +Pajčevina +[Spokes per colony] +Žarki na kolonijo +[Ring spacing] +Razmik obročev +[Thread width] +Širina niti +[Sag] +Povešenost +[Torn strands] +Pretrgane niti +[Hub size] +Velikost središča +[Dew drops] +Kapljice rose +[Spiral] +Spirala +[Spider webs need at least one colony.] +Pajčevina potrebuje vsaj eno kolonijo. [Sandbars] Peščene sipine [Tide pools] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 0e7f3f839..c0d8ef848 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1924,6 +1924,26 @@ Mŕtve ramená Močiare potrebujú aspoň jednu kolóniu. [Too many colonies for this map; use a bigger map or fewer colonies.] Príliš veľa kolónií pre túto mapu; použite väčšiu mapu alebo menej kolónií. +[Spider web] +Pavučina +[Spokes per colony] +Lúčov na kolóniu +[Ring spacing] +Rozostup kruhov +[Thread width] +Šírka vlákien +[Sag] +Priehyb +[Torn strands] +Roztrhnuté vlákna +[Hub size] +Veľkosť stredu +[Dew drops] +Kvapky rosy +[Spiral] +Špirála +[Spider webs need at least one colony.] +Pavučina potrebuje aspoň jednu kolóniu. [Sandbars] Piesočné lavice [Tide pools] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index f7c8d3f56..edbd40e1d 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1916,6 +1916,26 @@ OpenGL није доступан у овој верзији. Мочварама треба бар једна колонија. [Too many colonies for this map; use a bigger map or fewer colonies.] Превише колонија за ову мапу; користите већу мапу или мање колонија. +[Spider web] +Паукова мрежа +[Spokes per colony] +Жбица по колонији +[Ring spacing] +Размак прстенова +[Thread width] +Ширина нити +[Sag] +Угиб +[Torn strands] +Покидане нити +[Hub size] +Величина средишта +[Dew drops] +Капи росе +[Spiral] +Спирала +[Spider webs need at least one colony.] +Паукова мрежа захтева бар једну колонију. [Sandbars] Пешчани спрудови [Tide pools] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 56f725957..c32728463 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1926,6 +1926,26 @@ Sumpkanaler Sumpmarker kräver minst en koloni. [Too many colonies for this map; use a bigger map or fewer colonies.] För många kolonier för den här kartan; använd en större karta eller färre kolonier. +[Spider web] +Spindelnät +[Spokes per colony] +Ekrar per koloni +[Ring spacing] +Ringavstånd +[Thread width] +Trådbredd +[Sag] +Nedhäng +[Torn strands] +Trasiga trådar +[Hub size] +Navstorlek +[Dew drops] +Daggdroppar +[Spiral] +Spiral +[Spider webs need at least one colony.] +Spindelnät behöver minst en koloni. [Sandbars] Sandbankar [Tide pools] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 6c3798358..95c5b37c7 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1924,6 +1924,26 @@ Bataklıklar Bataklıklar için en az bir koloni gerekir. [Too many colonies for this map; use a bigger map or fewer colonies.] Bu harita için çok fazla koloni var; daha büyük bir harita kullanın veya koloni sayısını azaltın. +[Spider web] +Örümcek ağı +[Spokes per colony] +Koloni başına ışın +[Ring spacing] +Halka aralığı +[Thread width] +İplik genişliği +[Sag] +Sarkma +[Torn strands] +Kopuk iplikler +[Hub size] +Merkez boyutu +[Dew drops] +Çiy damlaları +[Spiral] +Spiral +[Spider webs need at least one colony.] +Örümcek ağı en az bir koloni gerektirir. [Sandbars] Kum tepecikleri [Tide pools] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 0af23ddd7..3866655fe 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1912,6 +1912,26 @@ OpenGL недоступний у цій збірці. Болотам потрібна хоча б одна колонія. [Too many colonies for this map; use a bigger map or fewer colonies.] Забагато колоній для цієї мапи; використайте більшу мапу або менше колоній. +[Spider web] +Павутина +[Spokes per colony] +Променів на колонію +[Ring spacing] +Крок кілець +[Thread width] +Ширина ниток +[Sag] +Провисання +[Torn strands] +Розірвані нитки +[Hub size] +Розмір центру +[Dew drops] +Краплі роси +[Spiral] +Спіраль +[Spider webs need at least one colony.] +Для павутини потрібна принаймні одна колонія. [Sandbars] Піщані банки [Tide pools] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index ceccfab1f..e7fffd113 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1912,6 +1912,26 @@ Kích thước khoảng trống Đầm lầy cần ít nhất một thuộc địa. [Too many colonies for this map; use a bigger map or fewer colonies.] Quá nhiều thuộc địa cho bản đồ này; hãy dùng bản đồ lớn hơn hoặc ít thuộc địa hơn. +[Spider web] +Mạng nhện +[Spokes per colony] +Nan mỗi thuộc địa +[Ring spacing] +Khoảng cách vòng +[Thread width] +Độ rộng sợi +[Sag] +Độ võng +[Torn strands] +Sợi đứt +[Hub size] +Kích thước tâm +[Dew drops] +Giọt sương +[Spiral] +Xoắn ốc +[Spider webs need at least one colony.] +Mạng nhện cần ít nhất một thuộc địa. [Sandbars] Cồn cát [Tide pools] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index f159f67dc..c3e471525 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1912,6 +1912,26 @@ OpenGL 在此版本中不可用。 沼泽地至少需要一个殖民地。 [Too many colonies for this map; use a bigger map or fewer colonies.] 此地图的殖民地太多;请使用更大的地图或减少殖民地。 +[Spider web] +蜘蛛网 +[Spokes per colony] +每个殖民地的辐射线 +[Ring spacing] +环间距 +[Thread width] +丝线宽度 +[Sag] +下垂 +[Torn strands] +断丝 +[Hub size] +中心大小 +[Dew drops] +露珠 +[Spiral] +螺旋 +[Spider webs need at least one colony.] +蜘蛛网至少需要一个殖民地。 [Sandbars] 沙洲 [Tide pools] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 095ac556c..bc5f2dda2 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1988,6 +1988,26 @@ OpenGL 在此版本中不可用。 沼澤地至少需要一個殖民地。 [Too many colonies for this map; use a bigger map or fewer colonies.] 此地圖的殖民地太多;請使用更大的地圖或減少殖民地。 +[Spider web] +蜘蛛網 +[Spokes per colony] +每個殖民地的輻射線 +[Ring spacing] +環間距 +[Thread width] +絲線寬度 +[Sag] +下垂 +[Torn strands] +斷絲 +[Hub size] +中心大小 +[Dew drops] +露珠 +[Spiral] +螺旋 +[Spider webs need at least one colony.] +蜘蛛網至少需要一個殖民地。 [Sandbars] 沙洲 [Tide pools] diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index 9e3caaf27..a28837df9 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -6,11 +6,11 @@ A generator is a registered module, not a new branch in a central dispatch switc - **`core/`** owns requests, control metadata, registration, seed streams, validation and results. It does not implement terrain recipes. - **`generators/`** contains one header/source pair per generator. The header defines its named options; the source owns its controls, its design (the layout as a pure function of the request) and the order it calls the shared stages in. A generator source holds only what is particular to its map. -- **`shared/`** is the toolkit every generator is built from: the torus grid and its floods (`Grid`), shapes (`Geometry`, `Wedge`), the terrain sketch (`Sketch`), noise (`LatticeNoise`, `HeightMap`), deposits (`Planting`, `Resources`), routes (`Roads`), colonies (`Settlements`, `BalancedStarts`), the pipeline stages (`Pipeline`), the height-field stages (`Terrain`) and the start scoring (`StartQuality`). These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) lists what each gives you. +- **`shared/`** is the toolkit every generator is built from: the torus grid and its floods (`Grid`), shapes (`Geometry`, `Wedge`), drawing strokes and shapes (`Drawing`), the terrain sketch (`Sketch`), noise (`LatticeNoise`, `HeightMap`), deposits (`Planting`, `Resources`), routes (`Roads`), colonies (`Settlements`, `BalancedStarts`), the pipeline stages (`Pipeline`), the height-field stages (`Terrain`) and the start scoring (`StartQuality`). These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) lists what each gives you. - **`shared/legacy/`** is the older area-grid toolkit (`Regions`, `Distances`, `StartingPositions`) the point-dispersion and height-field generators still build on. It does not compose with the tile masks the rest of `shared/` uses; do not start a new generator on it. - **`compatibility/`** owns the historical descriptor codec and overloaded-field conversion. New generators do not add fields to that descriptor. `MapGenerator.h` is only a small compatibility facade for older callers. -Extract on the second use. When a routine you are about to write already exists in another generator, move it into `shared/` and call it from both rather than copying it: every helper that was once copied between generators (the beach pass, the kit patch, the swarm clearance, the algae seeding, the road opener, the wedge frame) is now a shared module, and the next copy is a regression. A routine that genuinely only makes sense for one map stays in that generator. +Extract on the second use. When a routine you are about to write already exists in another generator, move it into `shared/` and call it from both rather than copying it: every helper that was once copied between generators (the beach pass, the kit patch and its frame, the farmland split, the random clumps, the swarm clearance, the algae seeding, the road opener, the wedge frame) is now a shared module, and the next copy is a regression. A routine that genuinely only makes sense for one map stays in that generator. `Map` retains general terrain-editing operations, including `rebuildTerrain()` for bulk undermap edits. `Game` retains team, unit and building mutation APIs. Neither dispatches generation algorithms. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index e4188aead..0b98c7313 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -22,16 +22,17 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Grid` | `Torus` wrap and offset arithmetic; `floodFrom`/`stepsFrom`, the one eight-connected breadth-first flood (with a step limit and the visit order for callers that need it); `walkableTiles` and `groundUnitTiles` passability masks; `unitTilesByTeam` and `firstColonyCutOff` | | `Topology` | `connectedRegions` component labelling with explicit wrap and neighbour policy; `regionAdjacency` and `graphDistances` over sparse labels | | `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid | +| `Drawing` | Drawing on the torus: `strokePath`, a thick path of points each with its own half width, rasterized through the wrap; `bezierPath`, a quadratic curve as such a path; `polarPoint`; `forEachTileInShape` and `fillShape`, a `RadialShape` filled at any centre and turned to any heading within its bounding box | | `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; `Blob`, a stretched, turned rough disc in that frame | | `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `writeUndermap` | | `LatticeNoise` | `PeriodicNoise`, value noise that tiles the torus exactly and samples anywhere, with `periodicNoise`/`fractalNoise` (integer fields per tile, octaves) and `torusNoise` (four octaves in [-1, 1]) sampled from it, plus `percentile` | | `HeightMap`, `Noise` | Perlin noise faded across the wrap, and the stamped height fields the height-field generators shape | -| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band), `stockIslands` | +| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region) | | `Resources` | The ambient layer and its fairness guards: `scaledCount`/`scaledShare`, `placeResourceClump`, `setScaledResource`, `scatterResources`, `guaranteeStartingResources`, `openCrampedStarts` | | `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared; `cheapestWalk`, the same search by any step cost (Everglades' fords, Symmetric arena's causeway routes) | | `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | -| `Pipeline` | The stages round the others: `settleColonies`, `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | +| `Pipeline` | The stages round the others: `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | | `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | | `StartQuality` | `scoreStarts`, the finished map's colony quality and fairness the service ranks candidates by | | `GenerationContext` | Named `std::mt19937` streams, `bounded` draws and `shuffle` | @@ -44,9 +45,9 @@ against the finished terrain. ## The designed generator -Nine generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, -City states, Tidal flats, Everglades) follow one shape, and the four newest of them are little -more than a sequence of shared stages: +Ten generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, +City states, Tidal flats, Everglades, Spider web) follow one shape, and the five newest of them +are little more than a sequence of shared stages: 1. `design(request, context)` computes the whole layout from the request and the context's named streams without touching the map, and returns it with a `failure` string when the @@ -55,8 +56,9 @@ more than a sequence of shared stages: then `settleColonies` with a home mask and an anchor per colony. 3. The kits go down with `plantKit` (three seeds, each grown from the nearest eligible tile), then the ambient layers - (`scatterResources` or the generator's own, `seedAlgae`, `stockIslands`), then - `clearAroundSwarms`, `guaranteeStartingResources` and `clearAroundSwarms` again. + (`scatterResources`, or the generator's own ranking fed to `plantFields` and `scatterClumps`, + `seedAlgae`, `stockIslands`), then `secureStartingCrops`: `clearAroundSwarms`, + `guaranteeStartingResources` and `clearAroundSwarms` again. 4. `openRoad` (or the generator's own cheapest-walk variant) keeps every walk the map promises open, clearing only the deposits in the way; `reopenCrampedStarts` runs at non-default amounts. 5. `validateWorld` calls `design` again on a fresh context, checks it with `designMismatch`, @@ -87,6 +89,7 @@ reused after a generator is retired. | `city-states` | 17 | City states | Its own — see below | | `tidal-flats` | 18 | Tidal flats | Its own — see below | | `everglades` | 19 | Everglades | Its own — see below | +| `spider-web` | 20 | Spider web | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -133,6 +136,7 @@ ambient layer but still leaves every colony a start. Maze keeps its explicit den | City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard) and the sea's algae and island prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | | Tidal flats | Every island's ambient fields and outcrops and every island's prize; each home's kit is unscaled | Central island (on): off, the middle of the map is flats and there is no orchard | | Everglades | The swamp's standing wood and wheat, its outcrops and groves, and the pools' algae; every home's kit is unscaled | None | +| Spider web | The threads' standing wheat and wood, the share of knots carrying stone, whether the dew drops and the hub carry fruit and stone, and the shallows' algae; every pad's kit is unscaled | Spiral (on): off, the capture threads are closed rings | `scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of @@ -205,7 +209,7 @@ fixed resource pass: treated like terrain, never cleared and never looked past. It wraps each boot tile onto the map first, since the height-field generators' fallback site search can hand over one past the edge. RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands, Ring world, City states, - Tidal flats and Everglades call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount + Tidal flats, Everglades and Spider web call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount other than 100. Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane down every passage, and its `validateWorld` confirms every colony can still walk to every @@ -600,6 +604,43 @@ as an arena. Fairness is statistical; the lobby keeps the best-scoring of severa present, no deposit on any levee tile, and every colony reachable on foot from colony 0, with water, buildings and every resource blocking. +## Spider web + +An orb web of land spun over open water. Spokes run from a hub at the centre of the map out to a +frame thread, capture threads cross between the spokes sagging towards the hub, and every colony +starts on a pad where its spoke meets the frame. Threads are wide enough to build on, but every +ground route follows them: the knots where threads cross are the places to hold, the hub's +orchard is the prize, and the water between is crossed only once colonies can swim. + +- **Web.** `spokes` per colony (raised on webs with few colonies so the web never reads as a + star, lowered on crowded ones so halfway out neighbouring spokes keep 2.5 thread widths apart), + each bowing sideways by a random amount that is greatest at mid-length. The frame joins the + spoke ends. Capture threads are drawn with `bezierPath` and `strokePath`, `ring-spacing` tiles + apart (squeezed on a small map to keep a whole turn between the hub and the frame) and + `thread-width` wide, spokes and frame two tiles wider. Each sags towards the hub by `sag` and a + random share of it; `torn-strands` percent of the distinct strands of a wedge tear, keeping a + stub hanging from each spoke. With `spiral` (on) the capture threads are a spiral with one arm + per colony that climbs one spacing per wedge; off, they are closed rings. +- **Fair by rotation.** A spiral arm climbing one spacing per wedge maps onto the next arm under a + turn of one wedge, so every capture thread and its images share a key. Every choice about a + thread — torn, sag, bow, stone at its knot — is a stateless roll of that key from the seed, and + dew drops are chosen in one wedge and turned round the centre into every other, so every colony + gets the same web. +- **Pads and hub.** Pads sit as far out as the wrap allows and shrink (from 13 tiles of radius + down to 7) until neighbouring pads keep six tiles of sea between them; only then is the request + refused. The hub is `hub-size` percent of the half side, with a pond once it is nine tiles across. +- **Resources.** Each pad has an unscaled kit of 40 wheat and 30 wood on the hub side of the swarm + and a quarry beyond it. About 28% of knots carry a small stone deposit, `dew-drops` islets per + colony carry a fruit grove or stone each, and the hub carries the orchard of all three fruits + and a quarry. The threads' own wheat and wood are ranked and split by `PeriodicNoise` sampled in + the wedge frame, so every colony's stretch of web is farmed alike. Algae seeds the shallows. +- **Routes opened, not hoped for.** Beaches keep the threads walkable, but a small hub can be + stocked shut, so after `secureStartingCrops` an `openRoad` from colony 0 onto the hub and to + every other colony clears any deposits in the way. +- **Checked, not assumed.** `validateWorld` rebuilds the design and requires every spoke's centre + line to be land, every colony reachable on foot from colony 0 and the hub reachable too, with + water, buildings and every resource blocking. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort @@ -632,7 +673,7 @@ as an arena. Fairness is statistical; the lobby keeps the best-scoring of severa the distance flood matches a Chebyshev oracle on the torus, with obstacles and repeated sources, and `test/MapGeneratorToolkitChecks.h` checks every module of `shared/` on a map built by hand — floods and their limits, beaches and islands, patches and algae bands, the - cheapest route, settlements and the colony walk, the crop guarantee through and around a + cheapest route, strokes and shape fills, fields and clumps, settlements and the colony walk, the crop guarantee through and around a wall, a buried colony's room, balanced starts, per-landmass scatter, lattice noise, the wedge frame and the context's shuffle — so a change to a module fails there before it shows up as a changed golden fingerprint downstream. diff --git a/src/SConscript b/src/SConscript index ac00999ef..a5389ad87 100644 --- a/src/SConscript +++ b/src/SConscript @@ -193,6 +193,7 @@ map/generator/generators/RiverGenerator.cpp map/generator/generators/RuggedArchipelagoGenerator.cpp map/generator/generators/ShatteredCoastGenerator.cpp map/generator/generators/StoneHighlandsGenerator.cpp +map/generator/generators/SpiderWebGenerator.cpp map/generator/generators/SwampGenerator.cpp map/generator/generators/SymmetricArenaGenerator.cpp map/generator/generators/TidalFlatsGenerator.cpp @@ -202,6 +203,7 @@ map/generator/shared/legacy/Distances.cpp map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp map/generator/shared/BalancedStarts.cpp +map/generator/shared/Drawing.cpp map/generator/shared/Geometry.cpp map/generator/shared/Grid.cpp map/generator/shared/LatticeNoise.cpp diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index c9d40ee9d..90f6cd958 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -13,6 +13,7 @@ #include "RiverGenerator.h" #include "RuggedArchipelagoGenerator.h" #include "ShatteredCoastGenerator.h" +#include "SpiderWebGenerator.h" #include "StoneHighlandsGenerator.h" #include "SwampGenerator.h" #include "SymmetricArenaGenerator.h" @@ -93,7 +94,7 @@ const GeneratorRegistry &GeneratorRegistry::builtins() {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), stoneHighlandsDefinition(), symmetricArenaDefinition(), ringWorldDefinition(), - cityStatesDefinition(), tidalFlatsDefinition(), evergladesDefinition(), + cityStatesDefinition(), tidalFlatsDefinition(), evergladesDefinition(), spiderWebDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), riverDefinition(), uniformDefinition()}); return registry; diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 9392f88bf..e29923555 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -818,23 +818,6 @@ void carveValleys(std::vector &terrain, const Layout &L, Generati } } -// Splits a set of tiles into wheat and wood by an unrelated noise field, so the two crops form -// separate patches rather than rings sorted by fertility. -void plantFields(Map &map, const Torus &t, std::vector tiles, int wheat, int wood, - HeightMap &split) -{ - const int total = std::min(int(tiles.size()), wheat + wood); - if (total <= 0) - return; - tiles.resize(total); - std::stable_sort( - tiles.begin(), tiles.end(), [&](int a, int b) - { return split.uiLevel(a % t.w, a / t.w, 2048) < split.uiLevel(b % t.w, b / t.w, 2048); }); - const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); - for (int k = 0; k < total; ++k) - map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheatShare ? CORN : WOOD, 1); -} - // Every home's kit, identical and unscaled: wheat and wood patches beside its lake, where the lake // keeps them growing, and a stone deposit further off; then its own scaled ambient farmland, // outcrops and a grove, as a self-sufficient base needs. @@ -867,19 +850,14 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C // creek, and the quarry behind the lake. const double reach = std::clamp(0.1 * L.g.depth(), 2.0, 6.0) * 1.3 + 3; const double flank = L.creekSide != 0 ? -L.creekSide : -1.0; - const auto at = [&](double along, double across, int within) - { - return KitSeed{ - h.lakeX + int(std::lround(along * std::cos(h.angle) - across * std::sin(h.angle))), - h.lakeY + int(std::lround(along * std::sin(h.angle) + across * std::cos(h.angle))), - within}; - }; + const KitFrame frame{h.lakeX, h.lakeY, h.angle}; const Kit kit = L.creekSide == 0 - ? Kit{at(-0.3 * reach, -reach, 14), at(-0.3 * reach, reach, 14), - at(reach + 8, 0, 12), kHomeWheat, kHomeWood, 2} - : Kit{at(-0.5 * reach, flank * reach, 14), - at(0.6 * reach, flank * (reach + 1), 14), at(reach + 8, 0, 12), - kHomeWheat, kHomeWood, 2}; + ? Kit{frame.at(-0.3 * reach, -reach, 14), + frame.at(-0.3 * reach, reach, 14), frame.at(reach + 8, 0, 12), + kHomeWheat, kHomeWood, 2} + : Kit{frame.at(-0.5 * reach, flank * reach, 14), + frame.at(0.6 * reach, flank * (reach + 1), 14), + frame.at(reach + 8, 0, 12), kHomeWheat, kHomeWood, 2}; plantKit(map, t, context, kit, eligible); // Ambient farmland on the home's fertile ground, in patches, then outcrops and a grove. std::vector ground; @@ -910,31 +888,19 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C for (const auto &entry : farm) chosen.push_back(entry.second); const int area = int(ground.size()); + const auto bySplit = [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }; plantFields(map, t, chosen, int(scaledCount(area * 4 / 100, o.wheat)), - int(scaledCount(area * 2 / 100, o.wood)), split); - const int outcrops = int(scaledCount(std::max(1, area / 2500), o.stone)); - for (int k = 0; k < outcrops; ++k) - for (int attempt = 0; attempt < 100; ++attempt) - { - const int at = ground[context.bounded("city-home-stone", ground.size())]; - if (eligible(at)) - { - placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, 1); - break; - } - } - const int groves = int(scaledCount(1, o.fruit)); - for (int k = 0; k < groves; ++k) - for (int attempt = 0; attempt < 100; ++attempt) - { - const int at = ground[context.bounded("city-home-fruit", ground.size())]; - if (eligible(at)) - { - placeResourceClump(map, context, {at % t.w, at / t.w}, - CHERRY + int(context.bounded("city-home-fruit", 3)), 1); - break; - } - } + int(scaledCount(area * 2 / 100, o.wood)), bySplit); + scatterClumps(context, t, ground, int(scaledCount(std::max(1, area / 2500), o.stone)), + "city-home-stone", eligible, + [&](MapGeneratorPoint p) { placeResourceClump(map, context, p, STONE, 1); }); + scatterClumps(context, t, ground, int(scaledCount(1, o.fruit)), "city-home-fruit", + eligible, + [&](MapGeneratorPoint p) { + placeResourceClump(map, context, p, + CHERRY + int(context.bounded("city-home-fruit", 3)), + 1); + }); } } @@ -992,7 +958,8 @@ void stockCommons(Map &map, const Layout &L, GenerationContext &context, const C chosen.push_back(entry.second); const int area = int(ground.size()); plantFields(map, t, chosen, int(scaledCount(area * 7 / 100, o.wheat)), - int(scaledCount(area * 4 / 100, o.wood)), split); + int(scaledCount(area * 4 / 100, o.wood)), + [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }); const auto pick = [&](const char *stream) { @@ -1162,11 +1129,9 @@ bool generate(Game &game, GenerationContext &context) stockCommons(map, L, context, o); stockIslands(map, context, islands, "city-islands"); seedAlgae(map, context, t, "city-algae", o.algae, AlgaeBand::shallows(2, 4)); - clearAroundSwarms(map, context, t, &line); // The kits already put wheat and wood a short walk from every swarm; this is only the backstop, // and the walls and the causeways' stone are designed and must never be cleared. - guaranteeStartingResources(game, context, 24, 32, 0, &line); - clearAroundSwarms(map, context, t, &line); + secureStartingCrops(game, context, t, 24, 32, 0, &line); clearRoads(map, L, line); context.stage = "city roads"; return openRoads(map, L, line, context); diff --git a/src/map/generator/generators/EvergladesGenerator.cpp b/src/map/generator/generators/EvergladesGenerator.cpp index 4bc3aeaa5..de615171c 100644 --- a/src/map/generator/generators/EvergladesGenerator.cpp +++ b/src/map/generator/generators/EvergladesGenerator.cpp @@ -320,16 +320,11 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context) { return L.clearingOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; const double a = std::atan2(double(t.offsetY(L.cy, home.y)), double(t.offsetX(L.cx, home.x))); - const auto at = [&](double along, double across, int within) - { - return KitSeed{home.x + int(std::lround(along * std::cos(a) - across * std::sin(a))), - home.y + int(std::lround(along * std::sin(a) + across * std::cos(a))), - within}; - }; + const KitFrame frame{home.x, home.y, a}; const double beside = L.g.pond * 1.3 + 3; plantKit(map, t, context, - {at(0, -beside, 10), at(0, beside, 10), at(-(L.g.clearing - 4), 0, 8), kHomeWheat, - kHomeWood, 2}, + {frame.at(0, -beside, 10), frame.at(0, beside, 10), + frame.at(-(L.g.clearing - 4), 0, 8), kHomeWheat, kHomeWood, 2}, eligible); } } @@ -360,40 +355,23 @@ void stockSwamp(Map &map, const Layout &L, GenerationContext &context, const Eve const int area = int(ground.size()); const int wood = int(scaledCount(std::int64_t(area) * kWoodShare / 100, o.wood)); const int wheat = int(scaledCount(std::int64_t(area) * kWheatShare / 100, o.wheat)); - const int total = std::min(area, wood + wheat); - std::vector chosen; - for (int k = 0; k < total; ++k) - chosen.push_back(byPatch[k].second); - std::stable_sort( - chosen.begin(), chosen.end(), [&](int p, int q) - { return split.uiLevel(p % t.w, p / t.w, 2048) < split.uiLevel(q % t.w, q / t.w, 2048); }); - const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wood + wheat)); - for (int k = 0; k < total; ++k) - map.setResource(chosen[k] % t.w, chosen[k] / t.w, k < wheatShare ? CORN : WOOD, 1); - const int outcrops = int(scaledCount(std::max(2, area / 1000), o.stone)); - for (int k = 0; k < outcrops; ++k) - for (int attempt = 0; attempt < 100; ++attempt) - { - const int at = ground[context.bounded("glades-stone", ground.size())]; - if (eligible(at)) - { - placeResourceClump(map, context, {at % t.w, at / t.w}, STONE, - 1 + int(context.bounded("glades-stone", 2))); - break; - } - } - const int groves = int(scaledCount(std::max(3, area / 1500), o.fruit)); - for (int k = 0; k < groves; ++k) - for (int attempt = 0; attempt < 100; ++attempt) - { - const int at = ground[context.bounded("glades-fruit", ground.size())]; - if (eligible(at)) - { - placeResourceClump(map, context, {at % t.w, at / t.w}, - CHERRY + int(context.bounded("glades-fruit", 3)), 2); - break; - } - } + std::vector byDensity; + for (const auto &entry : byPatch) + byDensity.push_back(entry.second); + plantFields(map, t, byDensity, wheat, wood, + [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }); + scatterClumps(context, t, ground, int(scaledCount(std::max(2, area / 1000), o.stone)), + "glades-stone", eligible, + [&](MapGeneratorPoint p) { + placeResourceClump(map, context, p, STONE, + 1 + int(context.bounded("glades-stone", 2))); + }); + scatterClumps(context, t, ground, int(scaledCount(std::max(3, area / 1500), o.fruit)), + "glades-fruit", eligible, + [&](MapGeneratorPoint p) { + placeResourceClump(map, context, p, + CHERRY + int(context.bounded("glades-fruit", 3)), 2); + }); } // Every colony must be able to walk to colony 0 at the start. Where the swamp or the sloughs box @@ -545,9 +523,7 @@ bool generate(Game &game, GenerationContext &context) furnishHomes(map, L, context); stockSwamp(map, L, context, o); seedAlgae(map, context, t, "glades-algae", o.algae, AlgaeBand::anyWater()); - clearAroundSwarms(map, context, t); - guaranteeStartingResources(game, context, 24, 32, 0); - clearAroundSwarms(map, context, t); + secureStartingCrops(game, context, t); context.stage = "glades routes"; openRoutes(game, L, context); return true; diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index 584f79731..bbe14b344 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -557,9 +557,7 @@ bool generate(Game &game, GenerationContext &context) /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, /*fruit=*/int(scaledCount(3, options.fruit))}); seedAlgae(map, context, t, "resources", options.algae, AlgaeBand::shallows(2, 5)); - clearAroundSwarms(map, context, t); - guaranteeStartingResources(game, context, 24, 32, 6); - clearAroundSwarms(map, context, t); + secureStartingCrops(game, context, t, 24, 32, 6); // The scatter above is sized by the resource amounts, and at the top of their range it can wall // a colony into its own clearing with nowhere to build; the reopened colony is cleared again. if (reopenCrampedStarts(game, context, diff --git a/src/map/generator/generators/SpiderWebGenerator.cpp b/src/map/generator/generators/SpiderWebGenerator.cpp new file mode 100644 index 000000000..67d1ec750 --- /dev/null +++ b/src/map/generator/generators/SpiderWebGenerator.cpp @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "SpiderWebGenerator.h" +#include "Drawing.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "LatticeNoise.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Wedge.h" +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Spider web: an orb web of land spun over open water. Spokes run out from a hub at the centre of +// the map to a frame thread, and capture threads cross between the spokes, sagging towards the hub +// the way silk does; every colony starts on a pad where a spoke meets the frame. Threads are wide +// enough to build on but every ground route follows them, so the knots where threads cross are the +// places to hold, the hub with its orchard is the prize, and the water between is only crossed once +// colonies can swim. Dew drops, small islets in the web's cells, carry a prize each. +// +// With the spiral on, the capture threads are a spiral with one arm per colony, climbing one ring +// spacing per wedge, so it maps onto itself under a turn of one wedge. Every other choice (which +// strands are torn, how far each sags, how each spoke bows, where the dew drops hang) is made once +// per wedge, keyed by a thread's place in the wedge, so every colony gets the same web turned round +// the centre and the layout is fair for any colony count. The design is a pure function of the +// request, so validateWorld rebuilds it and checks the finished world. +namespace +{ + +// A colony's pad and how small it may shrink on a crowded ring before the request is refused; its +// outline wobbles by up to this share of its radius. +constexpr int kPadRadius = 13; +constexpr int kMinimumPad = 7; +constexpr double kPadRoughness = 0.2; +// Sea kept between the pads and their images across the wrap, and between neighbouring pads. +constexpr double kWrapGap = 6; +constexpr double kPadGap = 6; +// A web with fewer spokes than this reads as a star, so small colony counts get more per colony. +constexpr int kMinimumSpokes = 6; +// The free zone between the hub and the first capture thread, and the gap between the last and the +// frame, as shares of the ring spacing. +constexpr double kFreeZone = 0.6; +constexpr double kOuterZone = 0.45; +// Halfway out, neighbouring spokes are at least this many thread widths apart. +constexpr double kSpokeRoom = 2.5; +// A spoke bows sideways by up to this share of the angle between spokes, most at mid-length. +constexpr double kSpokeBow = 0.12; +// At a sag of 100 a capture thread's middle hangs this share of its length towards the hub. +constexpr double kMaximumSag = 0.22; +// A torn strand keeps this share of its length at each end. +constexpr double kTornStub = 0.36; +// Every home starts identical: wheat and wood beside the swarm, a quarry behind it, all unscaled. +constexpr int kHomeWheat = 40; +constexpr int kHomeWood = 30; +// Knots carrying stone, in tenths of a percent at 100, and each one's size in tiles. +constexpr int kKnotStone = 280; +constexpr int kKnotStoneTiles = 5; +// The threads' standing wheat and wood, as shares of their grass at 100. +constexpr int kWheatShare = 8; +constexpr int kWoodShare = 5; +// Dew drops and the water kept round them. +constexpr double kDewRadius = 3; +constexpr double kDewMoat = 3; + +struct Geometry +{ + int teams, half, perColony, spokes; + double spokeAngle, offset; // spoke j points at phase + (j + offset) * spokeAngle + double threadHalf, spokeHalf, hubRadius, pondRadius, padRadius, padReach, frameRadius; + double spacing, firstRing, lastRing, sag; + bool spiral, fits; +}; + +Geometry geometryFor(const GenerationRequest &r) +{ + const SpiderWebOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; + g.threadHalf = o.threadWidth / 2.0; + g.spokeHalf = g.threadHalf + 1; + g.hubRadius = std::max(6.0, o.hubSize / 100.0 * g.half); + g.pondRadius = g.hubRadius >= 9 ? std::max(2.0, 0.3 * g.hubRadius) : 0.0; + g.spacing = o.ringSpacing; + g.sag = o.sag / 100.0 * kMaximumSag; + g.spiral = o.spiral; + // The pads sit as far out as the wrap allows, and shrink on a crowded ring until they fit. + for (int pad = kPadRadius; pad >= kMinimumPad; --pad) + { + g.padRadius = pad; + g.padReach = pad * (1 + kPadRoughness); + g.frameRadius = g.half - g.padReach - kWrapGap / 2; + g.fits = g.frameRadius > g.hubRadius + g.padReach && + (g.teams < 2 || + 2 * g.frameRadius * std::sin(kPi / g.teams) >= 2 * g.padReach + kPadGap); + if (g.fits) + break; + } + // A few colonies get more spokes each, so the web never reads as a star; many get fewer, so + // the spokes never crowd into one mass round the hub: halfway out, neighbouring spokes keep + // well over a thread's width of water between them. + const double middle = (g.hubRadius + g.frameRadius) / 2; + const int room = int(2 * kPi * middle / (kSpokeRoom * o.threadWidth)); + g.perColony = std::max({1, std::min(o.spokes, room / g.teams), + std::min((kMinimumSpokes + g.teams - 1) / g.teams, room / g.teams)}); + g.spokes = g.perColony * g.teams; + g.spokeAngle = 2 * kPi / g.spokes; + // The middle spoke of every wedge ends at its colony's pad. + g.offset = g.perColony % 2 ? 0.5 : 0.0; + // A small map squeezes its capture threads closer, down to two thread widths apart, so the web + // keeps at least one whole turn between the hub and the frame. + const double span = g.frameRadius - 0.8 * g.padReach - g.hubRadius; + g.spacing = std::min(g.spacing, + std::max(2.0 * o.threadWidth, span / (1 + kFreeZone + kOuterZone))); + g.firstRing = g.hubRadius + kFreeZone * g.spacing; + g.lastRing = g.frameRadius - std::max(kOuterZone * g.spacing, 0.8 * g.padReach); + return g; +} + +// A thread's place in its wedge, the same for its image in every wedge: a stateless roll from the +// request's seed, so a strand and its images are torn, sagged and bowed alike. +int roll(const GenerationRequest &r, const char *what, long long key) +{ + return int(GenerationContext::deriveSeed(r.seed, std::string(what) + std::to_string(key)) % + 1000); +} + +int wrapSpoke(long long j, int spokes) +{ + return int(((j % spokes) + spokes) % spokes); +} + +struct Thread +{ + ShapePoint from, control, to; + bool torn; +}; + +struct Drop +{ + double x, y, turn; + int prize; // 0, 1, 2 a fruit, 3 stone +}; + +struct Layout +{ + Torus t{1, 1}; + Geometry g{}; + int cx = 0, cy = 0; + double phase = 0; + std::vector bow; // per spoke of a wedge, in radians + std::vector land, water; // per tile + std::vector hub, thread; // per tile + std::vector padOf; // pad tiles: the colony, else -1 + std::vector dewOf; // dew drop tiles: the drop, else -1 + std::vector pads, knots; // pad centres by colony; stone knots + std::vector padAngle; // each pad's heading out from the hub + std::vector drops; + std::vector> spokeLines; // every spoke's centre line + std::string failure; + + /// Where spoke j crosses radius r: it bows sideways most at mid-length and not at all at the + /// hub or the frame. + ShapePoint onSpoke(long long j, double r) const + { + const double span = std::max(1.0, g.frameRadius - g.hubRadius); + const double along = std::clamp((r - g.hubRadius) / span, 0.0, 1.0); + const double angle = phase + (double(j) + g.offset) * g.spokeAngle + + bow[wrapSpoke(j, g.perColony)] * std::sin(kPi * along); + return polarPoint(cx, cy, r, angle); + } + /// A capture thread's radius where it meets spoke j on its k-th turn. The spiral climbs one + /// spacing per wedge, so it has one arm per colony and meets itself under a turn of a wedge. + double level(long long k, long long j) const + { + return g.firstRing + + g.spacing * (double(k) + (g.spiral ? double(j) / g.perColony : 0.0)); + } + /// The key every image of capture thread (k, j) shares: a turn of one wedge takes (k, j) to + /// (k - 1, j + perColony) on the spiral and to (k, j + perColony) on rings, and both give the + /// same key. + long long key(long long k, long long j) const + { + return g.spiral ? k * g.perColony + j + : k * g.perColony + wrapSpoke(j, g.perColony); + } + /// The capture thread from spoke j to spoke j + 1 on turn k, sagging towards the hub; false + /// when it lies outside the rings. + bool capture(const GenerationRequest &r, long long k, long long j, Thread &out) const + { + const double a = level(k, j), b = level(k, j + 1); + if (a < g.firstRing - 1e-9 || b > g.lastRing + 1e-9) + return false; + const long long id = key(k, j); + const ShapePoint from = onSpoke(j, a), to = onSpoke(j + 1, b); + const double mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2; + const double inward = std::max(1e-9, std::hypot(cx - mx, cy - my)); + const double pull = g.sag * std::hypot(to.x - from.x, to.y - from.y) * + (0.6 + 0.8 * roll(r, "web-sag/", id) / 1000.0); + out = {from, + {mx + (cx - mx) / inward * 2 * pull, my + (cy - my) / inward * 2 * pull}, + to, + false}; + return true; + } +}; + +void strokeThread(std::vector &mask, const Torus &t, const Thread &thread, + double halfWidth) +{ + const double length = std::hypot(thread.to.x - thread.from.x, thread.to.y - thread.from.y); + const int segments = std::max(4, int(length / 3)); + const std::vector path = + bezierPath(thread.from, thread.control, thread.to, halfWidth, halfWidth, segments); + if (!thread.torn) + { + strokePath(mask, t, path); + return; + } + const int stub = std::max(1, int(std::lround(kTornStub * segments))); + strokePath(mask, t, std::vector(path.begin(), path.begin() + stub + 1)); + strokePath(mask, t, std::vector(path.end() - stub - 1, path.end())); +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const SpiderWebOptions o(request); + const int n = t.size(), teams = g.teams, m = g.perColony; + if (!g.fits) + { + L.failure = "the colonies' pads do not fit round the web"; + return L; + } + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("web-layout", 3600) / 3600.0 * 2 * kPi; + for (int j = 0; j < m; ++j) + L.bow.push_back((roll(request, "web-bow/", j) / 500.0 - 1) * kSpokeBow * g.spokeAngle); + L.land.assign(n, 0); + L.hub.assign(n, 0); + L.padOf.assign(n, -1); + L.dewOf.assign(n, -1); + + // The spokes, tapering from the hub to the frame. + for (int j = 0; j < g.spokes; ++j) + { + std::vector path; + std::vector line; + for (double r = g.hubRadius / 2;; r = std::min(g.frameRadius, r + 3)) + { + const ShapePoint p = L.onSpoke(j, r); + const double along = (r - g.hubRadius / 2) / (g.frameRadius - g.hubRadius / 2); + path.push_back({p.x, p.y, g.spokeHalf - along * (g.spokeHalf - g.threadHalf - 0.5)}); + line.push_back(p); + if (r >= g.frameRadius) + break; + } + strokePath(L.land, t, path); + L.spokeLines.push_back(std::move(line)); + } + // The frame: a thread between every two neighbouring spoke ends, sagging inwards. + for (int j = 0; j < g.spokes; ++j) + { + const ShapePoint from = L.onSpoke(j, g.frameRadius), to = L.onSpoke(j + 1, g.frameRadius); + const double mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2; + const double inward = std::max(1e-9, std::hypot(L.cx - mx, L.cy - my)); + const double pull = 0.5 * g.sag * std::hypot(to.x - from.x, to.y - from.y); + strokeThread(L.land, t, + {from, + {mx + (L.cx - mx) / inward * 2 * pull, my + (L.cy - my) / inward * 2 * pull}, + to, + false}, + g.spokeHalf); + } + // The capture threads: every turn of the spiral, or every ring, from spoke to spoke. A torn + // strand keeps its two ends hanging from the spokes. Where a thread leaves a spoke is a knot. + // The torn share is exact: of the distinct strands of a wedge, those with the lowest rolls tear. + const long long turns = (long long)std::ceil((g.lastRing - g.firstRing) / g.spacing) + 1; + std::vector> strands; + for (long long k = -teams - 1; k <= turns; ++k) + for (long long j = 0; j < g.spokes; ++j) + if (Thread thread; L.capture(request, k, j, thread)) + strands.push_back({roll(request, "web-tear/", L.key(k, j)), L.key(k, j)}); + std::sort(strands.begin(), strands.end()); + strands.erase(std::unique(strands.begin(), strands.end()), strands.end()); + std::vector torn; + for (size_t s = 0; s < size_t(std::lround(strands.size() * o.tornStrands / 100.0)); ++s) + torn.push_back(strands[s].second); + for (long long k = -teams - 1; k <= turns; ++k) + for (long long j = 0; j < g.spokes; ++j) + { + Thread thread; + if (!L.capture(request, k, j, thread)) + continue; + const long long id = L.key(k, j); + thread.torn = std::find(torn.begin(), torn.end(), id) != torn.end(); + strokeThread(L.land, t, thread, g.threadHalf); + if (roll(request, "web-knot/", id) < + std::min(1000, scaledCount(kKnotStone, o.stone))) + L.knots.push_back(thread.from); + } + + // The hub, with a pond at its heart, and a pad for every colony where its spoke meets the frame. + const RadialShape hubShape(g.hubRadius, 0.2, context, "web-hub"); + fillShape(L.land, t, L.cx, L.cy, hubShape); + fillShape(L.hub, t, L.cx, L.cy, hubShape); + const RadialShape padShape(g.padRadius, kPadRoughness, context, "web-pads"); + for (int k = 0; k < teams; ++k) + { + const long long spoke = (long long)k * m + m / 2; + const ShapePoint centre = L.onSpoke(spoke, g.frameRadius); + const double heading = L.phase + (double(spoke) + g.offset) * g.spokeAngle; + L.pads.push_back(centre); + L.padAngle.push_back(heading); + forEachTileInShape(t, centre.x, centre.y, padShape, heading, + [&](int i, double, double) + { + L.land[i] = 1; + L.padOf[i] = k; + }); + } + + // Dew drops: islets hanging in the web's cells, chosen in one wedge where the water is widest + // and hung in every wedge by turning them round the centre, each well clear of every thread. + if (o.dewDrops > 0) + { + const std::vector fromLand = stepsFrom(t, L.land); + const RadialShape dewShape(kDewRadius, 0.25, context, "web-dew"); + const int need = int(std::ceil(dewShape.maximumRadius() + kDewMoat)); + const WedgeFrame wedges(t, L.phase, teams); + // Inside the frame's sagging chords, not out at sea between them. + const double enclosed = + g.frameRadius * std::cos(g.spokeAngle / 2) * (1 - g.sag) - g.spokeHalf - need; + std::vector open; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + const WedgeFrame::Cell cell = wedges.cell(x, y); + if (cell.k != 0 || cell.d > enclosed || fromLand[i] < need) + continue; + bool peak = true; + for (int dy = -1; dy <= 1 && peak; ++dy) + for (int dx = -1; dx <= 1 && peak; ++dx) + peak = fromLand[t.at(x + dx, y + dy)] <= fromLand[i]; + if (peak) + open.push_back(i); + } + context.shuffle(open.begin(), open.end(), "web-dew"); + std::vector chosen; + for (int i : open) + { + bool apart = true; + for (int c : chosen) + apart = apart && t.dist2(i % t.w, i / t.w, c % t.w, c / t.w) >= 4 * need * need; + if (!apart || int(chosen.size()) >= o.dewDrops) + continue; + const int prize = int(chosen.size()) % 4; + chosen.push_back(i); + const double dx = t.offsetX(L.cx, i % t.w), dy = t.offsetY(L.cy, i / t.w); + for (int w = 0; w < teams; ++w) + { + const double turn = w * 2 * kPi / teams; + const double x = L.cx + dx * std::cos(turn) - dy * std::sin(turn); + const double y = L.cy + dx * std::sin(turn) + dy * std::cos(turn); + const int drop = int(L.drops.size()); + L.drops.push_back({x, y, turn, prize}); + forEachTileInShape(t, x, y, dewShape, turn, + [&](int tile, double, double) + { + L.land[tile] = 1; + L.dewOf[tile] = drop; + }); + } + } + } + + L.water.assign(n, 0); + L.thread.assign(n, 0); + if (g.pondRadius > 0) + { + const RadialShape pond(g.pondRadius, 0.25, context, "web-hub"); + fillShape(L.water, t, L.cx, L.cy, pond); + } + for (int i = 0; i < n; ++i) + { + L.water[i] = L.water[i] || !L.land[i]; + L.thread[i] = L.land[i] && !L.hub[i] && L.padOf[i] < 0 && L.dewOf[i] < 0; + } + return L; +} + +// Every colony's kit, identical and unscaled: wheat and wood on the hub side of the swarm, either +// side of the spoke, and a quarry out beyond the swarm. +void furnishPads(Map &map, const Layout &L, GenerationContext &context) +{ + const Torus &t = L.t; + const std::vector reserved = swarmSurroundings(t, context); + for (int team = 0; team < L.g.teams; ++team) + { + const auto eligible = [&](int i) + { return L.padOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + const KitFrame frame{int(std::lround(L.pads[team].x)), int(std::lround(L.pads[team].y)), + L.padAngle[team]}; + plantKit(map, t, context, + {frame.at(-5, -6, 8), frame.at(-5, 6, 8), frame.at(8, 0, 6), kHomeWheat, + kHomeWood, 2}, + eligible); + } +} + +// Stone at the knots, a prize on every dew drop, the orchard of all three fruits round the hub's +// pond with a quarry, then the threads' own wheat and wood in patches. The patches are sampled in +// the wedge's frame, so every colony's stretch of web is farmed alike. +void stockWeb(Map &map, const Layout &L, GenerationContext &context, const SpiderWebOptions &o) +{ + const Torus &t = L.t; + const int n = t.size(); + const auto onThread = [&](int i) { return L.thread[i] && clearGround(map, i % t.w, i / t.w); }; + for (const ShapePoint &knot : L.knots) + if (const int seed = seedNear(t, int(std::lround(knot.x)), int(std::lround(knot.y)), 3, + onThread); + seed >= 0) + growPatch(map, t, seed, STONE, kKnotStoneTiles, onThread); + + for (size_t d = 0; d < L.drops.size(); ++d) + { + const Drop &drop = L.drops[d]; + const auto onDrop = [&](int i) + { return L.dewOf[i] == int(d) && clearGround(map, i % t.w, i / t.w); }; + const int seed = seedNear(t, int(std::lround(drop.x)), int(std::lround(drop.y)), 3, onDrop); + if (seed < 0) + continue; + const bool stone = drop.prize == 3; + if (scaledCount(1, stone ? o.stone : o.fruit) > 0) + growPatch(map, t, seed, stone ? STONE : CHERRY + drop.prize, 9, onDrop); + } + + const auto onHub = [&](int i) { return L.hub[i] && clearGround(map, i % t.w, i / t.w); }; + const double rho = L.g.pondRadius > 0 ? L.g.pondRadius * 1.25 + 4 : 0.5 * L.g.hubRadius; + const double spin = context.bounded("web-hub", 3600) / 3600.0 * 2 * kPi; + if (scaledCount(1, o.fruit) > 0) + for (int f = 0; f < 3; ++f) + { + const ShapePoint p = polarPoint(L.cx, L.cy, rho, spin + 2 * kPi * f / 3); + if (const int seed = seedNear(t, int(std::lround(p.x)), int(std::lround(p.y)), 6, onHub); + seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); + } + if (scaledCount(1, o.stone) > 0) + { + const ShapePoint p = polarPoint(L.cx, L.cy, rho + 2, spin + kPi / 3); + if (const int seed = seedNear(t, int(std::lround(p.x)), int(std::lround(p.y)), 6, onHub); + seed >= 0) + placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); + } + + const WedgeFrame wedges(t, L.phase, L.g.teams); + const PeriodicNoise patch(t.w, t.h, 12, context.stream("web-patch")); + const PeriodicNoise split(t.w, t.h, 7, context.stream("web-split")); + std::vector> ranked; + std::vector splitKey(n, 0.0); + for (int i = 0; i < n; ++i) + if (onThread(i)) + { + const WedgeFrame::Cell cell = wedges.cell(i % t.w, i / t.w); + ranked.push_back({-patch.at(cell.s, cell.d), i}); + splitKey[i] = split.at(cell.s, cell.d); + } + std::stable_sort(ranked.begin(), ranked.end()); + std::vector tiles; + for (const auto &entry : ranked) + tiles.push_back(entry.second); + const std::int64_t area = std::int64_t(tiles.size()); + plantFields(map, t, tiles, int(scaledCount(area * kWheatShare / 100, o.wheat)), + int(scaledCount(area * kWoodShare / 100, o.wood)), + [&](int i) { return splitKey[i]; }); +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "web layout"; + const SpiderWebOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.size(); + + context.stage = "web terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + if (L.water[i]) + terrain[i] = WATER; + layBeaches(terrain, t); + writeUndermap(map, terrain); + + context.stage = "web colonies"; + const auto pad = [&](int team) + { + std::vector home(n, 0); + for (int i = 0; i < n; ++i) + home[i] = L.padOf[i] == team && map.isGrass(i % t.w, i / t.w); + return home; + }; + // The swarm stands a little out from the pad's middle, away from the hub, so the kit and the + // spoke's landing lie in front of it; placeSettlement measures from the footprint's top-left. + const auto anchor = [&](int team) + { + const ShapePoint p = polarPoint(L.pads[team].x, L.pads[team].y, 3, L.padAngle[team]); + return MapGeneratorPoint(int(std::lround(p.x)) - 2, int(std::lround(p.y)) - 2); + }; + if (!settleColonies(game, context, "web-starts", pad, anchor)) + return false; + + context.stage = "web resources"; + furnishPads(map, L, context); + stockWeb(map, L, context, o); + seedAlgae(map, context, t, "web-algae", o.algae, AlgaeBand::shallows(2, 6, 45)); + secureStartingCrops(game, context, t); + + // Beaches keep the threads walkable, but a small hub can be stocked shut: keep a way open from + // colony 0 onto the hub and to every other colony, clearing only the deposits in the way. + context.stage = "web roads"; + const std::vector> workers = unitTilesByTeam(map, teams); + if (teams < 1 || workers[0].empty()) + return true; + if (!openRoad(map, t, workers[0], L.hub)) + { + context.detail = "no walk from colony 0 reaches the hub"; + return false; + } + for (int team = 1; team < teams; ++team) + if (!openRoad(map, t, workers[0], tileMask(t, workers[team]))) + { + context.detail = "no walk from colony 0 reaches colony " + std::to_string(team); + return false; + } + return true; +} + +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 1) + return "Spider webs need at least one colony."; + if (!geometryFor(r).fits) + return "Too many colonies for this map; use a bigger map or fewer colonies."; + return ""; +} + +// Checked on the finished world: every spoke is still land from the hub to the frame, and colony 0 +// can walk to every other colony and onto the hub, with water, buildings and every resource +// blocking. +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "web"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (size_t j = 0; j < L.spokeLines.size(); ++j) + for (const ShapePoint &p : L.spokeLines[j]) + if (map.isWater(t.x(int(std::lround(p.x))), t.y(int(std::lround(p.y))))) + return "Spoke " + std::to_string(j) + " is broken by water."; + const ColonyWalk walk = + walkFromFirstColony(map, context.request.nbTeams, "the web", "along the web"); + if (!walk.error.empty()) + return walk.error; + for (int i = 0; i < t.size(); ++i) + if (L.hub[i] && walk.steps[i] >= 0) + return ""; + return "The hub cannot be reached along the web."; +} +} // namespace + +SpiderWebOptions::SpiderWebOptions(const GenerationRequest &r) + : spokes(r.option("spokes")), ringSpacing(r.option("ring-spacing")), + threadWidth(r.option("thread-width")), sag(r.option("sag")), + tornStrands(r.option("torn-strands")), hubSize(r.option("hub-size")), + dewDrops(r.option("dew-drops")), spiral(r.option("spiral") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition spiderWebDefinition() +{ + return { + "spider-web", + 20, + "Spider web", + 1, + false, + // Spokes per colony (more on a web with few colonies); the spacing between capture threads + // and every thread's width in tiles; how far the capture threads sag and the share of them + // torn, in percent; the hub's radius as a share of the half side; dew drops per colony. + {{"spokes", "Spokes per colony", 1, 3, 1, 2, ControlGroup::Layout}, + {"ring-spacing", "Ring spacing", 20, 40, 2, 28, ControlGroup::Terrain}, + {"thread-width", "Thread width", 7, 15, 2, 11, ControlGroup::Terrain}, + {"sag", "Sag", 0, 100, 10, 70, ControlGroup::Terrain}, + {"torn-strands", "Torn strands", 0, 60, 5, 15, ControlGroup::Terrain}, + {"hub-size", "Hub size", 8, 24, 1, 14, ControlGroup::Terrain}, + {"dew-drops", "Dew drops", 0, 4, 1, 2, ControlGroup::Layout}, + // Off, the capture threads are closed rings rather than a spiral. + GeneratorControl::toggle("spiral", "Spiral", true, ControlGroup::Layout), + // The threads' standing wheat and wood, the knots' stone and the drops' and hub's prizes; + // every pad's kit is unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/SpiderWebGenerator.h b/src/map/generator/generators/SpiderWebGenerator.h new file mode 100644 index 000000000..238497e77 --- /dev/null +++ b/src/map/generator/generators/SpiderWebGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct SpiderWebOptions +{ + int spokes, ringSpacing, threadWidth, sag, tornStrands, hubSize, dewDrops; + bool spiral; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit SpiderWebOptions(const GenerationRequest &r); +}; +GeneratorDefinition spiderWebDefinition(); diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index 20006b9fa..94c19657f 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -279,17 +279,11 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const T { return L.islandOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; const double a = std::atan2(double(t.offsetY(L.cy, L.pondY[team])), double(t.offsetX(L.cx, L.pondX[team]))); - const auto at = [&](double along, double across, int within) - { - return KitSeed{ - L.pondX[team] + int(std::lround(along * std::cos(a) - across * std::sin(a))), - L.pondY[team] + int(std::lround(along * std::sin(a) + across * std::cos(a))), - within}; - }; + const KitFrame frame{L.pondX[team], L.pondY[team], a}; // Wheat and wood on the two sides of the pond, the quarry towards the map's centre. plantKit(map, t, context, - {at(0, -pondRadius, 12), at(0, pondRadius, 12), at(-(pondRadius + 4), 0, 10), - kHomeWheat, kHomeWood, 2}, + {frame.at(0, -pondRadius, 12), frame.at(0, pondRadius, 12), + frame.at(-(pondRadius + 4), 0, 10), kHomeWheat, kHomeWood, 2}, eligible); // Ambient farmland on the island's fertile ground, split two to one. std::vector ground; @@ -302,33 +296,16 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const T farm.push_back({-double(f), i}); } std::stable_sort(farm.begin(), farm.end()); + std::vector byFertility; + for (const auto &entry : farm) + byFertility.push_back(entry.second); const int area = int(ground.size()); - const int wheat = int(scaledCount(area * 4 / 100, o.wheat)), - wood = int(scaledCount(area * 2 / 100, o.wood)); - const int total = std::min(int(farm.size()), wheat + wood); - std::vector chosen; - for (int k = 0; k < total; ++k) - chosen.push_back(farm[k].second); - std::stable_sort(chosen.begin(), chosen.end(), - [&](int p, int q) - { - return split.uiLevel(p % t.w, p / t.w, 2048) < - split.uiLevel(q % t.w, q / t.w, 2048); - }); - const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); - for (int k = 0; k < total; ++k) - map.setResource(chosen[k] % t.w, chosen[k] / t.w, k < wheatShare ? CORN : WOOD, 1); - const int outcrops = int(scaledCount(1, o.stone)); - for (int k = 0; k < outcrops && !ground.empty(); ++k) - for (int attempt = 0; attempt < 100; ++attempt) - { - const int at2 = ground[context.bounded("flats-home-stone", ground.size())]; - if (eligible(at2)) - { - placeResourceClump(map, context, {at2 % t.w, at2 / t.w}, STONE, 1); - break; - } - } + plantFields(map, t, byFertility, int(scaledCount(area * 4 / 100, o.wheat)), + int(scaledCount(area * 2 / 100, o.wood)), + [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }); + scatterClumps(context, t, ground, int(scaledCount(1, o.stone)), "flats-home-stone", + eligible, [&](MapGeneratorPoint p) + { placeResourceClump(map, context, p, STONE, 1); }); } } @@ -468,9 +445,7 @@ bool generate(Game &game, GenerationContext &context) furnishHomes(map, L, context, o); stockIslands(map, L, context, o); seedAlgae(map, context, t, "flats-algae", o.algae, AlgaeBand::anyWater()); - clearAroundSwarms(map, context, t); - guaranteeStartingResources(game, context, 24, 32, 0); - clearAroundSwarms(map, context, t); + secureStartingCrops(game, context, t); return true; } diff --git a/src/map/generator/shared/Drawing.cpp b/src/map/generator/shared/Drawing.cpp new file mode 100644 index 000000000..b2bb6d06f --- /dev/null +++ b/src/map/generator/shared/Drawing.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Drawing.h" +#include +#include +namespace MapGeneration +{ +namespace +{ +void strokeSegment(std::vector &mask, const Torus &t, const StrokePoint &a, + const StrokePoint &b, unsigned char value) +{ + const double reach = std::max(a.halfWidth, b.halfWidth); + const int x0 = int(std::floor(std::min(a.x, b.x) - reach)), + x1 = int(std::ceil(std::max(a.x, b.x) + reach)); + const int y0 = int(std::floor(std::min(a.y, b.y) - reach)), + y1 = int(std::ceil(std::max(a.y, b.y) + reach)); + const double ux = b.x - a.x, uy = b.y - a.y, length2 = ux * ux + uy * uy; + for (int y = y0; y <= y1; ++y) + for (int x = x0; x <= x1; ++x) + { + const double px = x - a.x, py = y - a.y; + const double along = + length2 > 0 ? std::clamp((px * ux + py * uy) / length2, 0.0, 1.0) : 0.0; + const double dx = px - along * ux, dy = py - along * uy; + const double halfWidth = a.halfWidth + along * (b.halfWidth - a.halfWidth); + if (dx * dx + dy * dy < halfWidth * halfWidth) + mask[t.at(x, y)] = value; + } +} +} // namespace + +void strokePath(std::vector &mask, const Torus &t, + const std::vector &path, unsigned char value, bool closed) +{ + if (path.empty()) + return; + if (path.size() == 1) + strokeSegment(mask, t, path[0], path[0], value); + for (size_t i = 0; i + 1 < path.size(); ++i) + strokeSegment(mask, t, path[i], path[i + 1], value); + if (closed && path.size() > 2) + strokeSegment(mask, t, path.back(), path.front(), value); +} + +std::vector bezierPath(ShapePoint from, ShapePoint control, ShapePoint to, + double fromHalfWidth, double toHalfWidth, int segments) +{ + std::vector path; + segments = std::max(1, segments); + for (int k = 0; k <= segments; ++k) + { + const double s = double(k) / segments, r = 1 - s; + path.push_back({r * r * from.x + 2 * r * s * control.x + s * s * to.x, + r * r * from.y + 2 * r * s * control.y + s * s * to.y, + fromHalfWidth + s * (toHalfWidth - fromHalfWidth)}); + } + return path; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Drawing.h b/src/map/generator/shared/Drawing.h new file mode 100644 index 000000000..14885c1de --- /dev/null +++ b/src/map/generator/shared/Drawing.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Geometry.h" +#include "Grid.h" +#include +#include +namespace MapGeneration +{ +// Drawing on the torus: thick lines (threads, roads, channels, walls) as a path of points with a +// width at each, and filled radial shapes, rasterized onto tile masks. Coordinates are map tiles +// and may run past the edges; every tile is written through the wrap. + +/// A point on a stroked path and the path's half width there. The width is interpolated linearly +/// between points, and every point is a round joint, so a path turns without gaps. +struct StrokePoint +{ + double x, y, halfWidth; +}; + +/// The point `radius` tiles from (cx, cy) at `angle` radians. +inline ShapePoint polarPoint(double cx, double cy, double radius, double angle) +{ + return {cx + radius * std::cos(angle), cy + radius * std::sin(angle)}; +} + +/// Sets `value` on every tile whose centre lies within the path's half width of it: a tile at +/// (x, y) is covered when its distance to the nearest segment is under the half width +/// interpolated at the nearest point. A closed path joins its last point back to its first. +void strokePath(std::vector &mask, const Torus &, const std::vector &, + unsigned char value = 1, bool closed = false); + +/// `segments` + 1 points along the quadratic Bezier from `from` through the pull of `control` +/// to `to`, with the half width running linearly from one end's to the other's: a thread that +/// sags or bows. +std::vector bezierPath(ShapePoint from, ShapePoint control, ShapePoint to, + double fromHalfWidth, double toHalfWidth, int segments); +/// Visits every tile a RadialShape centred at (cx, cy) and turned by `turn` radians covers, +/// searching only its bounding box: `visit(tile, dx, dy)` with the tile's offset from the centre. +/// Wrapped tiles are visited through the torus, each once while the shape is narrower than the +/// map. Turning one shape to each of several headings stamps the same outline round a centre. +template +void forEachTileInShape(const Torus &t, double cx, double cy, const RadialShape &shape, double turn, + Visit visit) +{ + const int reach = int(std::ceil(shape.maximumRadius())) + 1; + const int x0 = int(std::lround(cx)), y0 = int(std::lround(cy)); + for (int y = y0 - reach; y <= y0 + reach; ++y) + for (int x = x0 - reach; x <= x0 + reach; ++x) + { + const double dx = x - cx, dy = y - cy; + if (std::hypot(dx, dy) < shape.radiusAt(std::atan2(dy, dx) - turn)) + visit(t.at(x, y), dx, dy); + } +} + +/// Sets `value` on every tile of a RadialShape centred at (cx, cy), turned by `turn`. +inline void fillShape(std::vector &mask, const Torus &t, double cx, double cy, + const RadialShape &shape, double turn = 0, unsigned char value = 1) +{ + forEachTileInShape(t, cx, cy, shape, turn, [&](int i, double, double) { mask[i] = value; }); +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Pipeline.cpp b/src/map/generator/shared/Pipeline.cpp index d68451fbc..f9b01f66c 100644 --- a/src/map/generator/shared/Pipeline.cpp +++ b/src/map/generator/shared/Pipeline.cpp @@ -2,6 +2,7 @@ #include "Pipeline.h" #include "Game.h" #include "GenerationContext.h" +#include "Planting.h" #include "Resources.h" namespace MapGeneration { @@ -16,6 +17,14 @@ bool reopenCrampedStarts(Game &game, GenerationContext &context, const ResourceA return true; } +void secureStartingCrops(Game &game, GenerationContext &context, const Torus &t, int wheatRange, + int woodRange, int clearRadius, const std::vector *keep) +{ + clearAroundSwarms(game.map, context, t, keep); + guaranteeStartingResources(game, context, wheatRange, woodRange, clearRadius, keep); + clearAroundSwarms(game.map, context, t, keep); +} + ColonyWalk walkFromFirstColony(const Map &map, int teams, const std::string &ground, const std::string &route) { diff --git a/src/map/generator/shared/Pipeline.h b/src/map/generator/shared/Pipeline.h index b2168d7a8..582889d28 100644 --- a/src/map/generator/shared/Pipeline.h +++ b/src/map/generator/shared/Pipeline.h @@ -32,6 +32,14 @@ bool settleColonies(Game &game, GenerationContext &context, const char *stream, return true; } +/// The usual last word on every colony's crops: clear the ground round the swarms, run the +/// wheat/wood guarantee (guaranteeStartingResources) and clear round the swarms again, since a +/// topped-up deposit may land there. Tiles of `keep` are designed deposits (walls) that are never +/// cleared and never looked past. +void secureStartingCrops(Game &, GenerationContext &, const Torus &, int wheatRange = 24, + int woodRange = 32, int clearRadius = 0, + const std::vector *keep = nullptr); + /// The resource amounts a generator's ambient layers are scaled by; every one is 100 by default. struct ResourceAmounts { diff --git a/src/map/generator/shared/Planting.h b/src/map/generator/shared/Planting.h index 9b3314589..05262f0d1 100644 --- a/src/map/generator/shared/Planting.h +++ b/src/map/generator/shared/Planting.h @@ -1,10 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include "GenerationContext.h" #include "Grid.h" #include "Map.h" #include "Resources.h" #include "Sketch.h" +#include #include +#include +#include #include struct GenerationContext; namespace MapGeneration @@ -98,6 +102,62 @@ void plantKit(Map &map, const Torus &t, GenerationContext &context, const Kit &k kit.stoneRadius); } +/// A home's own frame for laying out its kit: an origin and a facing, so a kit designed once as +/// offsets along and across the facing lands the same way round every home. +struct KitFrame +{ + int x, y; + double angle; + /// The seed `along` tiles down the facing and `across` tiles to its left, searched `within`. + KitSeed at(double along, double across, int within) const + { + return KitSeed{x + int(std::lround(along * std::cos(angle) - across * std::sin(angle))), + y + int(std::lround(along * std::sin(angle) + across * std::cos(angle))), + within}; + } +}; + +/// Farmland on chosen ground: the first `wheat` + `wood` of `tiles` (already in order of +/// preference, most fertile first, say) are taken, then ordered by `splitKey(tile)` and dealt +/// wheat first, in proportion. Splitting by a field unrelated to the preference makes the two +/// crops form separate patches rather than rings sorted by fertility. +template +void plantFields(Map &map, const Torus &t, std::vector tiles, int wheat, int wood, + SplitKey splitKey) +{ + const int total = std::min(int(tiles.size()), wheat + wood); + if (total <= 0) + return; + tiles.resize(total); + std::stable_sort(tiles.begin(), tiles.end(), + [&](int a, int b) { return splitKey(a) < splitKey(b); }); + const int wheatShare = int(std::int64_t(total) * wheat / std::max(1, wheat + wood)); + for (int k = 0; k < total; ++k) + map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheatShare ? CORN : WOOD, 1); +} + +/// `count` clumps dropped on random tiles of `ground`: for each, up to `attempts` tiles are drawn +/// from `stream` and the first `eligible` one gets `place(point)`, which places the clump and may +/// draw its type and size from the same stream. Returns how many were placed. +template +int scatterClumps(GenerationContext &context, const Torus &t, const std::vector &ground, + int count, const char *stream, Eligible eligible, Place place, int attempts = 100) +{ + int placed = 0; + for (int k = 0; k < count && !ground.empty(); ++k) + for (int attempt = 0; attempt < attempts; ++attempt) + { + const int at = ground[context.bounded(stream, std::uint32_t(ground.size()))]; + if (eligible(at)) + { + place(MapGeneratorPoint(at % t.w, at / t.w)); + ++placed; + break; + } + } + return placed; +} + /// A mask of the tiles within `clearance` of every colony's swarm footprint: the ground a kit /// must leave alone. std::vector swarmSurroundings(const Torus &, const GenerationContext &, diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index a47de1f34..b417eaa1d 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -285,7 +285,7 @@ class MapGeneratorDefaultsTest screen.descriptor.option("test-gap") == 5 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 19); + assert(registry.selectionIndex(101) == 20); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); diff --git a/test/MapGeneratorToolkitChecks.h b/test/MapGeneratorToolkitChecks.h index 6a4a46743..ff2ee7be0 100644 --- a/test/MapGeneratorToolkitChecks.h +++ b/test/MapGeneratorToolkitChecks.h @@ -5,6 +5,7 @@ // here before it shows up as a changed golden fingerprint somewhere downstream. Needs the // globals loaded: building and resource types. #include "BalancedStarts.h" +#include "Drawing.h" #include "Game.h" #include "GenerationContext.h" #include "Geometry.h" @@ -614,6 +615,84 @@ inline void shuffleChecks() "The test design could not be rebuilt: no room"); } +// strokePath: a thick line covers exactly the tiles within its half width, tapers between points, +// wraps across the seam and draws a lone point as a disc; bezierPath runs end to end through the +// pull of its control; fillShape turns an outline without changing its area. +inline void drawingChecks() +{ + const Torus t(32, 16); + std::vector mask(t.size(), 0); + strokePath(mask, t, {{2, 8, 2.5}, {12, 8, 2.5}}); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const double dx = x < 2 ? 2 - x : x > 12 ? x - 12 : 0, dy = y - 8; + assert(mask[t.at(x, y)] == (dx * dx + dy * dy < 2.5 * 2.5)); + } + std::fill(mask.begin(), mask.end(), 0); + strokePath(mask, t, {{-3, 4, 1.5}, {3, 4, 1.5}}, 7); + assert(mask[t.at(31, 4)] == 7 && mask[t.at(29, 4)] == 7 && mask[t.at(2, 4)] == 7); + assert(mask[t.at(27, 4)] == 0 && mask[t.at(31, 6)] == 0); + std::fill(mask.begin(), mask.end(), 0); + strokePath(mask, t, {{16, 8, 4}, {26, 8, 0.5}}); + assert(mask[t.at(16, 11)] && !mask[t.at(25, 9)] && mask[t.at(25, 8)]); + std::fill(mask.begin(), mask.end(), 0); + strokePath(mask, t, {{10, 10, 1.1}}); + assert(std::count(mask.begin(), mask.end(), 1) == 5); + const std::vector curve = bezierPath({0, 0}, {10, 10}, {20, 0}, 1, 3, 10); + assert(curve.size() == 11 && curve.front().x == 0 && curve.back().x == 20); + assert(std::abs(curve[5].y - 5) < 1e-9 && std::abs(curve[5].halfWidth - 2) < 1e-9); + + GenerationRequest request; + request.seed = 11; + GenerationContext context(request); + const RadialShape shape(5, 0.4, context, "shape"); + std::vector upright(t.size(), 0), turned(t.size(), 0); + fillShape(upright, t, 16, 8, shape); + fillShape(turned, t, 16, 8, shape, kPi / 2); + const long area = std::count(upright.begin(), upright.end(), 1); + assert(area > 40 && std::abs(area - std::count(turned.begin(), turned.end(), 1)) < area / 8); + assert(upright != turned); +} + +// plantFields deals the preferred tiles, wheat first by the split key, in proportion; KitFrame +// turns its offsets with its facing; scatterClumps stops after its attempts on ineligible ground. +inline void layerChecks() +{ + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus t(map); + std::vector tiles; + for (int x = 0; x < 20; ++x) + tiles.push_back(t.at(x, 5)); + plantFields(map, t, tiles, 6, 3, [](int i) { return -i; }); + assert(countResource(map, CORN) == 6 && countResource(map, WOOD) == 3); + assert(map.getResource(8, 5).type == CORN && map.getResource(0, 5).type == WOOD); + assert(map.getResource(9, 5).type == NO_RES_TYPE); + + const KitFrame east{10, 10, 0}, south{10, 10, kPi / 2}; + assert(east.at(4, 1, 3).x == 14 && east.at(4, 1, 3).y == 11 && east.at(4, 1, 3).within == 3); + assert(south.at(4, 1, 3).x == 9 && south.at(4, 1, 3).y == 14); + + GenerationRequest request; + request.seed = 3; + GenerationContext context(request); + std::vector ground{t.at(40, 40), t.at(50, 50)}; + int calls = 0; + assert(scatterClumps(context, t, ground, 4, "clumps", [&](int i) { return i == ground[1]; }, + [&](MapGeneratorPoint p) + { + assert(p.x == 50 && p.y == 50); + ++calls; + }) == 4 && + calls == 4); + assert(scatterClumps(context, t, ground, 2, "clumps", [](int) { return false; }, + [](MapGeneratorPoint) { assert(false); }) == 0); + assert(scatterClumps(context, t, {}, 2, "clumps", [](int) { return true; }, + [](MapGeneratorPoint) { assert(false); }) == 0); +} + inline void toolkitChecks() { floodChecks(); @@ -626,7 +705,9 @@ inline void toolkitChecks() noiseChecks(); wedgeChecks(); shuffleChecks(); + drawingChecks(); + layerChecks(); puts("PASS shared toolkit: floods, sketch, planting, roads, settlements, balanced starts, " - "scatter, lattice noise, wedge frame, shuffle"); + "scatter, lattice noise, wedge frame, shuffle, drawing, fields and clumps"); } } // namespace ToolkitChecks diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 30e3cf790..10fcad79b 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -133,3 +133,10 @@ macos-arm64 19 2 8 8 4 2 ok 10374245496345104115 macos-arm64 19 2 8 8 4 3 ok 3538515548796101002 macos-arm64 19 2 8 8 8 1 ok 638962071964486508 macos-arm64 19 2 9 9 4 1 ok 3483131854939269908 +macos-arm64 20 1 7 7 4 1 ok 4116360308347306944 +macos-arm64 20 1 8 8 2 1 ok 11338407907832521097 +macos-arm64 20 1 8 8 4 1 ok 17920614512797914752 +macos-arm64 20 1 8 8 4 2 ok 16764556104255874150 +macos-arm64 20 1 8 8 4 3 ok 7345786698497847795 +macos-arm64 20 1 8 8 8 1 ok 12653674183462480909 +macos-arm64 20 1 9 9 4 1 ok 2982078612374145132 From e95f988655190e20cbd2ff09f64872d7ffa3bece Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 21:21:37 -0400 Subject: [PATCH 107/136] Add the Coral map generator; fill rectangular maps; enrich Spider web Coral (legacy id 21, revision 1). Every colony is a sea fan of land: a trunk leaves its pad on the rim and forks level after level as it grows in towards the middle, so each colony's coral is a triangle opening inwards, and the water between sibling branches is a triangle too. Neighbouring fans reach into each other's gaps and meet in a tangle in the middle; land gets thinner and richer away from home (stone at the forks, fruit on the tips). The fan is grown once and turned round the map for every colony, and every branch must keep a strait of water from all land, from its copies in every other wedge and from its own copies, which is what makes neighbouring tips interleave. The trunk leans by the same angle for every colony, so the map turns like a pinwheel. A first version with radial trunks and fronds bent round the centre read as a spider web and was replaced by this forking design before review. Controls: branching, fork angle (scaled with map size: wider on small maps, narrower on big ones), branch width, strait width, lean, land bridges, home size, sand roads and the five resource amounts. Requests that cannot fit are refused up front with advice. Shared toolkit: - Drawing: bentPath, pathClearance, pathBounds, growBranches (grows a forking tree level by level, each branch offered to an accept callback), stretchPath, tracePath (a gap-free one-tile line), and stretched shape fills. - Geometry: Stretch, which places a layout designed in a circle on the shorter side onto a rectangular map as an ellipse; exactly the identity on square maps. WedgeFrame measures cells unstretched. - Sketch: sprinkleSand, decorative sand patches on inland grass. - Planting: algaeGrowthChance, the exact chance that algae on a tile passes Map::growResources' test (water within 15 tiles and solid sand within 30 at a reflected offset); seedAlgae can place its clumps on the best-growing share of a band, shared out equally between the colonies' wedges, since the engine's test is not rotation-symmetric. Rectangular maps: Fjord continent (revision 11), Tidal flats (5), Spider web and Coral now stretch to fill a rectangular map as an oval instead of sitting in a disc on the short side. City states stays round: any stretch gives its homes different shapes and its fairness fell from 0.97 to 0.65-0.8. City states' landing-walk tolerance is unchanged. Spider web (revision 2): the threads carry about twice the wheat and wood (20% and 13% of their grass), a few decorative sand patches, algae seeded where it can regrow, thread width thinned on maps under 256 tiles, a home size control (130% by default), and a sand roads toggle (on) that traces a one-tile sand line down every spoke and whole thread so nothing can grow or be built across a thread. Coral's roads run down every branch that forks on and every bridge; tips and torn stubs, which are dead ends, get none. Verified: MapGeneratorGoldenTest compares 168 rows with 0 failures (a 512x256 row per generator added; every square golden map of the other generators byte-identical, with only revision fields bumped for Fjord continent, Tidal flats and Spider web); --sweep has 0 failing combinations; stress runs over 64-512 maps, 1-12 colonies and every control extreme generated 1642/1862 Coral maps (median start fairness 0.966; every miss an up-front refusal) and 1842/1922 Spider web maps (median 0.960); rectangle stress (8 sizes, 1-12 colonies) gives median fairness 0.96 for both; MapGeneratorDefaultsTest, check_translations.py --strict and clang-format pass; the client builds. Home size, Fork angle, Lean, Branching and the Coral refusal strings are translated in all tables (machine-assisted, not native-reviewed). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 18 + data/texts.br.txt | 18 + data/texts.ca.txt | 18 + data/texts.cz.txt | 18 + data/texts.de.txt | 18 + data/texts.dk.txt | 18 + data/texts.en.txt | 18 + data/texts.eo.txt | 18 + data/texts.es.txt | 18 + data/texts.eu.txt | 18 + data/texts.fa.txt | 18 + data/texts.fi.txt | 18 + data/texts.fr.txt | 18 + data/texts.gr.txt | 18 + data/texts.hu.txt | 18 + data/texts.id.txt | 18 + data/texts.it.txt | 18 + data/texts.ja.txt | 18 + data/texts.keys.txt | 9 + data/texts.ko.txt | 18 + data/texts.nl.txt | 18 + data/texts.pl.txt | 18 + data/texts.pt.txt | 18 + data/texts.ro.txt | 18 + data/texts.ru.txt | 18 + data/texts.si.txt | 18 + data/texts.sk.txt | 18 + data/texts.sr.txt | 18 + data/texts.sv.txt | 18 + data/texts.tr.txt | 18 + data/texts.uk.txt | 18 + data/texts.vi.txt | 18 + data/texts.zh-cn.txt | 18 + data/texts.zh-tw.txt | 18 + docs/map-generators/ADDING_A_GENERATOR.md | 2 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 116 ++- src/SConscript | 1 + src/map/generator/core/GeneratorRegistry.cpp | 6 +- .../generator/generators/CoralGenerator.cpp | 927 ++++++++++++++++++ src/map/generator/generators/CoralGenerator.h | 12 + .../generators/FjordContinentGenerator.cpp | 52 +- .../generators/SpiderWebGenerator.cpp | 198 +++- .../generator/generators/SpiderWebGenerator.h | 4 +- .../generators/TidalFlatsGenerator.cpp | 27 +- src/map/generator/shared/Drawing.cpp | 126 +++ src/map/generator/shared/Drawing.h | 177 +++- src/map/generator/shared/Geometry.h | 38 + src/map/generator/shared/Planting.cpp | 99 +- src/map/generator/shared/Planting.h | 25 +- src/map/generator/shared/Sketch.h | 28 + src/map/generator/shared/Wedge.h | 25 +- test/MapGeneratorDefaultsTest.cpp | 2 +- test/MapGeneratorGoldenTest.cpp | 3 +- test/MapGeneratorToolkitChecks.h | 161 ++- test/map-generator-golden.txt | 70 +- 55 files changed, 2553 insertions(+), 149 deletions(-) create mode 100644 src/map/generator/generators/CoralGenerator.cpp create mode 100644 src/map/generator/generators/CoralGenerator.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 2055dba10..35133fa75 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1914,6 +1914,24 @@ OpenGL غير متوفر في هذا الإصدار. تحتاج المستنقعات إلى مستعمرة واحدة على الأقل. [Too many colonies for this map; use a bigger map or fewer colonies.] عدد المستعمرات كبير جدًا لهذه الخريطة؛ استخدم خريطة أكبر أو مستعمرات أقل. +[Coral] +المرجان +[Fork angle] +زاوية التفرّع +[Lean] +الميل +[Home size] +حجم الموطن +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +المرجان لا يتسع لهذه الخريطة؛ استخدم خريطة أكبر أو مضائق أضيق أو مستعمرات أقل. +[Branching] +التفرّع +[Branch width] +عرض الفروع +[Coral needs at least one colony.] +يحتاج المرجان إلى مستعمرة واحدة على الأقل. +[Coral needs a map at least 128 tiles across.] +يحتاج المرجان إلى خريطة بعرض 128 مربعًا على الأقل. [Spider web] شبكة العنكبوت [Spokes per colony] diff --git a/data/texts.br.txt b/data/texts.br.txt index e95ce3aae..dd225c1cd 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1912,6 +1912,24 @@ Geunioù Os pântanos precisam de pelo menos uma colônia. [Too many colonies for this map; use a bigger map or fewer colonies.] Re a drevadennoù evit ar gartenn-mañ; implijit ur gartenn vrasoc'h pe nebeutoc'h a drevadennoù. +[Coral] +Koural +[Fork angle] +Korn ar skourradur +[Lean] +Stouadur +[Home size] +Ment an diazez +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Ne zeu ket ar c'houral war ar gartenn-mañ; implijit ur gartenn vrasoc'h, kanolioù moanoc'h pe nebeutoc'h a drevadennoù. +[Branching] +Skourradurioù +[Branch width] +Ledander ar skourroù +[Coral needs at least one colony.] +Ezhomm en deus ar c'houral eus un drevadenn da vihanañ. +[Coral needs a map at least 128 tiles across.] +Ezhomm en deus ar c'houral eus ur gartenn 128 karrezenn ledan da vihanañ. [Spider web] Gwiad-kevnid [Spokes per colony] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 98dfa5fec..b1b838c95 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1924,6 +1924,24 @@ Aiguamolls Els aiguamolls necessiten almenys una colònia. [Too many colonies for this map; use a bigger map or fewer colonies.] Massa colònies per a aquest mapa; useu un mapa més gran o menys colònies. +[Coral] +Corall +[Fork angle] +Angle de ramificació +[Lean] +Inclinació +[Home size] +Mida de la base +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +El corall no cap en aquest mapa; utilitzeu un mapa més gran, estrets més estrets o menys colònies. +[Branching] +Ramificació +[Branch width] +Amplada de les branques +[Coral needs at least one colony.] +El corall necessita almenys una colònia. +[Coral needs a map at least 128 tiles across.] +El corall necessita un mapa d'almenys 128 caselles d'amplada. [Spider web] Teranyina [Spokes per colony] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index e2cec9a1c..f92401fd2 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1916,6 +1916,24 @@ Slepá ramena Mokřady potřebují alespoň jednu kolonii. [Too many colonies for this map; use a bigger map or fewer colonies.] Příliš mnoho kolonií pro tuto mapu; použijte větší mapu nebo méně kolonií. +[Coral] +Korál +[Fork angle] +Úhel větvení +[Lean] +Náklon +[Home size] +Velikost domova +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Korál se na tuto mapu nevejde; použijte větší mapu, užší průlivy nebo méně kolonií. +[Branching] +Větvení +[Branch width] +Šířka větví +[Coral needs at least one colony.] +Korál potřebuje alespoň jednu kolonii. +[Coral needs a map at least 128 tiles across.] +Korál potřebuje mapu širokou alespoň 128 polí. [Spider web] Pavučina [Spokes per colony] diff --git a/data/texts.de.txt b/data/texts.de.txt index 4f413d366..2ae7f1c44 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1916,6 +1916,24 @@ Altwasser Das Sumpfland braucht mindestens eine Kolonie. [Too many colonies for this map; use a bigger map or fewer colonies.] Zu viele Kolonien für diese Karte; wähle eine größere Karte oder weniger Kolonien. +[Coral] +Koralle +[Fork angle] +Gabelwinkel +[Lean] +Neigung +[Home size] +Heimatgröße +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Die Koralle passt nicht auf diese Karte; nimm eine größere Karte, schmalere Meerengen oder weniger Kolonien. +[Branching] +Verzweigung +[Branch width] +Astbreite +[Coral needs at least one colony.] +Korallen brauchen mindestens eine Kolonie. +[Coral needs a map at least 128 tiles across.] +Korallen brauchen eine Karte von mindestens 128 Feldern Breite. [Spider web] Spinnennetz [Spokes per colony] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index d120f5e63..cff4e98e7 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1992,6 +1992,24 @@ Sumpkanaler Sumplandet kræver mindst én koloni. [Too many colonies for this map; use a bigger map or fewer colonies.] For mange kolonier til dette kort; brug et større kort eller færre kolonier. +[Coral] +Koral +[Fork angle] +Forgreningsvinkel +[Lean] +Hældning +[Home size] +Hjemmets størrelse +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Koralen passer ikke på dette kort; brug et større kort, smallere stræder eller færre kolonier. +[Branching] +Forgrening +[Branch width] +Grenbredde +[Coral needs at least one colony.] +Koral kræver mindst én koloni. +[Coral needs a map at least 128 tiles across.] +Koral kræver et kort på mindst 128 felter i bredden. [Spider web] Spindelvæv [Spokes per colony] diff --git a/data/texts.en.txt b/data/texts.en.txt index eafa77dc5..64f04532e 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1904,6 +1904,24 @@ Sloughs Everglades need at least one colony. [Too many colonies for this map; use a bigger map or fewer colonies.] Too many colonies for this map; use a bigger map or fewer colonies. +[Coral] +Coral +[Fork angle] +Fork angle +[Lean] +Lean +[Home size] +Home size +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +The coral does not fit this map; use a bigger map, narrower straits or fewer colonies. +[Branching] +Branching +[Branch width] +Branch width +[Coral needs at least one colony.] +Coral needs at least one colony. +[Coral needs a map at least 128 tiles across.] +Coral needs a map at least 128 tiles across. [Spider webs need at least one colony.] Spider webs need at least one colony. [Spiral] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 4a7bc7270..c107f9cf8 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1914,6 +1914,24 @@ Marĉkanaloj Marĉoj bezonas almenaŭ unu kolonion. [Too many colonies for this map; use a bigger map or fewer colonies.] Tro da kolonioj por ĉi tiu mapo; uzu pli grandan mapon aŭ malpli da kolonioj. +[Coral] +Koralo +[Fork angle] +Angulo de branĉiĝo +[Lean] +Kliniĝo +[Home size] +Grando de la hejmo +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +La koralo ne konvenas al ĉi tiu mapo; uzu pli grandan mapon, pli mallarĝajn markolojn aŭ malpli da kolonioj. +[Branching] +Branĉiĝo +[Branch width] +Larĝo de branĉoj +[Coral needs at least one colony.] +Koralo bezonas almenaŭ unu kolonion. +[Coral needs a map at least 128 tiles across.] +Koralo bezonas mapon almenaŭ 128 kahelojn larĝan. [Spider web] Araneaĵo [Spokes per colony] diff --git a/data/texts.es.txt b/data/texts.es.txt index c95d8fbe6..acc06d804 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1916,6 +1916,24 @@ Ciénagas El pantanal necesita al menos una colonia. [Too many colonies for this map; use a bigger map or fewer colonies.] Demasiadas colonias para este mapa; usa un mapa más grande o menos colonias. +[Coral] +Coral +[Fork angle] +Ángulo de ramificación +[Lean] +Inclinación +[Home size] +Tamaño de la base +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +El coral no cabe en este mapa; usa un mapa más grande, estrechos más estrechos o menos colonias. +[Branching] +Ramificación +[Branch width] +Ancho de las ramas +[Coral needs at least one colony.] +El coral necesita al menos una colonia. +[Coral needs a map at least 128 tiles across.] +El coral necesita un mapa de al menos 128 casillas de ancho. [Spider web] Telaraña [Spokes per colony] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 1c05169b0..3e1941011 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1926,6 +1926,24 @@ Zingirak Zingirek gutxienez kolonia bat behar dute. [Too many colonies for this map; use a bigger map or fewer colonies.] Kolonia gehiegi mapa honetarako; erabili mapa handiagoa edo kolonia gutxiago. +[Coral] +Koralea +[Fork angle] +Adarkatze-angelua +[Lean] +Makurdura +[Home size] +Etxearen tamaina +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Koralea ez da mapa honetan sartzen; erabili mapa handiagoa, itsasarte estuagoak edo kolonia gutxiago. +[Branching] +Adarkadura +[Branch width] +Adarren zabalera +[Coral needs at least one colony.] +Koraleak gutxienez kolonia bat behar du. +[Coral needs a map at least 128 tiles across.] +Koraleak gutxienez 128 lauki zabaleko mapa behar du. [Spider web] Armiarma-sarea [Spokes per colony] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index fc744f4e0..421b0182c 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1914,6 +1914,24 @@ OpenGL در این بیلد موجود نیست. باتلاق‌ها دست‌کم به یک مستعمره نیاز دارند. [Too many colonies for this map; use a bigger map or fewer colonies.] تعداد مستعمره‌ها برای این نقشه زیاد است؛ از نقشهٔ بزرگ‌تر یا مستعمره‌های کمتر استفاده کنید. +[Coral] +مرجان +[Fork angle] +زاویه انشعاب +[Lean] +شیب +[Home size] +اندازه پایگاه +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +مرجان در این نقشه جا نمی‌شود؛ از نقشه‌ای بزرگ‌تر، تنگه‌های باریک‌تر یا مستعمره‌های کمتر استفاده کنید. +[Branching] +انشعاب +[Branch width] +پهنای شاخه‌ها +[Coral needs at least one colony.] +مرجان دست‌کم به یک مستعمره نیاز دارد. +[Coral needs a map at least 128 tiles across.] +مرجان به نقشه‌ای با پهنای دست‌کم ۱۲۸ خانه نیاز دارد. [Spider web] تار عنکبوت [Spokes per colony] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 17662bf2a..063e4ccf0 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1916,6 +1916,24 @@ Rimmet Suomaat tarvitsevat vähintään yhden siirtokunnan. [Too many colonies for this map; use a bigger map or fewer colonies.] Liikaa siirtokuntia tälle kartalle; käytä suurempaa karttaa tai vähemmän siirtokuntia. +[Coral] +Koralli +[Fork angle] +Haarautumiskulma +[Lean] +Kallistus +[Home size] +Kotialueen koko +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Koralli ei mahdu tälle kartalle; käytä suurempaa karttaa, kapeampia salmia tai vähemmän siirtokuntia. +[Branching] +Haarautuminen +[Branch width] +Oksien leveys +[Coral needs at least one colony.] +Koralli tarvitsee vähintään yhden siirtokunnan. +[Coral needs a map at least 128 tiles across.] +Koralli tarvitsee vähintään 128 ruutua leveän kartan. [Spider web] Hämähäkinseitti [Spokes per colony] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index bd2a9bbc8..b6f95fa36 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1926,6 +1926,24 @@ Marigots Les marécages ont besoin d'au moins une colonie. [Too many colonies for this map; use a bigger map or fewer colonies.] Trop de colonies pour cette carte ; utilisez une carte plus grande ou moins de colonies. +[Coral] +Corail +[Fork angle] +Angle de ramification +[Lean] +Inclinaison +[Home size] +Taille de la base +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Le corail ne tient pas sur cette carte ; utilisez une carte plus grande, des détroits plus étroits ou moins de colonies. +[Branching] +Ramification +[Branch width] +Largeur des branches +[Coral needs at least one colony.] +Le corail nécessite au moins une colonie. +[Coral needs a map at least 128 tiles across.] +Le corail nécessite une carte d'au moins 128 cases de large. [Spider web] Toile d'araignée [Spokes per colony] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 2af8d70b2..8ac46fca4 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1986,6 +1986,24 @@ Cortex Οι βαλτότοποι χρειάζονται τουλάχιστον μία αποικία. [Too many colonies for this map; use a bigger map or fewer colonies.] Πάρα πολλές αποικίες για αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη ή λιγότερες αποικίες. +[Coral] +Κοράλλι +[Fork angle] +Γωνία διακλάδωσης +[Lean] +Κλίση +[Home size] +Μέγεθος βάσης +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Το κοράλλι δεν χωράει σε αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη, στενότερα στενά ή λιγότερες αποικίες. +[Branching] +Διακλάδωση +[Branch width] +Πλάτος κλαδιών +[Coral needs at least one colony.] +Το κοράλλι χρειάζεται τουλάχιστον μία αποικία. +[Coral needs a map at least 128 tiles across.] +Το κοράλλι χρειάζεται χάρτη πλάτους τουλάχιστον 128 τετραγώνων. [Spider web] Ιστός αράχνης [Spokes per colony] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 63b3c4e1e..95030b18a 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1916,6 +1916,24 @@ Holtágak A mocsárvidékhez legalább egy kolónia kell. [Too many colonies for this map; use a bigger map or fewer colonies.] Túl sok kolónia ehhez a térképhez; használj nagyobb térképet vagy kevesebb kolóniát. +[Coral] +Korall +[Fork angle] +Elágazási szög +[Lean] +Dőlés +[Home size] +Otthon mérete +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +A korall nem fér el ezen a térképen; használj nagyobb térképet, keskenyebb szorosokat vagy kevesebb kolóniát. +[Branching] +Elágazás +[Branch width] +Ágszélesség +[Coral needs at least one colony.] +A korallhoz legalább egy kolónia kell. +[Coral needs a map at least 128 tiles across.] +A korallhoz legalább 128 mező széles térkép kell. [Spider web] Pókháló [Spokes per colony] diff --git a/data/texts.id.txt b/data/texts.id.txt index ff2f79502..d26014c30 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1912,6 +1912,24 @@ Rawa-rawa Rawa-rawa membutuhkan setidaknya satu koloni. [Too many colonies for this map; use a bigger map or fewer colonies.] Terlalu banyak koloni untuk peta ini; gunakan peta lebih besar atau lebih sedikit koloni. +[Coral] +Karang +[Fork angle] +Sudut percabangan +[Lean] +Kemiringan +[Home size] +Ukuran markas +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Karang tidak muat di peta ini; gunakan peta yang lebih besar, selat yang lebih sempit, atau lebih sedikit koloni. +[Branching] +Percabangan +[Branch width] +Lebar cabang +[Coral needs at least one colony.] +Karang memerlukan setidaknya satu koloni. +[Coral needs a map at least 128 tiles across.] +Karang memerlukan peta dengan lebar setidaknya 128 petak. [Spider web] Jaring laba-laba [Spokes per colony] diff --git a/data/texts.it.txt b/data/texts.it.txt index bdb728e47..88b65e29b 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1978,6 +1978,24 @@ Lanche Le paludi hanno bisogno di almeno una colonia. [Too many colonies for this map; use a bigger map or fewer colonies.] Troppe colonie per questa mappa; usa una mappa più grande o meno colonie. +[Coral] +Corallo +[Fork angle] +Angolo di ramificazione +[Lean] +Inclinazione +[Home size] +Dimensione della base +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Il corallo non entra in questa mappa; usa una mappa più grande, stretti più sottili o meno colonie. +[Branching] +Ramificazione +[Branch width] +Larghezza dei rami +[Coral needs at least one colony.] +Il corallo richiede almeno una colonia. +[Coral needs a map at least 128 tiles across.] +Il corallo richiede una mappa larga almeno 128 caselle. [Spider web] Ragnatela [Spokes per colony] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 688c73511..4afa3c9d5 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1912,6 +1912,24 @@ Globulation 2 のカーソルを表示します。 湿地帯には少なくとも一つのコロニーが必要です。 [Too many colonies for this map; use a bigger map or fewer colonies.] この地図にはコロニーが多すぎます。より大きな地図にするか、コロニーを減らしてください。 +[Coral] +サンゴ +[Fork angle] +分岐角度 +[Lean] +傾き +[Home size] +本拠地の大きさ +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +サンゴがこのマップに収まりません。より大きなマップ、より狭い海峡、またはより少ないコロニーにしてください。 +[Branching] +枝分かれ +[Branch width] +枝の幅 +[Coral needs at least one colony.] +サンゴには少なくとも1つのコロニーが必要です。 +[Coral needs a map at least 128 tiles across.] +サンゴには幅128マス以上のマップが必要です。 [Spider web] クモの巣 [Spokes per colony] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index f2dcb88e8..2c02496d1 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -950,6 +950,15 @@ [Sloughs] [Everglades need at least one colony.] [Too many colonies for this map; use a bigger map or fewer colonies.] +[Coral] +[Fork angle] +[Lean] +[Home size] +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +[Branching] +[Branch width] +[Coral needs at least one colony.] +[Coral needs a map at least 128 tiles across.] [Spider webs need at least one colony.] [Spiral] [Dew drops] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 6d0a07b39..08ac29ecf 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1912,6 +1912,24 @@ Globulation 2 커서를 표시합니다. 습지대에는 최소 한 개의 식민지가 필요합니다. [Too many colonies for this map; use a bigger map or fewer colonies.] 이 지도에는 식민지가 너무 많습니다. 더 큰 지도를 사용하거나 식민지를 줄이세요. +[Coral] +산호 +[Fork angle] +분기 각도 +[Lean] +기울기 +[Home size] +본거지 크기 +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +산호가 이 지도에 맞지 않습니다. 더 큰 지도, 더 좁은 해협 또는 더 적은 식민지를 사용하세요. +[Branching] +가지치기 +[Branch width] +가지 너비 +[Coral needs at least one colony.] +산호에는 식민지가 하나 이상 필요합니다. +[Coral needs a map at least 128 tiles across.] +산호에는 너비가 128칸 이상인 지도가 필요합니다. [Spider web] 거미줄 [Spokes per colony] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 46c14fc40..7f7fef7a9 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1940,6 +1940,24 @@ Moerassloten Het moerasland heeft minstens één kolonie nodig. [Too many colonies for this map; use a bigger map or fewer colonies.] Te veel kolonies voor deze kaart; gebruik een grotere kaart of minder kolonies. +[Coral] +Koraal +[Fork angle] +Vertakkingshoek +[Lean] +Helling +[Home size] +Grootte van de thuisbasis +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Het koraal past niet op deze kaart; gebruik een grotere kaart, smallere zeestraten of minder kolonies. +[Branching] +Vertakking +[Branch width] +Takbreedte +[Coral needs at least one colony.] +Koraal heeft minstens één kolonie nodig. +[Coral needs a map at least 128 tiles across.] +Koraal heeft een kaart van minstens 128 tegels breed nodig. [Spider web] Spinnenweb [Spokes per colony] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index bae4ced58..b5221264c 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1916,6 +1916,24 @@ Starorzecza Mokradła potrzebują co najmniej jednej kolonii. [Too many colonies for this map; use a bigger map or fewer colonies.] Za dużo kolonii jak na tę mapę; użyj większej mapy lub mniejszej liczby kolonii. +[Coral] +Koralowiec +[Fork angle] +Kąt rozgałęzienia +[Lean] +Pochylenie +[Home size] +Rozmiar bazy +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Koralowiec nie mieści się na tej mapie; użyj większej mapy, węższych cieśnin albo mniejszej liczby kolonii. +[Branching] +Rozgałęzienie +[Branch width] +Szerokość gałęzi +[Coral needs at least one colony.] +Koralowiec wymaga co najmniej jednej kolonii. +[Coral needs a map at least 128 tiles across.] +Koralowiec wymaga mapy o szerokości co najmniej 128 pól. [Spider web] Pajęczyna [Spokes per colony] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index cd42487fe..bbd1f3652 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1922,6 +1922,24 @@ Charcos Os pântanos precisam de pelo menos uma colónia. [Too many colonies for this map; use a bigger map or fewer colonies.] Colónias a mais para este mapa; use um mapa maior ou menos colónias. +[Coral] +Coral +[Fork angle] +Ângulo de ramificação +[Lean] +Inclinação +[Home size] +Tamanho da base +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +O coral não cabe neste mapa; use um mapa maior, estreitos mais estreitos ou menos colónias. +[Branching] +Ramificação +[Branch width] +Largura dos ramos +[Coral needs at least one colony.] +O coral precisa de pelo menos uma colónia. +[Coral needs a map at least 128 tiles across.] +O coral precisa de um mapa com pelo menos 128 casas de largura. [Spider web] Teia de aranha [Spokes per colony] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 280aa7853..fe4d7f3ae 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1916,6 +1916,24 @@ Mlaștini Mlaștinile au nevoie de cel puțin o colonie. [Too many colonies for this map; use a bigger map or fewer colonies.] Prea multe colonii pentru această hartă; folosește o hartă mai mare sau mai puține colonii. +[Coral] +Coral +[Fork angle] +Unghi de ramificare +[Lean] +Înclinare +[Home size] +Mărimea bazei +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Coralul nu încape pe această hartă; folosiți o hartă mai mare, strâmtori mai înguste sau mai puține colonii. +[Branching] +Ramificare +[Branch width] +Lățimea ramurilor +[Coral needs at least one colony.] +Coralul are nevoie de cel puțin o colonie. +[Coral needs a map at least 128 tiles across.] +Coralul are nevoie de o hartă lată de cel puțin 128 de pătrate. [Spider web] Pânză de păianjen [Spokes per colony] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index f40916b5f..24c91f186 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1914,6 +1914,24 @@ OpenGL недоступен в этой сборке. Болотам нужна хотя бы одна колония. [Too many colonies for this map; use a bigger map or fewer colonies.] Слишком много колоний для этой карты; используйте карту побольше или меньше колоний. +[Coral] +Коралл +[Fork angle] +Угол ветвления +[Lean] +Наклон +[Home size] +Размер базы +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Коралл не помещается на эту карту; возьмите карту побольше, проливы поуже или меньше колоний. +[Branching] +Ветвление +[Branch width] +Ширина ветвей +[Coral needs at least one colony.] +Для коралла нужна хотя бы одна колония. +[Coral needs a map at least 128 tiles across.] +Для коралла нужна карта шириной не менее 128 клеток. [Spider web] Паутина [Spokes per colony] diff --git a/data/texts.si.txt b/data/texts.si.txt index 0e3425c70..339a7a358 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1916,6 +1916,24 @@ Mrtvice Močvirja potrebujejo vsaj eno kolonijo. [Too many colonies for this map; use a bigger map or fewer colonies.] Preveč kolonij za ta zemljevid; uporabite večji zemljevid ali manj kolonij. +[Coral] +Korala +[Fork angle] +Kot razvejanja +[Lean] +Nagib +[Home size] +Velikost doma +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Korala ne gre na ta zemljevid; uporabite večji zemljevid, ožje ožine ali manj kolonij. +[Branching] +Razvejanost +[Branch width] +Širina vej +[Coral needs at least one colony.] +Korala potrebuje vsaj eno kolonijo. +[Coral needs a map at least 128 tiles across.] +Korala potrebuje zemljevid, širok vsaj 128 polj. [Spider web] Pajčevina [Spokes per colony] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index c0d8ef848..1bb7fc190 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1924,6 +1924,24 @@ Mŕtve ramená Močiare potrebujú aspoň jednu kolóniu. [Too many colonies for this map; use a bigger map or fewer colonies.] Príliš veľa kolónií pre túto mapu; použite väčšiu mapu alebo menej kolónií. +[Coral] +Koral +[Fork angle] +Uhol vetvenia +[Lean] +Náklon +[Home size] +Veľkosť domova +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Koral sa na túto mapu nezmestí; použite väčšiu mapu, užšie prielivy alebo menej kolónií. +[Branching] +Vetvenie +[Branch width] +Šírka vetiev +[Coral needs at least one colony.] +Koral potrebuje aspoň jednu kolóniu. +[Coral needs a map at least 128 tiles across.] +Koral potrebuje mapu širokú aspoň 128 polí. [Spider web] Pavučina [Spokes per colony] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index edbd40e1d..ef651d0aa 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1916,6 +1916,24 @@ OpenGL није доступан у овој верзији. Мочварама треба бар једна колонија. [Too many colonies for this map; use a bigger map or fewer colonies.] Превише колонија за ову мапу; користите већу мапу или мање колонија. +[Coral] +Корал +[Fork angle] +Угао гранања +[Lean] +Нагиб +[Home size] +Величина базе +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Корал не стаје на ову мапу; користите већу мапу, уже мореузе или мање колонија. +[Branching] +Гранање +[Branch width] +Ширина грана +[Coral needs at least one colony.] +Коралу је потребна бар једна колонија. +[Coral needs a map at least 128 tiles across.] +Коралу је потребна мапа широка бар 128 поља. [Spider web] Паукова мрежа [Spokes per colony] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index c32728463..e5cb57614 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1926,6 +1926,24 @@ Sumpkanaler Sumpmarker kräver minst en koloni. [Too many colonies for this map; use a bigger map or fewer colonies.] För många kolonier för den här kartan; använd en större karta eller färre kolonier. +[Coral] +Korall +[Fork angle] +Förgreningsvinkel +[Lean] +Lutning +[Home size] +Hemmets storlek +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Korallen får inte plats på den här kartan; använd en större karta, smalare sund eller färre kolonier. +[Branching] +Förgrening +[Branch width] +Grenbredd +[Coral needs at least one colony.] +Korall behöver minst en koloni. +[Coral needs a map at least 128 tiles across.] +Korall behöver en karta som är minst 128 rutor bred. [Spider web] Spindelnät [Spokes per colony] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 95c5b37c7..cd2ddfa28 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1924,6 +1924,24 @@ Bataklıklar Bataklıklar için en az bir koloni gerekir. [Too many colonies for this map; use a bigger map or fewer colonies.] Bu harita için çok fazla koloni var; daha büyük bir harita kullanın veya koloni sayısını azaltın. +[Coral] +Mercan +[Fork angle] +Dallanma açısı +[Lean] +Eğim +[Home size] +Üs boyutu +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Mercan bu haritaya sığmıyor; daha büyük bir harita, daha dar boğazlar ya da daha az koloni kullanın. +[Branching] +Dallanma +[Branch width] +Dal genişliği +[Coral needs at least one colony.] +Mercan en az bir koloni gerektirir. +[Coral needs a map at least 128 tiles across.] +Mercan en az 128 kare genişliğinde bir harita gerektirir. [Spider web] Örümcek ağı [Spokes per colony] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 3866655fe..b5735b56b 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1912,6 +1912,24 @@ OpenGL недоступний у цій збірці. Болотам потрібна хоча б одна колонія. [Too many colonies for this map; use a bigger map or fewer colonies.] Забагато колоній для цієї мапи; використайте більшу мапу або менше колоній. +[Coral] +Корал +[Fork angle] +Кут галуження +[Lean] +Нахил +[Home size] +Розмір бази +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +Корал не вміщується на цю мапу; візьміть більшу мапу, вужчі протоки або менше колоній. +[Branching] +Галуження +[Branch width] +Ширина гілок +[Coral needs at least one colony.] +Для корала потрібна принаймні одна колонія. +[Coral needs a map at least 128 tiles across.] +Для корала потрібна мапа завширшки щонайменше 128 клітинок. [Spider web] Павутина [Spokes per colony] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index e7fffd113..dd6de9ff3 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1912,6 +1912,24 @@ Kích thước khoảng trống Đầm lầy cần ít nhất một thuộc địa. [Too many colonies for this map; use a bigger map or fewer colonies.] Quá nhiều thuộc địa cho bản đồ này; hãy dùng bản đồ lớn hơn hoặc ít thuộc địa hơn. +[Coral] +San hô +[Fork angle] +Góc phân nhánh +[Lean] +Độ nghiêng +[Home size] +Kích thước căn cứ +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +San hô không vừa bản đồ này; hãy dùng bản đồ lớn hơn, eo biển hẹp hơn hoặc ít thuộc địa hơn. +[Branching] +Phân nhánh +[Branch width] +Độ rộng nhánh +[Coral needs at least one colony.] +San hô cần ít nhất một thuộc địa. +[Coral needs a map at least 128 tiles across.] +San hô cần bản đồ rộng ít nhất 128 ô. [Spider web] Mạng nhện [Spokes per colony] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index c3e471525..50bb216cb 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1912,6 +1912,24 @@ OpenGL 在此版本中不可用。 沼泽地至少需要一个殖民地。 [Too many colonies for this map; use a bigger map or fewer colonies.] 此地图的殖民地太多;请使用更大的地图或减少殖民地。 +[Coral] +珊瑚 +[Fork angle] +分枝角度 +[Lean] +倾斜 +[Home size] +基地大小 +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +珊瑚无法放入此地图;请使用更大的地图、更窄的海峡或更少的殖民地。 +[Branching] +分枝 +[Branch width] +枝宽 +[Coral needs at least one colony.] +珊瑚至少需要一个殖民地。 +[Coral needs a map at least 128 tiles across.] +珊瑚需要宽度至少为 128 格的地图。 [Spider web] 蜘蛛网 [Spokes per colony] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index bc5f2dda2..6de79c70f 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1988,6 +1988,24 @@ OpenGL 在此版本中不可用。 沼澤地至少需要一個殖民地。 [Too many colonies for this map; use a bigger map or fewer colonies.] 此地圖的殖民地太多;請使用更大的地圖或減少殖民地。 +[Coral] +珊瑚 +[Fork angle] +分枝角度 +[Lean] +傾斜 +[Home size] +基地大小 +[The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] +珊瑚無法放入此地圖;請使用更大的地圖、更窄的海峽或更少的殖民地。 +[Branching] +分枝 +[Branch width] +枝寬 +[Coral needs at least one colony.] +珊瑚至少需要一個殖民地。 +[Coral needs a map at least 128 tiles across.] +珊瑚需要寬度至少為 128 格的地圖。 [Spider web] 蜘蛛網 [Spokes per colony] diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index a28837df9..eab05d0d7 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -6,7 +6,7 @@ A generator is a registered module, not a new branch in a central dispatch switc - **`core/`** owns requests, control metadata, registration, seed streams, validation and results. It does not implement terrain recipes. - **`generators/`** contains one header/source pair per generator. The header defines its named options; the source owns its controls, its design (the layout as a pure function of the request) and the order it calls the shared stages in. A generator source holds only what is particular to its map. -- **`shared/`** is the toolkit every generator is built from: the torus grid and its floods (`Grid`), shapes (`Geometry`, `Wedge`), drawing strokes and shapes (`Drawing`), the terrain sketch (`Sketch`), noise (`LatticeNoise`, `HeightMap`), deposits (`Planting`, `Resources`), routes (`Roads`), colonies (`Settlements`, `BalancedStarts`), the pipeline stages (`Pipeline`), the height-field stages (`Terrain`) and the start scoring (`StartQuality`). These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) lists what each gives you. +- **`shared/`** is the toolkit every generator is built from: the torus grid and its floods (`Grid`), shapes (`Geometry`, `Wedge`), drawing strokes, shapes and grown branches (`Drawing`), the terrain sketch (`Sketch`), noise (`LatticeNoise`, `HeightMap`), deposits (`Planting`, `Resources`), routes (`Roads`), colonies (`Settlements`, `BalancedStarts`), the pipeline stages (`Pipeline`), the height-field stages (`Terrain`) and the start scoring (`StartQuality`). These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) lists what each gives you. - **`shared/legacy/`** is the older area-grid toolkit (`Regions`, `Distances`, `StartingPositions`) the point-dispersion and height-field generators still build on. It does not compose with the tile masks the rest of `shared/` uses; do not start a new generator on it. - **`compatibility/`** owns the historical descriptor codec and overloaded-field conversion. New generators do not add fields to that descriptor. `MapGenerator.h` is only a small compatibility facade for older callers. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 0b98c7313..eb3743e28 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -21,13 +21,13 @@ and Terrain take a `Game` because placing buildings and units needs its mutation |---|---| | `Grid` | `Torus` wrap and offset arithmetic; `floodFrom`/`stepsFrom`, the one eight-connected breadth-first flood (with a step limit and the visit order for callers that need it); `walkableTiles` and `groundUnitTiles` passability masks; `unitTilesByTeam` and `firstColonyCutOff` | | `Topology` | `connectedRegions` component labelling with explicit wrap and neighbour policy; `regionAdjacency` and `graphDistances` over sparse labels | -| `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid | -| `Drawing` | Drawing on the torus: `strokePath`, a thick path of points each with its own half width, rasterized through the wrap; `bezierPath`, a quadratic curve as such a path; `polarPoint`; `forEachTileInShape` and `fillShape`, a `RadialShape` filled at any centre and turned to any heading within its bounding box | -| `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; `Blob`, a stretched, turned rough disc in that frame | -| `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `writeUndermap` | +| `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `Stretch`, which places a layout designed in a circle on a map's shorter side onto a rectangular map as an ellipse touching all four sides (`toFill`, `apply`, `undo`, `heading`), exactly the identity on a square map; `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid | +| `Drawing` | Drawing on the torus: `strokePath`, a thick path of points each with its own half width, rasterized through the wrap; `tracePath`, a path traced one tile thick with no gaps (a sand road); `bezierPath`, a quadratic curve as such a path; `polarPoint`; `forEachTileInShape` and `fillShape`, a `RadialShape` filled at any centre and turned to any heading within its bounding box, optionally stretched into an oval; `stretchPath`, a designed path placed on the map by a `Stretch` with its half widths kept in tiles; `bentPath`, a tapered path leaving a point along a heading and bowing sideways; `pathClearance` and `pathBounds`, the water between two stroked paths (optionally ignoring a branch's root) and a cheap bounding circle; `growBranches`, a tree grown level by level by forking in two at every tip to a `ForkStyle`, every branch offered to a caller's `accept` before it is kept and a refused one retried once at half length | +| `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; given a `Stretch` it measures every cell in the round design frame, so the same design fills a rectangular map; `Blob`, a stretched, turned rough disc in that frame | +| `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `sprinkleSand`, decorative sand patches on inland grass following a caller's noise, kept a strip of grass away from every beach; `writeUndermap` | | `LatticeNoise` | `PeriodicNoise`, value noise that tiles the torus exactly and samples anywhere, with `periodicNoise`/`fractalNoise` (integer fields per tile, octaves) and `torusNoise` (four octaves in [-1, 1]) sampled from it, plus `percentile` | | `HeightMap`, `Noise` | Perlin noise faded across the wrap, and the stamped height fields the height-field generators shape | -| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region) | +| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band; optionally only its best-growing share, shared out equally between the colonies' wedges), `algaeGrowthChance` (each water tile's chance of passing the engine's algae growth test, which needs water within 15 tiles and solid sand within 30 at a reflected offset), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region) | | `Resources` | The ambient layer and its fairness guards: `scaledCount`/`scaledShare`, `placeResourceClump`, `setScaledResource`, `scatterResources`, `guaranteeStartingResources`, `openCrampedStarts` | | `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared; `cheapestWalk`, the same search by any step cost (Everglades' fords, Symmetric arena's causeway routes) | | `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics | @@ -45,8 +45,8 @@ against the finished terrain. ## The designed generator -Ten generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, -City states, Tidal flats, Everglades, Spider web) follow one shape, and the five newest of them +Eleven generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, +City states, Tidal flats, Everglades, Spider web, Coral) follow one shape, and the six newest of them are little more than a sequence of shared stages: 1. `design(request, context)` computes the whole layout from the request and the context's @@ -90,6 +90,7 @@ reused after a generator is retired. | `tidal-flats` | 18 | Tidal flats | Its own — see below | | `everglades` | 19 | Everglades | Its own — see below | | `spider-web` | 20 | Spider web | Its own — see below | +| `coral` | 21 | Coral | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -136,7 +137,8 @@ ambient layer but still leaves every colony a start. Maze keeps its explicit den | City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard) and the sea's algae and island prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | | Tidal flats | Every island's ambient fields and outcrops and every island's prize; each home's kit is unscaled | Central island (on): off, the middle of the map is flats and there is no orchard | | Everglades | The swamp's standing wood and wheat, its outcrops and groves, and the pools' algae; every home's kit is unscaled | None | -| Spider web | The threads' standing wheat and wood, the share of knots carrying stone, whether the dew drops and the hub carry fruit and stone, and the shallows' algae; every pad's kit is unscaled | Spiral (on): off, the capture threads are closed rings | +| Spider web | The threads' standing wheat and wood, the share of knots carrying stone, whether the dew drops and the hub carry fruit and stone, and the shallows' algae; every pad's kit is unscaled | Spiral (on): off, the capture threads are closed rings; Sand roads (on): off, the threads are grass from shore to shore | +| Coral | The branches' standing wheat and wood, the share of forks carrying stone, the tips' fruit groves and the shallows' algae; every pad's kit is unscaled | Sand roads (on): off, the branches are grass from shore to shore | `scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of @@ -209,7 +211,7 @@ fixed resource pass: treated like terrain, never cleared and never looked past. It wraps each boot tile onto the map first, since the height-field generators' fallback site search can hand over one past the edge. RuggedArchipelago, ShatteredCoast, Fjord, Watershed, Stone highlands, Ring world, City states, - Tidal flats, Everglades and Spider web call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount + Tidal flats, Everglades, Spider web and Coral call this, as do the height-field generators, and Concrete islands and Isles at any wheat or wood amount other than 100. Maze doesn't need it: its deposits are placed only along passage shores, leaving a clear lane down every passage, and its `validateWorld` confirms every colony can still walk to every @@ -284,7 +286,9 @@ The richest generator, and the one most of this framework's resource work was pr (`coreR`, currently 0.23x the continent radius) that every fjord stops short of, so it always stays connected land regardless of coastline roughness. A fjord is carved between every pair of angularly-neighboring teams as a smooth S-curve centerline from just outside the coast in to - `coreR` (or, in lake-connected mode, well inside the lake — see below). + `coreR` (or, in lake-connected mode, well inside the lake — see below). On a rectangular map + the continent is stretched along the longer side by `Stretch` on top of its own transform, so it + fills the map as an oval; square maps are unchanged. - **Central lake.** `lake-size` (0–90%, of `coreR`; 0 disables it) carves a lake at the exact center, ringed by a sandy no-man's-land wider than `Map::controlSand()`'s own coastal fringe (`sandy-lake-shore`, on by default; off, the lake has an ordinary beach). @@ -486,6 +490,11 @@ The commons is where the game is fought, richer towards its centre, where an orc three fruits stands round the central lake; once swimming pools let armies cross water anywhere, the causeways stop being the only way in. +City states stays round on a rectangular map, in a circle on the shorter side: stretching it gives +the homes different shapes (deep and narrow along the long axis, wide and shallow across it), and +fairness falls from about 0.97 to 0.65–0.8, because a home's fields and fertility depend on its +shape. + - **Geometry.** A pure function of the request (`geometryFor`). The commons' radius is `commons-size` percent of half the shorter side, the strait `strait-width` percent of the shorter side, and the homes reach out to the map's half side less a rim of sea, so opposite homes never @@ -557,6 +566,9 @@ on its island's edge, and expansion means taking another island whole. - **Checked, not assumed.** `validateWorld` rebuilds the design and requires every home's pond present and every colony and the central island reachable on foot from colony 0, with water, buildings and every resource blocking. +- **Rectangular maps.** The layout is designed in a circle on the map's shorter side and placed on + the map through a stretched `WedgeFrame`, so on a rectangular map it fills the map as an ellipse, + its islands, pools and lagoons stretched with it. Square maps are unchanged. ## Everglades @@ -617,7 +629,7 @@ orchard is the prize, and the water between is crossed only once colonies can sw each bowing sideways by a random amount that is greatest at mid-length. The frame joins the spoke ends. Capture threads are drawn with `bezierPath` and `strokePath`, `ring-spacing` tiles apart (squeezed on a small map to keep a whole turn between the hub and the frame) and - `thread-width` wide, spokes and frame two tiles wider. Each sags towards the hub by `sag` and a + `thread-width` wide (the width on a 256-tile map; a smaller map thins its threads with the square root of its size, to no less than 75%, so a 128-tile web still reads as threads), spokes and frame two tiles wider. Each sags towards the hub by `sag` and a random share of it; `torn-strands` percent of the distinct strands of a wedge tear, keeping a stub hanging from each spoke. With `spiral` (on) the capture threads are a spiral with one arm per colony that climbs one spacing per wedge; off, they are closed rings. @@ -626,20 +638,94 @@ orchard is the prize, and the water between is crossed only once colonies can sw thread — torn, sag, bow, stone at its knot — is a stateless roll of that key from the seed, and dew drops are chosen in one wedge and turned round the centre into every other, so every colony gets the same web. -- **Pads and hub.** Pads sit as far out as the wrap allows and shrink (from 13 tiles of radius - down to 7) until neighbouring pads keep six tiles of sea between them; only then is the request - refused. The hub is `hub-size` percent of the half side, with a pond once it is nine tiles across. +- **Pads and hub.** Pads sit as far out as the wrap allows, `home-size` percent (60–200, 130 by + default) of a standard pad of 13 tiles of radius, never more than 16% of the half side times that + percentage so a small map's pads leave room for the web, and shrink down to 7 until neighbouring + pads keep six tiles of sea between them; only then is the request refused. The hub is `hub-size` percent of the half side, with a pond once it is nine tiles across. - **Resources.** Each pad has an unscaled kit of 40 wheat and 30 wood on the hub side of the swarm and a quarry beyond it. About 28% of knots carry a small stone deposit, `dew-drops` islets per colony carry a fruit grove or stone each, and the hub carries the orchard of all three fruits and a quarry. The threads' own wheat and wood are ranked and split by `PeriodicNoise` sampled in - the wedge frame, so every colony's stretch of web is farmed alike. Algae seeds the shallows. + the wedge frame, so every colony's stretch of web is farmed alike: 20% of their grass under wheat + and 13% under wood, so any stretch of thread passes a field or a copse. A few dry sand patches + (7% of the threads' inland grass, three or more tiles from water) decorate the threads, sampled in + the wedge frame too. Algae is counted over the shallows up to ten tiles out, one clump per 30 + tiles, and seeded on each colony's best-growing 30% of that water, the same number of clumps in + every wedge. - **Routes opened, not hoped for.** Beaches keep the threads walkable, but a small hub can be stocked shut, so after `secureStartingCrops` an `openRoad` from colony 0 onto the hub and to every other colony clears any deposits in the way. - **Checked, not assumed.** `validateWorld` rebuilds the design and requires every spoke's centre line to be land, every colony reachable on foot from colony 0 and the hub reachable too, with water, buildings and every resource blocking. +- **Sand roads.** With `sand-roads` (on), a line of sand one tile thick (`tracePath`) runs down the + middle of every spoke and whole thread, off the pads and dew drops; torn stubs, which are dead + ends, get none. Every tile touching the sand loses the pure grass a deposit or a building needs, + so nothing can grow or be built across a thread and close it. Threads thin on small maps to no + less than 75% of the control, so grass remains either side of the road. +- **Rectangular maps.** The web is designed in a circle on the map's shorter side and placed on the + map by `Stretch`, so on a rectangular map it fills the map as an ellipse; widths stay in tiles. + Square maps are unchanged. + +## Coral + +Every colony is a sea fan of land. A single trunk leaves the colony's pad on the rim and forks, and +forks again, as it grows in towards the middle, so each colony's coral is a triangle opening +inwards: narrow and solid at home, wide and fingery at the far end. The water between sibling +branches is a triangle opening inwards too, the far ends of neighbouring fans reach into each +other's gaps, and all the fans meet in a tangle in the middle. Land gets thinner and richer the +further it is from home. + +- **Grown, not drawn.** The trunk leaves the pad heading for the map centre, turned off it by `lean` + degrees (every colony alike, so the map turns like a pinwheel); everything else is `growBranches`, + which splits every tip in two, each child turned `fork-angle` degrees (with jitter) from its + parent (the angle on a 256-tile map; smaller maps fork wider and bigger maps narrower with the + square root of the long side, between 0.7 and 1.45 times, since a small fan needs wide forks to + fill its wedge and a big fan's many levels curl into rings at a wide angle), a level at a time so both sides of every fork compete equally. `branching` sets the + levels on a 256-tile map, one more per doubling of the map and one fewer per halving; the trunk is + sized so the lengths of every level reach just past the centre, and where that trunk would be + longer than 30 tiles each level keeps more of its parent's length (up to all of it) instead, so + the fan fills out rather than hanging off a long bare trunk. On a small map levels are given up + until the trunk clears its pad, so on 128-tile maps branching above 4 changes nothing. + `branch-width` is the trunk's width; branches taper to about eight tiles. +- **Checked against every colony.** The fan is grown once, for colony 0, and turned round the map + centre for every other colony, so it is fair however random the growth. Every branch and bud must + keep `strait-width` tiles of water from all land already grown, from that land's copies in every + other wedge and from its own copies; a refused branch is retried at half length, then dropped + with its subtree. This refusal is what makes neighbouring tips interleave, and it keeps the land + from ever reading as spokes and rings. +- **Bridges.** `land-bridges` per pair of neighbours join the narrowest straits between a fan and + its neighbour's copy, 30 tiles apart, preferring straits away from the rim (where a bridge would + join two trunks just below their pads); the search widens until every boundary has a bridge. + With none, colonies meet only by swimming. +- **Pads.** Pads sit as far out as the wrap allows, `home-size` percent (60–200, 130 by default) of + a standard pad of 12 tiles of radius (less on a small map, at most 15% of the half side), and + shrink down to 7 until neighbouring pads keep six tiles of sea. A bigger pad is more room for the + first economy; it also brings the trunk's root in, so at large sizes the fan has less room. Maps + under 128 tiles across are refused, as are layouts whose trunk cannot clear its pad. +- **Resources.** Each pad has an unscaled kit of 40 wheat and 30 wood in front of the swarm and a + quarry behind it. Remoteness is the walk along the coral from the nearest pad: 40% of forks + carry stone growing from 3 to 9 tiles with it, and every tip a fruit grove from 3 to 12 tiles. The + branches' wheat and wood are ranked by `PeriodicNoise` sampled in the wedge frame, leaning + towards home: 30% of their grass under wheat and 21% under wood, since the branches are all the + land there is. A few dry sand patches (8% of the branches' inland grass, three or more tiles from + water, never on a pad, bud or bridge) decorate the wider branches. Algae is counted over the + shallows up to ten tiles out, one clump per 35 tiles, and seeded on each colony's best-growing 30% + of that water, the same number of clumps in every wedge. +- **Routes opened and checked.** After `secureStartingCrops`, an `openRoad` from every colony to + its trunk's first fork, and with bridges from colony 0 to every colony, clears any deposits in + the way. `validateWorld` rebuilds the design and requires every trunk's centre line to be land, + every colony able to walk to its first fork and, with bridges, every colony reachable on foot + from colony 0. +- **Sand roads.** With `sand-roads` (on), a line of sand one tile thick (`tracePath`) runs down the + middle of every branch that forks on and every bridge, off the pads and buds; dead-end tips get + none, since they carry no traffic and their narrow land would be left without fields. Every tile + touching the sand loses the pure grass a deposit or a building needs, so nothing can grow or be + built across the road. Branches never taper below about eight tiles (`branch-width` 7–17, 11 by + default, is the trunk's), so grass remains either side of the road. +- **Rectangular maps.** The fan is designed in a circle on the map's shorter side and placed on the + map by `Stretch`, so on a rectangular map it fills the map as an ellipse; widths stay in tiles. + Square maps are unchanged. ## Compatibility notes diff --git a/src/SConscript b/src/SConscript index a5389ad87..f8cf23f8f 100644 --- a/src/SConscript +++ b/src/SConscript @@ -194,6 +194,7 @@ map/generator/generators/RuggedArchipelagoGenerator.cpp map/generator/generators/ShatteredCoastGenerator.cpp map/generator/generators/StoneHighlandsGenerator.cpp map/generator/generators/SpiderWebGenerator.cpp +map/generator/generators/CoralGenerator.cpp map/generator/generators/SwampGenerator.cpp map/generator/generators/SymmetricArenaGenerator.cpp map/generator/generators/TidalFlatsGenerator.cpp diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 90f6cd958..fa1cf6f8f 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -3,6 +3,7 @@ #include "CityStatesGenerator.h" #include "ConcreteIslandsGenerator.h" #include "ContestedCommonsGenerator.h" +#include "CoralGenerator.h" #include "CraterLakesGenerator.h" #include "EvergladesGenerator.h" #include "FjordContinentGenerator.h" @@ -95,7 +96,8 @@ const GeneratorRegistry &GeneratorRegistry::builtins() shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), stoneHighlandsDefinition(), symmetricArenaDefinition(), ringWorldDefinition(), cityStatesDefinition(), tidalFlatsDefinition(), evergladesDefinition(), spiderWebDefinition(), - ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), - islandsDefinition(), swampDefinition(), riverDefinition(), uniformDefinition()}); + coralDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), + craterLakesDefinition(), islandsDefinition(), swampDefinition(), riverDefinition(), + uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/CoralGenerator.cpp b/src/map/generator/generators/CoralGenerator.cpp new file mode 100644 index 000000000..fd694218f --- /dev/null +++ b/src/map/generator/generators/CoralGenerator.cpp @@ -0,0 +1,927 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CoralGenerator.h" +#include "Drawing.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "LatticeNoise.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Wedge.h" +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Coral: every colony is a sea fan of land. A single trunk leaves the colony's pad on the rim of +// the map and forks, and forks again, as it grows in towards the middle, so each colony's coral is +// a triangle that opens out inwards: narrow and solid at home, wide and fingery at the far end. +// +// THE PLAY IDEA. Home is the apex of the fan, where there is one trunk to walk and one front to +// hold. Every fork doubles the ground to cover and halves the width of it, so pushing outward means +// long single-file supply lines onto ever thinner branches. The water between two sibling branches +// is a triangle opening inwards too, and the far ends of neighbouring fans reach into each other's +// triangles: the front between two colonies is a zigzag of fingertips, each a short swim from enemy +// land and a long walk from either home, and all the fans meet in a tangle in the middle of the +// map. The land is richer the further it is from home - stone at the forks, fruit on the buds at +// the tips - so the prizes sit exactly where the land is hardest to hold. A few thin land bridges +// join neighbouring fans so ground armies can meet; with none, colonies meet only once they can +// swim. +// +// WHY A FORKING TREE AND NOTHING RULED. Running trunks straight at the centre and bending side +// branches round it in arcs would make neat interlocking easy, but it draws a spider web: spokes, +// concentric threads and a hub (which Spider web already is). Here nothing follows the centre: the +// trunk leans off the line to the middle (every colony by the same amount, so the whole map turns +// like a pinwheel rather than forming a star), every branch splits in two at its tip at its own +// random angle and length, and the shape of a colony is whatever that growth makes of its room. +// +// WHY GROWTH IS CHECKED AGAINST THE NEIGHBOURS. The tree is grown once, for colony 0, and turned +// round the map centre for every other colony, so it is fair however random the growth. That only +// works if a branch accepted for colony 0 still fits once every copy is drawn, so every new branch +// must keep a strait of water (the strait width) from all land already grown, from that land's +// copies in every other colony's wedge, and from its own copies. The fans therefore grow until they +// meet a strait short of each other, and it is this refusal, not any layout rule, that makes the +// tips of two neighbours interleave: a branch heading into the neighbour's copy is stopped, a +// branch heading into the gap between two of the neighbour's branches keeps going. +// +// WHY LEVEL BY LEVEL. growBranches grows every branch of one level before the next. Grown depth +// first, the first fork's whole subtree would claim the room before its sibling had grown at all, +// and every fan would come out lopsided; grown a level at a time, the two sides of every fork +// compete on equal terms and the fan opens out evenly. +// +// WHY REFUSED RATHER THAN CLIPPED. A branch that would come within a strait of other land is +// offered once more at half length, then dropped with everything that would have grown from it. +// Clipping strokes would leave blunt stumps and slivers of water narrower than the strait; refusing +// whole branches keeps every gap at least a strait wide, which is what makes swimming the +// deliberate way across and keeps the land readable as coral. +// +// The design is a pure function of the request, so validateWorld rebuilds it and checks the world. +namespace +{ + +// A colony's pad, and how far it may shrink on a crowded rim before the request is refused; never +// more than kPadShare of the half side, so a small map's pads leave the fan room to grow. Its +// outline wobbles by up to kPadRoughness of its radius. Sea kept across the wrap and between pads. +constexpr int kPadRadius = 12; +constexpr int kMinimumPad = 7; +constexpr double kPadShare = 0.15; +constexpr double kPadRoughness = 0.2; +constexpr double kWrapGap = 6; +constexpr double kPadGap = 6; +// The trunk is this much wider (in half width) than the branch width control, so home reads as the +// solid base of the fan with room for the first buildings. +constexpr double kTrunkExtra = 1.5; +// Branches never taper below a half width of 4: about eight tiles across, which after its beaches +// and the sand road down its middle still keeps grass either side of the road, so the thinnest +// branches are worth holding rather than sandbars. +constexpr double kMinimumHalfWidth = 4.0; +// How each fork's children compare with their parent. Length shrinks slowly so the far levels are +// still long enough to reach the middle; width shrinks so the fan visibly thins as it goes. +constexpr double kLengthRatio = 0.8; +constexpr double kLengthJitter = 0.2; +constexpr double kWidthRatio = 0.85; +constexpr double kSpreadJitter = 0.35; +constexpr double kBend = 0.12; +// The lengths of every level add up to this share of the distance from the pad to the map centre: a +// little over, so the far tips reach the middle and have to compete there rather than stopping +// short in open water. The trunk is kept to kMaximumTrunk where it can be by letting each level keep +// more of its parent's length, up to all of it, so the fan fills out rather than hanging off a long +// bare trunk. +constexpr double kReachShare = 1.15; +constexpr double kMaximumTrunk = 30; +constexpr double kMaximumLengthRatio = 1.0; +// The branching control counts levels on a 256-tile map; each doubling of the map adds one, so a fan +// forks about as densely, tile for tile, whatever the map size. +constexpr int kReferenceHalf = 128; +// The fork angle control is the angle on a 256-tile map. Smaller maps open their forks wider and +// bigger maps narrower, with the square root of the size, within these shares: a small fan has +// few levels, and only wide forks spread them into a fan that fills its wedge, while a big fan's +// many levels at a wide angle curl back on themselves into rings. +constexpr double kMinimumSpreadScale = 0.7, kMaximumSpreadScale = 1.45; +// Every home starts identical: wheat and wood beside the swarm, a quarry behind it, all unscaled. +constexpr int kHomeWheat = 40; +constexpr int kHomeWood = 30; +// The branches' standing wheat and wood, as shares of their open grass at 100: enough that walking +// any branch passes a field or a copse, since the branches are all the land there is. Fields lean +// towards home by kHomeLean against the patch noise, so the trunk and first forks are the +// breadbasket. +constexpr int kWheatShare = 30; +constexpr int kWoodShare = 21; +// Dry patches of sand inside the branches, as a share of their inland grass, at least this many +// steps from water so a strip of grass always lies between patch and beach; and the size of the +// noise the patches follow, in tiles. +constexpr double kSandShare = 0.08; +constexpr int kSandInland = 3; +constexpr double kSandCell = 10; +// One algae clump per this many tiles of shallows at 100, all on the share of each colony's +// shallows where algae regrows most readily. +constexpr int kAlgaeTilesPerClump = 35; +constexpr double kAlgaeBestShare = 0.3; +constexpr double kHomeLean = 0.6; +// Forks carrying stone, in tenths of a percent at 100, and a deposit's size from the home end of +// the fan to the far end: remoteness pays. +constexpr int kForkStone = 400; +constexpr int kForkStoneNear = 3, kForkStoneFar = 9; +// A bud's fruit grove, in tiles, from the home end of the fan to the far end. +constexpr int kBudFruitNear = 3, kBudFruitFar = 12; +// Bridges are looked for first among straits at most this wide and away from the rim, and are kept +// this far apart. +constexpr double kBridgeSearch = 30; +constexpr double kBridgeApart = 30; + +struct Geometry +{ + int teams, half; + int wedges; // wedges the design is turned through: one per colony, at least two + double wedge; // radians per wedge + double trunkHalf, strait; + double padRadius, padReach, rootRadius; // the pad centres' distance from the map centre + double reach; // no land beyond this radius, clear of the wrap + double lean, spread; // radians + double trunkLength, lengthRatio; + int levels; // forks from the trunk to the tips + bool padsFit; // the pads fit round the rim + bool trunkFits; // and there is room inside them for a fan +}; + +Geometry geometryFor(const GenerationRequest &r) +{ + const CoralOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + // One colony is still designed as if it had a neighbour: its fan grows in half the map, with + // the other half open sea to grow towards. + g.wedges = std::max(2, g.teams); + g.wedge = 2 * kPi / g.wedges; + g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; + g.trunkHalf = o.branchWidth / 2.0 + kTrunkExtra; + g.strait = o.straitWidth; + g.reach = g.half - std::max(kWrapGap, g.strait) / 2; + g.padsFit = false; + // The home size control scales the pad, 100 being kPadRadius (and kPadShare on a small map): a + // bigger pad is more room to build the first economy before the colony has to push out along + // its trunk. It is a size to aim for, not a promise; a crowded rim shrinks it as ever. + const double homeScale = o.homeSize / 100.0; + const int largestPad = std::max( + kMinimumPad, + int(std::lround(std::min(kPadRadius * homeScale, kPadShare * homeScale * g.half)))); + for (int pad = largestPad; pad >= kMinimumPad && !g.padsFit; --pad) + { + g.padRadius = pad; + g.padReach = pad * (1 + kPadRoughness); + g.rootRadius = g.reach - g.padReach; + g.padsFit = + g.teams < 2 || 2 * g.rootRadius * std::sin(kPi / g.teams) >= 2 * g.padReach + kPadGap; + } + g.lean = o.lean * kPi / 180; + const double sizeScale = + std::clamp(std::sqrt(double(kReferenceHalf) / (std::max(1 << r.wDec, 1 << r.hDec) / 2)), + kMinimumSpreadScale, kMaximumSpreadScale); + g.spread = o.forkAngle * sizeScale * kPi / 180; + // The lengths of the trunk and every level below it form a geometric series; its sum, shortened + // by how far the branches turn off the line in, should reach just past the centre. With + // kLengthRatio the trunk is whatever length does that; where that is longer than kMaximumTrunk, + // each level keeps more of its parent's length until the trunk is short enough or every level is + // as long as the trunk. The number of levels is the control's, adjusted for map size only, so + // branching always changes the map. + const double target = kReachShare * g.rootRadius; + const auto series = [&](int levels, double ratio) + { + double sum = 0, scale = 1; + for (int level = 0; level <= levels; ++level, scale *= ratio) + sum += scale * std::cos(std::min(kPi / 3, level * g.spread / 2)); + return sum; + }; + // Levels follow the longer side: on a rectangular map the fan is stretched along it, so it needs + // the forks of the longer side to stay as dense there as on a square map of that size. + const int longHalf = std::max(1 << r.wDec, 1 << r.hDec) / 2; + int sizeLevels = 0; + for (int half = longHalf; half > kReferenceHalf; half /= 2) + ++sizeLevels; + for (int half = longHalf; half < kReferenceHalf; half *= 2) + --sizeLevels; + g.levels = std::max(1, o.branching + sizeLevels); + g.lengthRatio = kLengthRatio; + while (g.lengthRatio < kMaximumLengthRatio && + target / series(g.levels, g.lengthRatio) > kMaximumTrunk) + g.lengthRatio = std::min(kMaximumLengthRatio, g.lengthRatio + 0.01); + g.trunkLength = target / series(g.levels, g.lengthRatio); + // A trunk must clear its pad by a strait before it forks, or its first fork is refused against + // the pad and nothing grows. On a small map with many levels the series makes the trunk shorter + // than that, so levels are given up until it clears; only a map where even a single fork cannot + // clear is refused. + const double clearsPad = g.padReach + g.strait + kMinimumHalfWidth; + while (g.trunkLength < clearsPad && g.levels > 1) + g.trunkLength = target / series(--g.levels, g.lengthRatio); + g.trunkFits = g.trunkLength >= clearsPad; + return g; +} + +// What a piece of the design is. +enum class Kind +{ + Branch, // a branch of the fan: the trunk is branch 0 + Bud, // a disc of land at a branch's tip, for a fruit grove + Bridge, // a strip of land joining this fan to the next colony's + Obstacle // the pad, as a circle growth keeps clear of; drawn as a shape instead +}; + +// A stroked piece of land in colony 0's wedge. +struct Piece +{ + std::vector path; + PathBounds bounds; + Kind kind; + int branch; // the tree branch it is or ends, else -1 +}; + +// A point where a branch leaves its parent, and whether it carries stone: rolled once for colony 0, +// so every colony's copy of the fork gets the same. +struct Fork +{ + ShapePoint at; + bool stone; +}; + +// A bud in colony 0's wedge and its fruit. +struct Bud +{ + ShapePoint at; + int fruit; // 0, 1, 2: CHERRY + fruit +}; + +struct Layout +{ + Torus t{1, 1}; + Geometry g{}; + double cx = 0, cy = 0, phase = 0; + // The fan is designed and grown in a round frame on the map's shorter side; this places it on + // the map, stretched along the longer side so the corals fill a rectangular map. + Stretch stretch; + std::vector tree; // colony 0's fan: the trunk first + std::vector pieces; // every stroked piece of colony 0's coral + std::vector forks; // colony 0's forks + std::vector buds; // colony 0's buds, in the order their pieces were added + std::vector pads; + std::vector padAngle; // each pad's heading, outwards from the centre + std::vector land, water, bridge; // per tile + // Per tile: the colony whose pad it is, the bud it is part of (numbered over every colony), and + // the colony whose trunk tip it is; -1 elsewhere. + std::vector padOf, budOf, tipOf; + std::vector> trunkLines; // each colony's trunk centre line + // The centre lines of every branch that forks on and every bridge, for the sand roads. + std::vector> roads; + std::string failure; + + /// The axis of colony k's wedge: where its pad sits. + double axis(int k) const { return phase + (k + 0.5) * g.wedge; } + /// A point of colony 0's design turned onto colony k's wedge. + ShapePoint turn(ShapePoint p, int k) const + { + const double a = k * g.wedge, c = std::cos(a), s = std::sin(a); + const double dx = p.x - cx, dy = p.y - cy; + return {cx + dx * c - dy * s, cy + dx * s + dy * c}; + } + /// A point or path of colony 0's design turned onto colony k's wedge and placed on the map. + ShapePoint place(ShapePoint p, int k) const { return stretch.apply(cx, cy, turn(p, k)); } + std::vector place(const std::vector &path, int k) const + { + return stretchPath(turn(path, k), cx, cy, stretch); + } + std::vector turn(const std::vector &path, int k) const + { + std::vector turned; + for (const StrokePoint &p : path) + { + const ShapePoint q = turn(ShapePoint{p.x, p.y}, k); + turned.push_back({q.x, q.y, p.halfWidth}); + } + return turned; + } + + /// Whether `candidate`, growing from branch `parent`, keeps a strait from all land already in + /// the design, from that land's copies in every other wedge, and from its own copies. + /// + /// Every wedge is checked, not just the neighbours': the fans all converge on the middle of the + /// map, where a branch can come close to the copy of a colony two or more wedges round. + /// + /// A branch is meant to join the land it grows from, so its parent is exempt, and so is any + /// sibling forking from the same point (a fork's two children start together). A bud (a + /// one-point path) passes its branch as `parent`; that branch's siblings are exempt for it too, + /// so the bud on one side of a fork is not refused for the branch on the other. + bool keepsClear(const std::vector &candidate, int parent) const + { + const bool bud = candidate.size() == 1; + const ShapePoint root = + bud && parent >= 0 + ? ShapePoint{tree[parent].path.front().x, tree[parent].path.front().y} + : ShapePoint{candidate.front().x, candidate.front().y}; + const int forkParent = bud ? (parent >= 0 ? tree[parent].parent : -2) : parent; + for (const StrokePoint &p : candidate) + if (std::hypot(p.x - cx, p.y - cy) + p.halfWidth > g.reach) + return false; + // Only the stub of a branch buried in its parent's width is skipped: a longer allowance + // would skip short branches entirely and let them cross other land unchecked. The trunk + // grows out of the middle of its pad, so its whole stretch across the pad is skipped. + const double skip = bud ? 0.0 + : parent < 0 ? g.padReach + g.strait + candidate.front().halfWidth + : candidate.front().halfWidth + 1; + const PathBounds own = pathBounds(candidate); + const auto nearby = [&](const PathBounds &b, int k) + { + const ShapePoint c = turn(ShapePoint{b.x, b.y}, k); + return std::hypot(c.x - own.x, c.y - own.y) < own.radius + b.radius + g.strait; + }; + for (int k = 0; k < g.wedges; ++k) + { + for (const Piece &piece : pieces) + { + if (k == 0 && piece.kind == Kind::Branch && + (piece.branch == parent || + (forkParent >= 0 && tree[piece.branch].parent == forkParent && + std::hypot(piece.path.front().x - root.x, piece.path.front().y - root.y) < + 1))) + continue; + if (!nearby(piece.bounds, k)) + continue; + if (pathClearance(candidate, k ? turn(piece.path, k) : piece.path, skip) < g.strait) + return false; + } + if (k != 0 && nearby(own, k) && + pathClearance(candidate, turn(candidate, k), skip) < g.strait) + return false; + } + return true; + } + void add(std::vector path, Kind kind, int branch) + { + const PathBounds bounds = pathBounds(path); + pieces.push_back({std::move(path), bounds, kind, branch}); + } +}; + +// A stream of numbers in [0, 1) from the design's stream. mt19937's output is the same on every +// platform, unlike the standard distributions, so this keeps maps identical everywhere. +struct Rolls +{ + std::mt19937 &random; + double operator()() { return random() / 4294967296.0; } +}; + +// Joins this fan to the next colony's with up to `count` strips of land across the narrowest +// straits between them. Every point of this coral is paired with the nearest land of its copy one +// wedge on, the pairs are ranked by the water between them, and the narrowest are bridged, each +// kBridgeApart from the last so the crossings are spread along the front. Straits near the rim are +// passed over at first - there a bridge would join the two trunks just below their pads into a +// road between homes - and only straits up to kBridgeSearch wide are looked at; where that finds +// nothing (a small map, or two colonies far apart) the search widens until every boundary has a +// bridge, or ground armies could never meet. Chosen once and turned round, every boundary gets +// the same bridges. +void buildBridges(Layout &L, int count) +{ + const Geometry &g = L.g; + struct Crossing + { + double gap; + StrokePoint from, to; + }; + std::vector> images; + std::vector imageBounds; + for (const Piece &piece : L.pieces) + if (piece.kind == Kind::Branch || piece.kind == Kind::Bud) + { + images.push_back(L.turn(piece.path, 1)); + imageBounds.push_back(pathBounds(images.back())); + } + const double rim = g.rootRadius - g.padReach - 2 * g.strait; + std::vector crossings; + const auto collect = [&](double within, bool awayFromRim) + { + crossings.clear(); + for (const Piece &piece : L.pieces) + { + if (piece.kind != Kind::Branch && piece.kind != Kind::Bud) + continue; + for (size_t m = 0; m < images.size(); ++m) + { + const PathBounds &b = imageBounds[m]; + if (std::hypot(b.x - piece.bounds.x, b.y - piece.bounds.y) > + b.radius + piece.bounds.radius + within) + continue; + for (const StrokePoint &p : piece.path) + for (const StrokePoint &q : images[m]) + { + if (awayFromRim && std::max(std::hypot(p.x - L.cx, p.y - L.cy), + std::hypot(q.x - L.cx, q.y - L.cy)) > rim) + continue; + if (const double gap = + std::hypot(p.x - q.x, p.y - q.y) - p.halfWidth - q.halfWidth; + gap < within) + crossings.push_back({gap, p, q}); + } + } + } + }; + collect(kBridgeSearch, true); + if (crossings.empty()) + collect(kBridgeSearch, false); + if (crossings.empty()) + collect(INFINITY, false); + std::stable_sort(crossings.begin(), crossings.end(), + [](const Crossing &a, const Crossing &b) { return a.gap < b.gap; }); + std::vector chosen; + const double half = kMinimumHalfWidth - 0.5; + for (const Crossing &c : crossings) + { + if (int(chosen.size()) >= count) + break; + bool apart = true; + for (const ShapePoint &at : chosen) + apart = apart && std::hypot(at.x - c.from.x, at.y - c.from.y) >= kBridgeApart; + if (!apart) + continue; + chosen.push_back({c.from.x, c.from.y}); + L.add({{c.from.x, c.from.y, half}, {c.to.x, c.to.y, half}}, Kind::Bridge, -1); + } +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + L.g = geometryFor(request); + const Torus &t = L.t; + const Geometry &g = L.g; + const CoralOptions o(request); + const int n = t.size(), teams = g.teams; + if (!g.padsFit || !g.trunkFits) + { + L.failure = "the colonies' corals do not fit on the map"; + return L; + } + L.cx = t.w / 2; + L.stretch = Stretch::toFill(t.w, t.h); + L.cy = t.h / 2; + L.phase = context.bounded("coral-layout", 3600) / 3600.0 * 2 * kPi; + Rolls roll{context.stream("coral-growth")}; + + // Colony 0's pad comes first, so every branch keeps a strait from it and, through the copies, + // from every other colony's pad. + const ShapePoint pad0 = polarPoint(L.cx, L.cy, g.rootRadius, L.axis(0)); + L.add({{pad0.x, pad0.y, g.padReach}}, Kind::Obstacle, -1); + + // The fan. The trunk leaves the pad's centre heading for the map centre, turned off it by the + // lean; everything else is growBranches. A branch is added to the design the moment it is + // accepted, since growBranches keeps it straight after and every later branch must see it. + ForkStyle style; + style.spread = g.spread; + style.spreadJitter = kSpreadJitter; + style.lengthRatio = g.lengthRatio; + style.lengthJitter = kLengthJitter; + style.widthRatio = kWidthRatio; + style.bend = kBend; + style.minimumLength = std::max(5.0, kMinimumHalfWidth + g.strait); + style.minimumHalfWidth = kMinimumHalfWidth; + const auto accept = [&](const std::vector &path, int parent) + { + if (!L.keepsClear(path, parent)) + return false; + L.add(path, Kind::Branch, int(L.tree.size())); + return true; + }; + growBranches(L.tree, -1, pad0, L.axis(0) + kPi + g.lean, g.trunkLength, g.trunkHalf, g.levels, + style, roll, accept); + if (L.tree.empty()) + { + L.failure = "the trunk does not fit between the pads"; + return L; + } + + // Every fork, rolled for stone once; then a bud on every tip that keeps clear, rolled for its + // fruit. Both roll whether or not they are used, so the count never shifts later rolls. + const std::int64_t stonePerMille = + std::min(1000, scaledCount(kForkStone, o.stone)); + for (const Branch &branch : L.tree) + if (branch.parent >= 0) + L.forks.push_back( + {{branch.path.front().x, branch.path.front().y}, roll() * 1000 < stonePerMille}); + for (int b = 0; b < int(L.tree.size()); ++b) + { + const StrokePoint &tip = L.tree[b].path.back(); + const int fruit = int(roll() * 3); + if (!L.tree[b].leaf) + continue; + // A little wider than the tip it ends, so a few grass tiles survive inside its beach. + const std::vector bud{{tip.x, tip.y, std::max(4.0, tip.halfWidth + 1.5)}}; + if (L.keepsClear(bud, b)) + { + L.add(bud, Kind::Bud, b); + L.buds.push_back({{tip.x, tip.y}, fruit}); + } + } + + if (teams >= 2 && o.landBridges > 0) + buildBridges(L, o.landBridges); + + // Stamp every colony's copy: its pad, its fan and buds, its bridges to the next colony. + L.land.assign(n, 0); + L.bridge.assign(n, 0); + L.padOf.assign(n, -1); + L.budOf.assign(n, -1); + L.tipOf.assign(n, -1); + const RadialShape padShape(g.padRadius, kPadRoughness, context, "coral-pads"); + for (int k = 0; k < teams; ++k) + { + const ShapePoint centre = L.place(pad0, k); + L.pads.push_back(centre); + L.padAngle.push_back(L.stretch.heading(L.axis(k))); + forEachTileInShape( + t, centre.x, centre.y, padShape, L.axis(k), + [&](int i, double, double) + { + L.land[i] = 1; + L.padOf[i] = k; + }, + L.stretch); + const std::vector trunk = L.place(L.tree[0].path, k); + std::vector line; + for (const StrokePoint &p : trunk) + line.push_back({p.x, p.y}); + L.trunkLines.push_back(line); + // The trunk's tip, where it first forks: the ground every colony must be able to walk to. + const StrokePoint &tip = trunk.back(); + const int tipReach = int(std::ceil(tip.halfWidth)); + for (int dy = -tipReach; dy <= tipReach; ++dy) + for (int dx = -tipReach; dx <= tipReach; ++dx) + if (dx * dx + dy * dy <= tip.halfWidth * tip.halfWidth) + L.tipOf[t.at(int(std::lround(tip.x)) + dx, int(std::lround(tip.y)) + dy)] = k; + int budIndex = 0; + for (const Piece &piece : L.pieces) + { + if (piece.kind == Kind::Obstacle) + continue; + const std::vector turned = L.place(piece.path, k); + if (piece.kind == Kind::Bud) + { + // The same disc strokePath would draw for a one-point path, labelled as it goes. + const StrokePoint &c = turned.front(); + const int id = k * int(L.buds.size()) + budIndex++; + const int reach = int(std::ceil(c.halfWidth)) + 1; + for (int y = int(std::floor(c.y)) - reach; y <= int(std::ceil(c.y)) + reach; ++y) + for (int x = int(std::floor(c.x)) - reach; x <= int(std::ceil(c.x)) + reach; + ++x) + if ((x - c.x) * (x - c.x) + (y - c.y) * (y - c.y) < + c.halfWidth * c.halfWidth) + { + const int i = t.at(x, y); + L.land[i] = 1; + L.budOf[i] = id; + } + continue; + } + if (piece.kind == Kind::Bridge) + strokePath(L.bridge, t, turned); + strokePath(L.land, t, turned); + // Roads keep routes open, so they run down every branch that forks on and every bridge, + // but not down the tips: a dead end carries no traffic, and on a tip's narrow land the + // road would leave next to no grass for its fields. + if (piece.kind == Kind::Bridge || !L.tree[piece.branch].leaf) + L.roads.push_back(turned); + } + } + L.water.assign(n, 0); + for (int i = 0; i < n; ++i) + L.water[i] = !L.land[i]; + return L; +} + +void furnishPads(Map &map, const Layout &L, GenerationContext &context) +{ + const Torus &t = L.t; + const std::vector reserved = swarmSurroundings(t, context); + for (int team = 0; team < L.g.teams; ++team) + { + const auto eligible = [&](int i) + { return L.padOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + // Facing in, towards the trunk. + const KitFrame frame{int(std::lround(L.pads[team].x)), int(std::lround(L.pads[team].y)), + L.padAngle[team] + kPi}; + plantKit( + map, t, context, + {frame.at(3, -6, 8), frame.at(3, 6, 8), frame.at(-8, 0, 6), kHomeWheat, kHomeWood, 2}, + eligible); + } +} + +// The coral's resources, richer the further the ground is from home. Distance is the walk along the +// coral from the nearest pad, normalised to the farthest land: a fork halfway out carries a +// middling stone deposit, a bud at the far end a big grove. Fields are ranked by patch noise +// sampled in the wedge frame, leaning towards home, so the trunk and first forks are farmland. +void stockCoral(Map &map, const Layout &L, GenerationContext &context, const CoralOptions &o) +{ + const Torus &t = L.t; + const int n = t.size(); + std::vector pads(n, 0); + for (int i = 0; i < n; ++i) + pads[i] = L.padOf[i] >= 0; + const std::vector steps = stepsFrom(t, pads, L.land); + const std::vector &coral = L.land; + int farthest = 1; + for (int i = 0; i < n; ++i) + if (coral[i] && steps[i] >= 0) + farthest = std::max(farthest, steps[i]); + const auto remote = [&](double x, double y) + { + const int i = t.at(int(std::lround(x)), int(std::lround(y))); + return steps[i] >= 0 ? std::min(1.0, double(steps[i]) / farthest) : 1.0; + }; + const auto onBranch = [&](int i) + { + return coral[i] && L.padOf[i] < 0 && L.budOf[i] < 0 && !L.bridge[i] && + clearGround(map, i % t.w, i / t.w); + }; + + const int colonies = L.g.teams; + for (int k = 0; k < colonies; ++k) + for (const Fork &fork : L.forks) + if (fork.stone) + { + const ShapePoint at = L.place(fork.at, k); + const int tiles = int(std::lround( + kForkStoneNear + (kForkStoneFar - kForkStoneNear) * remote(at.x, at.y))); + if (const int seed = + seedNear(t, int(std::lround(at.x)), int(std::lround(at.y)), 3, onBranch); + seed >= 0) + growPatch(map, t, seed, STONE, tiles, onBranch); + } + + const int budsPerColony = int(L.buds.size()); + for (int k = 0; k < colonies; ++k) + for (int b = 0; b < budsPerColony; ++b) + { + const Bud &bud = L.buds[b]; + const ShapePoint at = L.place(bud.at, k); + const int id = k * budsPerColony + b; + const auto onBud = [&](int i) + { return L.budOf[i] == id && clearGround(map, i % t.w, i / t.w); }; + const int tiles = int(scaledCount( + std::lround(kBudFruitNear + (kBudFruitFar - kBudFruitNear) * remote(at.x, at.y)), + o.fruit)); + if (tiles <= 0) + continue; + if (const int seed = + seedNear(t, int(std::lround(at.x)), int(std::lround(at.y)), 3, onBud); + seed >= 0) + growPatch(map, t, seed, CHERRY + bud.fruit, tiles, onBud); + } + + const WedgeFrame wedges(t, L.phase, L.g.wedges, L.stretch); + const PeriodicNoise patch(t.w, t.h, 10, context.stream("coral-patch")); + const PeriodicNoise split(t.w, t.h, 7, context.stream("coral-split")); + std::vector> ranked; + std::vector splitKey(n, 0.0); + for (int i = 0; i < n; ++i) + if (onBranch(i)) + { + const WedgeFrame::Cell cell = wedges.cell(i % t.w, i / t.w); + const double home = steps[i] >= 0 ? 1 - std::min(1.0, double(steps[i]) / farthest) : 0; + ranked.push_back({-(patch.at(cell.s, cell.d) + kHomeLean * home), i}); + splitKey[i] = split.at(cell.s, cell.d); + } + std::stable_sort(ranked.begin(), ranked.end()); + std::vector tiles; + for (const auto &entry : ranked) + tiles.push_back(entry.second); + const std::int64_t area = std::int64_t(tiles.size()); + plantFields(map, t, tiles, int(scaledCount(area * kWheatShare / 100, o.wheat)), + int(scaledCount(area * kWoodShare / 100, o.wood)), + [&](int i) { return splitKey[i]; }); +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "coral layout"; + const CoralOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.size(); + + context.stage = "coral terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + if (L.water[i]) + terrain[i] = WATER; + // A few dry patches along the branches, sampled in the wedge frame so every colony's fan gets + // the same ones; never on a pad, a bud or a bridge. + { + std::vector branches(n, 0); + for (int i = 0; i < n; ++i) + branches[i] = L.land[i] && L.padOf[i] < 0 && L.budOf[i] < 0 && !L.bridge[i]; + const WedgeFrame wedges(t, L.phase, L.g.wedges, L.stretch); + const PeriodicNoise dry(t.w, t.h, kSandCell, context.stream("coral-sand")); + sprinkleSand(terrain, t, branches, kSandShare, kSandInland, + [&](int i) + { + const WedgeFrame::Cell cell = wedges.cell(i % t.w, i / t.w); + return dry.at(cell.s, cell.d); + }); + } + // The sand roads: a line of sand one tile thick (tracePath, the thinnest there is) down the + // middle of every branch that forks on and every bridge, off the pads and the buds, so homes + // keep their building room and every tip its grove. Every tile touching a sand corner loses the + // pure grass a deposit or a building needs, so nothing can grow or be built across the road. + if (o.sandRoads) + { + std::vector road(n, 0); + for (const std::vector &line : L.roads) + tracePath(road, t, line); + for (int i = 0; i < n; ++i) + if (road[i] && terrain[i] == GRASS && L.padOf[i] < 0 && L.budOf[i] < 0) + terrain[i] = SAND; + } + layBeaches(terrain, t); + writeUndermap(map, terrain); + + context.stage = "coral colonies"; + const auto pad = [&](int team) + { + std::vector home(n, 0); + for (int i = 0; i < n; ++i) + home[i] = L.padOf[i] == team && map.isGrass(i % t.w, i / t.w); + return home; + }; + // The swarm stands a little seaward of the pad's middle, so the kit and the trunk lie in front + // of it; placeSettlement measures from the footprint's top-left. + const auto anchor = [&](int team) + { + const ShapePoint p = polarPoint(L.pads[team].x, L.pads[team].y, 3, L.padAngle[team]); + return MapGeneratorPoint(int(std::lround(p.x)) - 2, int(std::lround(p.y)) - 2); + }; + if (!settleColonies(game, context, "coral-starts", pad, anchor)) + return false; + + context.stage = "coral resources"; + furnishPads(map, L, context); + stockCoral(map, L, context, o); + // Algae only grows in water with solid sand within reach (see algaeGrowthChance), so it is + // counted over the shallows but seeded on each colony's best-growing water, the same number of + // clumps in every wedge. + const WedgeFrame algaeWedges(t, L.phase, L.g.wedges, L.stretch); + seedAlgae(map, context, t, "coral-algae", o.algae, + AlgaeBand::shallows(1, 10, kAlgaeTilesPerClump).thriving(kAlgaeBestShare), + &algaeWedges); + secureStartingCrops(game, context, t); + + // Deposits only land on grass and beaches keep every branch walkable along its edge, but a thin + // trunk can still be stocked shut. Keep every colony's walk to its own trunk tip open, and with + // bridges, colony 0's walk to every colony. + context.stage = "coral roads"; + const std::vector> workers = unitTilesByTeam(map, teams); + for (int team = 0; team < teams; ++team) + { + std::vector tip(n, 0); + for (int i = 0; i < n; ++i) + tip[i] = L.tipOf[i] == team; + if (workers[team].empty() || !openRoad(map, t, workers[team], tip)) + { + context.detail = "colony " + std::to_string(team) + " cannot walk to its trunk's tip"; + return false; + } + } + if (o.landBridges > 0) + for (int team = 1; team < teams; ++team) + if (!openRoad(map, t, workers[0], tileMask(t, workers[team]))) + { + context.detail = "no walk from colony 0 reaches colony " + std::to_string(team); + return false; + } + return true; +} + +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 1) + return "Coral needs at least one colony."; + // Below 128 tiles across, a pad and a strait leave the fan nowhere to grow. + if (std::min(r.wDec, r.hDec) < 7) + return "Coral needs a map at least 128 tiles across."; + const Geometry g = geometryFor(r); + if (!g.padsFit) + return "Too many colonies for this map; use a bigger map or fewer colonies."; + // A trunk can clear its pad and still not fit: with many colonies and wide trunks or straits, + // neighbouring trunks crowd each other before they fork. That only happens on maps smaller than + // 256 tiles, where the design is quick to rebuild, so there it is rebuilt and the request refused + // with advice rather than failing every roll during generation. + const auto crowded = [&] + { + GenerationContext trial(r); + return g.half < kReferenceHalf && !design(r, trial).failure.empty(); + }; + if (!g.trunkFits || crowded()) + return "The coral does not fit this map; use a bigger map, narrower straits or fewer " + "colonies."; + return ""; +} + +// Checked on the finished world: every trunk is still land from its pad to its tip, every colony +// can walk to its own trunk's tip, and with land bridges colony 0 can walk to every colony; water, +// buildings and every resource block. +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "coral"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + const int teams = context.request.nbTeams; + for (size_t k = 0; k < L.trunkLines.size(); ++k) + for (const ShapePoint &p : L.trunkLines[k]) + if (map.isWater(t.x(int(std::lround(p.x))), t.y(int(std::lround(p.y))))) + return "Colony " + std::to_string(k) + "'s trunk is broken by water."; + const std::vector> workers = unitTilesByTeam(map, teams); + const std::vector walkable = walkableTiles(map); + for (int k = 0; k < teams; ++k) + { + if (workers[k].empty()) + return "Colony " + std::to_string(k) + " has no workers."; + const std::vector steps = stepsFrom(t, tileMask(t, workers[k]), walkable); + bool reached = false; + for (int i = 0; i < t.size() && !reached; ++i) + reached = L.tipOf[i] == k && steps[i] >= 0; + if (!reached) + return "Colony " + std::to_string(k) + " cannot walk to its trunk's tip."; + } + if (CoralOptions(context.request).landBridges > 0 && L.g.teams >= 2) + { + const ColonyWalk walk = walkFromFirstColony(map, teams, "the coral", "over the bridges"); + if (!walk.error.empty()) + return walk.error; + } + return ""; +} +} // namespace + +CoralOptions::CoralOptions(const GenerationRequest &r) + : branching(r.option("branching")), forkAngle(r.option("fork-angle")), + branchWidth(r.option("branch-width")), straitWidth(r.option("strait-width")), + lean(r.option("lean")), landBridges(r.option("land-bridges")), + homeSize(r.option("home-size")), sandRoads(r.option("sand-roads") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition coralDefinition() +{ + return { + "coral", + 21, + "Coral", + 1, + false, + // Forks from the trunk to the tips; how far each fork's children turn from their parent's + // heading, in degrees either side; the trunk's width in tiles (branches taper from it); the + // water kept between any two pieces of land; how far every trunk is turned off the line to + // the map centre, in degrees; land bridges per pair of neighbours. + {{"branching", "Branching", 3, 9, 1, 6, ControlGroup::Terrain}, + {"fork-angle", "Fork angle", 10, 40, 2, 28, ControlGroup::Terrain}, + {"branch-width", "Branch width", 7, 17, 2, 11, ControlGroup::Terrain}, + {"strait-width", "Strait width", 3, 12, 1, 5, ControlGroup::Terrain}, + {"lean", "Lean", 0, 60, 5, 25, ControlGroup::Layout}, + {"land-bridges", "Land bridges", 0, 3, 1, 1, ControlGroup::Layout}, + // Each home pad's radius as a percentage of the standard pad. + {"home-size", "Home size", 60, 200, 10, 130, ControlGroup::Layout}, + // Off, the branches are grass from shore to shore, with no sand road down the middle. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // The branches' standing wheat and wood, the forks' stone, the buds' fruit and the + // shallows' algae; every pad's kit is unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/CoralGenerator.h b/src/map/generator/generators/CoralGenerator.h new file mode 100644 index 000000000..ccb855b4c --- /dev/null +++ b/src/map/generator/generators/CoralGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct CoralOptions +{ + int branching, forkAngle, branchWidth, straitWidth, lean, landBridges, homeSize; + bool sandRoads; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit CoralOptions(const GenerationRequest &r); +}; +GeneratorDefinition coralDefinition(); diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 0fcf658e8..7df2c59aa 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -77,6 +77,20 @@ struct FjordLayout double coreR, lakeR, fjordInnerR, maxStretch; bool hasLake, lakeConnected; std::vector teamTheta; + // The continent is sized on the map's shorter side; on a rectangular map it is stretched along + // the longer side as well, so it fills the map as an oval rather than a disc in the middle. + Stretch fill; + + /// A map point in the continent's own round frame, and back: xf's rotation and elongation, with + /// the fill stretch undone first and applied last. + ShapePoint toShape(ShapePoint p) const + { + if (fill.sx == 1 && fill.sy == 1) + return xf.toShape(p); + const ShapePoint round = fill.undo(p.x - W / 2.0, p.y - H / 2.0); + return xf.toShape({W / 2.0 + round.x, H / 2.0 + round.y}); + } + ShapePoint toMap(ShapePoint q) const { return fill.apply(W / 2.0, H / 2.0, xf.toMap(q)); } }; FjordLayout computeLayout(Game &game, GenerationContext &context, @@ -127,8 +141,10 @@ FjordLayout computeLayout(Game &game, GenerationContext &context, } } - return FjordLayout{W, H, nbTeams, xf, coast, coreR, - lakeR, fjordInnerR, maxStretch, hasLake, lakeConnected, teamTheta}; + return FjordLayout{W, H, nbTeams, xf, + coast, coreR, lakeR, fjordInnerR, + maxStretch, hasLake, lakeConnected, teamTheta, + Stretch::toFill(W, H)}; } // 1) Stamp the continent. @@ -138,7 +154,7 @@ void stampContinent(Game &game, const FjordLayout &layout) { for (int x = 0; x < layout.W; ++x) { - const auto shaped = layout.xf.toShape({double(x), double(y)}); + const auto shaped = layout.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; double theta = atan2(v, u); double r = sqrt(u * u + v * v); @@ -203,9 +219,9 @@ std::vector> carveFjords(Game &game, GenerationCo double v = baseV + lateral * perpV; double width = mouthWidth * (1 - t) + tipWidth * t; - const auto mapped = layout.xf.toMap({u, v}); + const auto mapped = layout.toMap({u, v}); double mx = mapped.x, my = mapped.y; - int rad = (int)ceil(width * layout.maxStretch) + 1; + int rad = (int)ceil(width * layout.maxStretch * layout.fill.longest()) + 1; int ix = (int)lround(mx), iy = (int)lround(my); fjordCenterlines[k].push_back( MapGeneratorPoint(game.map.normalizeX(ix), game.map.normalizeY(iy))); @@ -215,7 +231,7 @@ std::vector> carveFjords(Game &game, GenerationCo { int nx = game.map.normalizeX(ix + dx); int ny = game.map.normalizeY(iy + dy); - const auto shaped = layout.xf.toShape({double(nx), double(ny)}); + const auto shaped = layout.toShape({double(nx), double(ny)}); double u2 = shaped.x, v2 = shaped.y; double dd = (u2 - u) * (u2 - u) + (v2 - v) * (v2 - v); if (dd <= width * width) @@ -244,7 +260,7 @@ void carveLake(Game &game, const FjordLayout &layout, bool sandyShore) { for (int x = 0; x < layout.W; ++x) { - const auto shaped = layout.xf.toShape({double(x), double(y)}); + const auto shaped = layout.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; if (u * u + v * v <= layout.lakeR * layout.lakeR) game.map.setUMatPos(x, y, WATER, 1); @@ -265,7 +281,7 @@ void carveLake(Game &game, const FjordLayout &layout, bool sandyShore) { if (game.map.isWater(x, y)) continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); + const auto shaped = layout.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; if (u * u + v * v <= sandOuterR * sandOuterR) game.map.setUMatPos(x, y, SAND, 1); @@ -306,10 +322,12 @@ void placeOutlierIslands(Game &game, GenerationContext &context, const FjordLayo { double theta = randomAngle(context); double r = (context.bounded("layout", 1000)) / 1000.0 * halfMapMargin; - int cx = game.map.normalizeX((int)lround(layout.W / 2.0 + r * cos(theta))); - int cy = game.map.normalizeY((int)lround(layout.H / 2.0 + r * sin(theta))); + int cx = + game.map.normalizeX((int)lround(layout.W / 2.0 + r * cos(theta) * layout.fill.sx)); + int cy = + game.map.normalizeY((int)lround(layout.H / 2.0 + r * sin(theta) * layout.fill.sy)); - const auto shaped = layout.xf.toShape({double(cx), double(cy)}); + const auto shaped = layout.toShape({double(cx), double(cy)}); const double shapeR = std::hypot(shaped.x, shaped.y); const double shapeTheta = atan2(shaped.y, shaped.x); if (shapeR < layout.coast.radiusAt(shapeTheta) + islandRadius + 8.0) @@ -384,7 +402,7 @@ std::vector anchorTeams(Game &game, const FjordLayout &layout int fx = -1, fy = -1; for (int tries = 0; tries < 30; ++tries) { - const auto mapped = layout.xf.toMap({r * cos(theta), r * sin(theta)}); + const auto mapped = layout.toMap({r * cos(theta), r * sin(theta)}); double mx = mapped.x, my = mapped.y; int ix = game.map.normalizeX((int)lround(mx)); int iy = game.map.normalizeY((int)lround(my)); @@ -398,7 +416,7 @@ std::vector anchorTeams(Game &game, const FjordLayout &layout } if (fx < 0) { - const auto mapped = layout.xf.toMap( + const auto mapped = layout.toMap( {(layout.coreR + 6.0) * cos(theta), (layout.coreR + 6.0) * sin(theta)}); double mx = mapped.x, my = mapped.y; fx = game.map.normalizeX((int)lround(mx)); @@ -472,7 +490,7 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou // landing on sand could miss every grass tile within its own radius and place nothing. if (!game.map.isGrass(x, y) || grid[y * layout.W + x] != 0) continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); + const auto shaped = layout.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; if (u * u + v * v <= (layout.coreR + 4.0) * (layout.coreR + 4.0)) { @@ -514,7 +532,7 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou { if (!game.map.isWater(x, y)) continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); + const auto shaped = layout.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; if (u * u + v * v <= innerLakeR * innerLakeR) lakeWater.push_back(MapGeneratorPoint(x, y)); @@ -542,7 +560,7 @@ void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout { if (!game.map.isWater(x, y)) continue; - const auto shaped = layout.xf.toShape({double(x), double(y)}); + const auto shaped = layout.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; double theta = atan2(v, u); double r = sqrt(u * u + v * v); @@ -755,7 +773,7 @@ GeneratorDefinition fjordContinentDefinition() return {"fjord-continent", 12, "Fjord continent", - 10, + 11, false, {{"continent-size", "Continent size", 28, 40, 2, 34, ControlGroup::Terrain}, {"coast-roughness", "Coast roughness", 10, 35, 1, 22, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/SpiderWebGenerator.cpp b/src/map/generator/generators/SpiderWebGenerator.cpp index 67d1ec750..5c010e48e 100644 --- a/src/map/generator/generators/SpiderWebGenerator.cpp +++ b/src/map/generator/generators/SpiderWebGenerator.cpp @@ -41,6 +41,9 @@ namespace // outline wobbles by up to this share of its radius. constexpr int kPadRadius = 13; constexpr int kMinimumPad = 7; +// ...and never more than this share of the half side (times the home size control), so a small +// map's pads leave room for the web. +constexpr double kPadShare = 0.16; constexpr double kPadRoughness = 0.2; // Sea kept between the pads and their images across the wrap, and between neighbouring pads. constexpr double kWrapGap = 6; @@ -65,9 +68,25 @@ constexpr int kHomeWood = 30; // Knots carrying stone, in tenths of a percent at 100, and each one's size in tiles. constexpr int kKnotStone = 280; constexpr int kKnotStoneTiles = 5; -// The threads' standing wheat and wood, as shares of their grass at 100. -constexpr int kWheatShare = 8; -constexpr int kWoodShare = 5; +// The threads' standing wheat and wood, as shares of their grass at 100: enough that walking any +// stretch of thread passes a field or a copse, since the threads are all the land there is. +constexpr int kWheatShare = 20; +constexpr int kWoodShare = 13; +// Dry patches of sand inside the threads, as a share of their inland grass, at least this many +// steps from water so a strip of grass always lies between patch and beach; and the size of the +// noise the patches follow, in tiles. +constexpr double kSandShare = 0.07; +constexpr int kSandInland = 3; +constexpr double kSandCell = 10; +// One algae clump per this many tiles of shallows at 100, all on the share of each colony's +// shallows where algae regrows most readily. +constexpr int kAlgaeTilesPerClump = 30; +constexpr double kAlgaeBestShare = 0.3; +// The thread width control is the width on a 256-tile map. A smaller map thins its threads with +// the square root of its size, down to this share, so a 128-tile web still reads as threads rather +// than one blob of land, while its threads keep grass either side of their sand road; bigger maps +// keep the control's width, which already reads as a web. +constexpr double kMinimumThreadScale = 0.75; // Dew drops and the water kept round them. constexpr double kDewRadius = 3; constexpr double kDewMoat = 3; @@ -87,7 +106,9 @@ Geometry geometryFor(const GenerationRequest &r) Geometry g; g.teams = std::max(1, r.nbTeams); g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; - g.threadHalf = o.threadWidth / 2.0; + const double threadWidth = + o.threadWidth * std::clamp(std::sqrt(g.half / 128.0), kMinimumThreadScale, 1.0); + g.threadHalf = threadWidth / 2.0; g.spokeHalf = g.threadHalf + 1; g.hubRadius = std::max(6.0, o.hubSize / 100.0 * g.half); g.pondRadius = g.hubRadius >= 9 ? std::max(2.0, 0.3 * g.hubRadius) : 0.0; @@ -95,7 +116,14 @@ Geometry geometryFor(const GenerationRequest &r) g.sag = o.sag / 100.0 * kMaximumSag; g.spiral = o.spiral; // The pads sit as far out as the wrap allows, and shrink on a crowded ring until they fit. - for (int pad = kPadRadius; pad >= kMinimumPad; --pad) + // The home size control scales the pad, 100 being kPadRadius: a bigger pad is more room to + // build the first economy before the colony has to push out along the threads. It is a size to + // aim for; a crowded ring shrinks it as ever. + const double homeScale = o.homeSize / 100.0; + const int largestPad = + std::max(kMinimumPad, int(std::lround(std::min(kPadRadius * homeScale, + kPadShare * homeScale * g.half)))); + for (int pad = largestPad; pad >= kMinimumPad; --pad) { g.padRadius = pad; g.padReach = pad * (1 + kPadRoughness); @@ -110,7 +138,7 @@ Geometry geometryFor(const GenerationRequest &r) // the spokes never crowd into one mass round the hub: halfway out, neighbouring spokes keep // well over a thread's width of water between them. const double middle = (g.hubRadius + g.frameRadius) / 2; - const int room = int(2 * kPi * middle / (kSpokeRoom * o.threadWidth)); + const int room = int(2 * kPi * middle / (kSpokeRoom * threadWidth)); g.perColony = std::max({1, std::min(o.spokes, room / g.teams), std::min((kMinimumSpokes + g.teams - 1) / g.teams, room / g.teams)}); g.spokes = g.perColony * g.teams; @@ -121,7 +149,7 @@ Geometry geometryFor(const GenerationRequest &r) // keeps at least one whole turn between the hub and the frame. const double span = g.frameRadius - 0.8 * g.padReach - g.hubRadius; g.spacing = std::min(g.spacing, - std::max(2.0 * o.threadWidth, span / (1 + kFreeZone + kOuterZone))); + std::max(2.0 * threadWidth, span / (1 + kFreeZone + kOuterZone))); g.firstRing = g.hubRadius + kFreeZone * g.spacing; g.lastRing = g.frameRadius - std::max(kOuterZone * g.spacing, 0.8 * g.padReach); return g; @@ -158,6 +186,9 @@ struct Layout Geometry g{}; int cx = 0, cy = 0; double phase = 0; + // The web is designed round the centre in a circle on the map's shorter side; this places it on + // the map, stretched along the longer side so the web fills a rectangular map. + Stretch stretch; std::vector bow; // per spoke of a wedge, in radians std::vector land, water; // per tile std::vector hub, thread; // per tile @@ -167,6 +198,7 @@ struct Layout std::vector padAngle; // each pad's heading out from the hub std::vector drops; std::vector> spokeLines; // every spoke's centre line + std::vector> roads; // every thread's centre line, on the map std::string failure; /// Where spoke j crosses radius r: it bows sideways most at mid-length and not at all at the @@ -215,21 +247,30 @@ struct Layout } }; -void strokeThread(std::vector &mask, const Torus &t, const Thread &thread, +// Strokes a thread onto the mask, and records its centre line as road unless it is torn. +void strokeThread(std::vector &mask, const Torus &t, Layout &L, const Thread &thread, double halfWidth) { + // A torn strand's stubs are dead ends and carry no traffic, so only whole threads get a road. + const auto stroke = [&](std::vector path, bool road) + { + strokePath(mask, t, path); + if (road) + L.roads.push_back(std::move(path)); + }; const double length = std::hypot(thread.to.x - thread.from.x, thread.to.y - thread.from.y); const int segments = std::max(4, int(length / 3)); - const std::vector path = - bezierPath(thread.from, thread.control, thread.to, halfWidth, halfWidth, segments); + const std::vector path = stretchPath( + bezierPath(thread.from, thread.control, thread.to, halfWidth, halfWidth, segments), L.cx, + L.cy, L.stretch); if (!thread.torn) { - strokePath(mask, t, path); + stroke(path, true); return; } const int stub = std::max(1, int(std::lround(kTornStub * segments))); - strokePath(mask, t, std::vector(path.begin(), path.begin() + stub + 1)); - strokePath(mask, t, std::vector(path.end() - stub - 1, path.end())); + stroke(std::vector(path.begin(), path.begin() + stub + 1), false); + stroke(std::vector(path.end() - stub - 1, path.end()), false); } Layout design(const GenerationRequest &request, GenerationContext &context) @@ -248,6 +289,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } L.cx = t.w / 2; L.cy = t.h / 2; + L.stretch = Stretch::toFill(t.w, t.h); L.phase = context.bounded("web-layout", 3600) / 3600.0 * 2 * kPi; for (int j = 0; j < m; ++j) L.bow.push_back((roll(request, "web-bow/", j) / 500.0 - 1) * kSpokeBow * g.spokeAngle); @@ -266,11 +308,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const ShapePoint p = L.onSpoke(j, r); const double along = (r - g.hubRadius / 2) / (g.frameRadius - g.hubRadius / 2); path.push_back({p.x, p.y, g.spokeHalf - along * (g.spokeHalf - g.threadHalf - 0.5)}); - line.push_back(p); + line.push_back(L.stretch.apply(L.cx, L.cy, p)); if (r >= g.frameRadius) break; } - strokePath(L.land, t, path); + std::vector placed = stretchPath(path, L.cx, L.cy, L.stretch); + strokePath(L.land, t, placed); + L.roads.push_back(std::move(placed)); L.spokeLines.push_back(std::move(line)); } // The frame: a thread between every two neighbouring spoke ends, sagging inwards. @@ -280,7 +324,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const double mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2; const double inward = std::max(1e-9, std::hypot(L.cx - mx, L.cy - my)); const double pull = 0.5 * g.sag * std::hypot(to.x - from.x, to.y - from.y); - strokeThread(L.land, t, + strokeThread(L.land, t, L, {from, {mx + (L.cx - mx) / inward * 2 * pull, my + (L.cy - my) / inward * 2 * pull}, to, @@ -309,30 +353,32 @@ Layout design(const GenerationRequest &request, GenerationContext &context) continue; const long long id = L.key(k, j); thread.torn = std::find(torn.begin(), torn.end(), id) != torn.end(); - strokeThread(L.land, t, thread, g.threadHalf); + strokeThread(L.land, t, L, thread, g.threadHalf); if (roll(request, "web-knot/", id) < std::min(1000, scaledCount(kKnotStone, o.stone))) - L.knots.push_back(thread.from); + L.knots.push_back(L.stretch.apply(L.cx, L.cy, thread.from)); } // The hub, with a pond at its heart, and a pad for every colony where its spoke meets the frame. const RadialShape hubShape(g.hubRadius, 0.2, context, "web-hub"); - fillShape(L.land, t, L.cx, L.cy, hubShape); - fillShape(L.hub, t, L.cx, L.cy, hubShape); + fillShape(L.land, t, L.cx, L.cy, hubShape, 0, 1, L.stretch); + fillShape(L.hub, t, L.cx, L.cy, hubShape, 0, 1, L.stretch); const RadialShape padShape(g.padRadius, kPadRoughness, context, "web-pads"); for (int k = 0; k < teams; ++k) { const long long spoke = (long long)k * m + m / 2; - const ShapePoint centre = L.onSpoke(spoke, g.frameRadius); + const ShapePoint centre = L.stretch.apply(L.cx, L.cy, L.onSpoke(spoke, g.frameRadius)); const double heading = L.phase + (double(spoke) + g.offset) * g.spokeAngle; L.pads.push_back(centre); - L.padAngle.push_back(heading); - forEachTileInShape(t, centre.x, centre.y, padShape, heading, - [&](int i, double, double) - { - L.land[i] = 1; - L.padOf[i] = k; - }); + L.padAngle.push_back(L.stretch.heading(heading)); + forEachTileInShape( + t, centre.x, centre.y, padShape, heading, + [&](int i, double, double) + { + L.land[i] = 1; + L.padOf[i] = k; + }, + L.stretch); } // Dew drops: islets hanging in the web's cells, chosen in one wedge where the water is widest @@ -342,7 +388,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const std::vector fromLand = stepsFrom(t, L.land); const RadialShape dewShape(kDewRadius, 0.25, context, "web-dew"); const int need = int(std::ceil(dewShape.maximumRadius() + kDewMoat)); - const WedgeFrame wedges(t, L.phase, teams); + const WedgeFrame wedges(t, L.phase, teams, L.stretch); // Inside the frame's sagging chords, not out at sea between them. const double enclosed = g.frameRadius * std::cos(g.spokeAngle / 2) * (1 - g.sag) - g.spokeHalf - need; @@ -372,20 +418,26 @@ Layout design(const GenerationRequest &request, GenerationContext &context) continue; const int prize = int(chosen.size()) % 4; chosen.push_back(i); - const double dx = t.offsetX(L.cx, i % t.w), dy = t.offsetY(L.cy, i / t.w); + // Turned round the centre in the round design frame, then stretched back onto the map. + const ShapePoint round = + L.stretch.undo(t.offsetX(L.cx, i % t.w), t.offsetY(L.cy, i / t.w)); for (int w = 0; w < teams; ++w) { const double turn = w * 2 * kPi / teams; - const double x = L.cx + dx * std::cos(turn) - dy * std::sin(turn); - const double y = L.cy + dx * std::sin(turn) + dy * std::cos(turn); + const ShapePoint at = + L.stretch.apply(L.cx, L.cy, + {L.cx + round.x * std::cos(turn) - round.y * std::sin(turn), + L.cy + round.x * std::sin(turn) + round.y * std::cos(turn)}); const int drop = int(L.drops.size()); - L.drops.push_back({x, y, turn, prize}); - forEachTileInShape(t, x, y, dewShape, turn, - [&](int tile, double, double) - { - L.land[tile] = 1; - L.dewOf[tile] = drop; - }); + L.drops.push_back({at.x, at.y, turn, prize}); + forEachTileInShape( + t, at.x, at.y, dewShape, turn, + [&](int tile, double, double) + { + L.land[tile] = 1; + L.dewOf[tile] = drop; + }, + L.stretch); } } } @@ -395,7 +447,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (g.pondRadius > 0) { const RadialShape pond(g.pondRadius, 0.25, context, "web-hub"); - fillShape(L.water, t, L.cx, L.cy, pond); + fillShape(L.water, t, L.cx, L.cy, pond, 0, 1, L.stretch); } for (int i = 0; i < n; ++i) { @@ -417,10 +469,10 @@ void furnishPads(Map &map, const Layout &L, GenerationContext &context) { return L.padOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; const KitFrame frame{int(std::lround(L.pads[team].x)), int(std::lround(L.pads[team].y)), L.padAngle[team]}; - plantKit(map, t, context, - {frame.at(-5, -6, 8), frame.at(-5, 6, 8), frame.at(8, 0, 6), kHomeWheat, - kHomeWood, 2}, - eligible); + plantKit( + map, t, context, + {frame.at(-5, -6, 8), frame.at(-5, 6, 8), frame.at(8, 0, 6), kHomeWheat, kHomeWood, 2}, + eligible); } } @@ -433,8 +485,8 @@ void stockWeb(Map &map, const Layout &L, GenerationContext &context, const Spide const int n = t.size(); const auto onThread = [&](int i) { return L.thread[i] && clearGround(map, i % t.w, i / t.w); }; for (const ShapePoint &knot : L.knots) - if (const int seed = seedNear(t, int(std::lround(knot.x)), int(std::lround(knot.y)), 3, - onThread); + if (const int seed = + seedNear(t, int(std::lround(knot.x)), int(std::lround(knot.y)), 3, onThread); seed >= 0) growPatch(map, t, seed, STONE, kKnotStoneTiles, onThread); @@ -457,20 +509,23 @@ void stockWeb(Map &map, const Layout &L, GenerationContext &context, const Spide if (scaledCount(1, o.fruit) > 0) for (int f = 0; f < 3; ++f) { - const ShapePoint p = polarPoint(L.cx, L.cy, rho, spin + 2 * kPi * f / 3); - if (const int seed = seedNear(t, int(std::lround(p.x)), int(std::lround(p.y)), 6, onHub); + const ShapePoint p = + L.stretch.apply(L.cx, L.cy, polarPoint(L.cx, L.cy, rho, spin + 2 * kPi * f / 3)); + if (const int seed = + seedNear(t, int(std::lround(p.x)), int(std::lround(p.y)), 6, onHub); seed >= 0) placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); } if (scaledCount(1, o.stone) > 0) { - const ShapePoint p = polarPoint(L.cx, L.cy, rho + 2, spin + kPi / 3); + const ShapePoint p = + L.stretch.apply(L.cx, L.cy, polarPoint(L.cx, L.cy, rho + 2, spin + kPi / 3)); if (const int seed = seedNear(t, int(std::lround(p.x)), int(std::lround(p.y)), 6, onHub); seed >= 0) placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); } - const WedgeFrame wedges(t, L.phase, L.g.teams); + const WedgeFrame wedges(t, L.phase, L.g.teams, L.stretch); const PeriodicNoise patch(t.w, t.h, 12, context.stream("web-patch")); const PeriodicNoise split(t.w, t.h, 7, context.stream("web-split")); std::vector> ranked; @@ -515,6 +570,31 @@ bool generate(Game &game, GenerationContext &context) for (int i = 0; i < n; ++i) if (L.water[i]) terrain[i] = WATER; + // A few dry patches along the threads, sampled in the wedge frame so every colony's stretch of + // web gets the same ones; never on the pads, the hub or the dew drops. + { + const WedgeFrame wedges(t, L.phase, L.g.teams, L.stretch); + const PeriodicNoise dry(t.w, t.h, kSandCell, context.stream("web-sand")); + sprinkleSand(terrain, t, L.thread, kSandShare, kSandInland, + [&](int i) + { + const WedgeFrame::Cell cell = wedges.cell(i % t.w, i / t.w); + return dry.at(cell.s, cell.d); + }); + } + // The sand roads: a line of sand one tile thick (tracePath, the thinnest there is) down the + // middle of every spoke and whole thread, off the pads and the dew drops, so the homes keep + // their building room. Every tile touching a sand corner loses the pure grass a deposit or a + // building needs, so nothing can grow or be built across a thread and close it. + if (o.sandRoads) + { + std::vector road(n, 0); + for (const std::vector &line : L.roads) + tracePath(road, t, line); + for (int i = 0; i < n; ++i) + if (road[i] && terrain[i] == GRASS && L.padOf[i] < 0 && L.dewOf[i] < 0) + terrain[i] = SAND; + } layBeaches(terrain, t); writeUndermap(map, terrain); @@ -539,7 +619,13 @@ bool generate(Game &game, GenerationContext &context) context.stage = "web resources"; furnishPads(map, L, context); stockWeb(map, L, context, o); - seedAlgae(map, context, t, "web-algae", o.algae, AlgaeBand::shallows(2, 6, 45)); + // Algae only grows in water with solid sand within reach (see algaeGrowthChance), so it is + // counted over the shallows but seeded on each colony's best-growing water, the same number of + // clumps in every wedge. + const WedgeFrame algaeWedges(t, L.phase, L.g.teams, L.stretch); + seedAlgae(map, context, t, "web-algae", o.algae, + AlgaeBand::shallows(1, 10, kAlgaeTilesPerClump).thriving(kAlgaeBestShare), + &algaeWedges); secureStartingCrops(game, context, t); // Beaches keep the threads walkable, but a small hub can be stocked shut: keep a way open from @@ -601,7 +687,9 @@ SpiderWebOptions::SpiderWebOptions(const GenerationRequest &r) : spokes(r.option("spokes")), ringSpacing(r.option("ring-spacing")), threadWidth(r.option("thread-width")), sag(r.option("sag")), tornStrands(r.option("torn-strands")), hubSize(r.option("hub-size")), - dewDrops(r.option("dew-drops")), spiral(r.option("spiral") != 0), + dewDrops(r.option("dew-drops")), homeSize(r.option("home-size")), + spiral(r.option("spiral") != 0), + sandRoads(r.option("sand-roads") != 0), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), stone(r.option("stone-amount")), algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) @@ -614,7 +702,7 @@ GeneratorDefinition spiderWebDefinition() "spider-web", 20, "Spider web", - 1, + 2, false, // Spokes per colony (more on a web with few colonies); the spacing between capture threads // and every thread's width in tiles; how far the capture threads sag and the share of them @@ -626,8 +714,12 @@ GeneratorDefinition spiderWebDefinition() {"torn-strands", "Torn strands", 0, 60, 5, 15, ControlGroup::Terrain}, {"hub-size", "Hub size", 8, 24, 1, 14, ControlGroup::Terrain}, {"dew-drops", "Dew drops", 0, 4, 1, 2, ControlGroup::Layout}, + // Each home pad's radius as a percentage of the standard pad. + {"home-size", "Home size", 60, 200, 10, 130, ControlGroup::Layout}, // Off, the capture threads are closed rings rather than a spiral. GeneratorControl::toggle("spiral", "Spiral", true, ControlGroup::Layout), + // Off, the threads are grass from shore to shore, with no sand road down the middle. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), // The threads' standing wheat and wood, the knots' stone and the drops' and hub's prizes; // every pad's kit is unscaled. GeneratorControl::percentage("wheat-amount", "Wheat amount"), diff --git a/src/map/generator/generators/SpiderWebGenerator.h b/src/map/generator/generators/SpiderWebGenerator.h index 238497e77..5653fe712 100644 --- a/src/map/generator/generators/SpiderWebGenerator.h +++ b/src/map/generator/generators/SpiderWebGenerator.h @@ -4,8 +4,8 @@ #include "GeneratorDefinition.h" struct SpiderWebOptions { - int spokes, ringSpacing, threadWidth, sag, tornStrands, hubSize, dewDrops; - bool spiral; + int spokes, ringSpacing, threadWidth, sag, tornStrands, hubSize, dewDrops, homeSize; + bool spiral, sandRoads; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit SpiderWebOptions(const GenerationRequest &r); }; diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index 94c19657f..c4d6f951f 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -112,6 +112,9 @@ struct Layout Geometry g{}; int cx = 0, cy = 0; double phase = 0; + // The flats are designed round the centre in a circle on the map's shorter side; this places + // them on the map, stretched along the longer side so the islands fill a rectangular map. + Stretch stretch; std::vector blobs; // in the wedge's frame std::vector homeX, homeY; // every home island's centre std::vector pondX, pondY; // and its pond's @@ -133,6 +136,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const TidalFlatsOptions o(request); L.cx = t.w / 2; L.cy = t.h / 2; + L.stretch = Stretch::toFill(t.w, t.h); L.phase = context.bounded("flats-layout", 3600) / 3600.0 * 2 * kPi; const double wedge = 2 * kPi / teams; const double arcHalfAt = [&](double d) { return teams < 2 ? kPi * d : kPi * d / teams; }(1.0); @@ -206,15 +210,15 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (int k = 0; k < teams; ++k) { const double a = L.phase + wedge * (k + 0.5); - L.homeX[k] = t.x(L.cx + int(std::lround(g.homeRing * std::cos(a)))); - L.homeY[k] = t.y(L.cy + int(std::lround(g.homeRing * std::sin(a)))); + L.homeX[k] = t.x(L.cx + int(std::lround(g.homeRing * std::cos(a) * L.stretch.sx))); + L.homeY[k] = t.y(L.cy + int(std::lround(g.homeRing * std::sin(a) * L.stretch.sy))); L.pondX[k] = L.homeX[k]; L.pondY[k] = L.homeY[k]; } const double pondRadius = std::clamp(0.2 * g.homeRadius, 3.0, 6.0); const RadialShape pond(pondRadius, 0.3, context, "flats-ponds"); const RadialShape oasis(2.5, 0.3, context, "flats-ponds"); - const WedgeFrame frame(t, L.phase, teams); + const WedgeFrame frame(t, L.phase, teams, L.stretch); for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) { @@ -375,16 +379,21 @@ void stockIslands(Map &map, const Layout &L, GenerationContext &context, const T for (int f = 0; f < 3; ++f) { const double a = spin + 2 * kPi * f / 3; - const int seed = seedNear(t, L.cx + int(std::lround(rho * std::cos(a))), - L.cy + int(std::lround(rho * std::sin(a))), 8, eligible); + const int seed = + seedNear(t, L.cx + int(std::lround(rho * std::cos(a) * L.stretch.sx)), + L.cy + int(std::lround(rho * std::sin(a) * L.stretch.sy)), 8, + eligible); if (seed >= 0) placeResourceClump(map, context, {seed % t.w, seed / t.w}, CHERRY + f, 2); } if (scaledCount(1, o.stone) > 0) if (const int seed = seedNear( - t, L.cx + int(std::lround((L.g.centralRadius - 4) * std::cos(spin + kPi / 3))), - L.cy + int(std::lround((L.g.centralRadius - 4) * std::sin(spin + kPi / 3))), 8, - eligible); + t, + L.cx + int(std::lround((L.g.centralRadius - 4) * std::cos(spin + kPi / 3) * + L.stretch.sx)), + L.cy + int(std::lround((L.g.centralRadius - 4) * std::sin(spin + kPi / 3) * + L.stretch.sy)), + 8, eligible); seed >= 0) placeResourceClump(map, context, {seed % t.w, seed / t.w}, STONE, 2); } @@ -512,7 +521,7 @@ GeneratorDefinition tidalFlatsDefinition() "tidal-flats", 18, "Tidal flats", - 4, + 5, false, // The home islands' radius as a share of the half side; extra islands, sandbars and // lagoons per colony; tide pools per 128x128 of flats. diff --git a/src/map/generator/shared/Drawing.cpp b/src/map/generator/shared/Drawing.cpp index b2bb6d06f..a0fa0b7c7 100644 --- a/src/map/generator/shared/Drawing.cpp +++ b/src/map/generator/shared/Drawing.cpp @@ -42,6 +42,39 @@ void strokePath(std::vector &mask, const Torus &t, strokeSegment(mask, t, path.back(), path.front(), value); } +void tracePath(std::vector &mask, const Torus &t, + const std::vector &path, unsigned char value) +{ + for (size_t i = 0; i < path.size(); ++i) + { + int x0 = int(std::lround(path[i].x)), y0 = int(std::lround(path[i].y)); + if (i + 1 == path.size()) + { + mask[t.at(x0, y0)] = value; + break; + } + const int x1 = int(std::lround(path[i + 1].x)), y1 = int(std::lround(path[i + 1].y)); + const int dx = std::abs(x1 - x0), dy = -std::abs(y1 - y0); + const int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1; + int error = dx + dy; + while (x0 != x1 || y0 != y1) + { + mask[t.at(x0, y0)] = value; + const int twice = 2 * error; + if (twice >= dy) + { + error += dy; + x0 += sx; + } + if (twice <= dx) + { + error += dx; + y0 += sy; + } + } + } +} + std::vector bezierPath(ShapePoint from, ShapePoint control, ShapePoint to, double fromHalfWidth, double toHalfWidth, int segments) { @@ -56,4 +89,97 @@ std::vector bezierPath(ShapePoint from, ShapePoint control, ShapePo } return path; } +std::vector bentPath(ShapePoint from, double heading, double length, double bend, + double fromHalfWidth, double toHalfWidth, int segments) +{ + const double ux = std::cos(heading), uy = std::sin(heading); + const ShapePoint to{from.x + length * ux, from.y + length * uy}; + // A quadratic Bezier's middle sits halfway between the chord's middle and its control point, + // so the control goes twice the bow off the chord, to the left of the heading (-uy, ux). + const double off = 2 * bend * length; + const ShapePoint control{(from.x + to.x) / 2 - off * uy, (from.y + to.y) / 2 + off * ux}; + return bezierPath(from, control, to, fromHalfWidth, toHalfWidth, segments); +} + +namespace +{ +// The distance from a point to a path's centre line, less the path's half width at the nearest +// point on it. +double gapToPath(double x, double y, const std::vector &path) +{ + double best = INFINITY; + if (path.size() == 1) + return std::hypot(x - path[0].x, y - path[0].y) - path[0].halfWidth; + for (size_t i = 0; i + 1 < path.size(); ++i) + { + const StrokePoint &a = path[i], &b = path[i + 1]; + const double ux = b.x - a.x, uy = b.y - a.y, length2 = ux * ux + uy * uy; + const double along = + length2 > 0 ? std::clamp(((x - a.x) * ux + (y - a.y) * uy) / length2, 0.0, 1.0) : 0.0; + const double gap = std::hypot(x - a.x - along * ux, y - a.y - along * uy) - + (a.halfWidth + along * (b.halfWidth - a.halfWidth)); + best = std::min(best, gap); + } + return best; +} +} // namespace + +double pathClearance(const std::vector &a, const std::vector &b, + double skip) +{ + // Both paths are resampled at least once a tile, so measuring from every point of each to the + // other's centre line cannot miss a long segment passing between two points. The part of `a` + // near its root is dropped before measuring in either direction. + const auto dense = [](const std::vector &path) + { + std::vector points; + if (path.empty()) + return points; + points.push_back(path.front()); + for (size_t i = 0; i + 1 < path.size(); ++i) + { + const StrokePoint &p = path[i], &q = path[i + 1]; + const int steps = std::max(1, int(std::ceil(std::hypot(q.x - p.x, q.y - p.y)))); + for (int k = 1; k <= steps; ++k) + { + const double f = double(k) / steps; + points.push_back({p.x + f * (q.x - p.x), p.y + f * (q.y - p.y), + p.halfWidth + f * (q.halfWidth - p.halfWidth)}); + } + } + return points; + }; + std::vector measured; + if (!a.empty()) + for (const StrokePoint &p : dense(a)) + if (std::hypot(p.x - a.front().x, p.y - a.front().y) >= skip) + measured.push_back(p); + if (measured.empty() || b.empty()) + return INFINITY; + double best = INFINITY; + for (const StrokePoint &p : measured) + best = std::min(best, gapToPath(p.x, p.y, b) - p.halfWidth); + for (const StrokePoint &p : dense(b)) + best = std::min(best, gapToPath(p.x, p.y, measured) - p.halfWidth); + return best; +} + +PathBounds pathBounds(const std::vector &path) +{ + if (path.empty()) + return {0, 0, 0}; + double x0 = path[0].x, x1 = x0, y0 = path[0].y, y1 = y0; + for (const StrokePoint &p : path) + { + x0 = std::min(x0, p.x); + x1 = std::max(x1, p.x); + y0 = std::min(y0, p.y); + y1 = std::max(y1, p.y); + } + PathBounds bounds{(x0 + x1) / 2, (y0 + y1) / 2, 0}; + for (const StrokePoint &p : path) + bounds.radius = + std::max(bounds.radius, std::hypot(p.x - bounds.x, p.y - bounds.y) + p.halfWidth); + return bounds; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Drawing.h b/src/map/generator/shared/Drawing.h index 14885c1de..2aa943cad 100644 --- a/src/map/generator/shared/Drawing.h +++ b/src/map/generator/shared/Drawing.h @@ -2,6 +2,7 @@ #pragma once #include "Geometry.h" #include "Grid.h" +#include #include #include namespace MapGeneration @@ -29,34 +30,192 @@ inline ShapePoint polarPoint(double cx, double cy, double radius, double angle) void strokePath(std::vector &mask, const Torus &, const std::vector &, unsigned char value = 1, bool closed = false); +/// Sets `value` on a line one tile thick through the path's points: each segment is traced from +/// its rounded ends with Bresenham's steps, so consecutive tiles always touch, at least at a corner, +/// and the line has no gaps. Half widths are ignored. The thinnest stroke there is: a sand road +/// down the middle of land that must stay open. +void tracePath(std::vector &mask, const Torus &, const std::vector &, + unsigned char value = 1); + /// `segments` + 1 points along the quadratic Bezier from `from` through the pull of `control` /// to `to`, with the half width running linearly from one end's to the other's: a thread that /// sags or bows. std::vector bezierPath(ShapePoint from, ShapePoint control, ShapePoint to, double fromHalfWidth, double toHalfWidth, int segments); +/// `segments` + 1 points along a path `length` tiles long leaving `from` along `heading` +/// (radians), bowing sideways by `bend` times its length at its middle (positive bows to the +/// left of the heading), its half width running from `fromHalfWidth` to `toHalfWidth`: a branch, +/// a twig or a root. A quadratic Bezier whose control point sits off the chord's middle. +std::vector bentPath(ShapePoint from, double heading, double length, double bend, + double fromHalfWidth, double toHalfWidth, int segments); + +/// The narrowest gap between two stroked paths: the least distance from any point of either to +/// the other's centre line, less both half widths there. Negative where they overlap. Points of +/// `a` nearer than `skip` tiles to its first point are ignored, so a branch can be measured +/// against the land it grows out of without its own root counting as a collision. The paths are +/// compared as given, without the wrap. +double pathClearance(const std::vector &a, const std::vector &b, + double skip = 0); + +/// The bounding circle of a path's stroke, for cheap early outs before pathClearance. +struct PathBounds +{ + double x, y, radius; +}; +PathBounds pathBounds(const std::vector &); + +/// One branch of a tree grown by forking (growBranches): its stroked path, the index of the +/// branch it forked from (-1 for the root) and how many forks separate it from the root. +struct Branch +{ + std::vector path; + int parent, depth; + PathBounds bounds; + double heading; // at its tip + bool leaf = true; // no child was accepted +}; + +/// How a tree forks. Every fork splits a tip in two, one child either side of the parent's +/// heading, each turned by `spread` radians give or take `spreadJitter` of that; children are +/// `lengthRatio` and `widthRatio` of their parent, lengths varied by `lengthJitter`, and each +/// bows by up to `bend` of its length. A child shorter than `minimumLength` is never proposed, +/// so a tree stops by itself as its twigs shrink. +struct ForkStyle +{ + double spread = 0.5, spreadJitter = 0.3; + double lengthRatio = 0.7, lengthJitter = 0.25, widthRatio = 0.75; + double bend = 0.1, minimumLength = 4, minimumHalfWidth = 1; +}; + +/// Grows a forking tree from one branch, a whole level at a time, and returns how many branches it +/// added. +/// +/// The root runs `length` tiles from `from` along `heading`; at its tip it forks in two, and each +/// child forks again until `forks` levels are spent or the twigs fall below the style's minimum. +/// Every branch is offered to `accept(path, parentIndex)` before it is kept, so the caller decides +/// what may grow: a tree that must keep water between its twigs and other land refuses the ones +/// that come too close. A refused branch is offered once more at half its length before it and +/// everything that would have grown from it are dropped, which lets a tree fill a tight space +/// with stubs rather than leave it bare. +/// +/// Growth is breadth first: every branch of one level is offered before any of the next. Grown +/// depth first, the first child's whole subtree would claim the room before its sibling had grown +/// at all, and a tree whose twigs compete for space would come out lopsided; level by level, the +/// two sides of every fork compete on equal terms and the tree fans out evenly. +/// +/// `roll()` returns a number in [0, 1). Each branch draws its bend and both children's turns and +/// lengths when it is offered, so its retry at half length reuses them; the rolls of a refused +/// branch's subtree are never drawn. Growth is deterministic - the same rolls and the same answers +/// from `accept` grow the same tree - so a generator that grows one tree and turns it round the map +/// gives every colony an identical copy. +template +int growBranches(std::vector &tree, int parent, ShapePoint from, double heading, + double length, double halfWidth, int forks, const ForkStyle &style, Roll &roll, + Accept &accept) +{ + // A branch waiting to be offered: where it starts and how it runs. + struct Sprout + { + int parent; + ShapePoint from; + double heading, length, halfWidth; + int forks; + }; + std::vector queue{{parent, from, heading, length, halfWidth, forks}}; + int added = 0; + for (size_t head = 0; head < queue.size(); ++head) + { + const Sprout sprout = queue[head]; + const double tipHalfWidth = + std::max(style.minimumHalfWidth, sprout.halfWidth * style.widthRatio); + const double bend = (2 * roll() - 1) * style.bend; + double turns[2], lengths[2]; + for (int side = 0; side < 2; ++side) + { + const double turn = style.spread * (1 + (2 * roll() - 1) * style.spreadJitter); + const double stretch = 1 + (2 * roll() - 1) * style.lengthJitter; + turns[side] = (side ? 1 : -1) * turn; + lengths[side] = sprout.length * style.lengthRatio * stretch; + } + for (const double share : {1.0, 0.5}) + { + const double reach = sprout.length * share; + if (reach < style.minimumLength) + break; + const int segments = std::max(2, int(std::ceil(reach / 3))); + std::vector path = bentPath(sprout.from, sprout.heading, reach, bend, + sprout.halfWidth, tipHalfWidth, segments); + if (!accept(path, sprout.parent)) + continue; + const StrokePoint tip = path.back(), before = path[path.size() - 2]; + const int self = int(tree.size()); + const int depth = sprout.parent < 0 ? 0 : tree[sprout.parent].depth + 1; + const PathBounds bounds = pathBounds(path); + const double tipHeading = std::atan2(tip.y - before.y, tip.x - before.x); + tree.push_back({std::move(path), sprout.parent, depth, bounds, tipHeading}); + if (sprout.parent >= 0) + tree[sprout.parent].leaf = false; + ++added; + if (sprout.forks > 0) + for (int side = 0; side < 2; ++side) + if (lengths[side] * share >= style.minimumLength) + queue.push_back({self, + {tip.x, tip.y}, + tipHeading + turns[side], + lengths[side] * share, + tipHalfWidth, + sprout.forks - 1}); + break; + } + } + return added; +} + /// Visits every tile a RadialShape centred at (cx, cy) and turned by `turn` radians covers, /// searching only its bounding box: `visit(tile, dx, dy)` with the tile's offset from the centre. /// Wrapped tiles are visited through the torus, each once while the shape is narrower than the -/// map. Turning one shape to each of several headings stamps the same outline round a centre. +/// map. Turning one shape to each of several headings stamps the same outline round a centre. With +/// a `stretch` the outline is stretched with it, so a round pad on a design becomes an oval on a +/// rectangular map. template void forEachTileInShape(const Torus &t, double cx, double cy, const RadialShape &shape, double turn, - Visit visit) + Visit visit, const Stretch &stretch = {}) { - const int reach = int(std::ceil(shape.maximumRadius())) + 1; + const int reachX = int(std::ceil(shape.maximumRadius() * stretch.sx)) + 1; + const int reachY = int(std::ceil(shape.maximumRadius() * stretch.sy)) + 1; const int x0 = int(std::lround(cx)), y0 = int(std::lround(cy)); - for (int y = y0 - reach; y <= y0 + reach; ++y) - for (int x = x0 - reach; x <= x0 + reach; ++x) + for (int y = y0 - reachY; y <= y0 + reachY; ++y) + for (int x = x0 - reachX; x <= x0 + reachX; ++x) { const double dx = x - cx, dy = y - cy; - if (std::hypot(dx, dy) < shape.radiusAt(std::atan2(dy, dx) - turn)) + const ShapePoint round = stretch.undo(dx, dy); + if (std::hypot(round.x, round.y) < shape.radiusAt(std::atan2(round.y, round.x) - turn)) visit(t.at(x, y), dx, dy); } } -/// Sets `value` on every tile of a RadialShape centred at (cx, cy), turned by `turn`. +/// Sets `value` on every tile of a RadialShape centred at (cx, cy), turned by `turn`, stretched by +/// `stretch`. inline void fillShape(std::vector &mask, const Torus &t, double cx, double cy, - const RadialShape &shape, double turn = 0, unsigned char value = 1) + const RadialShape &shape, double turn = 0, unsigned char value = 1, + const Stretch &stretch = {}) +{ + forEachTileInShape( + t, cx, cy, shape, turn, [&](int i, double, double) { mask[i] = value; }, stretch); +} + +/// A path laid out round (cx, cy) placed on the map by `stretch`: its points move, its half widths +/// stay in tiles. +inline std::vector stretchPath(const std::vector &path, double cx, + double cy, const Stretch &stretch) { - forEachTileInShape(t, cx, cy, shape, turn, [&](int i, double, double) { mask[i] = value; }); + std::vector placed; + placed.reserve(path.size()); + for (const StrokePoint &p : path) + { + const ShapePoint q = stretch.apply(cx, cy, {p.x, p.y}); + placed.push_back({q.x, q.y, p.halfWidth}); + } + return placed; } } // namespace MapGeneration diff --git a/src/map/generator/shared/Geometry.h b/src/map/generator/shared/Geometry.h index 98f09f71c..c7eef2059 100644 --- a/src/map/generator/shared/Geometry.h +++ b/src/map/generator/shared/Geometry.h @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include +#include #include #include #include @@ -11,6 +13,42 @@ struct ShapePoint { double x, y; }; + +/// How a layout designed round a map's centre, in a circle on the map's shorter side, fills a +/// rectangular map: offsets from the centre are scaled by `sx` across and `sy` down, so the circle +/// becomes an ellipse touching all four sides instead of leaving the long ends empty. Widths drawn +/// round a stretched point (a thread, a branch, a beach) stay in tiles, so land is no fatter along +/// the long axis. On a square map both scales are exactly 1 and nothing moves. +struct Stretch +{ + double sx = 1, sy = 1; + /// The stretch that fills a map of this size. + static Stretch toFill(int width, int height) + { + const double shorter = std::min(width, height); + return {width / shorter, height / shorter}; + } + /// Design point `p`, laid out round (cx, cy), placed on the map. + ShapePoint apply(double cx, double cy, ShapePoint p) const + { + // Unstretched points are returned untouched rather than moved out and back, which could + // change a square map's arithmetic in the last bit. + if (sx == 1 && sy == 1) + return p; + return {cx + (p.x - cx) * sx, cy + (p.y - cy) * sy}; + } + /// A map offset from the centre, back in the design's round frame. + ShapePoint undo(double dx, double dy) const { return {dx / sx, dy / sy}; } + /// The map heading of a design heading, once stretched. + double heading(double angle) const + { + // Unstretched, the heading is returned as it is, not rebuilt through atan2, so a square + // map's trigonometry is untouched to the last bit. + return sx == sy ? angle : std::atan2(std::sin(angle) * sy, std::cos(angle) * sx); + } + /// The larger of the two scales: how much farther any design distance may reach on the map. + double longest() const { return std::max(sx, sy); } +}; // Area-preserving stretch and rotation. Geometry stays independent of terrain and wrapping. class ShapeTransform { diff --git a/src/map/generator/shared/Planting.cpp b/src/map/generator/shared/Planting.cpp index 0f1076894..24b295354 100644 --- a/src/map/generator/shared/Planting.cpp +++ b/src/map/generator/shared/Planting.cpp @@ -2,6 +2,9 @@ #include "Planting.h" #include "GenerationContext.h" #include "Resources.h" +#include "Wedge.h" +#include +#include namespace MapGeneration { bool clearGround(const Map &map, int x, int y) @@ -34,8 +37,65 @@ void clearAroundSwarms(Map &map, const GenerationContext &context, const Torus & } } +namespace +{ +// Map::growResources' algae test, evaluated exactly for chosen tiles. Each offset is the difference +// of two independent draws of 0 to 15: -15 to 15, weighted 16 - |d| out of 256. +class AlgaeGrowth +{ + public: + AlgaeGrowth(const Map &map, const Torus &t) : t(t), water(t.size(), 0), sand(t.size(), 0) + { + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + water[y * t.w + x] = map.isWater(x, y); + sand[y * t.w + x] = map.isSand(x, y); + } + for (int d = -15; d <= 15; ++d) + weight[d + 15] = (16 - std::abs(d)) / 256.0; + // The sand the test looks for lies within 30 tiles; beyond that the chance is 0. + toSand = stepsFrom(t, sand); + } + double at(int i) const + { + if (!water[i] || toSand[i] < 0 || toSand[i] > 30) + return 0; + // Map sizes are powers of two, so wrapping is a mask; this loop runs 961 times a tile. + const int x = i % t.w, y = i / t.w, wm = t.w - 1, hm = t.h - 1; + double sum = 0; + for (int dy = -15; dy <= 15; ++dy) + { + const int waterRow = ((y + dy) & hm) * t.w, sandColumn = (x + 2 * dy) & wm; + double row = 0; + for (int dx = -15; dx <= 15; ++dx) + if (water[waterRow + ((x + dx) & wm)] && + sand[((y + 2 * dx) & hm) * t.w + sandColumn]) + row += weight[dx + 15]; + sum += row * weight[dy + 15]; + } + return sum; + } + + private: + const Torus &t; + std::vector water, sand; + std::vector toSand; + double weight[31]; +}; +} // namespace + +std::vector algaeGrowthChance(const Map &map, const Torus &t) +{ + const AlgaeGrowth growth(map, t); + std::vector chance(t.size(), 0.0); + for (int i = 0; i < t.size(); ++i) + chance[i] = growth.at(i); + return chance; +} + void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char *stream, - int algaePercent, const AlgaeBand &band) + int algaePercent, const AlgaeBand &band, const WedgeFrame *wedges) { const int n = t.w * t.h; std::vector water; @@ -61,10 +121,39 @@ void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char } if (water.empty()) return; - for (int clump = 0; clump < scaledCount(int(water.size()) / band.tilesPerClump, algaePercent); - ++clump) - placeResourceClump(map, context, water[context.bounded(stream, water.size())], ALGA, - band.clumpRadius); + // The count follows the whole band. With a best share, the clumps then go only on the water + // in it where algae regrows most readily, so the same amount of algae sits where it lasts. + const int clumps = scaledCount(int(water.size()) / band.tilesPerClump, algaePercent); + if (band.bestShare <= 0 && !wedges) + { + for (int clump = 0; clump < clumps; ++clump) + placeResourceClump(map, context, water[context.bounded(stream, water.size())], ALGA, + band.clumpRadius); + return; + } + // Only the band's own tiles are measured: the test costs 961 lookups a tile. + const AlgaeGrowth growth(map, t); + const int groups = wedges ? wedges->teams : 1; + std::vector>> byGroup(groups); + for (const MapGeneratorPoint &p : water) + { + const int i = p.y * t.w + p.x; + byGroup[wedges ? wedges->cell(p.x, p.y).k : 0].push_back( + {band.bestShare > 0 ? -growth.at(i) : 0.0, i}); + } + for (auto &group : byGroup) + { + if (group.empty()) + continue; + std::stable_sort(group.begin(), group.end()); + if (band.bestShare > 0) + group.resize(std::max(1, size_t(std::lround(group.size() * band.bestShare)))); + for (int clump = 0; clump < clumps / groups; ++clump) + { + const int i = group[context.bounded(stream, group.size())].second; + placeResourceClump(map, context, {i % t.w, i / t.w}, ALGA, band.clumpRadius); + } + } } void stockIslands(Map &map, GenerationContext &context, const std::vector &islands, diff --git a/src/map/generator/shared/Planting.h b/src/map/generator/shared/Planting.h index 05262f0d1..95ef8293c 100644 --- a/src/map/generator/shared/Planting.h +++ b/src/map/generator/shared/Planting.h @@ -13,6 +13,7 @@ struct GenerationContext; namespace MapGeneration { +struct WedgeFrame; // Deposits placed by hand: starter kits, prizes, algae. These are the primitives a generator's // own resource layout is built from; scatterResources (Resources.h) is the ambient layer that // fills in between them. @@ -168,6 +169,13 @@ std::vector swarmSurroundings(const Torus &, const GenerationCont void clearAroundSwarms(Map &, const GenerationContext &, const Torus &, const std::vector *keep = nullptr); +/// The chance, per water tile, that algae there passes Map::growResources' test to grow or spread +/// when the engine visits it. The engine draws an offset of up to 15 tiles each way (the difference +/// of two draws of 0 to 15) and grows the algae only if the tile at that offset is water and the +/// tile at the offset turned a quarter and doubled is sand. So algae thrives in water near beaches +/// and sand, and algae with no sand within 30 tiles never grows at all. Land tiles get 0. +std::vector algaeGrowthChance(const Map &, const Torus &); + /// Where algae goes: any water, or only water this many steps offshore. struct AlgaeBand { @@ -175,6 +183,16 @@ struct AlgaeBand /// One clump per this many eligible water tiles at an amount of 100, each this big. int tilesPerClump = 40; int clumpRadius = 1; + /// Above 0, the clumps counted over the whole band all go on this share of it where + /// algaeGrowthChance is highest, so they sit where algae can regrow. + double bestShare = 0; + /// The same band, with its clumps placed on its best-growing `share` of water. + AlgaeBand thriving(double share) const + { + AlgaeBand band = *this; + band.bestShare = share; + return band; + } static AlgaeBand anyWater(int tilesPerClump = 40) { return {-1, -1, tilesPerClump, 1}; } static AlgaeBand shallows(int nearest, int farthest, int tilesPerClump = 90) { @@ -182,9 +200,12 @@ struct AlgaeBand } }; -/// Algae clumps over the band, scaled by the amount. +/// Algae clumps over the band, scaled by the amount. With a wedge frame the clumps are shared out +/// equally between the colonies' wedges, each wedge's placed on its own best-growing water: the +/// engine's growth test is not symmetric under rotation (it turns its offset by reflecting it), so +/// one best share taken over the whole map would favour some wedges' water over others'. void seedAlgae(Map &, GenerationContext &, const Torus &, const char *stream, int algaePercent, - const AlgaeBand &); + const AlgaeBand &, const WedgeFrame *wedges = nullptr); /// Each island carries one themed prize at its middle: stone, a fruit or wheat, so finding one /// feels like a distinct find rather than an interchangeable resource dump. diff --git a/src/map/generator/shared/Sketch.h b/src/map/generator/shared/Sketch.h index 7fd66edce..d76020e91 100644 --- a/src/map/generator/shared/Sketch.h +++ b/src/map/generator/shared/Sketch.h @@ -2,6 +2,9 @@ #pragma once #include "Grid.h" #include "TerrainType.h" +#include +#include +#include #include class Map; struct GenerationContext; @@ -23,6 +26,31 @@ void writeUndermap(Map &, const TerrainSketch &); int countTiles(const TerrainSketch &, TerrainType); +/// Decorative patches of sand inside the land, away from any shore: of the grass tiles `eligible` +/// allows that lie at least `inland` steps from water, the `share` (0 to 1) with the highest +/// `noiseAt(tile)` turn to sand. Sampling smooth noise gives a few rounded patches rather than +/// speckle, and `inland` keeps a strip of grass between every patch and its beach, so the land +/// still reads as grass with the odd dry clearing. A generator that stamps one design into every +/// colony's wedge samples its noise in the wedge frame, so every colony gets the same patches. +/// Sand holds no deposit and no building, so patches are laid before resources and kept small. +template +void sprinkleSand(TerrainSketch &sketch, const Torus &t, const std::vector &eligible, + double share, int inland, NoiseAt noiseAt) +{ + std::vector water(sketch.size(), 0); + for (size_t i = 0; i < sketch.size(); ++i) + water[i] = sketch[i] == WATER; + const std::vector steps = stepsFrom(t, water); + std::vector> ranked; + for (int i = 0; i < t.size(); ++i) + if (eligible[i] && sketch[i] == GRASS && steps[i] >= inland) + ranked.push_back({-noiseAt(i), i}); + std::stable_sort(ranked.begin(), ranked.end()); + const size_t patches = size_t(std::lround(ranked.size() * std::clamp(share, 0.0, 1.0))); + for (size_t k = 0; k < patches; ++k) + sketch[ranked[k].second] = SAND; +} + /// A small island raised out in open water: its centre, how far its outline can reach, and /// every tile it covers. struct Island diff --git a/src/map/generator/shared/Wedge.h b/src/map/generator/shared/Wedge.h index 669d76f59..2693da1f5 100644 --- a/src/map/generator/shared/Wedge.h +++ b/src/map/generator/shared/Wedge.h @@ -17,30 +17,35 @@ struct WedgeFrame double phase; // where wedge 0 begins, in radians int teams; double wedge; // each wedge's angle + // How the round design is stretched to fill the map; cells are measured unstretched. + Stretch stretch; - WedgeFrame(const Torus &torus, double phase, int teams) + WedgeFrame(const Torus &torus, double phase, int teams, Stretch stretch = {}) : t(torus), cx(torus.w / 2), cy(torus.h / 2), phase(phase), teams(teams), - wedge(2 * kPi / teams) + wedge(2 * kPi / teams), stretch(stretch) { } /// A tile seen from the frame. struct Cell { - int dx, dy; // offset from the centre, the short way round - double d, theta; // distance and angle from the centre - double turn; // angle past the phase, in [0, 2 pi) - int k; // which wedge - double u; // how far round the wedge, 0 at one edge and 1 at the other - double s; // arc offset from the wedge's middle, in tiles + int dx, dy; // offset from the centre on the map, the short way round + double d, theta; // distance and angle from the centre, unstretched + double turn; // angle past the phase, in [0, 2 pi) + int k; // which wedge + double u; // how far round the wedge, 0 at one edge and 1 at the other + double s; // arc offset from the wedge's middle, in tiles }; Cell cell(int x, int y) const { Cell c; c.dx = t.offsetX(cx, x); c.dy = t.offsetY(cy, y); - c.d = std::hypot(double(c.dx), double(c.dy)); - c.theta = std::atan2(double(c.dy), double(c.dx)); + // Distance and angle are taken in the design's round frame, so a feature designed at a + // radius lands on the stretched ellipse; on a square map this divides by exactly 1. + const ShapePoint round = stretch.undo(c.dx, c.dy); + c.d = std::hypot(round.x, round.y); + c.theta = std::atan2(round.y, round.x); c.turn = std::fmod(c.theta - phase + 4 * kPi, 2 * kPi); place(c); return c; diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index b417eaa1d..06ed2c623 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -285,7 +285,7 @@ class MapGeneratorDefaultsTest screen.descriptor.option("test-gap") == 5 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 20); + assert(registry.selectionIndex(101) == 21); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); diff --git a/test/MapGeneratorGoldenTest.cpp b/test/MapGeneratorGoldenTest.cpp index 08f737c83..9f0b42121 100644 --- a/test/MapGeneratorGoldenTest.cpp +++ b/test/MapGeneratorGoldenTest.cpp @@ -139,7 +139,7 @@ Row roll(int id, int wDec, int hDec, int teams, std::uint32_t seed) } // The rows this platform keeps: every registered generator at its own defaults on the three -// lobby sizes, and at 256 with two and with eight colonies. +// lobby sizes and on a 512x256 rectangle, and at 256 with two and with eight colonies. std::vector goldenRows() { std::vector rows; @@ -151,6 +151,7 @@ std::vector goldenRows() rows.push_back(roll(id, 8, 8, defaults.nbTeams, seed)); rows.push_back(roll(id, 7, 7, defaults.nbTeams, 1)); rows.push_back(roll(id, 9, 9, defaults.nbTeams, 1)); + rows.push_back(roll(id, 9, 8, defaults.nbTeams, 1)); rows.push_back(roll(id, 8, 8, 2, 1)); rows.push_back(roll(id, 8, 8, 8, 1)); } diff --git a/test/MapGeneratorToolkitChecks.h b/test/MapGeneratorToolkitChecks.h index ff2ee7be0..1b99e7437 100644 --- a/test/MapGeneratorToolkitChecks.h +++ b/test/MapGeneratorToolkitChecks.h @@ -655,6 +655,161 @@ inline void drawingChecks() assert(upright != turned); } +// bentPath runs its length along its heading and bows to the left; tracePath is one tile thick and +// unbroken; pathClearance measures the water +// between two strokes and skips a branch's root; growBranches forks level by level to its depth, +// offers every branch to accept, retries a refused one at half length and grows the same tree from +// the same rolls. +inline void branchChecks() +{ + const std::vector straight = bentPath({0, 0}, 0, 20, 0, 3, 1, 10); + assert(straight.size() == 11 && std::abs(straight.back().x - 20) < 1e-9 && + std::abs(straight.back().y) < 1e-9 && std::abs(straight.back().halfWidth - 1) < 1e-9); + const std::vector bowed = bentPath({0, 0}, 0, 20, 0.1, 1, 1, 10); + assert(std::abs(bowed[5].y - 2) < 1e-9 && std::abs(bowed.back().x - 20) < 1e-9); + + const std::vector a{{0, 0, 1}, {20, 0, 1}}, b{{0, 6, 1}, {20, 6, 1}}; + assert(std::abs(pathClearance(a, b) - 4) < 1e-9 && std::abs(pathClearance(b, a) - 4) < 1e-9); + const std::vector crossing{{10, -5, 1}, {10, 5, 1}}; + assert(pathClearance(a, crossing) < 0); + const std::vector leaving{{10, 0, 1}, {10, 20, 1}}; + assert(pathClearance(leaving, a) < 0 && pathClearance(leaving, a, 4) > 1.9); + const std::vector dot{{10, 4, 1}}; + assert(std::abs(pathClearance(dot, a) - 2) < 1e-9); + // tracePath: one tile thick and unbroken, even along a shallow diagonal and across the wrap. + { + const Torus small(32, 16); + std::vector line(small.size(), 0); + tracePath(line, small, {{2, 3, 5}, {29, 11, 5}, {35, 12, 5}}); + for (int x = 2; x <= 29; ++x) + { + int column = 0; + for (int y = 0; y < small.h; ++y) + column += line[small.at(x, y)]; + assert(column >= 1 && column <= 2); + } + assert(line[small.at(2, 3)] && line[small.at(29, 11)] && line[small.at(3, 12)]); + assert(!line[small.at(15, 14)] && !line[small.at(10, 0)]); + } + const PathBounds bounds = pathBounds(a); + assert(std::abs(bounds.x - 10) < 1e-9 && std::abs(bounds.radius - 11) < 1e-9); + + ForkStyle style; + style.spreadJitter = 0; + style.lengthJitter = 0; + style.bend = 0; + style.minimumLength = 1; + const auto grow = [&](std::uint32_t seed, int forks, bool refuseSecond) + { + std::mt19937 random(seed); + const auto roll = [&] { return random() / 4294967296.0; }; + int offered = 0; + const auto accept = [&](const std::vector &, int) + { return !(refuseSecond && ++offered == 2); }; + std::vector tree; + growBranches(tree, -1, {0, 0}, 0, 16, 2, forks, style, roll, accept); + return tree; + }; + const std::vector full = grow(3, 2, false); + // Level by level: the root, its two children, then their four. + assert(full.size() == 7 && full[0].parent < 0 && !full[0].leaf && full[2].depth == 1 && + !full[2].leaf && full[3].depth == 2 && full[6].parent == 2 && full[6].leaf); + const std::vector again = grow(3, 2, false); + for (size_t i = 0; i < full.size(); ++i) + assert(full[i].path.back().x == again[i].path.back().x && + full[i].path.back().y == again[i].path.back().y); + // The first child is refused once and kept at half length, so its subtree is shorter too. + const std::vector retried = grow(3, 2, true); + assert(retried.size() == 7); + const auto length = [](const Branch &branch) + { + return std::hypot(branch.path.back().x - branch.path.front().x, + branch.path.back().y - branch.path.front().y); + }; + assert(std::abs(length(retried[1]) - length(full[1]) / 2) < 1e-6); +} + +// Stretch is exactly the identity on a square map and fills a rectangle as an ellipse; a stretched +// WedgeFrame measures cells in the round design frame; a stretched shape fill becomes an oval. +inline void stretchChecks() +{ + const Stretch square = Stretch::toFill(64, 64); + assert(square.sx == 1 && square.sy == 1 && square.heading(1.234) == 1.234); + const ShapePoint same = square.apply(32, 32, {10.1, 3.7}); + assert(same.x == 10.1 && same.y == 3.7); + const Stretch wide = Stretch::toFill(64, 16); + assert(wide.sx == 4 && wide.sy == 1 && wide.longest() == 4); + const ShapePoint far = wide.apply(32, 8, {40, 8}); + assert(far.x == 64 && far.y == 8); + assert(std::abs(wide.heading(kPi / 4) - std::atan2(1.0, 4.0)) < 1e-12); + + const Torus t(64, 16); + const WedgeFrame round(t, 0, 4, wide); + assert(std::abs(round.cell(32 + 20, 8).d - 5) < 1e-9); + assert(std::abs(round.cell(32, 8 + 5).d - 5) < 1e-9); + + GenerationRequest request; + request.seed = 3; + GenerationContext context(request); + const RadialShape disc(4, 0, context, "stretch"); + std::vector plain(t.size(), 0), oval(t.size(), 0); + fillShape(plain, t, 32, 8, disc); + fillShape(oval, t, 32, 8, disc, 0, 1, wide); + assert(plain[t.at(35, 8)] && !plain[t.at(40, 8)] && oval[t.at(45, 8)] && !oval[t.at(32, 12)]); + assert(std::count(oval.begin(), oval.end(), 1) > 3 * std::count(plain.begin(), plain.end(), 1)); +} + +// sprinkleSand turns only eligible inland grass, the noisiest share of it, and keeps a strip of +// grass by the shore; algaeGrowthChance is zero on land and far from sand, higher near sand, and +// matches the engine's offset weights exactly. +inline void dressingChecks() +{ + const Torus t(32, 32); + TerrainSketch sketch(t.size(), WATER); + for (int y = 4; y < 28; ++y) + for (int x = 4; x < 28; ++x) + sketch[t.at(x, y)] = GRASS; + std::vector eligible(t.size(), 1); + eligible[t.at(16, 16)] = 0; + sprinkleSand(sketch, t, eligible, 0.25, 3, [&](int i) { return double(i % t.w); }); + int sand = 0; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + if (sketch[t.at(x, y)] == SAND) + { + ++sand; + assert(x >= 21 && x <= 25 && y >= 6 && y <= 25 && !(x == 16 && y == 16)); + } + // Steps are Chebyshev: 20 by 20 tiles lie three or more from the water, less the one refused, + // and the quarter of them turned are the five highest columns. + assert(sand == int(std::lround((20 * 20 - 1) * 0.25)) && sketch[t.at(24, 12)] == SAND && + sketch[t.at(7, 12)] == GRASS); + + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus m(map); + for (int y = 0; y < m.h; ++y) + for (int x = 0; x < m.w; ++x) + map.setUMTerrain(x, y, x >= 40 && x < 48 ? SAND : WATER); + map.rebuildTerrain(); + const std::vector chance = algaeGrowthChance(map, m); + assert(chance[m.at(44, 10)] == 0); // sand, not water + assert(chance[m.at(36, 10)] > chance[m.at(20, 10)]); // nearer the sand grows faster + // A probability: the weights of all offsets sum to 1. + assert(chance[m.at(36, 10)] > 0 && chance[m.at(36, 10)] <= 1); + // Every tile of the sea sees the strip within 30 tiles, but no offset can reach sand from a + // map that has none. + for (int x = 0; x < m.w; ++x) + map.setUMTerrain(x, 5, WATER); + for (int y = 0; y < m.h; ++y) + for (int x = 40; x < 48; ++x) + map.setUMTerrain(x, y, WATER); + map.rebuildTerrain(); + const std::vector dry = algaeGrowthChance(map, m); + assert(std::count(dry.begin(), dry.end(), 0.0) == long(dry.size())); +} + // plantFields deals the preferred tiles, wheat first by the split key, in proportion; KitFrame // turns its offsets with its facing; scatterClumps stops after its attempts on ineligible ground. inline void layerChecks() @@ -706,8 +861,12 @@ inline void toolkitChecks() wedgeChecks(); shuffleChecks(); drawingChecks(); + branchChecks(); + stretchChecks(); + dressingChecks(); layerChecks(); puts("PASS shared toolkit: floods, sketch, planting, roads, settlements, balanced starts, " - "scatter, lattice noise, wedge frame, shuffle, drawing, fields and clumps"); + "scatter, lattice noise, wedge frame, shuffle, drawing, branches, stretch, sand patches, " + "algae growth, fields and clumps"); } } // namespace ToolkitChecks diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 10fcad79b..e3fa9907e 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -6,6 +6,7 @@ macos-arm64 0 1 8 8 4 1 ok 1882570766644760050 macos-arm64 0 1 8 8 4 2 ok 13897470516389625799 macos-arm64 0 1 8 8 4 3 ok 3342469568135303530 macos-arm64 0 1 8 8 8 1 ok 1882570766644760050 +macos-arm64 0 1 9 8 4 1 ok 5952847682975674388 macos-arm64 0 1 9 9 4 1 ok 9537656230795372076 macos-arm64 1 2 7 7 4 1 ok 2056248083838577766 macos-arm64 1 2 8 8 2 1 ok 4513806375703215835 @@ -13,6 +14,7 @@ macos-arm64 1 2 8 8 4 1 ok 15266936846292760467 macos-arm64 1 2 8 8 4 2 ok 14755082496502706273 macos-arm64 1 2 8 8 4 3 ok 7766703425182182688 macos-arm64 1 2 8 8 8 1 ok 15211747697609312326 +macos-arm64 1 2 9 8 4 1 ok 17904614402584393968 macos-arm64 1 2 9 9 4 1 ok 13823901920601588993 macos-arm64 2 2 7 7 4 1 ok 14759329394348325807 macos-arm64 2 2 8 8 2 1 ok 1334307081035450250 @@ -20,6 +22,7 @@ macos-arm64 2 2 8 8 4 1 ok 12800649820831069509 macos-arm64 2 2 8 8 4 2 ok 16247178421364950126 macos-arm64 2 2 8 8 4 3 ok 8829998086451332904 macos-arm64 2 2 8 8 8 1 ok 3393915502379318686 +macos-arm64 2 2 9 8 4 1 ok 3535127419635138437 macos-arm64 2 2 9 9 4 1 ok 11934512450853997163 macos-arm64 3 2 7 7 4 1 ok 8448347508778630572 macos-arm64 3 2 8 8 2 1 ok 18055009306025368179 @@ -27,6 +30,7 @@ macos-arm64 3 2 8 8 4 1 ok 9615729526066216419 macos-arm64 3 2 8 8 4 2 ok 8908276107237789891 macos-arm64 3 2 8 8 4 3 ok 7682200520761145636 macos-arm64 3 2 8 8 8 1 ok 16434396702117749324 +macos-arm64 3 2 9 8 4 1 ok 8810915806685158013 macos-arm64 3 2 9 9 4 1 ok 528287643730928710 macos-arm64 4 2 7 7 4 1 ok 11202276671897504324 macos-arm64 4 2 8 8 2 1 ok 9701754549885880600 @@ -34,6 +38,7 @@ macos-arm64 4 2 8 8 4 1 ok 11152758684431740930 macos-arm64 4 2 8 8 4 2 ok 12437321240535810528 macos-arm64 4 2 8 8 4 3 ok 9751465076302685550 macos-arm64 4 2 8 8 8 1 ok 12077514013485761639 +macos-arm64 4 2 9 8 4 1 ok 1365374750744909995 macos-arm64 4 2 9 9 4 1 ok 1854948760916881435 macos-arm64 5 1 7 7 4 1 ok 13850702411921678476 macos-arm64 5 1 8 8 2 1 ok 16875811593585503047 @@ -41,6 +46,7 @@ macos-arm64 5 1 8 8 4 1 ok 15081591080055302398 macos-arm64 5 1 8 8 4 2 ok 14333463520897961954 macos-arm64 5 1 8 8 4 3 ok 1927162392993477983 macos-arm64 5 1 8 8 8 1 ok 1417569272582492023 +macos-arm64 5 1 9 8 4 1 ok 12064970734501370021 macos-arm64 5 1 9 9 4 1 ok 6078319397555065000 macos-arm64 6 2 7 7 4 1 ok 10962798937752830582 macos-arm64 6 2 8 8 2 1 ok 13908422176345369582 @@ -48,6 +54,7 @@ macos-arm64 6 2 8 8 4 1 ok 12156463255653919960 macos-arm64 6 2 8 8 4 2 ok 8488290807097875252 macos-arm64 6 2 8 8 4 3 ok 13493861953378784496 macos-arm64 6 2 8 8 8 1 ok 5023916742560360564 +macos-arm64 6 2 9 8 4 1 ok 16957866786516727954 macos-arm64 6 2 9 9 4 1 ok 18148498138515235675 macos-arm64 7 2 7 7 4 1 ok 278576360011260021 macos-arm64 7 2 8 8 2 1 ok 8574781324117286872 @@ -55,6 +62,7 @@ macos-arm64 7 2 8 8 4 1 ok 15788142514004224693 macos-arm64 7 2 8 8 4 2 ok 5712377136822184348 macos-arm64 7 2 8 8 4 3 ok 13319374393828491671 macos-arm64 7 2 8 8 8 1 ok 8121618781306643342 +macos-arm64 7 2 9 8 4 1 ok 11480083053789646068 macos-arm64 7 2 9 9 4 1 ok 1708935503910148886 macos-arm64 8 1 7 7 4 1 ok 6473018063863562378 macos-arm64 8 1 8 8 2 1 ok 12651074798552895253 @@ -62,6 +70,7 @@ macos-arm64 8 1 8 8 4 1 ok 5644802038968384258 macos-arm64 8 1 8 8 4 2 ok 14702577180534027034 macos-arm64 8 1 8 8 4 3 ok 293881615974475555 macos-arm64 8 1 8 8 8 1 ok 13875648332759742026 +macos-arm64 8 1 9 8 4 1 ok 15251784855226955418 macos-arm64 8 1 9 9 4 1 ok 14481987140521717300 macos-arm64 9 2 7 7 4 1 ok 2277607816406551252 macos-arm64 9 2 8 8 2 1 ok 6984614415357599638 @@ -69,6 +78,7 @@ macos-arm64 9 2 8 8 4 1 ok 1045725267928572154 macos-arm64 9 2 8 8 4 2 ok 1933790560431647187 macos-arm64 9 2 8 8 4 3 ok 5877899263698877100 macos-arm64 9 2 8 8 8 1 ok 4204618854271989380 +macos-arm64 9 2 9 8 4 1 ok 13364805290811868144 macos-arm64 9 2 9 9 4 1 ok 17370525382688177735 macos-arm64 11 11 7 7 4 1 ok 7099819568130086231 macos-arm64 11 11 8 8 2 1 ok 6302098072410457647 @@ -76,20 +86,23 @@ macos-arm64 11 11 8 8 4 1 ok 35825363079579543 macos-arm64 11 11 8 8 4 2 ok 4919401921601200783 macos-arm64 11 11 8 8 4 3 ok 4565939337693971842 macos-arm64 11 11 8 8 8 1 ok 906849352031588290 +macos-arm64 11 11 9 8 4 1 ok 7686433018839407442 macos-arm64 11 11 9 9 4 1 ok 1888712339707412248 -macos-arm64 12 10 7 7 4 1 ok 7388571519173228055 -macos-arm64 12 10 8 8 2 1 ok 15699276765293966482 -macos-arm64 12 10 8 8 4 1 ok 11621882014804934120 -macos-arm64 12 10 8 8 4 2 ok 8547038603337966082 -macos-arm64 12 10 8 8 4 3 ok 4736503892792565969 -macos-arm64 12 10 8 8 8 1 ok 4268435493852729962 -macos-arm64 12 10 9 9 4 1 ok 13231033850504282907 +macos-arm64 12 11 7 7 4 1 ok 7388571519173228055 +macos-arm64 12 11 8 8 2 1 ok 15699276765293966482 +macos-arm64 12 11 8 8 4 1 ok 11621882014804934120 +macos-arm64 12 11 8 8 4 2 ok 8547038603337966082 +macos-arm64 12 11 8 8 4 3 ok 4736503892792565969 +macos-arm64 12 11 8 8 8 1 ok 4268435493852729962 +macos-arm64 12 11 9 8 4 1 ok 7943678003747689871 +macos-arm64 12 11 9 9 4 1 ok 13231033850504282907 macos-arm64 13 1 7 7 4 1 ok 8112226036058122700 macos-arm64 13 1 8 8 2 1 ok 9154120752318257696 macos-arm64 13 1 8 8 4 1 ok 2078930885184706426 macos-arm64 13 1 8 8 4 2 ok 13659661778465349112 macos-arm64 13 1 8 8 4 3 ok 13134881442491555389 macos-arm64 13 1 8 8 8 1 ok 16484172863350979963 +macos-arm64 13 1 9 8 4 1 ok 7377329900378929112 macos-arm64 13 1 9 9 4 1 ok 9971144189844359399 macos-arm64 14 2 7 7 4 1 ok 17202733506137778788 macos-arm64 14 2 8 8 2 1 ok 16088282592338295379 @@ -97,6 +110,7 @@ macos-arm64 14 2 8 8 4 1 ok 11925006621409592431 macos-arm64 14 2 8 8 4 2 ok 414349330464996680 macos-arm64 14 2 8 8 4 3 ok 760189253045661613 macos-arm64 14 2 8 8 8 1 ok 3322640947906419646 +macos-arm64 14 2 9 8 4 1 ok 2403928672037668991 macos-arm64 14 2 9 9 4 1 ok 1552916736750568752 macos-arm64 15 1 7 7 4 1 ok 18061507907613243474 macos-arm64 15 1 8 8 2 1 ok 14205129108449246087 @@ -104,6 +118,7 @@ macos-arm64 15 1 8 8 4 1 ok 1822623745155517541 macos-arm64 15 1 8 8 4 2 ok 13627699444976556672 macos-arm64 15 1 8 8 4 3 ok 4558451268999198918 macos-arm64 15 1 8 8 8 1 ok 6662980182189545293 +macos-arm64 15 1 9 8 4 1 ok 13301401750330037833 macos-arm64 15 1 9 9 4 1 ok 4317157755929348617 macos-arm64 16 1 7 7 4 1 ok 10199323229909488086 macos-arm64 16 1 8 8 2 1 ok 17554114376208289941 @@ -111,6 +126,7 @@ macos-arm64 16 1 8 8 4 1 ok 11072127706920976238 macos-arm64 16 1 8 8 4 2 ok 10075510292789336147 macos-arm64 16 1 8 8 4 3 ok 4396075304858481954 macos-arm64 16 1 8 8 8 1 ok 699982766957804005 +macos-arm64 16 1 9 8 4 1 ok 8928204734545682645 macos-arm64 16 1 9 9 4 1 ok 12471630520837885284 macos-arm64 17 5 7 7 4 1 ok 4983285384533269693 macos-arm64 17 5 8 8 2 1 ok 15410170018519406689 @@ -118,25 +134,37 @@ macos-arm64 17 5 8 8 4 1 ok 1496908417910164065 macos-arm64 17 5 8 8 4 2 ok 11861326676775640834 macos-arm64 17 5 8 8 4 3 ok 17308555237519110064 macos-arm64 17 5 8 8 8 1 ok 7621305293371525278 +macos-arm64 17 5 9 8 4 1 ok 12028787258565351075 macos-arm64 17 5 9 9 4 1 ok 11272053320214136168 -macos-arm64 18 4 7 7 4 1 ok 2131984367767490485 -macos-arm64 18 4 8 8 2 1 ok 3078796380374205351 -macos-arm64 18 4 8 8 4 1 ok 7736833362775075996 -macos-arm64 18 4 8 8 4 2 ok 3733351741626345607 -macos-arm64 18 4 8 8 4 3 ok 4515656875628782478 -macos-arm64 18 4 8 8 8 1 ok 12075523445877924400 -macos-arm64 18 4 9 9 4 1 ok 10761736745352955182 +macos-arm64 18 5 7 7 4 1 ok 2131984367767490485 +macos-arm64 18 5 8 8 2 1 ok 3078796380374205351 +macos-arm64 18 5 8 8 4 1 ok 7736833362775075996 +macos-arm64 18 5 8 8 4 2 ok 3733351741626345607 +macos-arm64 18 5 8 8 4 3 ok 4515656875628782478 +macos-arm64 18 5 8 8 8 1 ok 12075523445877924400 +macos-arm64 18 5 9 8 4 1 ok 2230785934352114637 +macos-arm64 18 5 9 9 4 1 ok 10761736745352955182 macos-arm64 19 2 7 7 4 1 ok 2704824689328339712 macos-arm64 19 2 8 8 2 1 ok 18374433395111261761 macos-arm64 19 2 8 8 4 1 ok 9505660943407981793 macos-arm64 19 2 8 8 4 2 ok 10374245496345104115 macos-arm64 19 2 8 8 4 3 ok 3538515548796101002 macos-arm64 19 2 8 8 8 1 ok 638962071964486508 +macos-arm64 19 2 9 8 4 1 ok 5247699519866569761 macos-arm64 19 2 9 9 4 1 ok 3483131854939269908 -macos-arm64 20 1 7 7 4 1 ok 4116360308347306944 -macos-arm64 20 1 8 8 2 1 ok 11338407907832521097 -macos-arm64 20 1 8 8 4 1 ok 17920614512797914752 -macos-arm64 20 1 8 8 4 2 ok 16764556104255874150 -macos-arm64 20 1 8 8 4 3 ok 7345786698497847795 -macos-arm64 20 1 8 8 8 1 ok 12653674183462480909 -macos-arm64 20 1 9 9 4 1 ok 2982078612374145132 +macos-arm64 20 2 7 7 4 1 ok 18081709455344930657 +macos-arm64 20 2 8 8 2 1 ok 10461128406841898608 +macos-arm64 20 2 8 8 4 1 ok 8551267361097346911 +macos-arm64 20 2 8 8 4 2 ok 1876765713161738678 +macos-arm64 20 2 8 8 4 3 ok 14363552148956198198 +macos-arm64 20 2 8 8 8 1 ok 565631609982434713 +macos-arm64 20 2 9 8 4 1 ok 15148124462838456046 +macos-arm64 20 2 9 9 4 1 ok 232385605498186525 +macos-arm64 21 1 7 7 4 1 ok 12210728782221235895 +macos-arm64 21 1 8 8 2 1 ok 5011243392932424661 +macos-arm64 21 1 8 8 4 1 ok 3050122448863222582 +macos-arm64 21 1 8 8 4 2 ok 8201580502558476028 +macos-arm64 21 1 8 8 4 3 ok 14159020535153467794 +macos-arm64 21 1 8 8 8 1 ok 3788158320130144245 +macos-arm64 21 1 9 8 4 1 ok 14597284168628427777 +macos-arm64 21 1 9 9 4 1 ok 7348842460505219783 From e0963648f1ca510bbda8d9de422da1a42ad88144 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 22:20:37 -0400 Subject: [PATCH 108/136] Document every map generator's logic and tuning values A comments-only pass over all twenty generators and the shared toolkit, so the next generator can be designed from what the existing ones learned. - docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md collects the engine rules a map designs for (corner terrain, grass never touching water, growth near water and away from sand, algae's sand probe, fruit conversion, stone never running out) and the start and play principles; the comments refer to it. - The 2002-2008 generators (Old random, Old islands, Concrete islands, Isles, Contested commons) and Leo Wandersleb's 2006 height-field pipeline (Islands, Swamp, River, Crater lakes) now carry their history, pieced together from git, and explain the numbers that looked arbitrary, several checked by experiment: Old random's brake exponent, Old islands' island size, River width against the water share, Crater lakes' density and size. - The designed generators gain game-rule headers, the sizes their defaults give on a 256 map, and explanations of their bare tuning numbers. - Two slips in legacy code are documented rather than changed: the height- field terrain total counts the fruit control as a weight, and Map::smoothResources, called by Old random, Old islands and the archipelago starts, reads the pre-2003 resource encoding and does nothing. No code changes: stripped of comments and whitespace every file matches its parent, and the golden test compares 168 rows with 0 failures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../GAME_RULES_FOR_MAP_DESIGN.md | 101 ++++++++++++ docs/map-generators/README.md | 2 + .../generators/CityStatesGenerator.cpp | 89 ++++++++++- .../generators/ConcreteIslandsGenerator.cpp | 75 ++++++++- .../generators/ContestedCommonsGenerator.cpp | 84 +++++++++- .../generator/generators/CoralGenerator.cpp | 34 ++-- .../generators/CraterLakesGenerator.cpp | 16 ++ .../generators/EvergladesGenerator.cpp | 30 +++- .../generators/FjordContinentGenerator.cpp | 90 +++++++++-- .../generator/generators/IslandsGenerator.cpp | 13 ++ .../generator/generators/IslesGenerator.cpp | 69 +++++++- .../generator/generators/MazeGenerator.cpp | 43 ++++- .../generators/RingWorldGenerator.cpp | 51 +++++- .../generator/generators/RiverGenerator.cpp | 17 ++ .../generators/RuggedArchipelagoGenerator.cpp | 83 +++++++++- .../generators/ShatteredCoastGenerator.cpp | 149 ++++++++++++++++-- .../generators/SpiderWebGenerator.cpp | 31 +++- .../generators/StoneHighlandsGenerator.cpp | 51 +++++- .../generator/generators/SwampGenerator.cpp | 11 ++ .../generators/SymmetricArenaGenerator.cpp | 49 +++++- .../generators/TidalFlatsGenerator.cpp | 42 +++++ .../generator/generators/UniformGenerator.cpp | 5 + .../generators/WatershedGenerator.cpp | 138 +++++++++++++++- src/map/generator/shared/BalancedStarts.cpp | 2 + src/map/generator/shared/Geometry.cpp | 5 + src/map/generator/shared/HeightMap.cpp | 36 +++++ src/map/generator/shared/LatticeNoise.cpp | 2 + src/map/generator/shared/Pipeline.cpp | 2 + src/map/generator/shared/Resources.cpp | 23 ++- src/map/generator/shared/Sketch.cpp | 3 + src/map/generator/shared/Terrain.cpp | 57 ++++++- src/map/generator/shared/legacy/Distances.cpp | 11 ++ src/map/generator/shared/legacy/Regions.cpp | 41 +++++ .../shared/legacy/StartingPositions.cpp | 36 ++++- 34 files changed, 1402 insertions(+), 89 deletions(-) create mode 100644 docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md diff --git a/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md new file mode 100644 index 000000000..0e016f06d --- /dev/null +++ b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md @@ -0,0 +1,101 @@ +# The game rules a map generator designs for + +Every choice a generator makes — how wide a passage is, how far wheat lies from a swarm, why a +beach is always sand — follows from a handful of engine rules and from what makes a Globulation 2 +game fun to play. This page collects both, with the code that enforces each rule, so a generator's +comments can say "because grass may not touch water" and a reader can check it here. + +## The rules of the terrain + +- **Terrain is drawn from corners.** The map stores an *undermap*: one terrain type (water, sand + or grass) per tile corner. A tile's terrain is derived from its four corners + (`Map::regenerateMap`), so a tile is pure grass only when all four of its corners are grass. + A single sand corner spoils the four tiles around it. This is why a one-cell sand road + (`tracePath`) blocks a strip about two tiles wide, and why generators stamp grass inside a + one-tile sand ring. +- **Grass may never touch water.** There is no grass-to-water tile graphic, so every grass corner + beside water must become sand. `Map::controlSand` does it in place in row order, which makes a + shoreline depend on scan direction; the designed generators use `layBeaches`, which applies the + same rule to every corner at once. +- **Buildings need pure grass.** `Map::isFreeForBuilding` requires grass, no resource and no unit + on every tile of the footprint (`Map::checkTile`). Sand is walkable but unbuildable, so a map's + building room is its grass, not its land. A swarm is 4×4; the start scorer counts free 4×4 + footprints as a colony's room (`StartQuality`). +- **Water blocks walking until a colony can swim.** Ground units cannot enter water + (`TileChecks::waterBlocks`) until they have trained at a swimming pool. Every island map, strait + and moat is therefore a *timing* rule: it separates colonies early and opens once pools are + built. Sand and grass are both walkable. +- **Resources block movement and building.** A unit cannot stand on a tile with a resource on it + (`isFreeForGroundUnit` rejects resources), and nothing can be built on one. Wheat, wood and algae + are *clearable* by clearing flags; stone and fruit are not. A band of deposits across a passage + closes it until it is cleared, which is why generators keep lanes and roads clear and run + `openRoad` after placing deposits. + +## The rules of resources + +| Resource | Used for | Where it can be | How it grows | +| --- | --- | --- | --- | +| Wheat (corn) | Swarms turn it into units; inns store it to feed units | Grass | Grows and spreads, one visit in three, only where a random probe finds water and the opposite probe finds no sand | +| Wood | Every building site's construction; many upgrades | Grass | Grows and spreads under the same water/no-sand test as wheat | +| Stone | Upgrades of inns, hospitals, towers, barracks, schools, racetracks and pools; stone walls | Grass | Never grows and never runs out (not shrinkable): a quarry is permanent. Cannot be cleared | +| Algae | School and swimming pool upgrades, the top defence tower | Water | Grows and spreads only where a random probe (up to 15 tiles) finds water and the probe turned a quarter and doubled (up to 30 tiles) finds solid sand (`algaeGrowthChance`) | +| Fruit (cherry, orange, prune) | Stocked in inns. Each kind an inn holds raises its happiness level (`Building::availableHappynessLevel`), and a hungry unit that can see a happier enemy inn than any of its own walks there and is converted to that team (`Team::findNearestFood`); traded at markets. A unit that ate fruit also carries less armour for it (`armorReductionPerHappyness`) | Grass | Regrows in place wherever it stands; never spreads, never cleared | + +The growth test lives in `Map::growResources` (`src/map/MapStep.cpp`), the resource table in +`src/game/entities/Resources.cpp`, and building needs in `src/game/entities/BuildingTypes*.cpp`. + +Consequences a generator has to design around: + +- **Farmland follows water.** Wheat and wood planted far from water never regrow; planted near + water they spread until cleared, harvested or blocked. Land far from any water is a desert for + the economy even when it is grass. +- **Sand beside farmland slows it.** The growth test refuses when its probe lands on sand, so a map + with lots of sand grows its fields back slowly — a lever, not just decoration. +- **Growth can shut a map.** Wheat and wood that spread unchecked cover grass, and covered grass is + unwalkable and unbuildable. Lanes, sand roads and levees exist to stop a map growing shut. +- **Fruit is a weapon.** A colony whose inns hold all three fruits can pull hungry enemy units + across to its side, so an orchard of all three kinds in contested ground is the strongest prize + a map can offer, and fruit spread unevenly between colonies is a real unfairness. +- **Stone is strategic, not consumable.** One quarry serves a colony forever, so stone placement + decides *where* colonies go for upgrades rather than how long they last. +- **Algae needs sand near its water.** Algae in open sea far from any beach never regrows. +- **Every placed deposit draws from the gameplay RNG.** `Map::setResource` draws each tile's amount + from the synchronised random stream in call order, so the order deposits are placed in is part of + the map. Reordering placement changes the map even when the positions are the same. + +## What makes a good start + +The shared tools encode a few measurable promises every generator is expected to keep: + +- **Food and wood within reach.** `guaranteeStartingResources` tops up any colony without wheat + within 24 steps or wood within 32 steps of its swarm. Early growth is walking time; a colony whose + wheat is 60 steps away starves while its neighbour builds. +- **Room to build.** `openCrampedStarts` makes sure a colony can walk to at least 16 free 4×4 + building sites within 24 steps. A colony walled in by its own deposits cannot grow at all. +- **Workers can leave the swarm.** A clear ring of `kSwarmClearance` tiles round each swarm. +- **Fair starts.** `scoreStarts` rates each colony on wheat and wood distance, fertility, deposit + depth, room and isolation from rivals; fairness is the weakest start divided by the strongest, + and the lobby keeps the best-scoring of several seeds. Designed generators get fairness by + construction instead: they design one colony's share and turn or mirror it onto every other + colony (`WedgeFrame`), so every colony's ground is the same. + +## What makes a good game + +These are judgement rules rather than engine rules, learned from playtesting on this branch: + +- **Contact should be a choice, not an accident.** Maps that put colonies behind water, walls or + narrow routes give a player time to build before the first fight, and make the routes worth + holding. A map where everyone touches from the first minute plays as a rush. +- **Remote ground should pay.** Coral puts fruit on its far tips, Spider web its orchard at the hub, + City states its orchard in the commons: the riskiest land carries the best prize, so expansion is a + decision. +- **Chokepoints need alternatives later.** Fords, causeways, passes and bridges decide early fights; + swimming pools (and Stone highlands' extra passes) keep a map from locking into a stalemate. +- **The map must stay traversable.** Growth, deposits and buildings can all close routes; a map + that silts shut is no fun. Roads, lanes and `openRoad` guard this. +- **Readable shapes.** A player should see at a glance where home, the front and the prize are. + Distinct landforms (a web, a fan, a ring, a maze) help; a new generator should also be compared + side by side with the existing ones, since two different algorithms can draw the same picture. +- **Defaults must work at every size.** 128×128, 256×256 and 512×512 maps, 2 to 12 colonies and + rectangular maps are all in play; tuning that looks good at one size often breaks another, which + is why several generators scale widths and angles with map size. diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index 5a110e0e7..8dacabfd0 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -17,3 +17,5 @@ Record investigation findings and validation results in the relevant pull reques See [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) for the shared toolkit a generator is built from, the designed-generator pipeline, the generator catalog, resource placement and colony fairness, and [Adding a generator](ADDING_A_GENERATOR.md) for the registry and module interfaces. +[Game rules for map design](GAME_RULES_FOR_MAP_DESIGN.md) collects the engine rules and play +principles every generator designs for, which the generators' comments refer to. diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index e29923555..b152b0d2a 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -47,6 +47,23 @@ using namespace MapGeneration; // The whole design is a pure function of the request, so validateWorld rebuilds it and checks the // finished world. Terrain is written straight to the undermap with an order-independent beach // pass, as Ring world does. +// +// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be +// cleared, so a stone wall is permanent, which is what makes the causeway the only door; water +// stops ground units until they swim, which makes the strait a timer rather than a wall forever; +// wheat and wood regrow only near water, which is why every home has its lake and its fields +// beside it; and fruit lets an inn pull hungry enemy units across, which makes the commons' +// orchard the prize worth leaving home for. The commons' frontier richness (60% by default) +// puts more of its resources toward the centre, so the farther a colony pushes, the more it gains. +// +// RECTANGULAR MAPS. Unlike the other ring-shaped generators, City states stays a circle on the +// shorter side and does not stretch to fill a rectangle. Stretching was tried: the homes then +// have different shapes at different angles, and the fairness score fell from 0.97 to between +// 0.65 and 0.8. Equal homes matter more here than filling the map. +// +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a commons 70 tiles in radius, a strait 10 wide, +// homes 42 tiles deep from the strait out to a 6-tile rim of sea at the wrap, and a home lake about +// 4 tiles in radius. namespace { @@ -135,6 +152,9 @@ Profile rollProfile(GenerationContext &context, const char *stream, int first, i p.count = count; for (int h = 0; h < count; ++h) { + // Each harmonic weighted 40% to 100% at random and divided by its number to the falloff, so + // higher harmonics are smaller; the peak is then found by sampling at 720 points and the + // curve scaled so its peak is exactly one, so the amplitude constants mean what they say. p.amplitude[h] = (0.4 + 0.6 * context.bounded(stream, 1000) / 1000.0) / std::pow(double(first + h), falloff); p.phase[h] = 2 * kPi * context.bounded(stream, 3600) / 3600.0; @@ -198,6 +218,8 @@ double lakeOffset(const Geometry &g, double lakeRadius) const double reach = 1.3 * lakeRadius + kLakeSeaGap; const double low = reach + 2.0; const double high = g.depth() - reach - 2.0; + // Six tenths of the way out: nearer the strait than the sea, so the swarm, between causeway and + // lake, is a short walk from both, and the land behind the lake is the home's own back country. return std::clamp(0.6 * g.depth(), low, std::max(low, high)); } @@ -248,6 +270,12 @@ struct Coasts // arc so the stretch is the same however many homes share the ring, and the flanks' bays // begin only beyond the lake and its clearance. double lakeRadius, lakeReach, flatArc, bayArc; + // The commons' coast: harmonics 1 to 4 per wedge for bays and headlands, and 6 to 11 at a + // smaller amplitude for a finer ripple. Profiles are periodic in the wedge, so every wedge gets + // the same coast. A home's lake is a tenth of its depth, 2 to 6 tiles: water for its fields, + // not a sea that eats its building room. The coasts hold flat for 4 tiles beyond the causeway + // and its shoulders and lanes, so the causeway lands square on a straight shore, and bays start + // only 6 tiles beyond the lake's clearance from the sea. Coasts(const Geometry &g, GenerationContext &context) : g(g), wedge(2 * kPi / g.teams), inner(g.innerRadius()), depth(g.depth()), coast(rollProfile(context, "city-coast", 1, 4, 1.2)), @@ -265,6 +293,8 @@ struct Coasts } double coastAt(double u, double d) const { + // The bays fade in over 12 tiles of arc past the flat stretch (10 for the home coasts), so + // the shore curves into them rather than stepping. const double away = awayFromMiddle(d * std::abs(u - 0.5) * wedge, flatArc, 12); return g.commonsRadius + (coastAmp * coast.at(u) + rippleAmp * ripple.at(u)) * away; } @@ -303,6 +333,9 @@ struct Features std::vector lakes, sandBlobs; std::vector> creek; int fordSegment = -1; + // Highland ridges run at 28% and 72% across the wedge, splitting the home into a middle valley + // with the lake and two side valleys; Barrens leave the middle 45% of the half arc as grass and + // put sand beyond it. double ridgeU[2] = {0.28, 0.72}; double passR = 0; double barrenShare = 0.45; @@ -321,6 +354,10 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext const Torus &t = L.t; const double wedge = c.wedge, inner = c.inner, depth = c.depth; // The heart of the commons. + // The heart's shapes as shares of the commons' radius: a lake 22% (15 tiles at the defaults); a + // crag a small 6% tarn inside a ring of stone at 22%; an island heart a 30% lake round a 12% + // island; a forest reaching 4 tiles plus 9% beyond the lake. Each is small enough to leave the + // commons most of its land. L.lakeR = std::max(3.0, kHeartShare * g.commonsRadius); L.ringR = std::max(8.0, kHeartShare * g.commonsRadius); L.islandR = 0; @@ -332,6 +369,8 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext L.islandR = std::max(5.0, 0.12 * g.commonsRadius); } L.forestR = L.lakeR + 4 + 0.09 * g.commonsRadius; + // A delta heart's rivers are forded 60% of the way out from the centre, between the heart and + // the landings. Features f(RadialShape(L.lakeR, L.heartKind == IslandHeart ? 0.2 : 0.3, context, "city-heart"), 0.6 * g.commonsRadius); // The home's features in the wedge's frame: f.lakes first (the main one first), then whatever @@ -356,6 +395,8 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext return true; }; if (L.homeKind == Marsh) + // A marsh adds up to 3 small lakes (radius 2 to 4) within 60% of the home's half arc and 8 + // tiles in from its inner and outer edges: more shore to farm, more ground to walk round. for (int attempt = 0; attempt < 400 && f.lakes.size() < 4; ++attempt) { const double r0 = @@ -372,6 +413,9 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext { const double side = context.bounded("city-features", 2) ? 1.0 : -1.0; L.creekSide = side; + // The creek bends out a third of the half arc to one flank, through two control points, and + // ends 6 tiles past the lake's sea gap from the strait; its ford is the middle segment, so + // the flank beyond the creek is a second field reached through one crossing. const double reachArc = 0.32 * g.arcHalf(f.mainLakeR); const double endR = inner + kLakeSeaGap + 6.0; f.creek = {{0.0, f.mainLakeR}, @@ -386,6 +430,9 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext // causeway past the lake; the sand keeps clear of every coast so the wall stays on the shore. // Sand patches, in every kind of home. const double homeArea = wedge * (g.outerRadius * g.outerRadius - inner * inner) / 2; + // Sand patches per 8192 tiles (the default 3 gives about 2 per home and 6 on the commons at + // 256x256), radius 3 to 7 with rough outlines, 5 tiles from every lake and patch in a home: + // they decorate, and slow growth near them, without walling anything off. const int homeSand = int(std::lround(o.sand * homeArea / 8192.0)); for (int attempt = 0; attempt < 400 && int(f.sandBlobs.size()) < homeSand; ++attempt) { @@ -405,6 +452,8 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext for (int attempt = 0; attempt < 400 && int(f.commonsSand.size()) < wantedSand; ++attempt) { const double a = context.bounded("city-sand", 3600) / 3600.0 * 2 * kPi; + // Commons sand goes from 45% of the radius out to 12 tiles short of its edge, off the heart + // and the landings, patches 3 to 8 in radius and 4 tiles apart. const double r0 = 0.45 * g.commonsRadius + context.bounded("city-sand", 1000) / 1000.0 * (0.55 * g.commonsRadius - 12); RadialShape shape(3.0 + context.bounded("city-sand", 1000) / 1000.0 * 5.0, 0.5, context, @@ -455,8 +504,8 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const for (int x = 0; x < t.w; ++x) { const int i = y * t.w + x; - // The wedge frame: which home, how far across it and how far along the arc from its middle, - // with the channels between homes bowed by this roll's bend. + // The wedge frame: which home, how far across it and how far along the arc from its + // middle, with the channels between homes bowed by this roll's bend. WedgeFrame::Cell cell = frame.cell(x, y); if (teams >= 2) frame.bend(cell, coasts.bendAt(cell.d)); @@ -531,6 +580,9 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const 0.0, 1.0); const double dist = std::hypot(s - (ax + tt * (bx - ax)), d - (ay + tt * (by - ay))); + // The creek is water within 1.6 tiles of its line, a channel just wide enough + // to stay water inside its own beaches; its ford is 5 tiles of that channel + // (2.5 each way), with a clear margin to 3.5. if (dist < 1.6) { if (int(seg) == f.fordSegment && std::abs(tt - 0.5) * std::sqrt(len2) < 2.5) @@ -553,6 +605,9 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const for (double ur : f.ridgeU) { const double sr = (ur - 0.5) * wedge * d; + // A ridge is stone 2.5 tiles across; its pass is 5 tiles along the ridge, + // with 3.5 and 4.5 tiles of clear ground round it so the pass stays + // walkable after deposits. if (std::abs(s - sr) < 1.25) { if (std::abs(d - f.passR) < 2.5) @@ -565,6 +620,8 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const L.clear[i] = 1; } } + // Barrens' sand keeps 8 tiles from both coasts and the channels, so the home's wall + // still stands on grass. if (L.homeKind == Barrens && d >= in + 8 && d <= out - 8 && std::abs(s) > f.barrenShare * g.arcHalf(d) && std::abs(s) < g.arcHalf(d) - 8) L.sand[i] = 1; @@ -580,6 +637,9 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const { if (inHeart && d >= L.islandR) L.lake[i] = 1; + // An island heart's fords run 4 tiles wide (2 each way) along each causeway's + // line from the lake shore to the island, so each colony's road leads onto it; + // 3.5 each way stays clear. if (inHeart && d >= L.islandR - 1 && d <= L.lakeR + 2.5 && toLanding < 2) { L.ford[i] = 1; @@ -594,6 +654,8 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const } if (L.heartKind == Crag) { + // The crag is a ring of stone 1.5 tiles thick, broken by a 5-tile gap on each + // causeway's line. if (d >= L.ringR && d < L.ringR + 1.5 && toLanding >= 2.5) L.ridge[i] = 1; if (d >= L.ringR - 2 && d < L.ringR + 3.5 && toLanding < 4) @@ -603,6 +665,9 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const { if (toBoundary < 2) { + // A delta's rivers run out along the wedge boundaries 4 tiles wide, cutting + // the commons into one sector per colony, each river forded 5 tiles long, + // so neighbours meet at a shared crossing. if (std::abs(d - f.deltaFordR) < 2.5) L.ford[i] = 1; else @@ -786,6 +851,10 @@ void carveValleys(std::vector &terrain, const Layout &L, Generati { const int at = ground[context.bounded("city-valleys", ground.size())]; const double inward = 1 - L.radius[at] / std::max(1.0, L.g.commonsRadius); + // Valley lakes are kept with a chance from 25% at the commons' edge to 100% at its centre: + // the frontier gets more water, and with it more farmland, the farther in it lies. Valleys + // per 16384 tiles of commons, radius 3 to 6 (grown with the map), stretched 1.2 to 2 times + // into long valleys. if (int(context.bounded("city-valleys", 100)) >= 25 + int(75 * inward)) continue; const int x = at % t.w, y = at / t.w; @@ -848,6 +917,9 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C }; // The kit: wheat and wood beside the lake on the swarm's side, on the flank away from any // creek, and the quarry behind the lake. + // The kit sits just past the lake's fullest outline (1.3) and its 3-tile beach, where wheat + // and wood regrow; the quarry 8 tiles further out behind the lake. With a creek, both crops + // go to the flank away from it, so a colony never has to cross the ford for its own food. const double reach = std::clamp(0.1 * L.g.depth(), 2.0, 6.0) * 1.3 + 3; const double flank = L.creekSide != 0 ? -L.creekSide : -1.0; const KitFrame frame{h.lakeX, h.lakeY, h.angle}; @@ -873,6 +945,8 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C float cut = 0; if (!levels.empty()) { + // The patch field (12-tile cells) cut at its 45th percentile keeps 55% of the fertile + // ground as candidate farmland, in patches with gaps to walk and build in. std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, levels.end()); cut = levels[levels.size() * 45 / 100]; @@ -889,6 +963,9 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C chosen.push_back(entry.second); const int area = int(ground.size()); const auto bySplit = [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }; + // A home: 4% of its ground ambient wheat and 2% wood on top of the kit, one outcrop per + // 2500 tiles and a single grove: self-sufficient but not rich, so the commons is worth the + // trip. plantFields(map, t, chosen, int(scaledCount(area * 4 / 100, o.wheat)), int(scaledCount(area * 2 / 100, o.wood)), bySplit); scatterClumps(context, t, ground, int(scaledCount(std::max(1, area / 2500), o.stone)), @@ -957,12 +1034,16 @@ void stockCommons(Map &map, const Layout &L, GenerationContext &context, const C for (const auto &entry : farm) chosen.push_back(entry.second); const int area = int(ground.size()); + // The commons: 7% wheat and 4% wood, nearly twice a home's density, weighted toward the centre; + // one outcrop per 2000 tiles and one grove per 2500, sampled by the same weight. plantFields(map, t, chosen, int(scaledCount(area * 7 / 100, o.wheat)), int(scaledCount(area * 4 / 100, o.wood)), [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }); const auto pick = [&](const char *stream) { + // Rejection sampling by the frontier weight: 200 draws finds a spot unless almost nothing + // is eligible. for (int attempt = 0; attempt < 200; ++attempt) { const int at = ground[context.bounded(stream, ground.size())]; @@ -997,6 +1078,8 @@ void stockCommons(Map &map, const Layout &L, GenerationContext &context, const C const int ax = L.cx + int(std::lround(rho * std::cos(a))), ay = L.cy + int(std::lround(rho * std::sin(a))); int seed = -1, nearest = INT_MAX; + // The orchard's three groves sit a third of a turn apart round the heart; each grows + // from the eligible tile nearest its point within 8 tiles. for (int dy = -8; dy <= 8; ++dy) for (int dx = -8; dx <= 8; ++dx) { @@ -1115,6 +1198,8 @@ bool generate(Game &game, GenerationContext &context) { const Home &h = L.homes[team]; const double lakeRadius = std::clamp(0.1 * L.g.depth(), 2.0, 6.0); + // The swarm stands 9 tiles short of the lake's near shore, and never within 8 tiles of the + // strait: between causeway and lake, a short walk from both. const double rho = std::max(h.coast + L.g.strait + 8.0, h.coast + L.g.strait + lakeOffset(L.g, lakeRadius) - lakeRadius - 9.0); diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.cpp b/src/map/generator/generators/ConcreteIslandsGenerator.cpp index bda1c02a8..d10242a3d 100644 --- a/src/map/generator/generators/ConcreteIslandsGenerator.cpp +++ b/src/map/generator/generators/ConcreteIslandsGenerator.cpp @@ -19,6 +19,42 @@ #include #include using namespace MapGeneration; + +// Concrete islands (id "concrete-islands", legacy id 5): a map cut into one big island per colony +// plus a few small neutral islands, separated by channels. +// +// HISTORY. Bradley Arsenault wrote it in July 2008 as "the first random map" of a new generation +// engine (the area-grid toolkit now in shared/legacy/Regions.cpp and StartingPositions.cpp), and +// revised it twice that week, then fixed maps too small for the colony count and improved its fruit +// and swarm placement. The name describes the look: the channels follow the straight boundaries of +// a weighted Voronoi-like split, so the islands have the blocky outline of poured concrete slabs +// rather than a natural coast. On this branch it gained resource amounts, extra island and channel +// controls and a Sandy beaches switch; its layout is otherwise the 2008 one. +// +// HOW IT WORKS. +// 1. Spread one point per colony and per extra island evenly over the map (splitUpPoints), then +// grow areas from them in proportion to their weights (splitUpArea): a colony weighs 10 and an +// extra island 1 to 3, so every colony's island is several times a neutral island. +// 2. Build a height field: 75 everywhere, plus noise of up to 15 either way for a rough coast. +// 3. Dig the channels: tiles within `channel-width` steps of an area boundary are lowered by 13 per +// step closer (see below), and the heights are read as water below 45, sand from 45 to 55 and +// grass above. +// 4. Put algae down the deepest middle of each channel, fill each neutral island half with wheat +// and half with a few fruit trees, and lay out each colony's island (divideUpPlayerLands: wheat +// and wood fields along its coast, stone in its interior, the swarm beside its wheat). +// +// GAME RULES IT LEANS ON (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): +// - Water blocks walking until a colony can swim: every island, colony and neutral, is reached by +// swimming only, so the neutral islands are the first prizes once pools are built. +// - Fruit is a weapon: the neutral islands' fruit is what lets a colony that takes them pull hungry +// enemy units to its inns. +// - Wheat and wood regrow near water, so divideUpPlayerLands puts every colony's fields on the +// zones nearest the coast. +// - Grass may not touch water: the 45-55 band is the beach; with Sandy beaches off, controlSand +// still leaves the one-tile ring the engine requires. +// +// Colonies get equal-weight islands but not equal shapes; fairness is statistical (the lobby keeps +// the best-scoring seed), with reopenCrampedStarts as the backstop at non-default amounts. static bool generate(Game &game, GenerationContext &context) { context.stage = "layout"; @@ -38,7 +74,9 @@ static bool generate(Game &game, GenerationContext &context) std::vector teamAreaNumbers; std::vector islandAreaNumbers; - // Add in team bases + // Add in team bases. weights1 (all 1) spreads the points evenly; weights2 is each area's growth + // rate in splitUpArea, so a colony's island (10) ends up several times a neutral island (1 to + // 3). for (int i = 0; i < context.request.nbTeams; ++i) { teamPoints.push_back(MapGeneratorPoint(0, 0)); @@ -67,7 +105,9 @@ static bool generate(Game &game, GenerationContext &context) splitUpPoints(game.map, context, grid, 0, teamPoints, weights1); splitUpArea(game.map, context, grid, 0, teamPoints, weights2, areaNumbers); - // Create a heightmap that will be used to give the map a rough edge + // Create a heightmap that will be used to give the map a rough edge. 75 sits 20 above the + // grass line (55) and 30 above the water line (45), and noise of up to 15 either way can never + // push an untouched tile into the sea: only the channels below make water. std::vector heights(game.map.getW() * game.map.getH(), 75); adjustHeightmapFromPerlinNoise(game.map, context, heights, 15); @@ -79,6 +119,17 @@ static bool generate(Game &game, GenerationContext &context) computeDistances(game.map, sources, obstacles, distances); // Locations near the border are deaper, thus causing more water + // + // A tile d steps from the nearest area boundary (d = 0 on it) inside the channel width is + // lowered by (channelWidth - 1 - d) * 13. With the default width 5 that is 52, 39, 26, 13 and 0 + // for d = 0 to 4, giving heights of 23, 36, 49, 62 and 75 before the noise of up to 15 either + // way. So the boundary tile is always water (at most 38), the tile beside it is water unless + // the noise lifts it by more than 8, the next is beach or water or grass depending on the + // noise, and beyond that is grass. Counting both sides of the boundary, a channel is about + // three to five tiles of water with a ragged edge. Each extra step of channel width adds 13 to + // every lowering, about one more tile of water on each side. 13 is close to the noise's swing, + // so the coast wobbles by about a tile: rough, but the boundary tile itself can never surface + // and break a channel. for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) @@ -96,6 +147,7 @@ static bool generate(Game &game, GenerationContext &context) { for (int y = 0; y < game.map.getH(); ++y) { + // Water under 45, a 10-high band of sand for the beach, grass above 55. int total_height = heights[y * game.map.getW() + x]; if (total_height < 45) game.map.setUMatPos(x, y, WATER, 1); @@ -107,8 +159,13 @@ static bool generate(Game &game, GenerationContext &context) } game.map.controlSand(); - // Go through the map again and place alga, down the deepest middle of every channel; the - // algae amount moves how deep that is. + // Go through the map again and place alga, down the deepest middle of every channel; the algae + // amount moves how deep that is. + // + // At the default amount the cut-off is 10: only the boundary tiles of a width-5 channel (23 + // before noise) reach it, and only where the noise dips by 13 or more, so algae is a broken + // thread down the channel's middle. Wider channels dig deeper and grow more of it. Algae needs + // sand within reach to regrow, which a channel a few tiles wide always has. const int algaeDepth = int(scaledCount(10, options.algae)); for (int x = 0; x < game.map.getW() && options.algae > 0; ++x) { @@ -122,7 +179,10 @@ static bool generate(Game &game, GenerationContext &context) } } - // Reset the grid, and recompute within the boundaries of the various islands + // Reset the grid, and recompute within the boundaries of the various islands: the same points + // and weights, grown again over grass only, so each area now covers exactly its island's + // buildable land and grid 0 is everything else (sea and beach), which divideUpPlayerLands + // measures from. for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) @@ -163,7 +223,8 @@ static bool generate(Game &game, GenerationContext &context) fillInResource(game.map, context, points, CORN, 2); points.clear(); - // Place some fruit + // Place some fruit: 1 to 6 trees of random kinds, so a neutral island may hold one, two + // or all three kinds of fruit, and is worth more to a colony the more kinds it has. int fruit_n = int(scaledCount(context.stream("layout")() % 6 + 1, options.fruit)); getAllPoints(game.map, grid, areaNumbers[1], points); chooseRandomPoints(game.map, context, points, fruit_n); @@ -199,6 +260,8 @@ GeneratorDefinition concreteIslandsDefinition() "Concrete islands", 1, false, + // Channel width is how many steps from a boundary are dug (about two tiles of water per + // step beyond 3); extra islands is the number of neutral islands. {{"channel-width", "Channel width", 5, 8, 1, 5, ControlGroup::Terrain, false}, {"extra-islands", "Extra islands", 0, 6, 1, 3, ControlGroup::Terrain, false}, // Off, islands meet their channels without a band of sand. diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp index 7c8550ec4..0aff42e00 100644 --- a/src/map/generator/generators/ContestedCommonsGenerator.cpp +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -15,6 +15,36 @@ #include #include using namespace MapGeneration; + +// Contested commons (id "contested-commons", legacy id 9): small home islands round one large rich +// island in the middle, the commons, behind a moat crossed by a few bridges. +// +// HISTORY. Written at the start of this branch (September 2026) on the 2008 area-grid toolkit, and +// made the lobby's first and default landscape. Its point spreading first ran into a fixed +// evaluation budget that refused more than four colonies at 256x256 and any colony count at 512; +// the whole-region search was rewritten to keep each tile's nearest two weighted distances, which +// generates every map it used to and the ones it refused. It gained resource amounts and switches +// later, and its generate was split into the five stages below. +// +// WHAT THE MAP IS, AND WHY. Every colony has a home island big enough to start an economy on but +// too small to win on: a wheat field, a treeline, a small quarry and room for a base. The commons +// holds everything worth fighting over - many fields and groves, a big quarry, and fruit of all +// three kinds - behind a forced ring of water with a few bridges. So the whole game is about the +// commons: who gets onto it first, who holds the bridges, and later who swims round them. +// +// GAME RULES IT LEANS ON (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): +// - Water blocks walking until a colony can swim: the moat makes the bridges chokepoints, and the +// moat is forced to water so no colony's peninsula can reach the commons on foot unfairly. +// - Fruit is a weapon: fruit groves exist only on the commons, so holding it is how a colony wins +// hungry enemy units to its inns. +// - Stone never runs out: one big quarry on the commons is a permanent strategic site. +// - Resources block movement: the commons' fields are small patches bordered by open zones, so the +// island stays walkable and no single forest walls off part of it. +// - Grass may not touch water: controlSand rings every coast after painting. +// +// Fairness: every home island is the same size and every colony is kept the same distance from the +// commons by the joint spread in disperseSeeds, but island shapes, zone layouts and bridge angles +// are random, so it is fair statistically; the lobby keeps the best-scoring of several seeds. namespace { void createJaggedIsland(Map &map, GenerationContext &context, std::vector &grid, int area, @@ -101,6 +131,13 @@ static void sizeIslands(Game &game, GenerationContext &context, minDist = (int)std::sqrt((double)game.map.warpDistSquare( L.teamPoints[0].x, L.teamPoints[0].y, L.commonsCenter.x, L.commonsCenter.y)); + // Jaggedness is RadialShape's roughness: harmonics whose amplitudes add up to at most roughness + // times stampRoughDisc's amplitude cap of 1.4, so a shape of radius r reaches at most + // r * (1 + roughness * 1.4) - hence the 1.4 below. 0.35 and 0.32 give coasts with clear + // headlands and bays (up to about 50% and 45% past the radius) without spikes thin enough to be + // pure sand; the commons is a touch smoother because it is much larger, and the same roughness + // on a big radius makes bigger bays. + // // Both islands are jagged, which means both can reach noticeably past their nominal // radius in a lucky direction -- comfortably more than a flat few-tile fudge factor once // the commons radius is large. Size everything off the worst case each jaggedness value @@ -109,6 +146,9 @@ static void sizeIslands(Game &game, GenerationContext &context, const double teamJaggedness = options.jaggedCoasts ? 0.35 : 0.0; const double commonsJaggedness = options.jaggedCoasts ? 0.32 : 0.0; + // Home size is a share of the closest colony-to-colony distance (25% by default), so homes + // scale with how far apart the spread could put the colonies; never under 10 tiles of radius, a + // floor that keeps homes usable on a small or crowded map. L.homeRadius = std::max(10, minDist * options.homeSize / 100); for (unsigned int i = 0; i < L.teamPoints.size(); ++i) createJaggedIsland(game.map, context, L.grid, L.teamAreaNumbers[i], L.teamPoints[i].x, @@ -126,6 +166,8 @@ static void sizeIslands(Game &game, GenerationContext &context, // Budget the gap as: nearest team's worst-case reach, a real moat, the commons' own // worst-case reach, and a small flat buffer on top of all that jaggedness math. const int teamReach = (int)std::ceil(L.homeRadius * (1.0 + teamJaggedness * 1.4)); + // The moat is a share of the home radius (35% by default) but never under 4 tiles, so after the + // beaches controlSand adds on both shores a clear band of open water remains. L.moatWidth = std::max(4, L.homeRadius * options.moatWidth / 100); const int roomAvailable = std::max(0, centerGap - teamReach - L.moatWidth - 3); const int roomLimitedRadius = @@ -154,6 +196,10 @@ static void sizeIslands(Game &game, GenerationContext &context, // split further down only ever touches the round island, never a narrow connecting strip. L.bridgeAreaNumber = L.areaNumber; L.areaNumber += 1; + // Each bridge is a strip 3 tiles wide along a random angle, from 2 tiles inside the commons' + // radius to 2 tiles past the moat, so it always joins solid land at both ends whatever the + // coast's jaggedness does. Random angles mean bridges need not face any particular colony, so a + // colony may be far from its nearest bridge: part of the contest. for (unsigned int b = 0; b < bridgeAngles.size() && options.moatBridges; ++b) { double theta = bridgeAngles[b] * kPi / 180.0; @@ -181,8 +227,14 @@ static void paintTerrain(Game &game, GenerationContext &context, { const int W = game.map.getW(); const int H = game.map.getH(); - // Base heightmap: noise everywhere, boosted wherever land has been carved out, so - // every island (and every bridge strip) comes out dry and everything else stays ocean. + // Base heightmap: noise everywhere, boosted wherever land has been carved out, so every island + // (and every bridge strip) comes out dry and everything else stays ocean. + // + // Sea is 40 plus noise of -20 to +19, at most 59: always under the water line (70). Claimed + // land rises by 60 to 80..119: land everywhere, sand where the noise dips it under 85 and grass + // above, so island edges get patches of wider beach, and elsewhere the one-tile sand ring + // controlSand adds. The 60 rise is what guarantees an island's shape: no claimed tile can ever + // be water, and no unclaimed one can ever be land. std::vector heights(W * H, 40); adjustHeightmapFromPerlinNoise(game.map, context, heights, 20); for (int y = 0; y < H; ++y) @@ -251,6 +303,10 @@ static void stockCommons(Game &game, GenerationContext &context, { double areaRatio = (double)(L.commonsRadius * L.commonsRadius) / (double)(L.homeRadius * L.homeRadius); + // Seven zones per home island's worth of radius (the square root of the area ratio): a + // commons four home radii across gets 28, capped at 24 so each zone keeps enough tiles to + // be a real field, and never under 7, the fewest that still hold two wood, two wheat, a + // quarry, a grove and an open zone. int zoneCount = (int)std::round(7.0 * std::sqrt(std::max(1.0, areaRatio))); zoneCount = std::max(7, std::min(24, zoneCount)); @@ -350,9 +406,11 @@ static void stockCommons(Game &game, GenerationContext &context, getAllPoints(game.map, L.grid, zoneAreas[z], pts); if (role[z] == ROLE_QUARRY) { - // One compact quarry, not a scatter: a single setResource call already - // grows a solid square footprint from its center, which reads as an actual - // deposit and leaves the rest of its zone clear to build on. + // One compact quarry, not a scatter: a single setResource call already grows a + // solid square footprint from its center, which reads as an actual deposit and + // leaves the rest of its zone clear to build on. Map::setResource's size n + // covers a square of (n / 2) * 2 + 1 tiles a side, so 5 is a 5x5 quarry: nearly + // three times the area of a home's 3x3 quarry, the commons' permanent stone. if (!pts.empty()) { MapGeneratorPoint quarry = pts[context.bounded("layout", pts.size())]; @@ -365,6 +423,9 @@ static void stockCommons(Game &game, GenerationContext &context, fillInResource(game.map, context, pts, CORN, 2); else if (role[z] == ROLE_FRUIT) { + // One fruit tree per 8 tiles of the zone, of random kinds: a grove open enough + // to walk through, since fruit can never be cleared, and with several zones + // likely to hold all three kinds between them. std::vector fruitPts = pts; chooseRandomPoints(game.map, context, fruitPts, std::max(2, (int)pts.size() / 8)); @@ -388,9 +449,13 @@ static bool settleHomes(Game &game, GenerationContext &context, // other generators do it: enough to bootstrap, not enough to make the commons optional. for (int i = 0; i < context.request.nbTeams; ++i) { - // Split the island into zones and fill most of them solid, instead of scattering a - // handful of individual tiles across the whole island: a real wheat field and a real - // treeline, not a light dusting. Two zones are left clear for the swarm and its workers. + // Split the island into zones and fill most of them solid, instead of scattering a handful + // of individual tiles across the whole island: a real wheat field and a real treeline, not + // a light dusting. Two zones are left clear for the swarm and its workers. Five equal + // zones: a treeline, a wheat field, a zone with one 3x3 quarry, and two left open for the + // swarm, its workers and the first buildings. fillInResource drops a 1x1 or 3x3 square on + // every tile of a zone, so a field zone is filled solid - a real field rather than a + // scatter. std::vector zoneWeights(5, 1); std::vector zoneAreas; for (int z = 0; z < 5; ++z) @@ -489,6 +554,9 @@ GeneratorDefinition contestedCommonsDefinition() "Contested commons", 2, false, + // Home island size is a percentage of the closest colony spacing; commons size a + // percentage of the home radius (limited by the room the spread leaves); moat width a + // percentage of the home radius. {{"home-island-size", "Home island size", 20, 35, 5, 25, ControlGroup::Terrain}, {"commons-size", "Commons size", 250, 500, 50, 400, ControlGroup::Terrain}, {"moat-width", "Moat width", 20, 50, 5, 35, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/CoralGenerator.cpp b/src/map/generator/generators/CoralGenerator.cpp index fd694218f..4fec4ce6f 100644 --- a/src/map/generator/generators/CoralGenerator.cpp +++ b/src/map/generator/generators/CoralGenerator.cpp @@ -92,14 +92,14 @@ constexpr double kSpreadJitter = 0.35; constexpr double kBend = 0.12; // The lengths of every level add up to this share of the distance from the pad to the map centre: a // little over, so the far tips reach the middle and have to compete there rather than stopping -// short in open water. The trunk is kept to kMaximumTrunk where it can be by letting each level keep -// more of its parent's length, up to all of it, so the fan fills out rather than hanging off a long -// bare trunk. +// short in open water. The trunk is kept to kMaximumTrunk where it can be by letting each level +// keep more of its parent's length, up to all of it, so the fan fills out rather than hanging off a +// long bare trunk. constexpr double kReachShare = 1.15; constexpr double kMaximumTrunk = 30; constexpr double kMaximumLengthRatio = 1.0; -// The branching control counts levels on a 256-tile map; each doubling of the map adds one, so a fan -// forks about as densely, tile for tile, whatever the map size. +// The branching control counts levels on a 256-tile map; each doubling of the map adds one, so a +// fan forks about as densely, tile for tile, whatever the map size. constexpr int kReferenceHalf = 128; // The fork angle control is the angle on a 256-tile map. Smaller maps open their forks wider and // bigger maps narrower, with the square root of the size, within these shares: a small fan has @@ -189,9 +189,9 @@ Geometry geometryFor(const GenerationRequest &r) // The lengths of the trunk and every level below it form a geometric series; its sum, shortened // by how far the branches turn off the line in, should reach just past the centre. With // kLengthRatio the trunk is whatever length does that; where that is longer than kMaximumTrunk, - // each level keeps more of its parent's length until the trunk is short enough or every level is - // as long as the trunk. The number of levels is the control's, adjusted for map size only, so - // branching always changes the map. + // each level keeps more of its parent's length until the trunk is short enough or every level + // is as long as the trunk. The number of levels is the control's, adjusted for map size only, + // so branching always changes the map. const double target = kReachShare * g.rootRadius; const auto series = [&](int levels, double ratio) { @@ -200,8 +200,8 @@ Geometry geometryFor(const GenerationRequest &r) sum += scale * std::cos(std::min(kPi / 3, level * g.spread / 2)); return sum; }; - // Levels follow the longer side: on a rectangular map the fan is stretched along it, so it needs - // the forks of the longer side to stay as dense there as on a square map of that size. + // Levels follow the longer side: on a rectangular map the fan is stretched along it, so it + // needs the forks of the longer side to stay as dense there as on a square map of that size. const int longHalf = std::max(1 << r.wDec, 1 << r.hDec) / 2; int sizeLevels = 0; for (int half = longHalf; half > kReferenceHalf; half /= 2) @@ -212,6 +212,8 @@ Geometry geometryFor(const GenerationRequest &r) g.lengthRatio = kLengthRatio; while (g.lengthRatio < kMaximumLengthRatio && target / series(g.levels, g.lengthRatio) > kMaximumTrunk) + // Raise the ratio a hundredth at a time: fine enough that the trunk lands just under its + // maximum. g.lengthRatio = std::min(kMaximumLengthRatio, g.lengthRatio + 0.01); g.trunkLength = target / series(g.levels, g.lengthRatio); // A trunk must clear its pad by a strait before it forks, or its first fork is refused against @@ -488,6 +490,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) style.lengthJitter = kLengthJitter; style.widthRatio = kWidthRatio; style.bend = kBend; + // A branch shorter than a strait plus the narrowest half width would be a stub that does not + // clear its parent's water, so forks stop there (at least 5 tiles). style.minimumLength = std::max(5.0, kMinimumHalfWidth + g.strait); style.minimumHalfWidth = kMinimumHalfWidth; const auto accept = [&](const std::vector &path, int parent) @@ -614,6 +618,9 @@ void furnishPads(Map &map, const Layout &L, GenerationContext &context) // Facing in, towards the trunk. const KitFrame frame{int(std::lround(L.pads[team].x)), int(std::lround(L.pads[team].y)), L.padAngle[team] + kPi}; + // Wheat and wood 6 tiles to either side, 3 toward the trunk; stone 8 tiles back toward the + // rim: the crops sit on the way out to the coral, the quarry behind the swarm, and no patch + // grows over another. plantKit( map, t, context, {frame.at(3, -6, 8), frame.at(3, 6, 8), frame.at(-8, 0, 6), kHomeWheat, kHomeWood, 2}, @@ -684,6 +691,9 @@ void stockCoral(Map &map, const Layout &L, GenerationContext &context, const Cor } const WedgeFrame wedges(t, L.phase, L.g.wedges, L.stretch); + // patch (10-tile cells) ranks where on the branches the fields go, with kHomeLean tilting them + // toward home; split (7-tile cells) picks wheat or wood. Both are read in the wedge's frame, so + // every fan is farmed alike. const PeriodicNoise patch(t.w, t.h, 10, context.stream("coral-patch")); const PeriodicNoise split(t.w, t.h, 7, context.stream("coral-split")); std::vector> ranked; @@ -828,8 +838,8 @@ std::string validateRequest(const GenerationRequest &r) return "Too many colonies for this map; use a bigger map or fewer colonies."; // A trunk can clear its pad and still not fit: with many colonies and wide trunks or straits, // neighbouring trunks crowd each other before they fork. That only happens on maps smaller than - // 256 tiles, where the design is quick to rebuild, so there it is rebuilt and the request refused - // with advice rather than failing every roll during generation. + // 256 tiles, where the design is quick to rebuild, so there it is rebuilt and the request + // refused with advice rather than failing every roll during generation. const auto crowded = [&] { GenerationContext trial(r); diff --git a/src/map/generator/generators/CraterLakesGenerator.cpp b/src/map/generator/generators/CraterLakesGenerator.cpp index fb25b380d..e8feaaf5e 100644 --- a/src/map/generator/generators/CraterLakesGenerator.cpp +++ b/src/map/generator/generators/CraterLakesGenerator.cpp @@ -6,6 +6,22 @@ #include "Terrain.h" #include using namespace MapGeneration; +// Crater lakes (id 4): Leo Wandersleb's crater map from January 2006, on the shared height-field +// pipeline (see Terrain.cpp). The field is round bowls pressed at random into high ground and then +// roughened (makeCraters in HeightMap.cpp). +// +// Lake density sets the bowl count, width * height * density / 30000, so it scales with map area +// (54 bowls at 256x256 and the default 25); lake size is each bowl's radius in tiles. The water +// weight (25, about 23% of the map at the defaults) then decides how deep the lakes fill. Because +// the water share is fixed, the two controls only share it out. Renders at 256x256, seed 1, water +// about 20% in every case: density 10 gives about 20 lakes, big and far apart; density 50 about a +// hundred small ponds; lake size 40 gives round, clean-edged lakes, since the wide bowls outweigh +// the noise; lake size 10 gives narrower bowls, and the noise then floods its own hollows into +// ragged lakes between them. +// +// Game rules: lakes are local obstacles, not barriers between colonies, so this plays as an open +// land map where water shapes the paths; farmland rings each lake, since wheat and wood regrow only +// near water. static bool generate(Game &game, GenerationContext &context) { const CraterLakesOptions options(context.request); diff --git a/src/map/generator/generators/EvergladesGenerator.cpp b/src/map/generator/generators/EvergladesGenerator.cpp index de615171c..c87676161 100644 --- a/src/map/generator/generators/EvergladesGenerator.cpp +++ b/src/map/generator/generators/EvergladesGenerator.cpp @@ -95,6 +95,10 @@ double expectedCoverage(const Geometry &g) { const double meanScale = (kDryScale + kWetScale) / 2; const double radius2 = g.poolRadius * g.poolRadius * meanScale * meanScale * 1.1; + // The 1.1 above is the rough outline's extra area and the 1.2 here the mean stretch of an + // ordinary pool (1 to 1.4); a slough is grown and stretched further instead, so its own 1.2 + // comes out. 1 - exp(-nominal) is the share covered by randomly overlapping shapes of that + // total area, since two pools on one tile cover it only once. const double pool = kPi * radius2 * 1.2; const double slough = pool * kSloughGrowth * kSloughGrowth * kSloughStretch / 1.2; const double nominal = ((1 - g.sloughs) * pool + g.sloughs * slough) / (g.spacing * g.spacing); @@ -145,11 +149,14 @@ Geometry geometryFor(const GenerationRequest &r) --g.clearing; else if (g.clearance > 0 || margin > 0) g.clearance = margin = 0; + // Widen the ring 2% of the half side at a time: the least change that makes room. else if (g.homeRing + 0.02 * g.half <= kHomeRingMax * g.half) g.homeRing += 0.02 * g.half; else break; } + // The pond is about a quarter of the clearing's radius (2 to 3 tiles): enough water for the + // kit's fields to regrow beside, and small enough to leave the clearing its building room. g.pond = g.clearing >= kPondClearing ? std::clamp(g.clearing * 0.23, 2.0, kPondRadius) : 0.0; g.buildRoom = std::clamp(g.clearing - 5, 0, kBuildRoom); // Pools that would drown the map are scaled down to the wettest playable coverage. @@ -234,9 +241,12 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.homes.push_back(home); } - // The pools: one per cell of a lattice that tiles the torus exactly, jittered, sized by a smooth - // wetness field so the country has wet reaches and dry ones, some grown and stretched into sloughs. + // The pools: one per cell of a lattice that tiles the torus exactly, jittered, sized by a + // smooth wetness field so the country has wet reaches and dry ones, some grown and stretched + // into sloughs. HeightMap wet(t.w, t.h, context.stream("glades-wet")); + // The wetness field's noise has cells 12 tiles across and its reaches span several of them, so + // wet and dry country runs across several pools rather than changing from one pool to the next. wet.makePlain(12); const int nx = std::max(1, int(std::lround(t.w / g.spacing))); const int ny = std::max(1, int(std::lround(t.h / g.spacing))); @@ -244,11 +254,15 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (int j = 0; j < ny; ++j) for (int i = 0; i < nx; ++i) { + // Each pool is nudged up to a third of a cell from the cell's centre: enough to hide + // the lattice, not enough for two pools to pile into one spot and leave a dry gap. const double px = (i + 0.5) * cellW + unit("glades-pools") * cellW / 3; const double py = (j + 0.5) * cellH + unit("glades-pools") * cellH / 3; const float wetness = std::clamp(wet(unsigned(t.x(int(px))), unsigned(t.y(int(py)))), 0.0f, 1.0f); const bool slough = context.bounded("glades-pools", 1000) < g.sloughs * 1000; + // Radius 70% to 130% of the pool size, scaled by wetness; outlines wobble by up to 45% + // of it, and ordinary pools are stretched up to 1.4 times, so no two pools look alike. const double radius = g.poolRadius * (0.7 + context.bounded("glades-pools", 601) / 1000.0) * (kDryScale + (kWetScale - kDryScale) * wetness) * (slough ? kSloughGrowth : 1); @@ -321,6 +335,9 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context) const double a = std::atan2(double(t.offsetY(L.cy, home.y)), double(t.offsetX(L.cx, home.x))); const KitFrame frame{home.x, home.y, a}; + // Wheat and wood start either side of the pond, just past its fullest outline (1.3) and + // beach (3 tiles), where they regrow; the quarry at the clearing's edge 4 tiles in, toward + // the map's centre, off the farmland. const double beside = L.g.pond * 1.3 + 3; plantKit(map, t, context, {frame.at(0, -beside, 10), frame.at(0, beside, 10), @@ -336,6 +353,10 @@ void stockSwamp(Map &map, const Layout &L, GenerationContext &context, const Eve const Torus &t = L.t; const int n = t.w * t.h; HeightMap patch(t.w, t.h, context.stream("glades-patch")); + // patch (noise cells 10 tiles across) decides where the swamp stands thickest, and split (cells + // 5 across, finer) whether a stretch is wood or wheat. Wood is most of the swamp (55% of its + // grass against 15% wheat): a thicket to cut through, which is also the building material an + // expanding colony needs. patch.makePlain(10); HeightMap split(t.w, t.h, context.stream("glades-split")); split.makePlain(5); @@ -360,6 +381,9 @@ void stockSwamp(Map &map, const Layout &L, GenerationContext &context, const Eve byDensity.push_back(entry.second); plantFields(map, t, byDensity, wheat, wood, [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }); + // One stone outcrop per 1000 tiles of swamp and one fruit grove per 1500, each of a random + // kind: stone lasts forever, so small scattered quarries are plenty; fruit is a prize to find + // in the thicket, not something to farm. scatterClumps(context, t, ground, int(scaledCount(std::max(2, area / 1000), o.stone)), "glades-stone", eligible, [&](MapGeneratorPoint p) { @@ -512,6 +536,8 @@ bool generate(Game &game, GenerationContext &context) const Home &home = L.homes[team]; const double a = std::atan2(double(t.offsetY(L.cy, home.y)), double(t.offsetX(L.cx, home.x))); + // 4 tiles past the pond's fullest outline: beyond its beach, on grass the swarm can stand + // on. const double back = L.g.pond > 0 ? L.g.pond * 1.3 + 4 : 0; return MapGeneratorPoint(home.x + int(std::lround(back * std::cos(a))) - 2, home.y + int(std::lround(back * std::sin(a))) - 2); diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp index 7df2c59aa..31263f5e0 100644 --- a/src/map/generator/generators/FjordContinentGenerator.cpp +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -15,6 +15,28 @@ #include #include using namespace MapGeneration; +// Fjord continent (id 12): one big continent in the middle of an ocean, cut by a fjord between +// every pair of neighbouring colonies, so each colony gets a peninsula of its own that hangs off a +// shared core. Written on this branch in September 2026 with the modular generator work; later +// passes added the ambient scatter and bank deposits, after playtesters found the map had lost +// personality with only counted deposits, and then the central lake and the lake-connected switch. +// +// THE DESIGN, IN GAME TERMS (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): +// - Home is the peninsula's tip, as far from the core as the peninsula allows. Neighbours are a +// fjord apart: close as the crow flies, but ground units must walk the long way round through +// the core until they can swim. A map where everyone is near everyone becomes one where +// contact is a choice of route. +// - The fjords are the economy. Wheat and wood regrow only near water, and every fjord bank gets +// both, so each peninsula is fed from the two fjords that flank it: the same count per colony, +// whatever the jitter did to the shapes. +// - The core is the prize: several stone clumps and groves of all three fruits (an inn holding all +// three pulls hungry enemy units across), round a lake with algae. In the default mode it is the +// one place every colony can reach by land, so it is where fights happen. +// - Resource islands out at sea are a bonus for the first colony to swim. +// +// STAGES: stamp the continent, carve the fjords, carve the lake, raise the outlier islands, lay +// beaches, anchor the colonies, check the land links, then place resources (core, sea, starter +// kits, ambient scatter, fjord banks) and run the start guarantees last. namespace { double randomAngle(GenerationContext &context) @@ -53,6 +75,8 @@ bool placeBankClump(Map &map, GenerationContext &context, if (length < 0.5) continue; const double nx = -ty / length * side, ny = tx / length * side; + // Walk out from the fjord's centre line: the first steps are still water, and 16 is past + // the widest fjord's bank (radius up to 12) and its beach. for (int distance = 2; distance <= 16; ++distance) { MapGeneratorPoint anchor( @@ -99,15 +123,24 @@ FjordLayout computeLayout(Game &game, GenerationContext &context, const int W = game.map.getW(); const int H = game.map.getH(); + // An oval looks the same turned half a circle, so half a turn covers every orientation. The + // stretch, 1 to 1.3, keeps the continent from looking stamped from a circle without letting + // peninsulas on the long axis grow much longer than those on the short one. const double rotation = randomAngle(context) * 0.5; const double elongation = 1.0 + context.bounded("layout", 1000) / 1000.0 * 0.3; ShapeTransform xf({W / 2.0, H / 2.0}, rotation, elongation); + // Continent size is the radius as a percentage of the shorter side: the default 34 gives a + // continent 68% of the map across (174 tiles on a 256 map). The coast wobbles by four harmonics + // (2, 3, 5 and 7 lobes) whose amplitudes add up to at most roughness x 1.4, so at the default + // roughness 22 the coast can bulge 31% past the base radius in its worst direction. That is why + // the size range stops at 40 and why the outlier islands below test the coast point by point. const double baseR = options.continentSize / 100.0 * std::min(W, H); RadialShape coast(baseR, options.roughness / 100.0, context, "coast", 1.4); - // Widened from 0.19 to make room for the central lake and its resource ring below without - // starving the fjords of length - coreR is still where every fjord stops short, just a little - // further out than before. + // The core: the disc every fjord stops short of, so it always stays one piece of land. 0.23 of + // the radius (20 tiles on the default 256 map) leaves the fjords three quarters of the radius + // to run, which is the length of each peninsula, while making room for the lake, its sand ring + // and the core's deposits (it was 0.19 before the lake existed). const double coreR = baseR * 0.23; // lake-size is a percentage of coreR; 0 disables the lake entirely. lake-connected decides // whether the fjords actually cut through to it (every peninsula then water-isolated from its @@ -194,9 +227,16 @@ std::vector> carveFjords(Game &game, GenerationCo double tipU = layout.fjordInnerR * cos(midTheta), tipV = layout.fjordInnerR * sin(midTheta); double perpU = -sin(midTheta), perpV = cos(midTheta); + // The S-curve's sideways swing: 17% of the arc between the two colonies' tips at the coast. + // It peaks at mid-length, where the arc is only about 60% as long, so two fjords swinging + // toward each other can narrow the peninsula between them to under half its width, but not + // cut it; much more and a peninsula could be pinched to a strip too thin to build on. double gap = std::min(d, 2 * kPi - d); double amplitude = gap * mouthR * 0.17; double phase = randomAngle(context); + // The mouth's radius: fjord width plus 0.6 to 2.0, so fjords differ a little from each + // other (the default 4 gives mouths 9 to 12 tiles across). Water that wide survives + // controlSand, and ground units cannot cross it until the colony can swim. double mouthWidth = options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; // Map::controlSand (MapTerrain.cpp) converts any water tile with a grass neighbor in its // own 3x3 neighborhood to sand - not just a coastal decoration, it can erase a channel @@ -207,6 +247,8 @@ std::vector> carveFjords(Game &game, GenerationCo // ~1.5; this targets comfortably above that floor. double tipWidth = layout.lakeConnected ? std::max(2.5, mouthWidth * 0.6) : 0.65; + // About one disc per tile of the fjord's straight length (at least 24), so consecutive + // discs overlap into one channel even where the tip is narrow and the curve swings. int steps = std::max(24, (int)(mouthR - layout.fjordInnerR)); for (int s = 0; s <= steps; ++s) { @@ -221,6 +263,8 @@ std::vector> carveFjords(Game &game, GenerationCo const auto mapped = layout.toMap({u, v}); double mx = mapped.x, my = mapped.y; + // The square scanned round each disc: the disc's radius in shape space, widened by the + // most any stretch could enlarge it on the map, plus a tile of margin. int rad = (int)ceil(width * layout.maxStretch * layout.fill.longest()) + 1; int ix = (int)lround(mx), iy = (int)lround(my); fjordCenterlines[k].push_back( @@ -274,6 +318,8 @@ void carveLake(Game &game, const FjordLayout &layout, bool sandyShore) // beach and grass to build and farm on right up to it. if (!sandyShore) return; + // Out to one and a half lake radii: at the defaults a lake about 9 tiles in radius gets sand + // out to about 14, and the core's grass beyond it (to coreR + 4, about 24) holds its deposits. const double sandOuterR = layout.lakeR + layout.lakeR * 0.5; for (int y = 0; y < layout.H; ++y) { @@ -307,6 +353,8 @@ void placeOutlierIslands(Game &game, GenerationContext &context, const FjordLayo // via the same xf.toShape transform) replaces the pessimistic global bound with an exact // local one, so an island can land close to a narrow stretch of coast even while the // coastline bulges out far away in some other direction. + // Island centres are drawn out to half the shorter side, less 6 tiles (stretched on a + // rectangle): the ocean between the continent and its own wrapped image across the map edge. double halfMapMargin = std::min(layout.W, layout.H) / 2.0 - 6.0; // No cap beyond the control's own range here - resource-islands now goes up to 20 for // players who want an island-heavy map, and each one is still an independent best-effort @@ -316,6 +364,9 @@ void placeOutlierIslands(Game &game, GenerationContext &context, const FjordLayo std::vector outlierRadii; for (int oi = 0; oi < outlierCount; ++oi) { + // Radius 5 to 8: room for one themed deposit and a small outpost. Each island keeps 8 tiles + // of open water from the coast and from other islands, more than beaches could bridge, so + // it is only reachable by swimming. int islandRadius = 5 + context.bounded("layout", 4); bool placed = false; for (int attempt = 0; attempt < 60 && !placed; ++attempt) @@ -394,6 +445,9 @@ std::vector anchorTeams(Game &game, const FjordLayout &layout { double theta = layout.teamTheta[i]; double maxR = layout.coast.radiusAt(theta); + // 8 tiles in from the coast: room for the swarm and its clear ring with the sea at its + // back. Stepping inward (at most 30 steps, stopping 2 tiles outside the core) is only for + // when that tile turned out to be water. double r = std::max(layout.coreR + 6.0, maxR - 8.0); double stepIn = (maxR - (layout.coreR + 2.0)) / 30.0; if (stepIn <= 0.0) @@ -492,6 +546,7 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou continue; const auto shaped = layout.toShape({double(x), double(y)}); double u = shaped.x, v = shaped.y; + // The core plus 4 tiles, so deposits also land where the peninsulas join it. if (u * u + v * v <= (layout.coreR + 4.0) * (layout.coreR + 4.0)) { grid[y * layout.W + x] = coreArea; @@ -539,6 +594,8 @@ void placeCoreResources(Game &game, GenerationContext &context, const FjordLayou } // The center clump above already guarantees at least one, so this is purely bonus // coverage for a lake big enough to have real interior room left over - no forced minimum. + // One more clump per 40 tiles of the lake's interior: algae spreads round a clump where it + // can grow, so a sparse seeding is enough. if (!lakeWater.empty()) for (int i = 0; i < scaledCount(int(lakeWater.size()) / 40, options.algae); ++i) placeResourceClump(game.map, context, @@ -565,11 +622,16 @@ void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout double theta = atan2(v, u); double r = sqrt(u * u + v * v); double shoreR = layout.coast.radiusAt(theta); + // 2 to 14 tiles off the coast: past the beach, yet near enough that algae's growth + // probe (water within 15 tiles, sand at the doubled offset within 30) can find the + // beach, so patches regrow rather than being mined out for good. if (r > shoreR + 2.0 && r < shoreR + 14.0) algaeWater.emplace_back(x, y); } } if (!algaeWater.empty()) + // One clump per 180 tiles of the band, at least one: a few patches along each stretch of + // coast, since algae is for upgrades, not everyday food. for (int i = 0; i < scaledCount(std::max(1, int(algaeWater.size()) / 180), algaePercent); ++i) placeResourceClump(game.map, context, @@ -579,7 +641,8 @@ void placeOpenSeaAlgae(Game &game, GenerationContext &context, const FjordLayout // 9) A light per-team starter kit so nobody is stuck waiting to reach the // fjord banks before they can build anything; the banks and the core are the -// map's real economy. +// map's real economy. The kit is a small clump of wheat, a small clump of wood and one stone, on +// land within 10 steps of the swarm (walking round water, not across it). bool placeStarterKits(Game &game, GenerationContext &context, const FjordLayout &layout, const std::vector &teamPts) { @@ -625,7 +688,10 @@ bool placeStarterKits(Game &game, GenerationContext &context, const FjordLayout // 11) Place bank resources last so settlement and regional deposits cannot // overwrite the guarantee. Every side of every fjord receives both resources -// at distinct points along its length. +// at distinct points along its length. Progress runs from the mouth (0) to the inner tip (1): +// wheat a third of the way in, nearer the colony at its peninsula's outer end, and wood two thirds +// in, toward the core. Each peninsula is flanked by two fjords, so every colony gets two wheat and +// two wood deposits on its own banks. bool placeBankResources(Game &game, GenerationContext &context, const FjordLayout &layout, const std::vector> &fjordCenterlines, const FjordContinentOptions &options) @@ -648,18 +714,21 @@ bool placeBankResources(Game &game, GenerationContext &context, const FjordLayou // on any single one of these existing, so a spot that doesn't pan out is simply // skipped rather than failing generation. A small jitter on each progress value // keeps the spacing from reading as mechanically regular. + // Two points in each stretch between the mouth, the wheat at 0.35, the wood at 0.68 and the + // tip, so the extras never land on the guaranteed deposits. static const double bankScatterProgress[] = {0.10, 0.22, 0.48, 0.58, 0.80, 0.92}; for (double progress : bankScatterProgress) { const double jitter = (context.bounded("resources", 41) - 20) / 1000.0; // +/-0.02 for (int side : {-1, 1}) { - // corn:wood at 2:1 (4:2 of 8) rather than even (3:3), stone's own 2/8 share - // unchanged - the same rebalance as the ambient scatter above, for consistency. + // wheat:wood at 2:1 (4 and 2 of 8) with stone 2 of 8, the same ratio as the ambient + // scatter in generate: in playtesting an even split read as too much wood. const int roll = context.bounded("resources", 8); const int resourceType = roll < 4 ? CORN : roll < 6 ? WOOD : STONE; // The amount controls place each rolled clump that many hundredths of a time: whole - // copies, and one more by chance from a stream of its own, so the rolls stay the same. + // copies, and one more by chance from a stream of its own, so the rolls stay the + // same. const int percent = resourceType == CORN ? options.wheat : resourceType == WOOD ? options.wood : options.stone; @@ -728,8 +797,9 @@ static bool generate(Game &game, GenerationContext &context) // makes. Algae is left at zero: the shoreline band in placeOpenSeaAlgae already places it // with a shape tuned to the coastline, and scattering more over open water would just // fight that. - // corn:wood at 2:1 rather than even - wood was reading as overrepresented in practice, and - // total density is held constant (was 18+18=36) rather than just adding more corn on top. + // wheat:wood at 2:1: in playtesting an even split (18 and 18) read as too much wood, so the + // same total of 36 was reshared rather than more wheat added on top. The numbers are densities + // that scatterResources scales by map area (wheat and wood per 1600 tiles, stone per 3000). scatterResources(game, context, {/*corn=*/int(scaledCount(24, options.wheat)), /*wood=*/int(scaledCount(12, options.wood)), diff --git a/src/map/generator/generators/IslandsGenerator.cpp b/src/map/generator/generators/IslandsGenerator.cpp index 7c314c353..35ba1617d 100644 --- a/src/map/generator/generators/IslandsGenerator.cpp +++ b/src/map/generator/generators/IslandsGenerator.cpp @@ -6,6 +6,19 @@ #include "Terrain.h" #include using namespace MapGeneration; +// Islands (id 3): Leo Wandersleb's island map from January 2006, one of the four maps built on the +// shared height-field pipeline (see Terrain.cpp for how a field becomes terrain and resources, and +// makeIslands in HeightMap.cpp for the shape). +// +// The field gets one hill per colony plus the extra islands, divided among the repeat patches, and +// the water weight sinks it until the requested share is sea. At the defaults (water 55, sand 3, +// grass 75, fruit 4) that is about 40% water, 55% grass and a few percent desert on the peaks. +// Nothing forces one island per colony: hills that overlap cancel into channels, noise breaks +// coasts, and the balanced start search picks the sites. So two colonies can share an island. +// +// Game rules: water blocks ground units until a colony trains swimmers, so the channels are a +// timer on first contact rather than a wall (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). +// Wheat and wood lie on the grass just above the shore, where they can regrow. static bool generate(Game &game, GenerationContext &context) { const IslandsOptions options(context.request); diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp index 2d24853de..d3b2053a0 100644 --- a/src/map/generator/generators/IslesGenerator.cpp +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -18,6 +18,35 @@ #include #include using namespace MapGeneration; + +// Isles (id "isles", legacy id 6): an oval island for every colony, joined to its neighbours by +// narrow land bridges. +// +// HISTORY. Bradley Arsenault added it in July 2008, two days after Concrete islands, "based on the +// Isles map" - a hand-made map of the time - using the same area-grid toolkit, and improved its +// algae and swarm placement that week. On this branch its 213-line generate was split into the +// stages below without changing its output, a queue overrun in computeDistances (crossing bridge +// lines repeat tiles) was fixed, and it gained resource amounts and Land bridges and Sandy beaches +// switches. +// +// WHAT THE MAP IS. Colonies start on islands of equal size spread evenly over the map. Where the +// straight line between two islands crosses no third island, a bridge a few tiles wide joins them, +// so the bridges are the only ground routes and fighting concentrates on them; everything else is +// open sea until swimming. With bridges off it is a pure islands map like Old islands, but with +// even spacing and equal island sizes. +// +// HOW THE TERRAIN IS MADE. Everything is a height field read at the end as water (below 90), beach +// (96 to 104) or grass (the rest). The sea starts at 50; islands and bridges are raised by distance +// from their shapes; noise of up to 45 either way roughens every coast. Because 45 is almost the +// whole gap between the sea (50) and the water line (90), coasts wander several tiles in and out. +// +// GAME RULES IT LEANS ON (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): +// - Water blocks walking until a colony can swim, so bridges are the only early routes. +// - Resources block movement: divideUpPlayerLands keeps fields on the coast zones and stone in the +// interior, and the algae patch is kept off the bridges so it cannot choke one. +// - Wheat and wood regrow near water; every colony's fields are its coastal zones. +// - Grass may not touch water: controlSand after painting rings every coast and bridge in sand. +// // What the stages of a roll hand each other: the area grid and the next free area number, each // colony's seed point, weight and area, the spacing the dispersion found, the height field the // islands and bridges are raised in, the last distance field, and the bridges' tiles and area. @@ -38,6 +67,13 @@ struct Layout }; // Spread the colonies apart and give each an oval island of its own. +// +// splitUpPoints spreads one point per colony as far apart as the map allows and returns the least +// distance between any two (minDist). Each island is a circle of island-size percent of that +// distance across, so with the default 60 there is always at least 40% of the spacing of open sea +// between two islands before the noise raises their coasts. The control's range (45 to 65) keeps it +// an islands map: the coast noise pushes shores out by several tiles, so much above 65 neighbouring +// islands start to touch, and much below 45 an island is too small for a colony's fields and base. static void layoutIslands(Game &game, GenerationContext &context, const IslesOptions &options, Layout &L) { @@ -61,6 +97,12 @@ static void layoutIslands(Game &game, GenerationContext &context, const IslesOpt } // Raise the islands in the height field, by distance from their edges. +// +// Island tiles (distance 1) rise by 100, to 150: far above the water line whatever the noise does. +// Outside, the rise falls by 10 a step, from 90 at the first step to nothing 11 steps out, so every +// island sits on a sloping shelf. A shelf tile d steps out stands at 50 + (11 - d) * 10 and is land +// when the noise lifts it to 90, which the noise (-45 to +44) can do from 2 steps out to 11: the +// coast wanders over that whole shelf, which is what makes the ovals read as natural islands. static void raiseIslands(Game &game, Layout &L) { // Construct a L.heightmap @@ -85,6 +127,13 @@ static void raiseIslands(Game &game, Layout &L) } // Join every pair of islands whose straight line crosses no third island with a land bridge. +// +// For every pair, one random tile of each island is drawn and the line between them tested: if any +// tile within bridgeWidth - 2 of the line belongs to a third island, there is no bridge (it would +// run over that island and give it a shortcut). A bridge's tiles are only claimed where they are +// more than bridgeWidth + 1 steps from any island, so the bridge does not widen the islands' +// shores. Random endpoints make bridges leave islands at varied angles rather than centre to +// centre. static void buildBridges(Game &game, GenerationContext &context, const IslesOptions &options, Layout &L) { @@ -159,7 +208,10 @@ static void paintTerrain(Game &game, GenerationContext &context, const IslesOpti Layout &L) { const int bridgeWidth = options.bridge_width; - // Stamp out the connectors + // Stamp out the connectors: the bridge's centre line (distance 1) rises by 100 like an island, + // and its sides fall off linearly to nothing at bridgeWidth steps, so a bridge is a ridge the + // noise erodes into a causeway a few tiles wide with ragged edges. A narrow bridge can be + // broken by the noise in places: the narrower the bridge, the more often it is cut. for (int x = 0; x < game.map.getW(); ++x) { for (int y = 0; y < game.map.getH(); ++y) @@ -174,6 +226,12 @@ static void paintTerrain(Game &game, GenerationContext &context, const IslesOpti } // Use the L.heightmap to put in water, grass, and sand + // + // Noise of -45 to +44. The thresholds read: water under 90, beach from 96 to 104, grass + // otherwise - including the narrow 90 to 95 band, which is grass right at the water's edge; it + // becomes beach anyway when controlSand rings the coast. Open sea (50) can reach 94 at the + // noise's very top, so a rare single grass speck can appear offshore; controlSand turns it to + // sand. adjustHeightmapFromPerlinNoise(game.map, context, L.heightmap, 45); for (int x = 0; x < game.map.getW(); ++x) { @@ -192,6 +250,12 @@ static void paintTerrain(Game &game, GenerationContext &context, const IslesOpti } // Re-divide the land between the colonies and give each an algae patch just off its coast. +// +// The land is split again over grass only, so each colony's area is its island's real buildable +// ground (the bridges keep their own area). The algae patch goes on a random tile exactly 8 steps +// from that ground: out past the beach in open water, near enough that the sand is within the +// 30-tile reach algae needs to regrow, and, with bridges, kept more than a bridge width from any +// bridge so it can never grow over the route. static bool placeAlgae(Game &game, GenerationContext &context, const IslesOptions &options, Layout &L) { @@ -279,6 +343,9 @@ GeneratorDefinition islesDefinition() "Isles", 2, false, + // Island size is each island's diameter as a percentage of the least distance between + // colonies (see layoutIslands for its range); bridge width is how many steps a bridge's + // ridge spreads. {{"island-size", "Island size", 45, 65, 5, 60, ControlGroup::Terrain, false}, {"bridge-width", "Land bridge width", 3, 6, 1, 4, ControlGroup::Terrain, false}, // Off, every colony's island stands alone in the sea. diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 52105cfcb..d8b7f9023 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -24,6 +24,19 @@ using namespace MapGeneration; // shifts shorelines unevenly. Each tile's terrain comes from its four undermap corners // (Map::regenerateMap), so a pure-grass tile needs grass at all four, and grass must never touch // water; every grass area here is therefore stamped inside a one-tile sand ring. +// +// WHY A MAZE PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Walls are stone, +// which can never be cleared, flanked by water, which ground units cannot cross until they swim, +// so the maze's routes are permanent for the whole early game. A spanning tree means exactly one +// route between any two cells, so every junction is a chokepoint worth holding, and each home is a +// cul-de-sac with a single door that is easy to defend. Loopiness adds a few second routes so a +// defended door can be flanked. Farmland lines the passages' shores, next to channel water where +// wheat and wood regrow, and sand roads keep that growth from ever sealing a passage. Fruit +// sits at the ends of dead ends, off every through route, so fetching it is a deliberate raid. +// +// THE SIZES AT THE DEFAULTS (256x256, cell size 32, channel width 2): an 8x8 grid of 64 cells; +// each half-cell of 16 tiles is 9 of passage, 5 of sand ring, flank and spine, and 2 of channel, +// so passages are 19 tiles wide and two passages are 14 tiles apart across a wall. namespace { @@ -36,11 +49,11 @@ enum Direction }; // Walking out from a cell's centre towards a wall: the passage's grass, then two walkable tiles of -// its sand ring, then channel-width all-water tiles, then the wall - two walkable tiles of flank and -// the stone spine on the boundary itself. Any tile with a non-water corner is walkable, so without -// that water a unit could step onto a wall's flank and follow the wall around the maze; a single -// water tile is enough, since a unit can't step across a tile it can't stand on. The ring, flank -// and spine account for this many tiles of every half-cell. +// its sand ring, then channel-width all-water tiles, then the wall - two walkable tiles of flank +// and the stone spine on the boundary itself. Any tile with a non-water corner is walkable, so +// without that water a unit could step onto a wall's flank and follow the wall around the maze; a +// single water tile is enough, since a unit can't step across a tile it can't stand on. The ring, +// flank and spine account for this many tiles of every half-cell. constexpr int kWallFootprint = 5; // Half the width of the narrowest passage allowed: a 9-wide chamber still seats the 4x4 swarm @@ -387,6 +400,8 @@ void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) continue; const int ox = x - cx, oy = y - cy; const int u = ox * fx + oy * fy, v = ox * rx + oy * ry; + // A 9x9 square round the swarm stays clear: its 4x4 footprint plus the ring its first + // workers step out into. if (std::abs(u) <= 4 && std::abs(v) <= 4) continue; if (v < -lane) @@ -476,6 +491,8 @@ void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, auto clump = [&](int resourceType, int size) { int seed = -1; + // A few tries for a free seed tile; late in the scatter most of the pool is taken and a + // clump is simply skipped. for (int attempt = 0; attempt < 32 && seed < 0; ++attempt) { const int candidate = pool[context.bounded("resources", pool.size())]; @@ -511,6 +528,10 @@ void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, for (const auto &layer : {std::pair{CORN, o.corn}, std::pair{WOOD, o.wood}, std::pair{STONE, o.stone}}) { + // Density per 256 shore tiles, so the defaults (wheat 48, wood 24, stone 16) cover about + // 19%, 9% and 6% of the shore band, in clumps of 4 to 12 tiles: big enough to be worth a + // trip, small enough to leave room between them. Wheat is twice wood, the ratio that read + // right in playtesting on the other generators. int remaining = shore * layer.second / 256; for (int attempt = 0; remaining > 0 && attempt < shore; ++attempt) remaining -= @@ -535,6 +556,8 @@ void seedAlgae(Map &map, GenerationContext &context, int algae) water.emplace_back(x, y); if (water.empty()) return; + // Density per 400 water tiles (the default 24 seeds about 6% of the channels), in clumps of up + // to 5 tiles. The attempt cap only ends the loop on a map with almost no free water. int remaining = int(water.size()) * algae / 400; for (int attempt = 0; remaining > 0 && attempt < int(water.size()) / 4 + 16; ++attempt) remaining -= placeResourceClump(map, context, @@ -751,6 +774,8 @@ GeneratorDefinition mazeDefinition() false, false, {24, 32, 40, 48}}, + // Cells snap to 24, 32, 40 or 48 tiles; the default 32 gives 64 cells on a 256 map, + // enough corridors to feel like a maze without passages narrower than a small base. // Open water on each side of a wall's stone line; passages widen to fill the rest. {"channel-width", "Channel width", 1, 6, 1, 2, ControlGroup::Layout}, {"loopiness", @@ -763,9 +788,11 @@ GeneratorDefinition mazeDefinition() false, false, {0, 5, 10, 20, 35, 50}}, - // Densities for the deposits scattered along the passages (per 256 shore tiles); - // homes always get the same fixed amounts. Fruit is the size, in tiles, of the - // treasure at every dead end that isn't a home. + // Loopiness is the share of non-home cells that get one extra opening: the default 5 + // opens about 3 walls on a 256 map, a few flanking routes without dissolving the maze. + // Densities for the deposits scattered along the passages (per 256 shore tiles); homes + // always get the same fixed amounts. Fruit is the size, in tiles, of the treasure at + // every dead end that isn't a home. {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, diff --git a/src/map/generator/generators/RingWorldGenerator.cpp b/src/map/generator/generators/RingWorldGenerator.cpp index bbe14b344..eef5b8d53 100644 --- a/src/map/generator/generators/RingWorldGenerator.cpp +++ b/src/map/generator/generators/RingWorldGenerator.cpp @@ -31,6 +31,20 @@ using namespace MapGeneration; // seam can't be seen. Terrain is written straight to the undermap with an order-independent beach // pass rather than Map::controlSand(), whose in-place raster scan makes shorelines depend on scan // order. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Every colony has the same +// situation: one neighbour each way along the belt, sea behind it, and colonies alternating between +// the two coasts by default so neighbours are not simply lined up on one shore. Contact is along +// one axis, so a colony can concentrate its defence on two fronts, and the sea at its back is a +// timer rather than a wall: once it can swim, a colony can raid across the ocean or take the +// resource islands, which ground units can never reach. Every home stands 6 tiles from the water, +// so each start has the same fertile shore within reach (wheat and wood regrow only near water). +// Lakes break up the interior with more shoreline to farm, and the last step clears a walkable +// route all the way round the ring, so deposits never start the game cutting the belt in two. +// +// THE SIZES AT THE DEFAULTS (256x256, belt 45%, roughness 50): the belt averages 115 tiles across, +// its centre line winds up to 28 tiles either way, its width swings up to 13 tiles and its coast +// wanders up to 26 more; the ocean between the coasts is at least 31 tiles. namespace { @@ -88,6 +102,9 @@ Axes axesFor(int width, int height) // receives its steepest step between neighbouring samples. std::vector closedCurve(int length, double falloff, std::mt19937 &rng, double &slope) { + // One harmonic per 40 tiles of ring (2 to 10), each weighted 30% to 100% at random and divided + // by (k + 1) to the falloff: the centre line uses falloff 1.6, so it is dominated by a few long + // bends, and the width uses 1.2, so it varies on shorter stretches too. const int harmonics = std::clamp(length / 40, 2, 10); std::vector amplitude(harmonics), phase(harmonics); for (int k = 0; k < harmonics; ++k) @@ -145,6 +162,10 @@ Belt shapeBelt(const Axes &axes, GenerationContext &context, const RingWorldOpti const double oceanHalf = std::max(kOceanHalf, 0.06 * breadth); const double budget = std::max(0.0, std::min(breadth / 2 - oceanHalf - meanHalf, meanHalf - kSpineHalf - 1)); + // The width swings by 22% of the half-width and the coast's roughness adds up to 90% of it at + // roughness 100. If the two together would push a coast past the spine or into the opposite + // coast's ocean, both shrink in proportion, which is what lets every control be pushed to its + // end without breaking the ring. double swing = 0.22 * meanHalf; double roughness = options.coastRoughness / 100.0 * 0.9 * meanHalf; if (swing + roughness > budget) @@ -157,6 +178,8 @@ Belt shapeBelt(const Axes &axes, GenerationContext &context, const RingWorldOpti const std::vector bend = closedCurve(length, 1.6, rng, bendSlope); const std::vector widthCurve = closedCurve(length, 1.2, rng, swingSlope); // A belt that doesn't wind still draws its curve, so the rest of the belt is unchanged. + // The centre line winds up to 11% of the breadth either way, capped so it never leans more than + // kMaxBendSlope per tile: a steeper lean would pinch the belt where it turns. double bendAmplitude = options.windingBelt ? 0.11 * breadth : 0.0; if (bendSlope > 0) bendAmplitude = std::min(bendAmplitude, kMaxBendSlope / bendSlope); @@ -193,6 +216,10 @@ void carveLakes(std::vector &terrain, const Belt &belt, Generatio if (density <= 0 || dry.empty()) return; const std::vector shore = stepsFrom(Torus{width, height}, water, land); + // Lakes per 4096 tiles of belt: the default 2 gives about 14 on a 256 map. Each is 4 to 8 tiles + // in radius, grown with the square root of the map's breadth over 128 (0.8 to 1.6 times), so + // lakes stay in proportion on bigger maps without becoming seas; 40 tries each, and a lake that + // fits nowhere is skipped. const int wanted = int(std::lround(density * double(dry.size()) / 4096.0)); const double scale = std::clamp(std::sqrt(belt.axes.breadth() / 128.0), 0.8, 1.6); struct Lake @@ -207,8 +234,8 @@ void carveLakes(std::vector &terrain, const Belt &belt, Generatio const int x = at % width, y = at / width; const RadialShape shape((4 + context.bounded("lakes", 5)) * scale, 0.35, context, "lakes"); const double reach = shape.maximumRadius(); - // The centre line leans at most kMaxBendSlope per tile, so a lake's far side can sit that much - // nearer the spine than its centre. + // The centre line leans at most kMaxBendSlope per tile, so a lake's far side can sit that + // much nearer the spine than its centre. if (shore[at] < reach + kLakeShore || std::abs(belt.across(x, y)) < kSpineHalf + (1 + kMaxBendSlope) * reach + 1) continue; @@ -282,6 +309,8 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, boo std::vector placed; for (int team = 0; team < teams; ++team) { + // Colonies are spaced evenly round the ring with up to 8% of a slot of jitter, so the + // layout is fair without looking ruled. const double jitter = (int(context.bounded("colonies", 2001)) - 1000) / 1000.0 * 0.08 * slot; const double target = first + team * slot + jitter; @@ -289,6 +318,11 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, boo const double spacing = 0.5 * slot; int bestX = -1, bestY = -1; double bestScore = std::numeric_limits::max(); + // Search within 12% of a slot of the colony's target first, widening to 25% and 50% only + // when nothing fits, so the spacing stays as even as the terrain allows. A site scores 3 + // per tile away from the ideal 6-tile distance to water, 1 per window of distance from its + // target, and 6 for being on the wrong coast: shore distance matters most, since that is + // what makes the starts equal. for (double reach : {0.12, 0.25, 0.5}) { const double window = std::max(3.0, reach * slot); @@ -331,6 +365,7 @@ bool placeColonies(Game &game, GenerationContext &context, const Belt &belt, boo return false; } std::vector home(area, 0); + // The settlement's ground: the 10x10 square round the 4x4 swarm, 3 tiles each way. for (int dy = -3; dy <= 6; ++dy) for (int dx = -3; dx <= 6; ++dx) { @@ -390,6 +425,9 @@ void furnishHomes(Game &game, GenerationContext &context) } return best; }; + // Wheat and wood 5 to 9 steps out on the ground nearest water, where they regrow, at least + // 6 tiles apart so one never grows over the other; stone 7 to 12 steps out on the ground + // farthest from water, where it takes nothing from farming. const int wheat = pick(5, 9, true, -1); if (wheat >= 0) growPatch(map, t, wheat, CORN, kHomeWheat, eligible); @@ -537,6 +575,8 @@ bool generate(Game &game, GenerationContext &context) options.resourceIslands > 0 ? std::max(1, int(std::lround(options.resourceIslands * double(t.size()) / 16384.0))) : 0; + // 40 candidate draws per island; each keeps 7 tiles of open water from every coast, too wide + // for any beach to bridge, so islands are reached only by swimming. const std::vector islands = raiseIslands(terrain, t, context, {"islands", wantedIslands, 40, kIslandMoat}); layBeaches(terrain, t); @@ -556,7 +596,11 @@ bool generate(Game &game, GenerationContext &context) /*wood=*/int(scaledCount(12, options.wood)), /*stone=*/int(scaledCount(10, options.stone)), /*algae=*/0, /*fruit=*/int(scaledCount(3, options.fruit))}); + // Algae 2 to 5 tiles off the shore, one clump per 90 tiles of that band: beside the beaches it + // needs in order to regrow. seedAlgae(map, context, t, "resources", options.algae, AlgaeBand::shallows(2, 5)); + // The branch-wide start promises: wheat within 24 steps and wood within 32 of every swarm, with + // 6 tiles clear round it. secureStartingCrops(game, context, t, 24, 32, 6); // The scatter above is sized by the resource amounts, and at the top of their range it can wall // a colony into its own clearing with nowhere to build; the reopened colony is cleared again. @@ -595,7 +639,8 @@ bool floodAround(const Map &map, const Axes &axes, const std::vector &sourc { if (winding[i] != kUnreached) continue; - // Each source starts at its image nearest the first, so a colony astride the seam is consistent. + // Each source starts at its image nearest the first, so a colony astride the seam is + // consistent. winding[i] = int(std::lround(double(anchor - axes.u(i % width, i / width)) / length)); reached.push_back(i); } diff --git a/src/map/generator/generators/RiverGenerator.cpp b/src/map/generator/generators/RiverGenerator.cpp index 6abd95f71..980b62fe3 100644 --- a/src/map/generator/generators/RiverGenerator.cpp +++ b/src/map/generator/generators/RiverGenerator.cpp @@ -6,6 +6,23 @@ #include "Terrain.h" #include using namespace MapGeneration; +// River (id 2): Leo Wandersleb's river map from January 2006 (nct fixed it on merge, giszmo patched +// gaps in the river bed a week later), on the shared height-field pipeline (see Terrain.cpp). The +// shape is one river bed that loops across the whole torus (makeRiver in HeightMap.cpp). +// +// River width is the bed's diameter as a percentage of the mean side: 35% is about 90 tiles on a +// 256x256 map. The water weight, not the width, sets how much of the map floods (35% at the +// defaults), so the two trade off. Renders at 256x256, seed 3: at width 20 the bed holds a fraction +// of the water and the rest floods the noise's hollows as ponds all over both banks; at 35 it is +// one river with a few ponds; at 65 the whole share fits in the bed, whose gentle slopes turn the +// resource bands into long parallel stripes of stone, wheat and wood along each bank, with dry, +// resource-less grass beyond. The default sits where the river reads as a river yet ponds still +// bring water, and so regrowing farmland, inland. The winding switch only removes the meander; +// everything else stays the same. +// +// Game rules: the river splits the map into banks that meet only across water, so colonies on +// opposite banks cannot fight until they swim, and the banks are the best farmland on the map +// (wheat and wood regrow only near water). static bool generate(Game &game, GenerationContext &context) { const RiverOptions options(context.request); diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index b81fdb945..5c0785816 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -19,6 +19,33 @@ #include using namespace MapGeneration; +// Old islands (id "rugged-archipelago", legacy id 8): one island per colony in open sea. +// +// HISTORY. nuage added this "Islands map generator system" in August 2002, days after the first +// random generator (Old random), and reworked it twice that week for "more fair, nicer, real +// beach". It was the game's islands map until Leo Wandersleb's height-field Islands took the name +// in 2006, when giszmo kept it as "old islands". On this branch its 274-line terrain function was +// split into the growth passes below without changing its output, dead stores of legacy amounts +// went, and it gained resource amounts and an Extra starting deposit switch. +// +// WHAT THE MAP IS. Every colony starts alone on its own roughly round island, the same kit on every +// island in the same arrangement (wood north, wheat west, stone south, a reinforcing field to the +// south-east, algae offshore to the east), with open water between islands. It is the purest "build +// up, then swim" map: nobody can reach anybody on foot, so the early game is pure economy and the +// first contact comes when someone builds a swimming pool. +// +// GAME RULES IT LEANS ON (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): +// - Water blocks walking until a colony can swim, so separate islands delay all contact. +// - Grass may not touch water: Map::controlSand rings every island in sand, and spreadBeaches can +// widen that ring. Sand is walkable but unbuildable, so wide beaches shrink a colony's room. +// - Wheat and wood regrow only near water; on an island of this size every field is near the sea. +// - Algae needs water with sand in reach, which a beach right beside it gives. +// +// Fairness is statistical: islands grow by random accretion, so they differ in shape, and a small +// or irregular island can squeeze its deposits; the shared backstops at the end of generate() top +// up short colonies, and the lobby keeps the best-scoring seed. Everything draws from the "terrain" +// stream, so a seed reproduces the map. + // The eight directions the growth passes below draw from: a pair of opposite neighbours at one // or two tiles, in the order the draw has always indexed them. static const int kGrowthDirections[8][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}, @@ -34,9 +61,22 @@ static int plantBootstraps(Map &map, GenerationContext &context, int *bootX = context.bootX.data(); int *bootY = context.bootY.data(); int nbIslands = context.request.nbTeams; + // The number of growth passes, which is roughly each island's radius: the mean side of the map + // times island_size/400, divided by the square root of the colony count so the islands' total + // area stays about the same however many colonies share the map. With the default 65 on a + // 256x256 map that is 41 passes for 4 colonies and 29 for 8; never fewer than 8, the least that + // grows an island a 4x4 swarm and its kit fit on. + // + // The control's narrow range (50 to 70) is what keeps this an islands map. Rendered on 256x256 + // with 4 colonies and 512x512 with 8: at 50 the islands are small and well apart; at 65 and 70 + // neighbouring islands on a crowded map start to touch and merge, and much above 70 they merge + // into continents and the map stops being about swimming. int islandsSize = (int)(((w + h) * options.island_size) / (400.0 * sqrt((double)nbIslands))); if (islandsSize < 8) islandsSize = 8; + // Each seed keeps a colony's share of the map from every other, as a squared distance: seeds + // sit about the side of a square of w*h/colonies apart, so islands grown to similar radii + // rarely overlap. int minDistSquare = (w * h) / nbIslands; int c = 0; @@ -54,6 +94,8 @@ static int plantBootstraps(Map &map, GenerationContext &context, } if (failed) { + // Rejection sampling: draw again. After 65536 failed draws in a row the map is too + // crowded for that spacing, so the spacing is halved and the count restarts. i--; if (c++ > 65536) { @@ -67,6 +109,8 @@ static int plantBootstraps(Map &map, GenerationContext &context, { bootX[i] = x; bootY[i] = y; + // The island's seed: a 7x6 block of grass corners holding the 4x4 swarm and its + // workers. for (int dx = -1; dx < 6; dx++) for (int dy = 0; dy < 6; dy++) map.setUMTerrain(x + dx, y + dy, GRASS); @@ -78,6 +122,14 @@ static int plantBootstraps(Map &map, GenerationContext &context, // Grow the islands: for so many passes, every non-grass tile on a checkerboard draws one of // sixteen values and, for the eight that name a direction, turns to grass when either // neighbour that way already is. +// +// This is random accretion, like frost growing on glass: each pass a water tile tests one direction +// half the time (draws 0 to 7 of 16) and joins the island if a grass tile lies that way, so the +// coast advances by up to a tile or two a pass, unevenly, and the outline comes out ragged +// ("rugged"); on a 256x256 map with 4 colonies the 41 passes grow islands about 75 tiles across. +// Tiles are visited on +// the two halves of a checkerboard in turn so a tile that has just turned grass does not feed its +// neighbour in the same sweep, which would let growth race along rows in scan order. static void expandIslands(Map &map, std::mt19937 &rng, int passes) { const int w = map.getW(), h = map.getH(); @@ -98,8 +150,9 @@ static void expandIslands(Map &map, std::mt19937 &rng, int passes) } } -// Close single-tile gaps: a tile between two grass tiles, in any of the four directions, -// becomes grass, twice over. +// Close single-tile gaps: a tile between two grass tiles, in any of the four directions, becomes +// grass, twice over. Accretion leaves pinholes and one-tile inlets; left in, each would become a +// sand pit (grass may not touch water) that wastes building room in the middle of an island. static void smoothGrass(Map &map) { const int w = map.getW(), h = map.getH(); @@ -121,6 +174,11 @@ static void smoothGrass(Map &map) // Widen the beaches: for so many passes, every tile on a four-by-four lattice draws one of // eight values; the first four turn it to sand between sand and water along that direction, // the last four between sand and sand. +// +// Sand beside sand and water grows the beach outwards into the sea and inwards over grass; sand +// between two sands fills gaps in it. Visiting one tile in sixteen per sweep (sixteen offsets in +// turn) keeps growth from racing in scan order. `beach-size` (0 to 4, default 1) is the pass count: +// every pass costs grass (room to build) and adds walkable sand round the island. static void spreadBeaches(Map &map, std::mt19937 &rng, int passes) { const int w = map.getW(), h = map.getH(); @@ -177,6 +235,15 @@ static void resources(Game &game, GenerationContext &context, islandsSize = 8; // let's add resources... Each deposit is a square sized to the island; the amount controls // scale its area around the same centre. + // + // Every colony gets the same kit laid out the same way round its swarm: each deposit walks from + // the swarm towards the coast in its own direction, counts the grass (d), and becomes a square + // `amount` tiles a side centred on that run (`p` is the centre's distance). The square is the + // run less a margin of islandsSize/4, the number of rounds the deposits were meant to spread + // for, so a grown field would still stop short of the beach, and less a small constant per + // resource (wood 2, wheat 0, the reinforcing field 3) that shrinks wood and the extra field a + // little against wheat. Stone is one tile. Map::setResource draws each tile's starting amount + // from the gameplay RNG, so the order of these calls is part of the map. int smoothResources = islandsSize / 4; for (int s = 0; s < context.request.nbTeams; s++) { @@ -214,6 +281,10 @@ static void resources(Game &game, GenerationContext &context, } // STONE + // The walk south measures d, but the stone is placed at the wheat's centre distance `p`, + // which is not recomputed here: an original slip. The stone lands p tiles south, where p + // is how far west the wheat field sits, not where the southern grass ends. It is one tile + // and almost always on the island, so it is kept rather than moving every colony's quarry. for (d = 0; d < islandsSize; d++) if (!map.isGrass(bootX[s], bootY[s] + d)) break; @@ -232,6 +303,8 @@ static void resources(Game &game, GenerationContext &context, smallestResource == CORN ? options.wheat : options.wood); // ALGAE + // East to the first water, then out past it by the spreading margin, so the algae sits a + // little offshore of the beach, where it has water all round and sand in reach to regrow. for (d = 0; d < 2 * islandsSize; d++) if (map.isWater(bootX[s] + d, bootY[s])) break; @@ -242,6 +315,10 @@ static void resources(Game &game, GenerationContext &context, } // Let's smooth resources... + // Meant to fray the squares into fields; a no-op today. Map::smoothResources still looks for + // resources in the terrain layer, where they have not lived since 2003, so no tile matches and + // no random number is drawn (removing it leaves maps byte-identical). Kept as the design's + // intent; the deposits stay visible squares. map.smoothResources(smoothResources * 2); } @@ -274,6 +351,8 @@ GeneratorDefinition ruggedArchipelagoDefinition() "Old islands", 1, false, + // Island size scales the growth passes (see plantBootstraps for why its range is narrow); + // beach size is the number of beach-widening passes. {{"island-size", "Island size", 50, 70, 1, 65, ControlGroup::Terrain, false}, {"beach-size", "Beach size", 0, 4, 1, 1, ControlGroup::Terrain, false}, // Off, an island gets no fourth deposit of whichever of wheat or wood came out smaller. diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 71a4f2626..2eae37664 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -20,6 +20,50 @@ #include using namespace MapGeneration; +// Old random (id "shattered-coast", legacy id 7): the game's original random map generator. +// +// HISTORY. Luc-Olivier de Charrière ("nuage") wrote it in August 2002 as the RandomMapGenerator +// that first let network games start without exchanging a map file: paint every tile water, sand or +// grass at random in the requested shares, smooth the noise into blobs, seat colonies on the widest +// grass, and hand each colony a few square deposits found by looking along the eight compass +// directions. When Leo Wandersleb's height-field generators (Islands, Swamp, River, Crater lakes) +// replaced it in January 2006, giszmo brought it back "marked as old" so players kept the look. On +// this branch it moved into the generator framework, was split into the stages below without +// changing a byte of its output, gained resource amounts and a Colony meadows switch, stopped +// refusing crowded maps (see placeColonies) and lost a bias that made every colony wood-poor (see +// resources). +// +// WHAT THE MAP IS. A shattered coastline: grass, sand and water interleaved at every scale, with no +// large-scale structure at all. That is its charm and its weakness. Nothing guarantees a colony +// water for its fields, room to build, or a fair share of anything; fairness comes only from the +// lobby keeping the best-scoring of several seeds and from the shared backstops at the end of +// generate(). Newer generators design structure first and texture second; this one is all texture. +// +// GAME RULES IT LEANS ON (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): +// - Grass may not touch water, so Map::controlSand turns every grass corner beside water into sand +// after painting. Heavy smoothing leaves long coasts and so a lot of that forced sand. +// - Wheat and wood only regrow near water. The fine patchwork keeps water close to almost every +// grass tile, which is why this map's farmland regrows well despite its randomness. +// - Buildings need pure grass: the widest grass patches become homes (placeColonies), and each home +// is stamped a patch of grass big enough for the 4x4 swarm and its first buildings. +// +// Every draw here comes from named streams ("simulation", "terrain"), so a seed reproduces the map. +// Several odd-looking details (the scratch map's offsets, the algae search's comparison) are +// original behaviour kept on purpose: changing them changes every map this generator makes. + +// A dry run of the paint-and-smooth process on a small scratch map, used only to learn what shares +// of water, sand and grass a painting ends up with after `smooth` passes (fitTerrainMix below). +// It reports the shares through the three out-parameters and never touches the real map. +// +// The scratch map is square and a power of two, so wrapping is a bit mask (`& m`): 32 tiles a side +// for 0 to 3 smoothing passes, 64 for 4 to 7 and 128 for 8, big enough that the blobs smoothing +// grows are small against the map and the measured shares settle. Its smoothing is a simplified +// copy of smoothPatchwork's, without the over-share brake. Its first two pairs of neighbours match +// the real pass (across and down, then the two diagonals), but its last two pairs were mistyped in +// the original index arithmetic (`h << 1` is two rows, not two columns): they look one row and two +// columns away, straight up and down, two columns and three rows away, and two columns and one row +// away, where the real pass looks two tiles across, down and along each diagonal. The simulation +// only feeds an estimate that fitTerrainMix corrects, so it is kept as it was. static void simulateRandomMap(GenerationContext &context, int smooth, double baseWater, double baseSand, double baseGrass, double *finalWater, double *finalSand, double *finalGrass) @@ -36,12 +80,16 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas // callers passing a literal) per tile per draw instead of changing which numbers come out. std::mt19937 &rng = context.stream("simulation"); + // The shares become integer weights out of 0x7FFF (32767), fine enough that a share of a + // fraction of a percent still paints some tiles, and small enough that `rng() % totalRatio` + // has no noticeable bias. int totalRatio = 0x7FFF; int waterRatio = (int)(baseWater * ((double)totalRatio)); int sandRatio = (int)(baseSand * ((double)totalRatio)); int grassRatio = (int)(baseGrass * ((double)totalRatio)); totalRatio = waterRatio + sandRatio + grassRatio; + // All three shares zero: paint the three terrains equally rather than divide by zero. if (totalRatio == 0) { waterRatio = 1; @@ -76,6 +124,9 @@ static void simulateRandomMap(GenerationContext &context, int smooth, double bas assert(false); // Want's to sing ? } + // Each pass: a tile takes the terrain its two opposite neighbours share, trying one of each + // pair of directions in turn (horizontal or vertical, then the two diagonals, then two steps + // out). `rng() & 4` tests one bit of a draw: a coin flip for which of each pair to look along. for (int i = 0; i < smooth; i++) for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) @@ -214,9 +265,19 @@ static void countTerrain(const Map &map, int &waterCount, int &sandCount, int &g } } -// Smoothing eats into whichever terrain is rarest, so the shares the patchwork is painted with -// are not the shares the map should end up with. There is no closed form; this searches for -// painting shares whose simulated result lands on the requested ones. +// Smoothing eats into whichever terrain is rarest, so the shares the patchwork is painted with are +// not the shares the map should end up with. There is no closed form; this searches for painting +// shares whose simulated result lands on the requested ones. +// +// The search is a crude secant-style method on the three shares, run once per smoothing level from +// 1 up to the requested one so each level starts from the previous level's answer: +// - alpha is the current guess. Simulating it gives an error against the requested shares. +// - beta is a proportional correction: each share scaled by requested / achieved, renormalised. +// Simulating beta gives a second error. +// - The two errors' projection says how far along the alpha-to-beta line the answer lies; gamma +// tries eleven points on that line (cf from 0 to proj in tenths) and keeps the one whose +// simulated error is smallest as the next alpha. Each simulation is a full scratch map, so this +// search, not the real map, is most of this generator's running time at high smoothing. static TerrainMix fitTerrainMix(GenerationContext &context, const TerrainMix &base, int smooth) { const double baseWater = base.water, baseSand = base.sand, baseGrass = base.grass; @@ -323,7 +384,8 @@ static TerrainMix fitTerrainMix(GenerationContext &context, const TerrainMix &ba return {alphaWater, alphaSand, alphaGrass}; } -// Every tile draws its terrain independently, in the fitted shares. +// Every tile draws its terrain independently, in the fitted shares: pure white noise, which the +// smoothing passes then grow into blobs. static void paintPatchwork(Map &map, std::mt19937 &rng, const TerrainMix &mix) { int totalRatio = 0x7FFF; @@ -369,9 +431,19 @@ static void paintPatchwork(Map &map, std::mt19937 &rng, const TerrainMix &mix) static const int kSmoothingDirections[8][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}, {2, 0}, {0, 2}, {2, 2}, {2, -2}}; -// Each pass makes a tile take the terrain of two matching opposite neighbours, one direction -// after another, but a terrain already over its requested share is allowed to spread only -// rarely, the more so the further over it is. +// Each pass makes a tile take the terrain of two matching opposite neighbours, one direction after +// another, but a terrain already over its requested share is allowed to spread only rarely, the +// more so the further over it is. +// +// The brake: a terrain over its share by `err` (a fraction of the map) may spread into a tile only +// when a fresh 32-bit draw is at least err^(1/8) of the range. The eighth root makes the brake very +// strong for tiny overshoots: 1% over blocks 56% of spreads, 10% over blocks 75%. Measured on +// 256x256 maps with the default weights (40 water, 4 sand, 60 grass) and 3 passes, six seeds: pure +// water tiles come to 18% of the map with this brake, against 26% with a linear brake (err^1) or +// none at all, because water that cannot spread stays broken into many small pools with long +// shores. That fragmentation is the "shattered" look; at 8 passes the difference mostly disappears, +// since by then the blobs are large anyway. The first pass has no brake (allowed is zeroed), so the +// initial noise always gets one free round of clumping before the shares are policed. static void smoothPatchwork(Map &map, std::mt19937 &rng, int smooth, const TerrainMix &base) { const int w = map.getW(), h = map.getH(); @@ -426,9 +498,14 @@ static void smoothPatchwork(Map &map, std::mt19937 &rng, int smooth, const Terra } } -// Seat every colony on grass, each a colony's share of the grass from the others, and give -// each a meadow unless the colonies are to start in the shattered terrain itself. The counts -// are the map's before sand control ran. +// Seat every colony on grass, each a colony's share of the grass from the others, and give each a +// meadow unless the colonies are to start in the shattered terrain itself. The counts are the map's +// before sand control ran. +// +// Spacing: minDistSquare = width * height * grassShare / colonies is the area of one colony's share +// of the grass, used as a squared distance, so colonies sit about the side of a square of that much +// grass apart - further apart on grassy maps and with few colonies, closer on watery or crowded +// ones. static bool placeColonies(Map &map, GenerationContext &context, const ShatteredCoastOptions &options, int grassCount, double totalCount) { @@ -447,6 +524,10 @@ static bool placeColonies(Map &map, GenerationContext &context, // find every remaining patch too close; rather than refuse the map, such a colony accepts a // nearer patch, down to half the spacing, then a quarter. A map that seats every colony at // full spacing never reaches the relaxation, so its starts are as they always were. + // A patch is measured cheaply, not exactly: every horizontal run of more than 7 grass tiles (a + // run that could hold the 4x4 swarm with room either side) is measured vertically through its + // middle, and the run's width times that height stands in for its area. The largest such + // rectangle that keeps its spacing from the colonies already placed wins. auto widestPatch = [&](int team, int spacingSquare, int &maxX, int &maxY) { int maxSurface = 0; @@ -520,11 +601,18 @@ static bool placeColonies(Map &map, GenerationContext &context, bootX[team] = maxX; bootY[team] = maxY; + // A 7x6 block of grass corners under the boot tile: the 4x4 swarm with a ring for its + // workers, so the swarm always fits whatever the patch's real outline. for (int dx = -1; dx < 6; dx++) for (int dy = 0; dy < 6; dy++) map.setUMTerrain(maxX + dx, maxY + dy, GRASS); } + // The meadow: two overlapping grass squares, 5 tiles plus a fraction of the colony spacing a + // side (about 25 on a 256x256 map with 4 colonies and the default weights), so a home has clear + // building room that grows with the ground each colony is meant to have. The 4.5 divisor keeps + // a meadow to about a quarter of the spacing, so meadows of colonies seated at full spacing + // stay apart; colonies seated at relaxed spacing (below) can have meadows that touch. int squareSize = 5 + (int)(sqrt((double)minDistSquare) / 4.5); for (int team = 0; team < nbTeams && options.colony_meadows; team++) { @@ -552,6 +640,9 @@ static bool terrain(Game &game, GenerationContext &context, const ShatteredCoast countTerrain(map, waterCount, sandCount, grassCount); const double totalCount = (double)(waterCount + sandCount + grassCount); + // Sand control twice: once so the patches colonies are measured on are real grass (grass beside + // water has just become beach), and again because the grass stamped for each swarm and meadow + // may itself touch water. map.controlSand(); if (!placeColonies(map, context, options, grassCount, totalCount)) return false; @@ -568,9 +659,21 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa int *bootX = context.bootX.data(); int *bootY = context.bootY.data(); int nbTeams = context.request.nbTeams; + // How far along a compass direction a colony looks for ground to put a deposit on: the mean + // side of the map shared out between the colonies, so deposits stay in a colony's own half of + // the way to its neighbours. int limitDist = (w + h) / (2 * nbTeams); - // let's add resources to old map generator + // Each colony gets its deposits along compass directions from its swarm, one direction per + // deposit, so they spread round the home rather than piling up on one side. For each deposit, + // every unused direction is walked out from 5 tiles (clear of the swarm) to limitDist, tracking + // the current run of grass: a gap after a run of 4 or more ends the walk, a gap after a shorter + // run restarts the count (so a sliver of grass between water does not count as a field). The + // direction scoring best on distance + run width wins, and a square deposit is centred on the + // middle of that run. `amount` is the deposit's side in tiles (the request's legacy + // per-resource amounts, 7 by default), scaled in area by the amount controls. Map::setResource + // draws each tile's starting amount from the gameplay RNG, so the order below is part of the + // map. for (int team = 0; team < nbTeams; team++) { int smallestWidth = limitDist; @@ -595,6 +698,8 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa resOrder[3] = CORN; int primaryWidth[2] = {0, 0}; + // The fourth, reinforcing deposit scores distance and width double: it goes to the + // furthest, widest run left, where there is room for a second field of the scarcer crop. int distWeight[4]; distWeight[0] = 1; distWeight[1] = 1; @@ -654,13 +759,17 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa dx *= d; dy *= d; - // Every deposit is a square of `amount` tiles a side; the amount controls scale its area. + // Every deposit is a square of `amount` tiles a side; the amount controls scale its + // area. int amount = context.request.resourceAmounts[res]; if (amount > 0) setScaledResource(map, bootX[team] + dx, bootY[team] + dy, res, amount, options.percent(res)); } + // One more deposit of whichever resource landed on the narrowest run, looking up to twice + // as far: a colony whose wheat or wood field is squeezed gets a second field somewhere + // roomier. if (smallestWidth < limitDist) { int maxDir = 0; @@ -701,6 +810,11 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa options.percent(smallestResource)); } + // Algae: the same walk over water in all eight directions. Its comparison reads + // `dist + width > width + maxWidth`, which reduces to dist > maxWidth rather than the grass + // searches' dist + width > maxDist + maxWidth: a slip in the original that biases algae + // towards whichever direction first runs further than the widest water run so far. It is + // kept, since fixing it moves every colony's algae on every map this generator has made. int maxDir = 0; int maxWidth = 0; int maxDist = 0; @@ -736,7 +850,13 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa setScaledResource(map, bootX[team] + dx, bootY[team] + dy, ALGA, amount, options.algae); } - // Let's smooth resources... + // Map::smoothResources was meant to fray the square deposits into natural fields, three rounds + // per tile of the largest deposit side. It does nothing today: it still looks for resources + // encoded in the terrain layer (terrain values 272 and up), and resources have lived in their + // own layer since the 2003 resource rework, so no tile ever matches and no random number is + // drawn. Removing the call leaves every map byte-identical (checked on seeds of this generator + // and Old islands). That is why this generator's deposits are visible squares. The call is kept + // so the code still says what the design intended; making it work would change every map. int maxAmount = 0; for (int r = 0; r < 4; r++) if (maxAmount < context.request.resourceAmounts[r]) @@ -774,6 +894,9 @@ GeneratorDefinition shatteredCoastDefinition() "Old random", 2, false, + // The three terrain weights are relative (40/4/60 asks for 38% water, 4% sand, 58% grass + // before sand control adds the beaches); smoothing is the number of passes, which sets the + // scale of the blobs from speckle (1) to broad coasts (8). {{"water", "Water weight", 0, 100, 1, 40, ControlGroup::Terrain, false, true}, {"sand", "Sand weight", 0, 100, 1, 4, ControlGroup::Terrain, false, true}, {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, diff --git a/src/map/generator/generators/SpiderWebGenerator.cpp b/src/map/generator/generators/SpiderWebGenerator.cpp index 5c010e48e..26a4cd22e 100644 --- a/src/map/generator/generators/SpiderWebGenerator.cpp +++ b/src/map/generator/generators/SpiderWebGenerator.cpp @@ -34,6 +34,14 @@ using namespace MapGeneration; // per wedge, keyed by a thread's place in the wedge, so every colony gets the same web turned round // the centre and the layout is fair for any colony count. The design is a pure function of the // request, so validateWorld rebuilds it and checks the finished world. +// +// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): every thread is land in +// water, so wheat and wood regrow along its whole length, which is why threads carry fields, and +// why a sand road runs down their middle: growth refuses where its probe finds sand, and nothing +// grows on sand itself, so the road stays a lane and a thread never grows shut. Buildings need pure +// grass, so a thread must be wide enough to keep grass either side of that road. Fruit lets an inn +// pull hungry enemy units across, which makes the hub's orchard the prize at the one place every +// spoke leads. namespace { @@ -110,6 +118,9 @@ Geometry geometryFor(const GenerationRequest &r) o.threadWidth * std::clamp(std::sqrt(g.half / 128.0), kMinimumThreadScale, 1.0); g.threadHalf = threadWidth / 2.0; g.spokeHalf = g.threadHalf + 1; + // The hub is a share of the half side, at least 6 tiles; from 9 tiles up it holds a pond 30% of + // its radius (at least 2), so the orchard round it stands beside water with room for its + // groves. g.hubRadius = std::max(6.0, o.hubSize / 100.0 * g.half); g.pondRadius = g.hubRadius >= 9 ? std::max(2.0, 0.3 * g.hubRadius) : 0.0; g.spacing = o.ringSpacing; @@ -237,6 +248,9 @@ struct Layout const ShapePoint from = onSpoke(j, a), to = onSpoke(j + 1, b); const double mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2; const double inward = std::max(1e-9, std::hypot(cx - mx, cy - my)); + // Each strand sags by 60% to 140% of the sag share of its length. The curve's middle + // control point is set at twice the pull, since a quadratic curve passes only halfway to + // its control point. const double pull = g.sag * std::hypot(to.x - from.x, to.y - from.y) * (0.6 + 0.8 * roll(r, "web-sag/", id) / 1000.0); out = {from, @@ -333,7 +347,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } // The capture threads: every turn of the spiral, or every ring, from spoke to spoke. A torn // strand keeps its two ends hanging from the spokes. Where a thread leaves a spoke is a knot. - // The torn share is exact: of the distinct strands of a wedge, those with the lowest rolls tear. + // The torn share is exact: of the distinct strands of a wedge, those with the lowest rolls + // tear. const long long turns = (long long)std::ceil((g.lastRing - g.firstRing) / g.spacing) + 1; std::vector> strands; for (long long k = -teams - 1; k <= turns; ++k) @@ -359,7 +374,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.knots.push_back(L.stretch.apply(L.cx, L.cy, thread.from)); } - // The hub, with a pond at its heart, and a pad for every colony where its spoke meets the frame. + // The hub, with a pond at its heart, and a pad for every colony where its spoke meets the + // frame. const RadialShape hubShape(g.hubRadius, 0.2, context, "web-hub"); fillShape(L.land, t, L.cx, L.cy, hubShape, 0, 1, L.stretch); fillShape(L.hub, t, L.cx, L.cy, hubShape, 0, 1, L.stretch); @@ -469,6 +485,9 @@ void furnishPads(Map &map, const Layout &L, GenerationContext &context) { return L.padOf[i] == team && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; const KitFrame frame{int(std::lround(L.pads[team].x)), int(std::lround(L.pads[team].y)), L.padAngle[team]}; + // Wheat and wood 6 tiles to either side of the pad's centre and 5 back from it, stone 8 + // tiles the other way along the pad's axis: the three far enough apart that no patch grows + // over another, all inside the pad. plantKit( map, t, context, {frame.at(-5, -6, 8), frame.at(-5, 6, 8), frame.at(8, 0, 6), kHomeWheat, kHomeWood, 2}, @@ -500,10 +519,14 @@ void stockWeb(Map &map, const Layout &L, GenerationContext &context, const Spide continue; const bool stone = drop.prize == 3; if (scaledCount(1, stone ? o.stone : o.fruit) > 0) + // A dew drop's prize is a patch of 9 tiles of fruit or stone: a reason to swim out, not an + // economy. growPatch(map, t, seed, stone ? STONE : CHERRY + drop.prize, 9, onDrop); } const auto onHub = [&](int i) { return L.hub[i] && clearGround(map, i % t.w, i / t.w); }; + // The orchard's groves stand 4 tiles past the pond's fullest outline (1.25), clear of its + // beach, or halfway out on a hub without a pond. const double rho = L.g.pondRadius > 0 ? L.g.pondRadius * 1.25 + 4 : 0.5 * L.g.hubRadius; const double spin = context.bounded("web-hub", 3600) / 3600.0 * 2 * kPi; if (scaledCount(1, o.fruit) > 0) @@ -526,6 +549,10 @@ void stockWeb(Map &map, const Layout &L, GenerationContext &context, const Spide } const WedgeFrame wedges(t, L.phase, L.g.teams, L.stretch); + // patch (12-tile cells) ranks where on the threads the fields go, so they come in runs with + // bare thread between; split (7-tile cells) picks wheat or wood, so a run of fields changes + // crop now and then. Both are read in the wedge's frame, so every colony's threads are farmed + // alike. const PeriodicNoise patch(t.w, t.h, 12, context.stream("web-patch")); const PeriodicNoise split(t.w, t.h, 7, context.stream("web-split")); std::vector> ranked; diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 4c633888b..98433de48 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -33,6 +33,16 @@ using namespace MapGeneration; // all, and loopiness opens extra passes for routes to flank along. Colonies take the roomiest // valleys, one each while they last, and ponds fill the basins around them. The whole design is a // pure function of the request, so validateWorld rebuilds it and checks the finished world. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Stone blocks movement and +// can never be cleared, so ridges are permanent walls, and the passes are the only doors: a valley +// is a room whose passes are worth holding, and loopiness (30% of spare ridgelines by default) +// makes sure a held pass can be flanked. Stone is also the upgrade resource, so every colony's +// quarry is simply the nearest ridge; stone is everywhere and never runs out, and the fight is +// over passes and fertile ground instead. Wheat and wood regrow only near water, so each valley's +// ponds decide how much farmland it can carry; colonies take the roomiest valleys. The ring road +// along every ridge (nothing placed within 3 tiles of it) keeps every pass reachable however much +// grows. namespace { @@ -126,6 +136,8 @@ std::vector scatterSites(const Torus &t, int spacing, GenerationContext &c rows[r] = bucketWindow(r, gy, 1); std::vector> buckets(size_t(gx) * gy); std::vector sites; + // 60 darts per expected site: enough to saturate the map, after which nearly every dart lands + // too close to an existing site. for (std::int64_t attempt = 0; attempt < expected * 60; ++attempt) { const int x = int(context.bounded("highlands-sites", t.w)); @@ -161,6 +173,10 @@ std::vector scatterSites(const Torus &t, int spacing, GenerationContext &c std::vector labelCells(const Torus &t, const std::vector &sites, int spacing, GenerationContext &context) { + // The warp's noise has features one and a half valleys across and moves positions by up to 30% + // of the spacing: enough to bend the straight edges of nearest-site cells into wandering + // ridgelines, not enough to tear a cell into scraps (which the pocket rule would then have to + // fill with stone). const int period = spacing * 3 / 2; const std::vector warpX = fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); @@ -350,6 +366,8 @@ bool carvePass(Layout &L, GenerationContext &context, int a, int b, std::vector< band = std::max(1, band); const size_t first = context.bounded("highlands-passes", std::uint32_t(band)); const int lo = -(width - 1) / 2, hi = width / 2; + // Up to 12 tries, starting at a random point among the candidates within a quarter of the best + // one's distance from the ridge's ends, so passes are central but not all at the exact middle. for (size_t k = 0; k < candidates.size() && k < 12; ++k) { const int c = candidates[k < band ? (first + k) % band : k]; @@ -404,6 +422,9 @@ bool chooseHomes(Layout &L, GenerationContext &context, int teams) if (L.ridgeDistance[i] < depth) continue; const double d = std::sqrt(double(t.dist2(i % t.w, i / t.w, pole % t.w, pole / t.w))); + // Closeness to the wanted offset from the valley's deepest point weighs 64 times a tile + // of extra distance from the ridge: the offset decides, and room from the ridge only + // breaks ties. const int key = std::abs(int(std::lround(d)) - offset) * 64 - L.ridgeDistance[i]; if (key < bestKey) { @@ -497,6 +518,9 @@ void growPond(Layout &L, const std::vector &depth, const std::vector & const auto key = [&](int tile) { const double d = std::sqrt(double(L.t.dist2(sx, sy, tile % L.t.w, tile / L.t.w))); + // A tile of distance weighs 1500 of the noise's 0..65535, so the noise's full swing is + // worth about 44 tiles: across a pond's few-tile radius the noise shapes the outline, and + // distance only stops it running off along a trough. return depth[tile] + int(std::lround(d * 1500)); }; std::priority_queue, std::greater> frontier; @@ -528,6 +552,8 @@ void placePonds(Layout &L, GenerationContext &context, int spacing, int pondSize { const Torus &t = L.t; const int n = t.w * t.h; + // The depth field's features are half a valley across (at least 8 tiles), so a pond fills one + // hollow rather than several. const std::vector depth = fractalNoise(t.w, t.h, std::max(8, spacing / 2), 2, context.stream("highlands-ponds")); L.pond.assign(n, 0); @@ -751,8 +777,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const int i = y * t.w + x; if (!L.ridge[i]) continue; - // Not near/far: both are legacy macros in Windows' windef.h that expand to nothing, which - // turns `int near[3]` into a structured binding declaration and fails to compile on mingw. + // Not near/far: both are legacy macros in Windows' windef.h that expand to nothing, + // which turns `int near[3]` into a structured binding declaration and fails to compile + // on mingw. int nearIds[3], farIds[3]; const int count = nearbyValleys(L, x, y, 2, nearIds, 3); if (count >= 3) @@ -809,6 +836,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) "passes could not join every valley (" + std::to_string(groups) + " groups remain)"; return L; } + // Rounded to the nearest pass: loopiness is the share of ridgelines left closed by the spanning + // tree that get a pass too. const int extra = int((std::int64_t(spare.size()) * o.loopiness + 50) / 100); for (size_t k = 0, added = 0; k < spare.size() && int(added) < extra; ++k) if (carvePass(L, context, spare[k].first, spare[k].second, exclusive[spare[k]], alongRidge, @@ -889,6 +918,8 @@ void furnishHome(Map &map, const Layout &L, int bootX, int bootY, int homeValley const Torus &t = L.t; const int cx = bootX + 2, cy = bootY + 2; int ox = 0, oy = 1, nearest = INT_MAX; + // The nearest pond within 24 tiles gives the direction down to the water; a colony's first pond + // is centred about 11 tiles away (kHomePondReach), so one is always found. for (int dy = -24; dy <= 24; ++dy) for (int dx = -24; dx <= 24; ++dx) if (L.pond[t.at(cx + dx, cy + dy)] && dx * dx + dy * dy < nearest) @@ -899,6 +930,9 @@ void furnishHome(Map &map, const Layout &L, int bootX, int bootY, int homeValley } const double length = std::max(1.0, std::sqrt(double(ox * ox + oy * oy))); const double ux = ox / length, uy = oy / length; + // The wheat and wood patches are anchored 7 tiles out, 50 degrees to either side of the way + // down to the pond: both near the water they regrow by, with the direct path from swarm to pond + // open between them. const double cosine = std::cos(50.0 * 3.14159265358979 / 180.0); const double sine = std::sin(50.0 * 3.14159265358979 / 180.0); for (int side : {1, -1}) @@ -947,6 +981,10 @@ void scatterFarmland(Map &map, GenerationContext &context, const Layout &L, const Torus &t = L.t; const int n = t.w * t.h; const Fertility::Field fertility = Fertility::forMap(map, false); + // split (8-tile features) chooses wheat or wood, so the crops alternate in patches; patch + // (16-tile features, cut at its 45th percentile) keeps 55% of the eligible ground, leaving + // walkable gaps between fields. Farmland goes 2 to 9 steps from a pond: past its sand beach, + // and near enough to regrow. const std::vector split = periodicNoise(t.w, t.h, 8, context.stream("highlands-farmland")); const std::vector patch = fractalNoise(t.w, t.h, 16, 2, context.stream("highlands-farmland")); @@ -975,6 +1013,9 @@ void scatterFarmland(Map &map, GenerationContext &context, const Layout &L, for (int v = 0; v < L.valleys; ++v) { std::vector &tiles = pool[v]; + // Farmland in proportion to the valley's pond water (110 tiles per 100), since water is + // what it grows by, but never more than two thirds of the eligible ground, so a valley with + // big ponds is not paved with fields. const int wanted = std::min(int(std::int64_t(pondTiles[v]) * kFarmlandPercent / 100), int(tiles.size()) * 2 / 3); // Two thirds wheat and the rest wood, each scaled by its amount; more than the default may @@ -1050,6 +1091,10 @@ void plantFruit(Map &map, GenerationContext &context, const Layout &L, if (valleys.empty()) return; context.shuffle(valleys.begin(), valleys.end(), "highlands-fruit"); + // Groves per 128x128 tiles (the default 4 gives 16 on a 256 map), each 6 trees within about 3 + // tiles of its anchor, 3 to 8 steps from a pond. Kinds are dealt in turn and, by default, + // groves avoid home valleys: fruit lets an inn pull hungry enemy units across, so it is a prize + // to take from a neutral valley, not something a colony starts next to. const int groves = std::max(1, int(std::int64_t(fruit) * n / 16384)); const int firstType = int(context.bounded("highlands-fruit", 3)); for (int g = 0; g < groves; ++g) @@ -1099,6 +1144,8 @@ bool generate(Game &game, GenerationContext &context) { const Home &h = L.homes[team]; std::vector home(n, 0); + // The settlement's ground: the 21x21 square round the home that lies in its valley, off the + // ring road, and 3 steps clear of ponds. for (int dy = -10; dy <= 10; ++dy) for (int dx = -10; dx <= 10; ++dx) { diff --git a/src/map/generator/generators/SwampGenerator.cpp b/src/map/generator/generators/SwampGenerator.cpp index 53e51adf7..a4749943e 100644 --- a/src/map/generator/generators/SwampGenerator.cpp +++ b/src/map/generator/generators/SwampGenerator.cpp @@ -6,6 +6,17 @@ #include "Terrain.h" #include using namespace MapGeneration; +// Swamp (id 1): Leo Wandersleb's swamp from January 2006, on the shared height-field pipeline (see +// Terrain.cpp). The field is almost pure noise (makeSwamp), so the water is scattered ponds and +// sloughs everywhere rather than one shape. +// +// Swamp has no sand or desert weight: water is water / (1 + water + grass) of the map, 36% at the +// defaults, and the rest is grass (the 1 only guards against both weights at 0). Its only sand is +// the beaches controlSand has to lay, because grass may not touch water, so every pond is ringed +// in sand. With water everywhere, wheat and wood can regrow almost anywhere, and a swamp's economy +// is rich; the cost is that movement and building room are broken up by ponds a colony must walk +// round until it can swim. Smoothing defaults to 6 (features about 30 tiles) so ponds are big +// enough to read, and grass between them wide enough to build on. static bool generate(Game &game, GenerationContext &context) { const HeightFieldOptions terrain = HeightFieldOptions::fromRequest(context.request, true); diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index 465a0eac2..79c79f579 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -37,6 +37,15 @@ using MapGeneration::scaledShare; // row-order pass is replaced by the same rule applied to every corner at once, per-tile random // clumps by deterministic growth, and the amounts the engine RNG gives each resource tile are // equalised over its orbit. validateWorld then checks the invariance on the finished world. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). It is the tournament map: +// no colony can blame its start, because every colony's ground is an exact image of every other's. +// The prize in the middle is the strongest a map can offer, an orchard of all three fruits (an inn +// stocked with all three pulls hungry enemy units across) with stone among it, and the moat, which +// ground units cannot cross until they swim, funnels every early attack onto the causeways, the +// chokepoints worth holding. Each home has its own pond, because wheat and wood regrow only near +// water, so no colony has to leave home to keep its farmland alive. Only 2, 4 and 8 colonies are +// offered, because those are the counts the square's symmetries can serve exactly. namespace { @@ -170,6 +179,8 @@ std::int64_t tileRadius2(int width, int height, int x, int y) } bool within(std::int64_t radius2, double radius) { + // radius2 is in doubled coordinates, so the radius is doubled (squared, times 4) to compare: + // all integer arithmetic on the squared side, so every tile in an orbit gets the same answer. return double(radius2) < 4.0 * radius * radius; } @@ -258,6 +269,10 @@ std::vector homeCandidates(const Arena &a, Fit &fit) { const int w = a.width, h = a.height, shortSide = std::min(w, h); const int step = std::max(1, shortSide / 128); + // Homes lie at least 4 tiles outside the moat plus their own clear disc, so a swarm never + // stands on the moat's shore, and within 45% of the shorter side from the centre, so the way to + // the centre is unambiguous on the torus (beyond half the side it would be shorter the other + // way round). const double inner = a.centre + a.moat + kHomeRadius + 4, outer = 0.45 * shortSide; std::vector homes; bool spaced = false; @@ -278,6 +293,9 @@ std::vector homeCandidates(const Arena &a, Fit &fit) if (spacing < kMinimumSpacing) continue; spaced = true; + // Neighbouring causeways must leave at least 5 tiles of shore between them where they + // land, or two colonies' causeways would merge into one wide bridge and the chokepoints + // would be lost. if (gateSeparation(a, gateAngles(a, p)) * a.centre < a.causewayWidth + 5) continue; homes.push_back({u, v, spacing, radius}); @@ -292,6 +310,10 @@ Home chooseHome(const std::vector &homes, GenerationContext &context, int double best = 0; for (const Home &home : homes) best = std::max(best, home.spacing); + // Keep only homes within 10% (at least 2 tiles) of the widest spacing, then of those the ones + // within 15% of the shorter side of the nearest to the centre: colonies as far apart as the map + // allows, but not pushed out to the far corners, so the orchard stays a reachable prize. The + // random pick among them varies maps without giving up either. const double floor = best - std::max(2.0, 0.1 * best); double nearest = 1e9; for (const Home &home : homes) @@ -364,6 +386,8 @@ std::vector orbitNoise(GenerationContext &context, const Symmetry &s, std::vector raw(size_t(w) * h), summed(size_t(w) * h, 0); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) + // Noise as integers (12 bits) so the sum over an orbit is exact: floating-point sums in + // different orders could differ in the last bit and break the symmetry. raw[size_t(y) * w + x] = int(noise(x, y) * 4096); for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) @@ -430,6 +454,8 @@ bool carvePaths(const Arena &a, const Layout &l, Terrain &t) std::vector route(n, 0); for (double angle : l.gates) { + // The route to each causeway aims 3.5 tiles past the moat's outer edge, on the causeway's + // landing, so the path ends on dry ground at the bridge rather than in the water. const double reach = a.centre + a.moat + 3.5; const int target = wrap(int(std::lround(h / 2.0 + reach * std::sin(angle))), h) * w + wrap(int(std::lround(w / 2.0 + reach * std::cos(angle))), w); @@ -502,6 +528,8 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La const double toHome = torusDistance(w, h, p, l.home); const double toPond = torusDistance(w, h, p, l.pond); homeDisc[i] = toHome <= kHomeRadius; + // Lakes keep out of a zone 4 tiles round the home disc and 7 round its pond, so the + // pond's farmland and the swarm's workers have land to use. homeZone[i] = toHome <= kHomeRadius + 4 || toPond <= kPondRadius + 7; pond[i] = toPond <= kPondRadius; gate[i] = onCauseway(a, l, p, 1.5, 1.5, a.causewayWidth / 2); @@ -673,6 +701,9 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou water[i] = map.isWater(x, y); land[i] = !water[i]; const Point p = tilePoint(w, h, x, y); + // Nothing is planted within 1.5 tiles past the home's clear disc, nor within 3 to 4 + // tiles of where a causeway lands (2 tiles to each side beyond its width), so neither + // the swarm nor a bridge can be walled in by deposits. homeClear[i] = torusDistance(w, h, p, l.home) <= kHomeRadius + 1.5; landing[i] = onCauseway(a, l, p, 3.0, 4.0, a.causewayWidth / 2 + 2); } @@ -701,6 +732,9 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou const size_t i = size_t(y) * w + x; const bool open = grass[i] && !blocked[i] && !within(tileRadius2(w, h, x, y), a.centre + a.moat + kApron + 2); + // Farmland 2 to 7 steps from water, where it regrows and off the beach; stone at least + // 3 steps from water, where it takes no farmland; algae at least 2 steps out from land, + // off the shore. farm[i] = open && waterSteps[i] >= 2 && waterSteps[i] <= 7; rock[i] = open && waterSteps[i] >= 3; alga[i] = water[i] && landSteps[i] >= 2; @@ -708,6 +742,9 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou // The amount controls scale each layer on top of richness: wheat and wood their shares of the // farmland (2:1 at the defaults, which use the shares exactly as they were). const bool farmDefault = o.wheat == 100 && o.wood == 100; + // At richness 1 farmland takes 30% of its eligible ground, two thirds wheat and a third wood; + // stone 1.2% of its ground (a few outcrops, since stone never runs out) and algae 10% of its + // water. Farmland is capped at 90% so a very rich map still leaves room to walk. const double wheatShare = scaledShare(0.3 * richness * 2 / 3, o.wheat); const double woodShare = scaledShare(0.3 * richness / 3, o.wood); const double farmShare = @@ -798,14 +835,17 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou { return p->value != q->value ? p->value > q->value : p->key < q->key; }); const size_t stones = o.orchardStone + // A fifth of the orchard's orbits become stone, taking the highest noise values, + // but never so many that fewer than three orbits are left for fruit: the orchard + // must hold every kind. ? std::min(ranked.size() - 3, size_t(std::lround(0.2 * double(ranked.size())))) : 0; for (size_t k = 0; k < stones; ++k) ranked[k]->stone = true; std::sort(ranked.begin(), ranked.end(), [](const Orbit *p, const Orbit *q) { return p->radius2 != q->radius2 ? p->radius2 < q->radius2 : p->key < q->key; }); - // The fruit amount keeps that share of the fruit orbits nearest the centre, never fewer than - // three so every fruit stays; the rest are left as open grass. + // The fruit amount keeps that share of the fruit orbits nearest the centre, never fewer + // than three so every fruit stays; the rest are left as open grass. const size_t fruitOrbits = ranked.size() - stones; const size_t keptFruit = std::clamp(size_t(scaledCount(std::int64_t(fruitOrbits), o.fruit)), @@ -837,6 +877,8 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou for (int x = 0; x < w; ++x) { const size_t i = size_t(y) * w + x; + // The kit may use ground up to 12 tiles past the home's clear disc and at least 2 steps + // from water. kitGround[i] = grass[i] && !blocked[i] && waterSteps[i] >= 2 && torusDistance(w, h, tilePoint(w, h, x, y), l.home) <= kHomeRadius + 12; } @@ -879,6 +921,9 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou return placed; }; const Point side{-std::sin(l.pondAngle), std::cos(l.pondAngle)}; + // Wheat and wood 4.5 tiles to either side of the pond, where they regrow and the way from swarm + // to pond stays open; stone a third of a turn round from the pond, just outside the clear disc, + // away from the farmland. const double stoneAngle = l.pondAngle + 2 * kPi / 3; const int wheatKit = grow({l.pond.x + 4.5 * side.x, l.pond.y + 4.5 * side.y}, CORN, kKitFarmland); diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index c4d6f951f..ca88d9c0a 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -38,6 +38,17 @@ using namespace MapGeneration; // colony gets the same neighbourhood turned round the centre, and the layout is fair for any // colony count. The whole design is a pure function of the request, so validateWorld rebuilds // it and checks the finished world. +// +// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): buildings need pure +// grass, so the islands are the only building ground; growth refuses when its probe finds sand, +// so the flats stay bare; and farmland needs water, so every island's pond is what keeps its +// fields alive. Neutral islands arrive already under wheat, which blocks building until cleared, +// so taking one is a visible commitment. The central orchard of all three fruits is the prize: +// an inn stocked with them pulls hungry enemy units across. +// +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies, island size 20): home islands about 25 tiles in +// radius on a ring 74 tiles from the centre, a central island about 17 in radius, and a pond of +// radius 5 on each home island. namespace { @@ -71,6 +82,10 @@ Geometry geometryFor(const GenerationRequest &r) g.teams = std::max(1, r.nbTeams); g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; g.homeRing = kHomeRing * g.half; + // The central island is 13% of the half side (at least 8 tiles, enough for its pond and the + // orchard round it). Roughness 100 lets a coast wobble up to 35% of its radius: rough enough to + // read as a natural island, while two neighbouring islands, spaced for their fullest extent, + // still keep kIslandGap of sand between them. g.centralRadius = o.centralIsland ? std::max(8.0, kCentralShare * g.half) : 0.0; g.amplitude = o.coastRoughness / 100.0 * 0.35; // The control is the islands' radius as a share of the half side; with many colonies on the @@ -83,6 +98,8 @@ Geometry geometryFor(const GenerationRequest &r) std::min(radius, (2 * g.homeRing * std::sin(kPi / g.teams) - kIslandGap) / (2 * reach)); radius = std::min(radius, (g.half - kIslandGap / 2.0 - g.homeRing) / reach); if (g.centralRadius > 0) + // 1.3 covers the central island's own wobble (its roughness is 60% of the others', at most + // 21%), so a home island never runs into it. radius = std::min(radius, (g.homeRing - g.centralRadius * 1.3 - kIslandGap) / reach); g.homeRadius = std::floor(radius); return g; @@ -153,6 +170,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) blobs.back().reach = blobs.back().shape.maximumRadius(); const auto fits = [&](double s0, double r0, double reach, double gap) { + // A feature keeps 6 tiles from the very centre (plus the central island and a gap when + // there is one) and 4 from the edge of the design circle, stays inside its wedge by half a + // gap, and keeps its gap from every feature already placed (kIslandGap from a home island). if (r0 - reach < 6 + g.centralRadius * 1.3 + (g.centralRadius > 0 ? gap : 0)) return false; if (r0 + reach > g.half - 4) @@ -171,6 +191,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const auto scatter = [&](Kind kind, int wanted, double low, double high, double roughness, double gap, const char *stream, bool stretched) { + // Features are drawn at random across the wedge, from 8 tiles out from the centre to 8 + // tiles short of the design circle, with 80 tries each; one that fits nowhere is left out, + // the same for every wedge. Lagoons are stretched 1.2 to 2 times and turned, so they read + // as long tidal channels rather than round pools. for (int attempt = 0, placed = 0; attempt < wanted * 80 && placed < wanted; ++attempt) { const double r0 = 8 + context.bounded(stream, 1000) / 1000.0 * (g.half - 16); @@ -189,6 +213,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) scatter(Neutral, o.extraIslands, std::max(kNeutralMinimum, kNeutralLow * g.homeRadius), std::max(kNeutralMinimum + 2, kNeutralHigh * g.homeRadius), 0.35, kIslandGap, "flats-islands", false); + // Sandbars and lagoons keep 6 tiles of sand round them, tide pools 4: small water keeps the + // flats walkable round it. Lagoons are 8 to 14 tiles in radius; tide pools 1.5 to 4, counted + // per 16384 tiles of wedge so their density is the same on every map size. scatter(Sandbar, o.sandbars, kSandbarLow, kSandbarHigh, 0.3, 6, "flats-sandbars", false); scatter(Lagoon, o.lagoons, 8.0, 14.0, 0.35, 6, "flats-lagoons", true); const double wedgeArea = wedge * g.half * g.half / 2; @@ -215,6 +242,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.pondX[k] = L.homeX[k]; L.pondY[k] = L.homeY[k]; } + // A home's pond is a fifth of its island's radius, 3 to 6 tiles: wide enough to stay water + // inside its own beach, and small enough to leave the island its building room. Oasis ponds are + // 2.5. const double pondRadius = std::clamp(0.2 * g.homeRadius, 3.0, 6.0); const RadialShape pond(pondRadius, 0.3, context, "flats-ponds"); const RadialShape oasis(2.5, 0.3, context, "flats-ponds"); @@ -231,6 +261,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { L.grass[i] = 1; L.islandOf[i] = -2; + // The central island's pond: 22% of its radius, at least 1.5 tiles. if (d < std::max(1.5, 0.22 * g.centralRadius)) L.water[i] = 1; continue; @@ -276,6 +307,9 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const T const Fertility::Field fertility = Fertility::forMap(map, false); HeightMap split(t.w, t.h, context.stream("flats-home-split")); split.makePlain(6); + // The kit's distance from the pond's centre: its fullest radius (1.3 for the wobble) plus a + // 3-tile beach, so wheat and wood start on the first grass past the sand, as near the water as + // they can grow. const double pondRadius = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 3; for (int team = 0; team < L.g.teams; ++team) { @@ -304,6 +338,9 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const T for (const auto &entry : farm) byFertility.push_back(entry.second); const int area = int(ground.size()); + // 4% of the island under ambient wheat and 2% under wood, on its most fertile ground + // (nearest the pond), on top of the kit: an island is a home, so most of it stays free to + // build on. plantFields(map, t, byFertility, int(scaledCount(area * 4 / 100, o.wheat)), int(scaledCount(area * 2 / 100, o.wood)), [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }); @@ -373,6 +410,8 @@ void stockIslands(Map &map, const Layout &L, GenerationContext &context, const T { const auto eligible = [&](int i) { return L.islandOf[i] == -2 && clearGround(map, i % t.w, i / t.w); }; + // The orchard's three groves sit a third of a turn apart, 4 tiles out from the central + // pond's edge, where fruit is beside water; the quarry near the island's rim. const double rho = std::max(1.5, 0.22 * L.g.centralRadius) + 4; const double spin = context.bounded("flats-prizes", 3600) / 3600.0 * 2 * kPi; if (scaledCount(1, o.fruit) > 0) @@ -443,6 +482,8 @@ bool generate(Game &game, GenerationContext &context) { const double a = std::atan2(double(t.offsetY(L.cy, L.homeY[team])), double(t.offsetX(L.cx, L.homeX[team]))); + // The swarm stands 6 tiles past the pond's fullest edge, on the side away from the centre: + // its fields between it and the pond, and the rest of the island between it and the flats. const double back = std::clamp(0.2 * L.g.homeRadius, 3.0, 6.0) * 1.3 + 6; return MapGeneratorPoint(L.pondX[team] + int(std::lround(back * std::cos(a))) - 2, L.pondY[team] + int(std::lround(back * std::sin(a))) - 2); @@ -463,6 +504,7 @@ std::string validateRequest(const GenerationRequest &r) if (r.nbTeams < 1) return "Tidal flats need at least one colony."; const Geometry g = geometryFor(r); + // Below radius 9 an island cannot hold a pond, its kit and room for a base. if (g.homeRadius < 9) return "The home islands do not fit on this map; use a bigger map, fewer colonies or " "smaller islands."; diff --git a/src/map/generator/generators/UniformGenerator.cpp b/src/map/generator/generators/UniformGenerator.cpp index 89c0820b4..dbc33dd00 100644 --- a/src/map/generator/generators/UniformGenerator.cpp +++ b/src/map/generator/generators/UniformGenerator.cpp @@ -6,6 +6,11 @@ #include "Terrain.h" #include using namespace MapGeneration; +// Uniform terrain (id 0): the map editor's blank canvas, not a game map. It fills the whole map +// with the one terrain picked on the new-map screen (water, sand or grass) and adds a single team +// with no swarm, so a designer starts from nothing. It is editor-only (the definition's true below) +// and has no starting colonies (the final false), so it is offered only in the editor, and +// validation and the golden test expect exactly one team from it instead of one per colony. static bool generate(Game &game, GenerationContext &context) { game.map.makeHomogenMap(context.request.terrainType); diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index 6fbdbb4e3..9592e9af3 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -37,6 +37,20 @@ using namespace MapGeneration; // Terrain is stamped on the undermap with a symmetric version of Map::controlSand's shoreline // rule, then controlSand itself is run and must change nothing: the shores are already what it // would make of them, without its row-order raster pass silting narrow channels unevenly. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Rivers are the map's +// walls and its wealth at once: wheat and wood regrow only near water, so the best land is right +// against the channels a colony cannot cross, and fords are the few places armies can. Colonies +// start on the floodplain (5 to 11 steps from water), preferably on different banks, so a +// neighbour is usually across a river and contact runs through the fords. Stone lies out at the +// dry edge of the uplands and fruit at confluences, so both mean leaving the river you live on. +// Every guarantee (fords that really cross, channels no one can step over, a land route between +// all colonies) is checked on the finished world by validateWorld, not assumed. +// +// THE SIZES AT THE DEFAULTS (256x256, density 5, width 2, dryness 5, fords 3): a sea band 44 +// tiles deep; about 41 tributaries joining a trunk, springs about 25 tiles apart; channels 3.6 +// tiles in radius at a spring, widening to about 6.6 on the trunk; a ford every 44 tiles of river; +// uplands turning to sand from 12 to 24 tiles away from water. namespace { @@ -148,6 +162,10 @@ struct Ripple Ripple makeRipple(GenerationContext &context, double base, double amplitude, int period) { + // Whole numbers of cycles along the coast, so it joins itself across the seam: one long swell + // and three shorter ripples with falling weights. scale adds cycles on longer coasts so the + // ripples keep about the same length in tiles on every map size, and each amplitude is drawn at + // 50% to 100% of its weight. static const double harmonics[4] = {1, 3, 5, 9}, weights[4] = {0.35, 0.3, 0.2, 0.15}; Ripple ripple; ripple.base = base; @@ -243,6 +261,9 @@ struct Layout class ChannelIndex { public: + // 16-tile buckets: a lookup cell a bit wider than the widest channel plus its margin, so + // checking the 3x3 cells round a point finds every channel that could touch it. Only speed + // depends on this. explicit ChannelIndex(const Layout &layout) : layout(layout), w(layout.frame.width), h(layout.frame.height), columns(std::max(1, w / 16)), rows(std::max(1, h / 16)), cells(size_t(columns) * rows) @@ -335,6 +356,8 @@ class RiverPlanner : along(frame.along), across(frame.across), north(north), south(south), clearance(clearance), northMargin(northMargin), southMargin(southMargin) { + // 24-tile buckets, about two clearances, so the 3x3 cells round a point hold every river + // point within the clearance. Only speed depends on this. columns = std::max(1, along / 24); rows = std::max(1, across / 24); cellW = double(along) / columns; @@ -447,6 +470,10 @@ class RiverPlanner const Vec delta{centred(to.x - from.x, along), to.y - from.y}; const Vec end = from + delta; const double span = std::max(1.0, length(delta)); + // The curve arrives leaning 40% into the parent river's direction of flow, so tributaries + // join at the acute, downstream-pointing angle of real rivers rather than head-on. The + // control points a third of the way along each end are the usual choice for a smooth cubic; + // 3 samples per tile are resampled to kSpacing afterwards. const Vec blend = normalized(endDirection * 0.4 + normalized(delta) * 0.6); const Vec c1 = from + delta * 0.33, c2 = end - blend * (0.33 * span); const int steps = std::max(16, int(span * 3)); @@ -459,6 +486,11 @@ class RiverPlanner end * (t * t * t)); } const std::vector base = resample(raw); + // The meander swings a tenth of the path's length to each side, at most 6.5 tiles: enough + // to read as a winding river, while staying inside the clearance (about 13 tiles at the + // default width) so a meander never bends into a neighbouring river. One wave per 34 tiles, + // plus a quarter-weight wave 2.3 times as fast (not a whole multiple, so the pattern does + // not visibly repeat). const double amplitude = std::min(6.5, 0.1 * span) * meander * meanderScale; const double waves = std::max(1.0, span / 34.0); const double phase1 = between(context, kRiverStream, 0, 2 * kPi); @@ -486,6 +518,8 @@ class RiverPlanner if (reach.distributary) return false; const double s = i * kSpacing; + // Not within 8 tiles of either end: a junction right at a spring or mouth would read as two + // rivers touching, not one joining the other. if (s < 8 || reach.length() - s < 8) return false; // Junctions on the same bank well apart leave straight stretches between them to ford, and @@ -533,6 +567,7 @@ class RiverPlanner { Verdict verdict; const size_t n = points.size(); + // A river under 12 tiles long is a pond, not a tributary, and cannot hold a ford. if ((n - 1) * kSpacing < 12) return verdict; const double joinZone = clearance + 4; @@ -587,8 +622,8 @@ class RiverPlanner alive[pick] = 0; continue; } - // A cached distance can only be optimistic (junctions since added rule points out), so a - // spring whose real best is further than cached waits its proper turn. + // A cached distance can only be optimistic (junctions since added rule points out), so + // a spring whose real best is further than cached waits its proper turn. if (list.front().distance2 > cached[pick] + 1e-9) { cached[pick] = list.front().distance2; @@ -599,6 +634,9 @@ class RiverPlanner int added = -1; for (const Attachment &a : list) { + // Up to 12 join points, skipping any within 6 tiles (squared 36) of one already + // tried, since nearby points fail for the same reason; a spring that fits none is + // dropped and a fresh one sampled next round. if (tried.size() >= 12 || added >= 0) break; const Vec p = reaches[a.reach].points[a.index]; @@ -608,9 +646,12 @@ class RiverPlanner if (repeat) continue; tried.push_back(p); - // A full meander first, then a gentler one. A river joins whatever river it runs into: - // a path refused for passing too near another river is tried again as its tributary. + // A full meander first, then a gentler one. A river joins whatever river it runs + // into: a path refused for passing too near another river is tried again as its + // tributary. int target = a.reach, index = a.index; + // A refused path may hop to the river that blocked it, up to 3 times; the gentler + // meander (0.35) is tried when the full one bends into something. for (int hop = 0; hop < 3 && added < 0; ++hop) { Verdict last; @@ -631,7 +672,8 @@ class RiverPlanner } if (added >= 0 || last.blocking < 0) break; - // Nothing joins the delta itself: a path that runs into it joins the trunk above it. + // Nothing joins the delta itself: a path that runs into it joins the trunk + // above it. const int blocking = reaches[last.blocking].distributary ? trunk : last.blocking; const int next = @@ -693,22 +735,32 @@ void chooseFords(Layout &layout, const WatershedOptions &o) const Reach &reach = layout.reaches[r]; const int n = int(reach.points.size()); const double len = reach.length(); + // A reach shorter than 20 tiles, or than half the ford spacing, gets no ford: it is short + // enough to walk round, through its spring. if (len < std::max(20.0, spacing / 2)) continue; auto valid = [&](int i) { + // 16 points is 8 tiles: a ford needs straight channel on both sides of it, so not at + // the very ends. if (i < 16 || i >= n - 16) return false; const double radius = reach.radius[i], span = radius + kFordReach; + // Not within 6 tiles of the coast beyond its own span, where the ford would run into + // the beach. if (reach.coastGap[i] < span + 6) return false; for (int j : layout.joins[r]) + // Not within 8 tiles past the channel's radius of a junction, where a crossing + // would touch two rivers. if (std::abs(i - j) * kSpacing < radius + 8) return false; // Only on a straight stretch, eight tiles either way, where the channel runs on in line // beyond both sides of the ford and is no wider across than its radius. const Vec before = reach.points[i] - reach.points[i - 16]; const Vec after = reach.points[i + 16] - reach.points[i]; + // At most a 25 degree bend across the 16 tiles centred on the ford: on a sharper bend + // the sand strip would cut the channel obliquely and could leave water round one end. if (dot(normalized(before), normalized(after)) < std::cos(25 * kPi / 180)) return false; // The crossing, and dry ground a little past both of its ends, touch no other channel: @@ -756,6 +808,9 @@ void chooseFords(Layout &layout, const WatershedOptions &o) const double span = reach.radius[spot.index] + kFordReach; bool apart = true; for (int c : chosen) + // Fords on one reach at least three quarters of the spacing apart, and 10 tiles + // clear of any other ford, so each crossing is its own chokepoint rather than a + // wide shared one. apart = apart && std::abs(c - spot.index) * kSpacing >= 0.75 * spacing; for (const Ford &f : layout.fords) apart = apart && @@ -789,11 +844,19 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo // One band of sea along the map's edge: deep off the delta, and a narrow strait along the // uplands on the far side of the torus seam. + // The sea band grows with the map but stays between 16 and 64 tiles: 44 on a 256 map. It is a + // band on the torus, so a third of it (the north ripple, 0.35 of it) lies past the seam as a + // narrow strait off the uplands and two thirds as the deep water off the delta. The delta coast + // ripples more (0.2 of the band) than the strait (0.08, plus a tile). const double sea = std::clamp(std::round(8 + 0.14 * across), 16.0, 64.0); layout.north = makeRipple(context, 0.35 * sea, 0.08 * sea + 1.0, frame.along); layout.south = makeRipple(context, across - 0.65 * sea, 0.2 * sea, frame.along); const double minimumRadius = channelRadius(o.riverWidth); + // How fast rivers widen downstream, more for wider settings. The clearance keeps two channels + // of the estimated width apart by 5 tiles of floodplain between their banks; the margins keep + // every river's banks 8 tiles from the strait and 6 from the delta coast, so no river spills + // into the sea along its side. const double gain = 0.35 + 0.1 * o.riverWidth; const double estimate = minimumRadius + 0.5; const double clearance = 2 * estimate + 5; @@ -804,15 +867,22 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo // The delta: one to three distributaries fanning out from an apex to the coast. Without a // delta there is no lobe and only the middle mouth is kept; the others are still planned, so // every later draw is the one it would have been. + // The apex sits 12 to 44 tiles inland (37 on a 256 map) and mouths are spread 18 to 44 tiles + // apart along the coast (41): wide enough that the land between two distributaries holds a + // colony's fields, and no more mouths than fit in 70% of the coast. const double deltaLength = std::clamp(0.12 * across + 6, 12.0, 44.0); const double spread = std::clamp(0.16 * along, 18.0, 44.0); const int mouthLimit = std::max(1, int(0.7 * along / spread) + 1); const int mouthCount = std::min(mouthLimit, 1 + int(context.bounded(kLayoutStream, 3))); const double u0 = between(context, kLayoutStream, 0, along); layout.south.lobeCentre = u0; + // The delta pushes the coast out by 30% of the sea band in a bump somewhat wider than the + // mouths' spread, the land a real delta builds from silt. layout.south.lobeHeight = o.delta ? 0.3 * sea : 0.0; layout.south.lobeWidth = 1.1 * spread + 6; const Vec apex{u0, layout.south.at(u0) - deltaLength}; + // How the trunk's flow divides among 1, 2 or 3 distributaries, the middle one a little larger, + // so each mouth's width follows its share. static const double shares[3][3] = {{1, 0, 0}, {0.5, 0.5, 0}, {0.32, 0.36, 0.32}}; std::vector mouths; for (int m = 0; m < mouthCount; ++m) @@ -830,6 +900,9 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo } // The trunk rises well up in the uplands; every other spring joins the network it starts. + // The trunk's head lies within 15% of the coast's length to either side of the delta, and 10% + // to 30% of the way down from the uplands' margin toward the apex: high enough that the trunk + // crosses most of the land and every later spring has a river to run down to. const double headU = u0 + between(context, kLayoutStream, -0.15, 0.15) * along; const double top = layout.north.at(headU) + northMargin; const Vec head{headU, @@ -843,10 +916,16 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo const int trunk = layout.trunk; const double landArea = along * (across - sea); + // One spring per 6700 tiles of land at density 1, per 1300 at the default 5 (about 41 + // tributaries on a 256 map). Springs are spread by 70% of the spacing their share of land would + // give, loose enough that the tree still gets its branches and tight enough that no stretch of + // upland is left without a river. const int wanted = int(std::lround(landArea * o.riverDensity * 1.5e-4)); const double sourceSpacing = 0.7 * std::sqrt(landArea / (wanted + 1.0)); // A spring that finds no way onto the network is dropped, so springs are sampled again wherever - // there is still room until enough have joined it. + // there is still room until enough have joined it. Up to six rounds of resampling, each with up + // to 60 tries per missing spring: a cap on the work, not a promise; when the land is too + // crowded by rivers for another spring to join, the map simply has fewer tributaries. for (int round = 0; round < 6; ++round) { const int missing = wanted - (int(planner.reaches.size()) - trunk - 1); @@ -856,7 +935,8 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo for (int attempt = 0; attempt < 60 * missing && int(sources.size()) < missing; ++attempt) { const double u = between(context, kRiverStream, 0, along); - // Upstream of the delta's apex: nearer the sea than that, there is no river to run down to. + // Upstream of the delta's apex: nearer the sea than that, there is no river to run down + // to. const double lo = layout.north.at(u) + northMargin; const double hi = std::min(layout.south.at(u) - (deltaLength + southMargin), apex.y - 6); @@ -886,6 +966,9 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo children[planner.reaches[r].parent].push_back(r); } auto radiusFor = [&](double flow) + // Width grows with the square root of the springs upstream, as a river's width grows with the + // square root of its discharge, and is capped 3.5 tiles above the spring's width so the trunk + // stays crossable at fords and does not swallow its floodplain. { return std::min(minimumRadius + 3.5, minimumRadius + gain * (std::sqrt(flow) - 1)); }; for (int r = trunk; r < count; ++r) { @@ -913,6 +996,7 @@ Layout planLayout(int width, int height, const WatershedOptions &o, GenerationCo const double last = double(reach.points.size() - 1); reach.radius.resize(reach.points.size()); for (size_t i = 0; i < reach.points.size(); ++i) + // Distributaries widen by 0.8 tiles toward the sea, so the mouths flare. reach.radius[i] = radius + 0.8 * (i / last); } @@ -1064,6 +1148,10 @@ std::vector stampTerrain(const Layout &layout, const WatershedOpt if (terrain[i] != GRASS) continue; const double n = 0.7 * coarse.at(x, y) + 0.3 * fine.at(x, y); + // Distance is in fifths of a tile. Ground dries to sand from `reach` tiles out + // (22.7 at dryness 1, 17.5 at 5 and 11 at 10; 0 skips drying), pushed 6 tiles + // either way by the noise so the desert edge is ragged, and never within 11 tiles + // of water: the floodplain where fields regrow and colonies build. if (distance[i] > 5 * std::max(11.0, reach + 12.0 * (n - 0.5))) terrain[i] = SAND; } @@ -1126,6 +1214,8 @@ template std::string checkFords(const Layout &layout, Water wat for (const Ford &f : layout.fords) { for (int side : {-1, 1}) + // 2.5 tiles past the ford's edge up- and downstream there must be open water, so the + // ford really interrupts a channel. if (!wetNear(f.center + f.along * (side * (kFordHalfWidth + 2.5)))) return "A ford" + where(f) + " does not cross a channel."; for (int a = -1; a <= 1; ++a) @@ -1290,6 +1380,8 @@ std::vector chooseSites(const Layout &layout, const Tiles &t, t.waterSteps[c] < kColonyWaterNear || t.waterSteps[c] > kColonyWaterFar) continue; bool open = true; + // An 8x8 square of grass round the 4x4 swarm, so it has pure grass to stand on and a + // ring for its workers. for (int dy = -2; dy <= 5 && open; ++dy) for (int dx = -2; dx <= 5 && open; ++dx) open = t.grass[size_t(wrapIndex(y + dy, h)) * w + wrapIndex(x + dx, w)] != 0; @@ -1297,6 +1389,8 @@ std::vector chooseSites(const Layout &layout, const Tiles &t, for (size_t k = 0; open && k < layout.fords.size(); ++k) { const Ford &f = layout.fords[k]; + // Colonies start 12 tiles clear of every ford, so no swarm sits on a crossing and + // blocks it. open = sq(centred(centre.x - f.center.x, w)) + sq(centred(centre.y - f.center.y, h)) >= sq(f.span + 12); @@ -1306,6 +1400,8 @@ std::vector chooseSites(const Layout &layout, const Tiles &t, // How fertile the ground a young colony works is, around the swarm. double sum = 0; int samples = 0; + // Fertility sampled on a 3-tile grid over the 24x24 square round the swarm: the ground + // its first workers will farm. for (int dy = -10; dy <= 13; dy += 3) for (int dx = -10; dx <= 13; dx += 3, ++samples) sum += fertility.at(wrapIndex(x + dx, w), wrapIndex(y + dy, h)); @@ -1330,6 +1426,7 @@ std::vector chooseSites(const Layout &layout, const Tiles &t, std::vector best; double bestScore = -1; std::vector nearest(candidates.size()); + // 32 greedy spreads from random first sites, keeping the best; 16 on the fertile half first. for (int trial = 0; trial < 32; ++trial) { // Half the trials in the fertile half; if no spacing fits there, the rest use every site. @@ -1359,6 +1456,8 @@ std::vector chooseSites(const Layout &layout, const Tiles &t, bool shared = false; for (int k : chosen) shared = shared || candidates[k].region == candidates[c].region; + // A site on a bank that already holds a colony counts as 30% as far away: colonies + // spread across banks first, so neighbours face each other over a river. const double score = nearest[c] * (shared ? 0.3 : 1.0); if (score > pickScore) { @@ -1455,6 +1554,9 @@ void placeStarterKits(Game &game, GenerationContext &context, int teams, const T for (int x = 0; x < w; ++x) { const int steps = distance[size_t(y) * w + x]; + // Kits go 6 to 22 steps from the colony's workers and outside the 15-tile square + // round the swarm (7.5 each way), so they neither crowd the swarm nor lie beyond an + // early worker's walk. if (steps < 6 || steps > 22 || !map.isGrass(x, y) || !map.isResourceAllowed(x, y, CORN)) continue; @@ -1499,6 +1601,10 @@ void placeStarterKits(Game &game, GenerationContext &context, int teams, const T std::pair{STONE, 1}}) { const bool stone = kit.first == STONE; + // Wheat and wood 6 to 10 steps out on the most fertile ground, where they regrow, and + // at least 100 degrees apart round the swarm so one never walls off the other; stone 10 + // to 14 steps out on the least fertile ground, where it takes nothing from farming, at + // least 60 degrees from both. const auto [spot, size] = pick(stone ? 10 : 6, stone ? 14 : 10, !stone, used, stone ? 60 : 100, kit.second); if (!spot) @@ -1519,6 +1625,8 @@ void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, Map &map = game.map; const int w = t.w, h = t.h; std::mt19937 &random = context.stream("watershed-farmland"); + // patch (6-tile features) breaks the ribbons into clumps with bare bank between them; crop + // (20-tile features) picks the crop, so a stretch of bank tends to one of them. const PeriodicNoise patch(w, h, 6, random), crop(w, h, 20, random); struct Candidate { @@ -1526,6 +1634,7 @@ void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, int index; }; std::vector order; + // Ground under 5% fertility never gets farmland: crops there would never regrow once harvested. const std::uint32_t floor = Fertility::kScale / 20; for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) @@ -1539,6 +1648,9 @@ void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, } std::sort(order.begin(), order.end(), [](const Candidate &a, const Candidate &b) { return a.score != b.score ? a.score > b.score : a.index < b.index; }); + // Farmland covers a fifth of the eligible ground: dense ribbons along the banks, with room left + // to walk and build between them. crop below 0.62 is wheat, the threshold that gives about two + // wheat clumps for each wood. int budget = int(order.size()) / 5; // Other wheat or wood amounts than the default give each crop a budget of its own: its share // of the candidates the crop field gives it, scaled. @@ -1558,6 +1670,7 @@ void placeFarmland(Game &game, GenerationContext &context, const Tiles &t, if (shared ? budget <= 0 : cornBudget <= 0 && woodBudget <= 0) break; const int x = c.index % w, y = c.index / w; + // Two clumps in five are the larger radius 3 where the grass is deep enough to hold one. const int radius = context.bounded("resources", 5) < 2 && t.grassDepth[c.index] > 3 ? 3 : 2; if (!reservations.free(x, y, radius)) continue; @@ -1582,6 +1695,9 @@ void placeStone(Game &game, GenerationContext &context, const Tiles &t, for (int x = 0; x < w; ++x) { const size_t i = size_t(y) * w + x; + // Stone goes on ground at least 9 steps from water, off the farmland, either where the + // grass meets the dry sand (within 2 tiles of it) or deep in the uplands (14 steps or + // more): a trip away from the river. if (!t.grass[i] || keepClear[i] || t.waterSteps[i] < 9) continue; bool edge = false; @@ -1591,6 +1707,9 @@ void placeStone(Game &game, GenerationContext &context, const Tiles &t, if (edge || t.waterSteps[i] >= 14) candidates.push_back(int(i)); } + // One outcrop per 3000 tiles (21 on a 256 map), each 20 tiles from the next, radius 1 or 2: + // stone never runs out, so a scattering of small quarries is enough and spreading them makes + // colonies travel. const int outcrops = int(scaledCount(std::max(2, w * h / 3000), stonePercent)); std::vector placed; for (int attempt = 0; @@ -1619,6 +1738,9 @@ void placeFruit(Game &game, GenerationContext &context, const Layout &layout, co const int w = t.w, h = t.h; std::vector junctions = layout.junctions; context.shuffle(junctions.begin(), junctions.end(), "resources"); + // One grove per 6000 tiles (10 on a 256 map, at least 3), each a small clump 5 to 12 tiles from + // a confluence on its most fertile ground, the kinds dealt in turn so all three are on the map + // and a colony must travel for the ones it lacks. const int groves = int(scaledCount(std::max(3, w * h / 6000), fruitPercent)); int type = int(context.bounded("resources", 3)), placed = 0; for (size_t j = 0; j < junctions.size() && placed < groves; ++j) @@ -1668,6 +1790,7 @@ void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, co if (dx * dx + dy * dy <= 100 && t.water[i] && t.waterSteps[i] == 0) water.push_back(int(i)); } + // Three clumps within 10 tiles of each mouth, where river and sea meet beside sand. for (int clump = 0; clump < scaledCount(3, algaePercent) && !water.empty(); ++clump) { const int i = water[context.bounded("resources", water.size())]; @@ -1691,6 +1814,7 @@ void placeAlgae(Game &game, GenerationContext &context, const Layout &layout, co if (c.y < layout.north.at(c.x) - 1 || c.y > layout.south.at(c.x) + 1) shallows.push_back(int(i)); } + // One small clump per 60 tiles of sea within 3 tiles of a shore: near sand, so it regrows. for (int clump = 0; clump < scaledCount(int(shallows.size()) / 60, algaePercent); ++clump) { const int i = shallows[context.bounded("resources", shallows.size())]; diff --git a/src/map/generator/shared/BalancedStarts.cpp b/src/map/generator/shared/BalancedStarts.cpp index fbbd42cab..661b76ce4 100644 --- a/src/map/generator/shared/BalancedStarts.cpp +++ b/src/map/generator/shared/BalancedStarts.cpp @@ -193,6 +193,8 @@ bool chooseBalancedStarts(Game &game, GenerationContext &context, int minDistSqu exact.reserve(sites.size()); for (const auto &s : sites) { + // 32 steps: the wood range of the start guarantee, the farthest a start's resources are + // allowed to be. const int built = scoreAsBuilt(s.second % w, s.second / w, 32); if (built >= 0) exact.push_back({built, s.second}); diff --git a/src/map/generator/shared/Geometry.cpp b/src/map/generator/shared/Geometry.cpp index d292e1b37..26f377d84 100644 --- a/src/map/generator/shared/Geometry.cpp +++ b/src/map/generator/shared/Geometry.cpp @@ -31,6 +31,11 @@ RadialShape::RadialShape(double r, double roughness, GenerationContext &context, roughness >= 1 || !std::isfinite(amplitudeMaximum) || amplitudeMaximum < 0.6 || roughness * amplitudeMaximum >= 1) throw std::invalid_argument("Invalid radial shape"); + // Four harmonics of 2, 3, 5 and 7 lobes round the shape. Prime lobe counts never line up with + // each other, so the outline has no repeating symmetry, and the weights, falling and summing to + // one, keep the 2-lobe stretch dominant with finer bays on top. Each is drawn at 60% of its + // weight up to amplitudeMaximum times it, so roughness x amplitudeMaximum is the most the + // radius can move, which is why the product must stay under 1 (a radius that could reach zero). constexpr double falloff[] = {0.42, 0.28, 0.18, 0.12}; for (size_t i = 0; i < amplitude.size(); ++i) { diff --git a/src/map/generator/shared/HeightMap.cpp b/src/map/generator/shared/HeightMap.cpp index b4e085ea3..068d753ab 100644 --- a/src/map/generator/shared/HeightMap.cpp +++ b/src/map/generator/shared/HeightMap.cpp @@ -49,6 +49,9 @@ void HeightMap::init(unsigned int width, unsigned int height) _stamp.clear(); } +// The stamp: a round bowl, 0 at its centre rising as (1 - cos(pi * d / r)) / 2 to 1 at radius r, +// and .9999 outside. lower() presses it into the field (keeping the lower value) to dig a crater or +// a river bed; differenceStamp() uses its complement, a hill of height 1, to raise an island. void HeightMap::makeStamp(unsigned int radius) { _r = std::max(1u, radius); @@ -111,6 +114,11 @@ inline void HeightMap::differenceStamp(unsigned int coordX, unsigned int coordY) } } +// Blends the field with noise: field * (1 - weight) + noise * weight. The noise is four copies of +// Perlin noise read at different offsets and cross-faded with the fader functions above, so it +// wraps seamlessly at every edge: the map is a torus and a coast must not break at the border. +// The weight is each shape's balance between design and chaos: .1 for the river (the bed must +// survive), .7 islands, .8 craters, .99 swamp and plain (all noise). inline void HeightMap::addNoise(float weight, float smoothingFactor) { assert((weight > 0) && (weight <= 1.0)); @@ -137,6 +145,14 @@ inline void HeightMap::addNoise(float weight, float smoothingFactor) } } +// Islands: `count` hills, then noise. Hill centres are drawn at random and rejected while any +// earlier centre lies within `mindist` on both axes, where mindist is half the side of each +// island's share of the map. Each hill has radius 2 * mindist, the whole side of that share, so +// neighbouring hills overlap heavily. differenceStamp takes |hill - field|, so where two hills +// overlap they cancel into a valley rather than add up: the overlaps become the channels between +// islands. Noise at weight 0.7 then roughens the coasts; the histogram decides how much is above +// water. Islands asks for (colonies + extra islands) / 2^repeat hills, one or more per colony per +// patch. void HeightMap::makeIslands(unsigned int count, float smoothingFactor) { assert(count); @@ -187,6 +203,20 @@ void HeightMap::makeIslands(unsigned int count, float smoothingFactor) normalize(); } +// River: a river bed pressed along a straight line from a random point to that point moved a whole +// map width, height or both, so on the torus the line ends where it began and the river is one +// closed loop across the map, with no source and no mouth. On a rectangle it runs along the long +// side. River width is a percentage of the mean side (the stamp's diameter). +// +// The meander constants (winding river): the bed is moved sideways by two octaves of 1D noise, +// feature lengths about 153 and 13 steps with weights 300 and 50. This noise stays within about +// +-0.5, so the two swing by up to +-175, and the -175 turns that into a one-sided offset of 0 to +// -350: the river only ever bends to one side of its line, which on a torus is just a shift. The +// meander is computed twice, forwards from the start and backwards from the end, and cross-faded +// with (1 -+ cos) weights that sum to 2, so the offset is the same at both ends and the loop closes +// without a jump. Divided by 4, the river could in theory wander up to 175 tiles off its line; in +// practice the noise stays well inside its range and the river is a gentle wave. Only 0.1 of noise +// is added afterwards, so the bed dominates: the water is the river, not scattered ponds. void HeightMap::makeRiver(unsigned int maxDiameter, float smoothingFactor, bool winding) { /// riverRadius refers to the distance between center of the river and the maximum distance that @@ -250,6 +280,12 @@ void HeightMap::makeRiver(unsigned int maxDiameter, float smoothingFactor, bool normalize(); } +// Crater lakes: `craterCount` bowls of radius `craterRadius` pressed at random into high ground, +// then noise at weight 0.8. lower() keeps the minimum, so overlapping bowls merge into one lake. +// The generator asks for width * height * density / 30000 craters, about 54 on a 256x256 map at the +// default density of 25, which is one crater per 1200 tiles: with radius 25 each bowl covers about +// 2000, so the bowls would cover the map more than once over; but only their deepest parts fall +// under the water share, so the lakes come out as round ponds much smaller than the bowls. void HeightMap::makeCraters(unsigned int craterCount, unsigned int craterRadius, float smoothingFactor) { diff --git a/src/map/generator/shared/LatticeNoise.cpp b/src/map/generator/shared/LatticeNoise.cpp index 3eb0ca572..414ae3ed3 100644 --- a/src/map/generator/shared/LatticeNoise.cpp +++ b/src/map/generator/shared/LatticeNoise.cpp @@ -64,6 +64,8 @@ std::vector fractalNoise(int w, int h, int period, int octaves, std::mt1993 std::vector torusNoise(int width, int height, std::mt19937 &rng) { + // Octaves from 32 tiles down to 4, weights falling 0.4 to 0.1: the broad swings of a coastline + // dominate, with detail down to a few tiles, about the size of a building. static const std::pair octaves[] = {{32, 0.4}, {16, 0.3}, {8, 0.2}, {4, 0.1}}; std::vector field(size_t(width) * height, 0.0); for (const auto &octave : octaves) diff --git a/src/map/generator/shared/Pipeline.cpp b/src/map/generator/shared/Pipeline.cpp index f9b01f66c..f0ab31d1b 100644 --- a/src/map/generator/shared/Pipeline.cpp +++ b/src/map/generator/shared/Pipeline.cpp @@ -12,6 +12,8 @@ bool reopenCrampedStarts(Game &game, GenerationContext &context, const ResourceA { if (!amounts.scaled()) return false; + // At least 16 free 4x4 building sites within 24 steps of every swarm: room for a first base + // (inns, huts, a school) without clearing anything. openCrampedStarts(game, context, 16, 24, protectedWalls); guaranteeStartingResources(game, context, wheatRange, woodRange, clearRadius, protectedWalls); return true; diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp index 0f2481bd3..159e1989e 100644 --- a/src/map/generator/shared/Resources.cpp +++ b/src/map/generator/shared/Resources.cpp @@ -233,7 +233,8 @@ void scatterFarmland(Map &map, const Fertility::Field &fertility, HeightMap &spl if (map.getResource(x, y).type != NO_RES_TYPE) continue; // Wood and corn share the same terrain requirement (grass), so either stands in for - // eligibility here - which of the two a tile ends up with is decided below, by the split. + // eligibility here - which of the two a tile ends up with is decided below, by the + // split. if (!map.isResourceAllowed(x, y, CORN)) continue; const std::uint32_t f = fertility.at(x, y); @@ -333,6 +334,9 @@ void scatterResources(Game &game, GenerationContext &context, const ResourceDens int numComponents = 0; const std::vector landComponent = computeComponents(map, false, numComponents); + // Densities are tiles per 1600 tiles of map for wheat and wood (a 40x40 square, about one + // colony's working area), per 3000 for stone and per 800 for algae: the default 24 wheat covers + // 1.5% of the map. Being per area, the scatter looks the same on every map size. scatterFarmland(map, fertility, splitNoise, density.corn * area / 1600, density.wood * area / 1600, landComponent, numComponents); scatterBand(map, noise, STONE, density.stone * area / 3000, landComponent, numComponents); @@ -487,6 +491,9 @@ void guaranteeStartingResources(Game &game, GenerationContext &context, int whea Map &map = game.map; if (protectedWalls && protectedWalls->size() != size_t(map.getW()) * map.getH()) throw GenerationFailure("Protected wall mask does not match the map size"); + // Floods reach two and a half times the farthest range, so a deposit just out of range can + // still be found and a wall beyond it measured; closeRange, half the wheat range, marks the + // grass a topped-up deposit is tried on first, so it lands clearly within reach. const int exploreLimit = std::max(wheatRange, woodRange) * 5 / 2; const int closeRange = wheatRange / 2; // Below this many reached tiles a team is badly boxed in, but that has two very different @@ -514,6 +521,9 @@ void guaranteeStartingResources(Game &game, GenerationContext &context, int whea }; bool underServed = reach.wheatDist < 0 || reach.wheatDist > wheatRange || reach.woodDist < 0 || reach.woodDist > woodRange; + // Up to six rounds of wall clearing per colony, flooding again after each: a cap on the + // work for a pocket that stays small, which is then taken to be genuinely small land rather + // than a wall. for (int attempt = 0; underServed && pocketSize() < minPocketTiles && attempt < 6; ++attempt) { @@ -576,8 +586,8 @@ void openCrampedStarts(Game &game, GenerationContext &context, int sites, int ra WorkerReach reach = reachFromWorkers(map, team, range); if (reach.sites >= sites) continue; - // Rings measured with resources ignored, so they walk out through the wall itself instead of - // stopping at its inner face the way the colony's own flood does. + // Rings measured with resources ignored, so they walk out through the wall itself instead + // of stopping at its inner face the way the colony's own flood does. const int bootX = map.normalizeX(context.bootX[team]), bootY = map.normalizeY(context.bootY[team]); const std::vector open = terrainOnlyReach(map, bootX, bootY, range, protectedWalls); @@ -586,9 +596,10 @@ void openCrampedStarts(Game &game, GenerationContext &context, int sites, int ra if (open[p] >= 1 && open[p] <= range && map.isResource(p % w, p / w) && !(protectedWalls && (*protectedWalls)[p])) rings[open[p]].push_back(p); - // One ring at a time, nearest first, re-measuring after each: the ring that finally gives the - // colony its room is the last one cleared, so nothing further out is touched. A colony on a - // genuinely small spot — a real islet, with nothing to open up — just runs out of rings. + // One ring at a time, nearest first, re-measuring after each: the ring that finally gives + // the colony its room is the last one cleared, so nothing further out is touched. A colony + // on a genuinely small spot — a real islet, with nothing to open up — just runs out of + // rings. for (int ring = 1; ring <= range && reach.sites < sites; ++ring) { if (rings[ring].empty()) diff --git a/src/map/generator/shared/Sketch.cpp b/src/map/generator/shared/Sketch.cpp index 01a20dc83..a39dbaa89 100644 --- a/src/map/generator/shared/Sketch.cpp +++ b/src/map/generator/shared/Sketch.cpp @@ -56,6 +56,9 @@ std::vector raiseIslands(TerrainSketch &terrain, const Torus &t, Generat if (placement.wanted <= 0 || sea.empty()) return islands; const std::vector offshore = stepsFrom(t, land, water); + // Islands are radius 4 to 6, grown with the square root of the map's shorter side over 128 (1 + // to 1.6 times): big enough on every map to hold a beach and a prize, and not a continent on a + // 512 map. const double scale = std::clamp(std::sqrt(std::min(t.w, t.h) / 128.0), 1.0, 1.6); for (int attempt = 0; int(islands.size()) < placement.wanted && attempt < placement.wanted * placement.attemptsPerIsland; diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp index 9306707e9..850ecba98 100644 --- a/src/map/generator/shared/Terrain.cpp +++ b/src/map/generator/shared/Terrain.cpp @@ -14,6 +14,30 @@ #include #include #include +// The height-field pipeline behind Islands, Swamp, River and Crater lakes. +// +// HISTORY. Leo Wandersleb wrote these generators in January 2006 (merged by nct), replacing the +// original random generators (Old random, Old islands), which giszmo brought back as "old" a week +// later. donkyhotay added automatic fruit groves in March 2006 and giszmo cut that code down and +// commented it; giszmo also added "repeative landscapes" (the repeat control) in 2006-07. On this +// branch the pipeline was split into the stages below, colony sites came to be chosen after the +// resources exist and for equality (BalancedStarts), and resource amounts and a hilltop stone +// switch were added; the default maps still come out of Leo's thresholds unchanged. +// +// THE IDEA. One smooth height field decides everything, by rank rather than by value. The lobby's +// terrain weights are turned into tile counts, and a histogram of the field finds the heights below +// which exactly that many tiles lie: the lowest tiles are water, the next beach, the next grass, +// and anything higher is desert. Resources are bands of the same ranking: algae in the deepest +// water, stone just above the beach, wheat and wood on the low grass above the stone. So a +// generator only has to shape the field (islands, craters, a river), and the map always comes out +// in the shares the player asked for, whatever the shape. +// +// GAME RULES IT LEANS ON (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): +// - Wheat and wood regrow only near water, so farmland is the band of grass just above the +// waterline, and its thickness is capped by whichever of water and grass is scarcer. +// - Algae needs water with sand in reach, which the shallow rim of the deepest water gives; stone +// right above the beach is the first land a colony walks onto. +// - Grass may not touch water: controlSand after painting rings every shore. namespace MapGeneration { std::vector heightFieldResourceControls() @@ -51,6 +75,11 @@ void openStartsBuriedByAmounts(Game &game, GenerationContext &context, {options.wheat, options.wood, options.stone, options.algae}); } +// Repeat landscape: the field is built at a fraction of the map's size and stamped several times, +// so every colony can get the same patch of terrain. Each of `repeat` halvings goes to the longer +// remaining side, so 1 halves the long side, 2 makes a 2x2 tiling on a square map, and so on up to +// 32 copies. With a patch per colony this is giszmo's fair-by-repetition answer from 2006, long +// before the designed generators' rotation trick. HeightFieldTiling heightFieldTiling(int w, int h, int repeat) { /// respect symmetry-requirements @@ -76,6 +105,10 @@ HeightFieldLevels classifyHeightField(HeightMap &hm, const HeightFieldTiling &ti /// undermap. unsigned int waterTiles, sandTiles, grassTiles, wheatWoodTiles, algaeTiles; /// grass + sand + water + desert as from the gui + // The fruit control (a grove count, 4 by default) is added to the total too, as if it were a + // terrain weight: a slip in the original that shrinks every terrain's share a little and hands + // that share to desert (the part of the field above the grass). At the defaults it is a few + // percent of the map; it is kept because removing it changes every height-field map. unsigned int totalGSWFromUI = options.water + options.sand + options.grass + options.desert + options.fruit; if (options.swamp) @@ -91,6 +124,8 @@ HeightFieldLevels classifyHeightField(HeightMap &hm, const HeightFieldTiling &ti grassTiles = static_cast(options.grass) / totalGSWFromUI * wHeightMap * hHeightMap; } /// wheat/wood needs ground to stand on and water. So: + // half the scarcer of water and grass: a watery map has little land to farm, a dry map little + // shore for fields to regrow against. Algae takes the deepest sixth of the water. wheatWoodTiles = waterTiles < grassTiles ? waterTiles / 2 : grassTiles / 2; algaeTiles = scaledCount(waterTiles / 6, options.algae); // A third of that share is stone, right above the beach, and the rest farmland above the stone; @@ -110,6 +145,8 @@ HeightFieldLevels classifyHeightField(HeightMap &hm, const HeightFieldTiling &ti unsigned(std::min(landTop, farmStart + scaledCount(farmTiles, options.wood))); /// histogram[i] collects the count of all terrain levels == i + // 2048 bins over the normalised 0..1 field; each threshold snaps to a bin edge, so the shares + // come out close to, not exactly, what was asked. int histogram[2048]; memset(histogram, 0, 2048 * sizeof(int)); @@ -167,7 +204,7 @@ void paintHeightFieldTerrain(Map &map, HeightMap &hm, const HeightFieldTiling &t else if (hm(y * wHeightMap + x) < levels.grass) tmpUndermap = GRASS; else - tmpUndermap = SAND; + tmpUndermap = SAND; // desert: the highest ground dries out for (int yRepeat = 0; yRepeat < tiling.hRepeat; yRepeat++) for (int xRepeat = 0; xRepeat < tiling.wRepeat; xRepeat++) map.setUMTerrain( @@ -206,6 +243,11 @@ void paintHeightFieldResources(Map &map, HeightMap &hm, const HeightFieldTiling } // patch to get smooth areas of wheat and wood: // if the map is ascending at x+w/2,y set wheat. else set wood + // + // The slope is read half the field away, not at the tile itself: on the tile's own + // slope, every hill would come out wheat on one side and wood on the other, in rings + // round it. Read far away it is an unrelated smooth pattern, so the two crops form + // blobs across the farmland band instead. else if (hm((x + wHeightMap / 2) % wHeightMap + wHeightMap * y) < hm((x + wHeightMap / 2 + 1) % wHeightMap + wHeightMap * y)) { @@ -241,6 +283,10 @@ bool chooseHeightFieldStarts(Game &game, GenerationContext &context) // rectangle first, and everyone after it takes what is left — stays as a fallback for maps // where no set of sites can reach both resources at all. int nbTeams = context.request.nbTeams; + // A fifth of each colony's share of the map, as a squared distance: colonies at least the side + // of a square of that area apart (about 57 tiles on a 256x256 map with 4 colonies). Looser than + // Old random's full share, because the balanced search below then prefers equal sites over + // distant ones; the fifth leaves it enough candidate sets to choose from. int minDistSquare = (int)((double)w * h / (double)nbTeams / 5); if (minDistSquare <= 0) { @@ -315,6 +361,12 @@ bool chooseHeightFieldStarts(Game &game, GenerationContext &context) return true; } +// donkyhotay's fruit groves (2006): each grove picks a kind of fruit and a random free grass tile, +// then walks to free grass neighbours planting 1 to 10 trees, so a grove is a small winding cluster +// of one kind, copied into every repeat patch. The count is fixed, not scaled by map size (see the +// TODO below). Groves land anywhere, near a colony or not, so which colony can stock all three +// fruits in its inns (and so convert hungry enemy units) is luck of the draw on these maps, unless +// repeat gives each colony its own patch. bool plantHeightFieldGroves(Map &map, GenerationContext &context, const HeightFieldTiling &tiling, int count) { @@ -380,6 +432,9 @@ bool generateHeightField(Game &game, GenerationContext &context, const HeightFie { Map &map = game.map; /// to influence the roughness + // The noise's feature size in tiles: 15 at smoothing 1 up to 36 at smoothing 8 (the defaults, 4 + // or 6, give 24 or 30). The +4 keeps even the roughest setting's features several swarms wide, + // so land stays in pieces big enough to build on. const float smoothingFactor = (float)(options.smoothing + 4) * 3; const HeightFieldTiling tiling = heightFieldTiling(map.getW(), map.getH(), options.repeat); /// lets generate a patch of perlin noise. That's a smooth mapping R^2 to ]0;1[ diff --git a/src/map/generator/shared/legacy/Distances.cpp b/src/map/generator/shared/legacy/Distances.cpp index f93595919..ab93fd497 100644 --- a/src/map/generator/shared/legacy/Distances.cpp +++ b/src/map/generator/shared/legacy/Distances.cpp @@ -16,6 +16,7 @@ using namespace MapGeneration; namespace MapGeneration { +// Raises the height of every listed tile by `value` (a tile listed twice is raised twice). void adjustHeightmapFromPoints(Map &map, std::vector &points, std::vector &heightmap, int value) { @@ -25,6 +26,11 @@ void adjustHeightmapFromPoints(Map &map, std::vector &points, } } +// Adds smooth noise of -spread to spread - 1 to every tile: a plain HeightMap (makePlain(4)) +// quantised to 2 * spread levels. The field is real Perlin gradient noise (GenerationNoise) with +// one noise cell every 4 tiles, made seamless across the map's wrap by +// cross-fading four offset copies (HeightMap::addNoise), then stretched to fill 0 to 1. Used to +// roughen coasts. void adjustHeightmapFromPerlinNoise(Map &map, GenerationContext &context, std::vector &heights, int spread) { @@ -43,6 +49,9 @@ void adjustHeightmapFromPerlinNoise(Map &map, GenerationContext &context, std::v } } +// Walking distance (8-way steps, through the wrap) from the nearest source to every tile, in the +// 2008 toolkit's encoding: sources read 1, each step out one more, obstacle tiles -1, tiles the +// flood cannot reach 0. Callers subtract 1 for a plain step count. void computeDistances(Map &map, std::vector &sources, std::vector &obstacles, std::vector &heightmap) { @@ -61,6 +70,8 @@ void computeDistances(Map &map, std::vector &sources, heightmap[i] = !open[i] ? -1 : steps[i] < 0 ? 0 : steps[i] + 1; } +// The mean of a distance field over one area's tiles (0 for an empty area). divideUpPlayerLands +// uses it to sort a colony's zones from nearest the water to farthest. int computeAverageDistance(Map &map, std::vector &grid, int areaN, const std::vector &heightmap) { diff --git a/src/map/generator/shared/legacy/Regions.cpp b/src/map/generator/shared/legacy/Regions.cpp index 1994eb267..867f37d16 100644 --- a/src/map/generator/shared/legacy/Regions.cpp +++ b/src/map/generator/shared/legacy/Regions.cpp @@ -54,6 +54,10 @@ void collectPointsColumnOrder(int w, int h, Pred pred, std::vector &grid, int areaN, std::vector &weights, std::vector &areaNumbers) { @@ -72,6 +76,9 @@ bool divideUpArea(Map &map, GenerationContext &context, std::vector &grid, return true; } +// Labels every tile inside the axis-aligned ellipse of the given width and height centred on +// (x, y) with areaN, through the wrap. The test is x^2 * h^2 + y^2 * w^2 < w^2 * h^2 in integers +// (on half-axes), so it has no rounding drift across platforms. void createOval(Map &map, std::vector &grid, int areaN, int x, int y, int width, int height) { std::int64_t h2 = std::int64_t(height / 2) * (height / 2); @@ -93,6 +100,25 @@ void createOval(Map &map, std::vector &grid, int areaN, int x, int y, int w } } +// Places one point per entry of `points` inside area areaN, as far apart as it can, and returns the +// least distance between any two (half the smaller map side for a single point, 0 on failure). This +// is how the 2008 toolkit spreads colonies evenly without any geometry: it only needs a grid of +// area labels, so it works on any shape of land, wrapped round the torus. +// +// Stage 1, farthest-point sampling: a random tile of the area seeds a walking-distance flood (other +// areas block it); the first point is a random tile at the flood's greatest distance, and each next +// point a random tile farthest from all points placed so far. This gives a good spread in one pass. +// +// Stage 2, relaxation: repeatedly move each point to the tile that maximises its smallest weighted +// squared straight-line distance to the other points, until no point moves (or maxPasses runs out). +// Local searches only the 7x7 around the point, so points creep; WholeRegion searches every free +// tile of the area, so points jump. A weight scales the distances measured *to* that point, so a +// light point is the one whose distance binds: Contested commons gives its commons seed half a +// colony's weight, and every colony then keeps its distance from the commons first. With equal +// weights, as every other caller passes, this is plain maximin spacing. +// +// Last, the points (and weights with them) are shuffled, so which colony lands on which point is +// random rather than decided by the order the sampling happened to find them in. int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, int areaN, std::vector &points, std::vector &weights, PointSearch search, int maxPasses) @@ -372,6 +398,14 @@ int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, return int(std::sqrt(double(minDist))); } +// Grows a region from each point over area areaN until the area is used up, and labels each tile +// with its region's area number. A multi-source flood where every source advances at its own rate: +// each round a region earns `weights[p]` tiles of credit and spends one per tile it claims, so a +// region of weight 10 grows ten times as fast as one of weight 1 and ends up with about ten times +// the ground where they compete. Frontier tiles are kept in a list with each new tile inserted at a +// random position, so growth picks its next tile at random from the frontier: regions come out as +// organic blobs rather than the diamonds a breadth-first flood makes. Claims go 8-way. With +// grassOnly, regions only claim grass, so a region covers only its buildable land. void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, int areaN, std::vector &points, std::vector &weights, std::vector &areaNumbers, bool grassOnly) @@ -492,6 +526,9 @@ void getAllOtherPoints(Map &map, std::vector &grid, int areaN, w, map.getH(), [&](int x, int y) { return grid[y * w + x] != areaN; }, points); } +// The tiles of a straight line from (x1, y1) towards (x2, y2), the short way round the torus, as a +// 4-connected staircase (a diagonal step adds both tiles of the corner), so a stroke along it has +// no diagonal gaps a unit could not walk across. The end tile itself is not included. void getAllPointsLine(Map &map, int x1, int y1, int x2, int y2, std::vector &points) { @@ -554,6 +591,8 @@ void getAllPointsLine(Map &map, int x1, int y1, int x2, int y2, } } +// Every tile with a differently labelled tile among its eight neighbours: the boundaries between +// areas, which Concrete islands digs its channels along. void findBorderPoints(Map &map, std::vector &grid, std::vector &points) { const int w = map.getW(); @@ -571,6 +610,8 @@ void findBorderPoints(Map &map, std::vector &grid, std::vector &points, int n) { diff --git a/src/map/generator/shared/legacy/StartingPositions.cpp b/src/map/generator/shared/legacy/StartingPositions.cpp index 6f67e04c6..c72d6d7eb 100644 --- a/src/map/generator/shared/legacy/StartingPositions.cpp +++ b/src/map/generator/shared/legacy/StartingPositions.cpp @@ -21,6 +21,23 @@ using namespace MapGeneration; namespace MapGeneration { +// Lays out every colony's own land for the point-dispersion generators (Concrete islands, Isles): +// fields, stone, the swarm and its workers, all inside the colony's area of `grid` (area 0 is +// everything that is not a colony's buildable land, which the distances below are measured from). +// +// The layout, written in 2008, follows the growth rules: wheat and wood only regrow near water, so +// the fields go where the land is closest to the sea, and the base, which needs no water, goes in +// the interior. Each colony's area is cut into twelve equal zones (divideUpArea) and the zones +// sorted by their mean distance from area 0: +// - the three nearest the water become wheat, +// - the next three wood, +// - the six farthest are the base: stone, and the swarm's candidate sites. Within a field zone, a +// tile gets its resource where a height field clears 50. That field is 50, plus noise of up to 5 +// either way, minus the tile's distance from the water, plus a rise of 10 for the zone (scaled by +// the amount control). So at the default amount a field covers its zone's tiles up to about 10 +// steps from the water, give or take the noise: a coastal band with a soft inner edge, never the +// whole zone on a large island. The swarm goes on a free 4x4 footprint 1 or 2 steps from where +// the default wheat field lies, so the first workers reach food at once. bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector &grid, std::vector &teamAreaNumbers, int &areaNumber, const PlayerLandResources &resources) @@ -51,7 +68,8 @@ bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector areaWeights; std::vector areaNumbers; for (int j = 0; j < 12; ++j) @@ -125,7 +143,9 @@ bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector stoneLocations = baseLocations; chooseRandomPoints(game.map, context, stoneLocations, numberOfStone); @@ -298,6 +318,11 @@ Building *addBuilding(Game &game, int x, int y, int team, int type, int level, namespace MapGeneration { +// Builds each colony's swarm and workers on its boot tile for Old islands, first stamping two +// overlapping grass squares of 5 + islandSize / 10 tiles (11 at the default island size 65) so the +// swarm and a first few buildings fit whatever the island's ragged outline. Workers stand in a row +// of four above the swarm. Like Old random's, its closing Map::smoothResources does nothing today +// (resources left the terrain layer it reads in 2003), so no deposit is frayed. bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSize) { for (int s = 0; s < context.request.nbTeams; s++) @@ -337,6 +362,9 @@ bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSi return true; } +// Builds each colony's swarm and workers on its boot tile for Old random and the height-field +// generators, after stamping two overlapping 5x5 squares of grass with no resource on them: a clear +// 7x7 home for the 4x4 swarm and the workers, who stand in rows of four just above it. bool placeStarts(Game &game, GenerationContext &context) { for (int s = 0; s < context.request.nbTeams; s++) @@ -349,6 +377,10 @@ bool placeStarts(Game &game, GenerationContext &context) if (game.mapHeader.getNumberOfTeams() <= s) game.addTeam(); + // Two overlapping 5x5 stamps (setUMatPos fills a square of side 5 round its point) turn + // corners x+0..x+4, y-2..y+4 to grass and clear their resources: exactly the corners the + // 4x4 swarm's tiles need to be pure grass, plus the rows above where its workers appear. + // This is the five by seven box BalancedStarts scores sites as they will be built. game.map.setUMatPos(context.bootX[s] + 2, context.bootY[s] + 0, GRASS, 5); game.map.setUMatPos(context.bootX[s] + 2, context.bootY[s] + 2, GRASS, 5); game.map.setNoResource(context.bootX[s] + 2, context.bootY[s] + 0, 5); From 25dce094b3b12900deee6984ef788217909343eb Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 23:00:24 -0400 Subject: [PATCH 109/136] Make Map::smoothResources fray deposits again Old random and Old islands close with Map::smoothResources, nuage's 2002 pass that frays the square deposits setResource stamps into natural fields. Since resources left the terrain layer in 2003 it had read terrain values of 272 and up and silently done nothing. It now applies the same rule to the Resource struct: each wood, wheat, stone or algae tile, half the time, grows its amount (the smaller, the likelier) or sprouts a small tile of its kind on a random free neighbour of the right terrain, in row order and in place as before. Over 12 seeds at 256x256 with 2, 4 and 8 colonies: Old random's deposits roughly double and Old islands' wheat and wood grow about a quarter; wheat and wood sit closer to every swarm, the start score rises in all six cases, fairness moves by at most 0.03, and free building sites near each colony fall by a third to a half. No generation failures. Sprouts spread evenly in all four directions. Old random moves to revision 3 and Old islands to revision 2; their golden rows are regenerated. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- src/map/MapTerrain.cpp | 48 +++++++++++++------ .../generators/RuggedArchipelagoGenerator.cpp | 13 ++--- .../generators/ShatteredCoastGenerator.cpp | 15 +++--- .../shared/legacy/StartingPositions.cpp | 5 +- test/map-generator-golden.txt | 32 ++++++------- 5 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/map/MapTerrain.cpp b/src/map/MapTerrain.cpp index b2f53caf3..044941980 100644 --- a/src/map/MapTerrain.cpp +++ b/src/map/MapTerrain.cpp @@ -244,7 +244,9 @@ Uint16 Map::lookup(Uint8 tl, Uint8 tr, Uint8 bl, Uint8 br) const #include //also the Perlin Noise stuff uses random that is not based on syncRand +#include "GlobalContainer.h" #include "Map.h" +#include "MapInternal.h" #include "Unit.h" #include "Utilities.h" @@ -284,29 +286,45 @@ void Map::controlSand(void) } } +// The random generators' closing pass (nuage, 2002): the resource growth step run over the whole map +// `times` times, without its water test, to fray the square deposits setResource stamps into +// natural fields. Each wood, wheat, stone or algae tile, half the time, either grows (the smaller +// it is, the likelier) or, once it is large, sprouts a small tile of its kind on a random free +// neighbour of the right terrain. Tiles are visited in row order and updated in place, as the +// original did, so a sprout can grow again later in the same pass. +// +// When resources moved out of the terrain values in 2003 this kept reading the old encoding +// (terrain values of 272 and up) and silently stopped doing anything; this is the same rule over +// the Resource struct. void Map::smoothResources(int times) { for (int s=0; s=0)&&(d<40)&&(syncRand()&4)) + Resource &r=tiles[coordToIndex(x, y)].resource; + if (r.type!=WOOD && r.type!=CORN && r.type!=STONE && r.type!=ALGA) + continue; + if (!(syncRand()&4)) + continue; + const ResourceType *rt=globalContainer->resourcesTypes.get(r.type); + if (int(r.amount)-RESOURCE_INITIAL_AMOUNT<=int(syncRand()&3)) { - if (l<=(int)(syncRand()&3)) - setTerrain(x, y, d+273); - else + if (r.amountsizesCount) + r.amount++; + } + else + { + int dx, dy; + Unit::dxDyFromDirection(syncRand()&7, &dx, &dy); + const int nx=normalizeX(x+dx), ny=normalizeY(y+dy); + if (getResource(nx, ny).type==NO_RES_TYPE && isResourceAllowed(nx, ny, r.type)) { - // we extend resource: - int dx, dy; - Unit::dxDyFromDirection(syncRand()&7, &dx, &dy); - int nx=x+dx; - int ny=y+dy; - if (getGroundUnit(nx, ny)==NOGUID) - if (((r==WOOD||r==CORN||r==STONE)&&isGrass(nx, ny))||((r==ALGA)&&isWater(nx, ny))) - setTerrain(nx, ny, 272+(r*10)+((syncRand()&1)*5)); + Resource &sprout=tiles[coordToIndex(nx, ny)].resource; + sprout.type=r.type; + sprout.variety=syncRand()%rt->varietiesCount; + sprout.amount=RESOURCE_INITIAL_AMOUNT; + sprout.animation=0; } } } diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp index 5c0785816..088713e0e 100644 --- a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -314,11 +314,12 @@ static void resources(Game &game, GenerationContext &context, setScaledResource(map, bootX[s] + p, bootY[s], ALGA, amount, options.algae); } - // Let's smooth resources... - // Meant to fray the squares into fields; a no-op today. Map::smoothResources still looks for - // resources in the terrain layer, where they have not lived since 2003, so no tile matches and - // no random number is drawn (removing it leaves maps byte-identical). Kept as the design's - // intent; the deposits stay visible squares. + // Let's smooth resources... Frays the squares into fields: each of the smoothResources * 2 + // rounds grows small deposit tiles and sprouts large ones onto a free neighbour. This is why + // each deposit above is smoothResources tiles smaller than its run of grass: the rounds grow it + // back out. From 2003 until revision 2 Map::smoothResources read the old resource encoding and + // did nothing, leaving the deposits as squares; working, it adds about a quarter more wheat and + // wood. map.smoothResources(smoothResources * 2); } @@ -349,7 +350,7 @@ GeneratorDefinition ruggedArchipelagoDefinition() "rugged-archipelago", 8, "Old islands", - 1, + 2, false, // Island size scales the growth passes (see plantBootstraps for why its range is narrow); // beach size is the number of beach-widening passes. diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp index 2eae37664..4685227ee 100644 --- a/src/map/generator/generators/ShatteredCoastGenerator.cpp +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -850,13 +850,12 @@ static void resources(Game &game, GenerationContext &context, const ShatteredCoa setScaledResource(map, bootX[team] + dx, bootY[team] + dy, ALGA, amount, options.algae); } - // Map::smoothResources was meant to fray the square deposits into natural fields, three rounds - // per tile of the largest deposit side. It does nothing today: it still looks for resources - // encoded in the terrain layer (terrain values 272 and up), and resources have lived in their - // own layer since the 2003 resource rework, so no tile ever matches and no random number is - // drawn. Removing the call leaves every map byte-identical (checked on seeds of this generator - // and Old islands). That is why this generator's deposits are visible squares. The call is kept - // so the code still says what the design intended; making it work would change every map. + // Map::smoothResources frays the square deposits into natural fields, three rounds per tile of + // the largest deposit side: each round grows small deposit tiles and sprouts large ones onto a + // free neighbour. From 2003 until revision 3 it read the old resource encoding and did nothing, + // so the deposits stayed visible squares. Working, it roughly doubles the deposits (over 12 + // seeds at 256x256: wheat 251 to 551 tiles, wood 467 to 1022 at 4 colonies) and brings them + // closer to the swarms, at the cost of about half the building sites near each colony. int maxAmount = 0; for (int r = 0; r < 4; r++) if (maxAmount < context.request.resourceAmounts[r]) @@ -892,7 +891,7 @@ GeneratorDefinition shatteredCoastDefinition() "shattered-coast", 7, "Old random", - 2, + 3, false, // The three terrain weights are relative (40/4/60 asks for 38% water, 4% sand, 58% grass // before sand control adds the beaches); smoothing is the number of passes, which sets the diff --git a/src/map/generator/shared/legacy/StartingPositions.cpp b/src/map/generator/shared/legacy/StartingPositions.cpp index c72d6d7eb..64ec19930 100644 --- a/src/map/generator/shared/legacy/StartingPositions.cpp +++ b/src/map/generator/shared/legacy/StartingPositions.cpp @@ -321,8 +321,9 @@ namespace MapGeneration // Builds each colony's swarm and workers on its boot tile for Old islands, first stamping two // overlapping grass squares of 5 + islandSize / 10 tiles (11 at the default island size 65) so the // swarm and a first few buildings fit whatever the island's ragged outline. Workers stand in a row -// of four above the swarm. Like Old random's, its closing Map::smoothResources does nothing today -// (resources left the terrain layer it reads in 2003), so no deposit is frayed. +// of four above the swarm. Its closing Map::smoothResources runs islandSize / 10 rounds of +// deposit fraying before the island's own deposits exist, so it only touches what is already on +// the map; Old islands frays its deposits again after placing them. bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSize) { for (int s = 0; s < context.request.nbTeams; s++) diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index e3fa9907e..58b4e8b05 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -56,22 +56,22 @@ macos-arm64 6 2 8 8 4 3 ok 13493861953378784496 macos-arm64 6 2 8 8 8 1 ok 5023916742560360564 macos-arm64 6 2 9 8 4 1 ok 16957866786516727954 macos-arm64 6 2 9 9 4 1 ok 18148498138515235675 -macos-arm64 7 2 7 7 4 1 ok 278576360011260021 -macos-arm64 7 2 8 8 2 1 ok 8574781324117286872 -macos-arm64 7 2 8 8 4 1 ok 15788142514004224693 -macos-arm64 7 2 8 8 4 2 ok 5712377136822184348 -macos-arm64 7 2 8 8 4 3 ok 13319374393828491671 -macos-arm64 7 2 8 8 8 1 ok 8121618781306643342 -macos-arm64 7 2 9 8 4 1 ok 11480083053789646068 -macos-arm64 7 2 9 9 4 1 ok 1708935503910148886 -macos-arm64 8 1 7 7 4 1 ok 6473018063863562378 -macos-arm64 8 1 8 8 2 1 ok 12651074798552895253 -macos-arm64 8 1 8 8 4 1 ok 5644802038968384258 -macos-arm64 8 1 8 8 4 2 ok 14702577180534027034 -macos-arm64 8 1 8 8 4 3 ok 293881615974475555 -macos-arm64 8 1 8 8 8 1 ok 13875648332759742026 -macos-arm64 8 1 9 8 4 1 ok 15251784855226955418 -macos-arm64 8 1 9 9 4 1 ok 14481987140521717300 +macos-arm64 7 3 7 7 4 1 ok 17606064467284084149 +macos-arm64 7 3 8 8 2 1 ok 13195220428486063337 +macos-arm64 7 3 8 8 4 1 ok 15902815871131779878 +macos-arm64 7 3 8 8 4 2 ok 18406432865124272517 +macos-arm64 7 3 8 8 4 3 ok 2922633456080255781 +macos-arm64 7 3 8 8 8 1 ok 15346042196395066810 +macos-arm64 7 3 9 8 4 1 ok 16173449937850616615 +macos-arm64 7 3 9 9 4 1 ok 5698846428085401433 +macos-arm64 8 2 7 7 4 1 ok 4658555237636300612 +macos-arm64 8 2 8 8 2 1 ok 14143132148239399264 +macos-arm64 8 2 8 8 4 1 ok 10445762206825023853 +macos-arm64 8 2 8 8 4 2 ok 11928184670408982532 +macos-arm64 8 2 8 8 4 3 ok 11414080712598699583 +macos-arm64 8 2 8 8 8 1 ok 17098982314650317944 +macos-arm64 8 2 9 8 4 1 ok 219026690225498048 +macos-arm64 8 2 9 9 4 1 ok 11398981807552515483 macos-arm64 9 2 7 7 4 1 ok 2277607816406551252 macos-arm64 9 2 8 8 2 1 ok 6984614415357599638 macos-arm64 9 2 8 8 4 1 ok 1045725267928572154 From 52c55c9d08fd6cb5475dc03a60af1dc40dd9e8c7 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sat, 12 Sep 2026 23:00:24 -0400 Subject: [PATCH 110/136] Add sand roads to City states A new Sand roads toggle (Layout, on by default) lays sand roads two undermap vertices wide that nothing can grow over or be built on. They run from the heart of the commons along every home's axis and over its causeway to a main street inside the gate that follows the strait to both flanks. Side streets turn inland past both sides of the swarm and stop a few tiles behind it, short of the lake and the kit's fields. A ring road at half the commons' radius, or at the fords around a delta heart, joins every home's road. Every home gets the same roads in its own frame. Stone stands only on grass, so a road vertex is placed only where it cannot open a wall: off ridges and causeway shoulders, 5 tiles from any water and 3 from any sand patch inside a home, 2 from water on the commons, and clear of the swarm's square. Road sand never carries the sea's margin inland, and valley lakes keep clear of roads. Across 128, 256 and 512 maps with 2 to 12 colonies (6 seeds each) every roll validates, fairness is unchanged, and free building sites near each colony fall by about 35-40% at 256 and 60% at 128. With the toggle off, maps are byte-identical to revision 5. City states moves to revision 7; its golden rows are regenerated. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 11 + .../generators/CityStatesGenerator.cpp | 207 ++++++++++++++++-- .../generators/CityStatesGenerator.h | 2 +- test/map-generator-golden.txt | 16 +- 4 files changed, 214 insertions(+), 22 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index eb3743e28..bd1b49112 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -511,6 +511,17 @@ shape. leaves a sand lane outside the stone that a unit can land on and walk along but never leave; the causeway as a barrier, and as the ground kept clear of deposits with both its approaches, includes its lanes. Home lakes keep seven tiles from the sea so the two beaches never meet. +- **Sand roads.** With `sand-roads` (on), a line of sand two undermap vertices thick, which nothing + can grow over or be built on, runs from the heart of the commons along every home's axis, over + its causeway, to a main street seven tiles inside the gate that follows the strait out to both + flanks. Side streets turn inland from it past both sides of the swarm and at its ends, stopping a + few tiles behind the swarm and at most half the home's depth in, short of the lake and the kit's + fields. A ring road at half the commons' radius, or at the fords when the heart is a delta, joins + every home's road. A vertex only turns to sand on land, off ridges and causeway shoulders, at + least five tiles from any water and three from any sand patch inside a home, and two from water on + the commons, and clear of the swarm's square. Sand there could open a gap in a wall, since stone + stands only on grass. Where a road would break a rule it simply stops. The sea's margin never + spreads along a road, and valley lakes keep clear of roads. - **Every roll differs.** Both coasts are random harmonic profiles periodic in the wedge (`coast-roughness`): bays and headlands with a finer ripple on the commons' coast, which the strait follows, and bays into the flanks of every home's inner and outer coasts, all held flat diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index b152b0d2a..0482cb430 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -4,6 +4,7 @@ #include "Game.h" #include "GenerationContext.h" #include "Grid.h" +#include "Drawing.h" #include "Geometry.h" #include "HeightMap.h" #include "Pipeline.h" @@ -108,6 +109,20 @@ constexpr int kOrchardReach = 5; constexpr int kLandingSpread = 12; // A home needs this much depth for a creek, ridges or a band of sand to fit round its lake. constexpr int kFeatureDepth = 30; +// Sand roads (see planSandRoads). The ring road runs at this share of the commons' radius, halfway +// from the landings to the centre, and keeps this much ground from the heart's outer edge and +// from the landings' clear approaches. In a home, roads keep this far from any water, so they never +// reach a wall on its shore, and in the commons this far; the branches reach +// at most this share of the home's depth in from the strait, leaving the rest to build on. +constexpr double kRingShare = 0.5; +constexpr double kRingHeartGap = 5.0; +constexpr int kRoadSeaGap = 5; +constexpr int kRoadWaterGap = 2; +constexpr int kRoadSandGap = 3; +constexpr double kRoadDepthShare = 0.5; +constexpr double kStreetInset = 7.0; +// Sand vertices across a road. +constexpr int kRoadWidth = 2; // What a home is made of, and what the heart of the commons is. One of each per map. enum HomeKind @@ -238,6 +253,15 @@ enum Region : signed char HomeLand = 2 }; +// Where the swarm stands along its home's axis, from the centre: 9 tiles short of the lake's near +// shore, and never within 8 tiles of the strait, between causeway and lake. +double swarmRadius(const Geometry &g, double coast) +{ + const double lakeRadius = std::clamp(0.1 * g.depth(), 2.0, 6.0); + return std::max(coast + g.strait + 8.0, + coast + g.strait + lakeOffset(g, lakeRadius) - lakeRadius - 9.0); +} + // The terrain design: a pure function of the request, rebuilt by validateWorld. struct Layout { @@ -253,6 +277,8 @@ struct Layout std::vector region; std::vector homeOf; // HomeLand tiles: which home, else -1 std::vector lake, river, ford, sand, ridge, strip, causeway, road, clear; + // The sand roads: undermap vertices turned to sand, and the tiles with a corner on one. + std::vector sandRoad, roadTile; std::vector radius, angle; std::string failure; }; @@ -700,6 +726,155 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const } } +// The sand roads: a line of sand two vertices thick (tracePath, widened) that no deposit can grow over and +// nothing can be built on, like a real road that growth closes in on from both sides. It makes +// the causeways and the commons' main routes stay walkable however the fields spread, while the +// fields right beside a road turn it into a chokepoint. +// +// Every home gets the same road, drawn in its own frame (along its axis from the map's centre, +// and across it): from the heart of the commons out along the axis, over the causeway, to a fork +// a few tiles inside the gate; two arms swing out round either side of the swarm's square and +// each forks again, one twig turning in behind the swarm towards the lake and one out towards the +// flank. Nothing reaches more than half the home's depth in from the strait, so most of the home +// stays open for buildings. A ring road at half the commons' radius joins every home's road; with +// a river delta at the heart it runs at the delta's fords instead, so it crosses the rivers where +// they can be walked. +// +// A vertex is only turned to sand where that cannot break the design: on land, off every ridge +// and causeway shoulder (stone stands only on grass, so sand there would open a gap in a wall), +// at least kRoadSeaGap from any water inside a home (off the coast wall) and kRoadWaterGap from +// water in the commons, and clear of the swarm's square. Where a road meets water or a wall it simply +// breaks off, as a road stops at a lake shore. +void planSandRoads(Layout &L, const Features &f) +{ + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.w * t.h, teams = g.teams; + L.sandRoad.assign(n, 0); + L.roadTile.assign(n, 0); + std::vector water(n, 0), forbidden(n, 0); + for (int i = 0; i < n; ++i) + { + water[i] = L.region[i] == Sea || L.lake[i] || L.river[i]; + forbidden[i] = L.ridge[i] || (L.strip[i] && !L.road[i]); + } + const std::vector fromWater = stepsFrom(t, water), fromSand = stepsFrom(t, L.sand); + + std::vector line(n, 0); + const auto trace = [&](const std::vector &path) { tracePath(line, t, path); }; + std::vector> swarms; // each home's swarm footprint, top-left + const double arcHalf = g.arcHalf(g.innerRadius()); + for (int k = 0; k < teams; ++k) + { + const Home &h = L.homes[k]; + const double ca = std::cos(h.angle), sa = std::sin(h.angle); + // A point `along` tiles out from the centre on the home's axis and `across` to one side. + const auto at = [&](double along, double across) -> ShapePoint + { return {L.cx + along * ca - across * sa, L.cy + along * sa + across * ca}; }; + const double gate = h.coast + g.strait, swarm = swarmRadius(g, h.coast) - gate; + // The main street crosses the axis kStreetInset tiles in from the gate, following the + // strait at that distance out to either flank; side streets turn inland from it past both + // sides of the swarm and at its ends, and stop a few tiles behind the swarm, short of the + // lake and the kit's fields beside it. + // Between the coast wall and the swarm's square: never inside the square, and never nearer + // the strait than the wall allows. + const double street = std::clamp(swarm - 5, double(kRoadSeaGap), kStreetInset); + const double side = std::clamp(0.35 * arcHalf, 6.0, 11.0); + const double reach = std::min(std::max(side + 8.0, 0.55 * arcHalf), 24.0); + const double deep = std::min(swarm + 4, kRoadDepthShare * g.depth()); + trace({{at(0, 0).x, at(0, 0).y, 0}, {at(gate + street, 0).x, at(gate + street, 0).y, 0}}); + for (double sign : {-1.0, 1.0}) + { + std::vector main; + const double radius = gate + street; + for (int step = 0; step <= 12; ++step) + { + const double across = sign * reach * step / 12; + const double a = across / radius; + const ShapePoint p = at(radius * std::cos(a), radius * std::sin(a)); + main.push_back({p.x, p.y, 0}); + } + trace(main); + const auto inland = [&](double across, double to) + { + const double a = across / radius; + const ShapePoint from = at(radius * std::cos(a), radius * std::sin(a)); + const ShapePoint end = at((gate + to) * std::cos(a), (gate + to) * std::sin(a)); + trace({{from.x, from.y, 0}, {end.x, end.y, 0}}); + }; + inland(sign * side, deep); + if (reach > side + 6) + inland(sign * reach, std::max(street + 4, deep - 4)); + } + swarms.push_back({int(std::lround(L.cx + (gate + swarm) * ca)) - 2, + int(std::lround(L.cy + (gate + swarm) * sa)) - 2}); + } + if (teams >= 1) + { + const double heartEdge = L.heartKind == IslandHeart ? L.lakeR * 1.3 + : L.heartKind == Crag ? L.ringR + 2 + : L.heartKind == ForestHeart ? std::max(L.forestR, L.lakeR * 1.3) + : L.lakeR * 1.3 + kOrchardReach + 2; + double ring = L.heartKind == Delta ? f.deltaFordR : kRingShare * g.commonsRadius; + ring = std::max(ring, heartEdge + kRingHeartGap); + if (ring <= g.commonsRadius - kLanding - 4) + { + const int segments = std::max(24, int(2 * kPi * ring / 3)); + std::vector circle; + for (int s = 0; s <= segments; ++s) + { + const double a = L.phase + 2 * kPi * s / segments; + circle.push_back({L.cx + ring * std::cos(a), L.cy + ring * std::sin(a), 0}); + } + trace(circle); + } + } + + // Two vertices wide: every traced vertex also marks the vertices right, below and diagonally + // below-right of it, a square brush that is two thick whatever way the road runs. A road then + // spoils three tiles across, wide enough to read as a road and still a chokepoint between + // fields. + std::vector wide(n, 0); + for (int i = 0; i < n; ++i) + if (line[i]) + for (int dy = 0; dy < kRoadWidth; ++dy) + for (int dx = 0; dx < kRoadWidth; ++dx) + wide[t.at(i % t.w + dx, i / t.w + dy)] = 1; + for (int i = 0; i < n; ++i) + { + if (!wide[i] || water[i] || fromWater[i] < kRoadWaterGap) + continue; + const int x = i % t.w, y = i / t.w; + // A home's lakes can be walled too, where their beach joins the sea's, so inside a home a + // road keeps the wall's distance from every water. + // A sand patch that touches a beach is walled as part of the shore, so a home's roads also + // keep off sand patches by a wall's width. + if (L.homeOf[i] >= 0 && !L.strip[i] && + (fromWater[i] < kRoadSeaGap || (fromSand[i] >= 0 && fromSand[i] < kRoadSandGap))) + continue; + // The four tiles sharing this corner: none may be stone-bearing design. + bool clear = true; + for (int dy = -1; dy <= 0 && clear; ++dy) + for (int dx = -1; dx <= 0; ++dx) + if (forbidden[t.at(x + dx, y + dy)]) + { + clear = false; + break; + } + // Nor inside the swarm's square with two tiles to spare round its 4x4 footprint. + for (const auto &sw : swarms) + if (clear && std::abs(t.offsetX(sw.first + 2, x)) <= 4 && + std::abs(t.offsetY(sw.second + 2, y)) <= 4) + clear = false; + if (!clear) + continue; + L.sandRoad[i] = 1; + for (int dy = -1; dy <= 0; ++dy) + for (int dx = -1; dx <= 0; ++dx) + L.roadTile[t.at(x + dx, y + dy)] = 1; + } +} + Layout design(const GenerationRequest &request, GenerationContext &context) { Layout L; @@ -733,6 +908,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const Features f = rollFeatures(request, context, g, c, L); rasterize(L, g, c, f); + if (L.failure.empty() && o.sandRoads) + planSandRoads(L, f); + else + { + L.sandRoad.assign(size_t(t.w) * t.h, 0); + L.roadTile.assign(size_t(t.w) * t.h, 0); + } return L; } @@ -767,7 +949,8 @@ std::vector seaMargin(const Map &map, const Layout &L) const int x = i % t.w, y = i / t.w; if (map.isWater(x, y)) continue; - beach[i] = map.getTerrainType(x, y) != GRASS; + // A sand road is not a beach: it must not carry the sea's margin inland. + beach[i] = map.getTerrainType(x, y) != GRASS && !L.roadTile[i]; for (int dy = -1; dy <= 2 && !margin[i]; ++dy) for (int dx = -1; dx <= 2; ++dx) if (sea[t.at(x + dx, y + dy)]) @@ -830,7 +1013,7 @@ void carveValleys(std::vector &terrain, const Layout &L, Generati { water[i] = terrain[i] == WATER; land[i] = !water[i]; - keep[i] = L.clear[i] || L.causeway[i]; + keep[i] = L.clear[i] || L.causeway[i] || L.sandRoad[i]; if (L.region[i] == Commons && land[i]) ground.push_back(i); } @@ -1165,7 +1348,7 @@ bool generate(Game &game, GenerationContext &context) terrain[i] = WATER; if (L.ford[i]) terrain[i] = SAND; - if (L.sand[i] && terrain[i] == GRASS) + if ((L.sand[i] || L.sandRoad[i]) && terrain[i] == GRASS) terrain[i] = SAND; } carveValleys(terrain, L, context, o.valleys); @@ -1197,12 +1380,7 @@ bool generate(Game &game, GenerationContext &context) const auto anchor = [&](int team) { const Home &h = L.homes[team]; - const double lakeRadius = std::clamp(0.1 * L.g.depth(), 2.0, 6.0); - // The swarm stands 9 tiles short of the lake's near shore, and never within 8 tiles of the - // strait: between causeway and lake, a short walk from both. - const double rho = - std::max(h.coast + L.g.strait + 8.0, - h.coast + L.g.strait + lakeOffset(L.g, lakeRadius) - lakeRadius - 9.0); + const double rho = swarmRadius(L.g, h.coast); return MapGeneratorPoint(L.cx + int(std::lround(rho * std::cos(h.angle))) - 2, L.cy + int(std::lround(rho * std::sin(h.angle))) - 2); }; @@ -1331,9 +1509,10 @@ CityStatesOptions::CityStatesOptions(const GenerationRequest &r) causewayWidth(r.option("causeway-width")), coastRoughness(r.option("coast-roughness")), valleys(r.option("valleys")), resourceIslands(r.option("resource-islands")), sand(r.option("sand")), frontier(r.option("frontier-richness")), - stoneWalls(r.option("stone-walls") != 0), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) + stoneWalls(r.option("stone-walls") != 0), sandRoads(r.option("sand-roads") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) { } @@ -1343,7 +1522,7 @@ GeneratorDefinition cityStatesDefinition() "city-states", 17, "City states", - 5, + 7, false, // The commons' radius as a share of half the shorter side, the strait's width as a // share of the shorter side, the causeway's road in tiles; valleys per 128x128 of @@ -1361,6 +1540,8 @@ GeneratorDefinition cityStatesDefinition() {"frontier-richness", "Frontier richness", 0, 100, 10, 60, ControlGroup::Resources}, // Off, no stone: the causeways are plain roads and the homes' coasts are open. GeneratorControl::toggle("stone-walls", "Stone walls", true, ControlGroup::Layout), + // Off, no sand roads: the causeways and the commons are grass from shore to shore. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), // Every home's ambient fields, outcrops and grove, the commons and the islands' prizes; // every home's kit and the causeways' stone stay as they are. GeneratorControl::percentage("wheat-amount", "Wheat amount"), diff --git a/src/map/generator/generators/CityStatesGenerator.h b/src/map/generator/generators/CityStatesGenerator.h index 399665b1b..d5f8efe2b 100644 --- a/src/map/generator/generators/CityStatesGenerator.h +++ b/src/map/generator/generators/CityStatesGenerator.h @@ -6,7 +6,7 @@ struct CityStatesOptions { int commonsSize, straitWidth, causewayWidth, coastRoughness, valleys, resourceIslands, sand, frontier; - bool stoneWalls; + bool stoneWalls, sandRoads; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit CityStatesOptions(const GenerationRequest &r); }; diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 58b4e8b05..4227f3784 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -128,14 +128,14 @@ macos-arm64 16 1 8 8 4 3 ok 4396075304858481954 macos-arm64 16 1 8 8 8 1 ok 699982766957804005 macos-arm64 16 1 9 8 4 1 ok 8928204734545682645 macos-arm64 16 1 9 9 4 1 ok 12471630520837885284 -macos-arm64 17 5 7 7 4 1 ok 4983285384533269693 -macos-arm64 17 5 8 8 2 1 ok 15410170018519406689 -macos-arm64 17 5 8 8 4 1 ok 1496908417910164065 -macos-arm64 17 5 8 8 4 2 ok 11861326676775640834 -macos-arm64 17 5 8 8 4 3 ok 17308555237519110064 -macos-arm64 17 5 8 8 8 1 ok 7621305293371525278 -macos-arm64 17 5 9 8 4 1 ok 12028787258565351075 -macos-arm64 17 5 9 9 4 1 ok 11272053320214136168 +macos-arm64 17 7 7 7 4 1 ok 8389792356515183180 +macos-arm64 17 7 8 8 2 1 ok 14675185244106174055 +macos-arm64 17 7 8 8 4 1 ok 15876097599295168520 +macos-arm64 17 7 8 8 4 2 ok 7336089206298276893 +macos-arm64 17 7 8 8 4 3 ok 7241957065654810466 +macos-arm64 17 7 8 8 8 1 ok 4371133845777740965 +macos-arm64 17 7 9 8 4 1 ok 10114259367286371870 +macos-arm64 17 7 9 9 4 1 ok 18004714215950592062 macos-arm64 18 5 7 7 4 1 ok 2131984367767490485 macos-arm64 18 5 8 8 2 1 ok 3078796380374205351 macos-arm64 18 5 8 8 4 1 ok 7736833362775075996 From b86c59bb97015458a6990c875d4daec494333011 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 10:54:38 -0400 Subject: [PATCH 111/136] Add hexagon and warped cells to the Maze on a shared tiling toolkit Maze gains a Cell shape choice (Squares, Hexagons) and a Warp control (0-100, default 0). The maze logic now runs on any polygon tiling of the torus, and its terrain is drawn from one distance field to the walls instead of stamped rectangles: - Tessellation (new): square and pointy-topped hexagon tilings with edges as identities and per-cell corner wrap shifts, so a torus two cells wide is exact; tile labelling, lattice symmetries, and warpCorners, which moves corners at random while keeping walls that share no corner a passage apart and every cell whole. - GraphMaze (new): home pockets, spanning tree, doors, loops and dead ends, moved out of Maze and generalised to any cell graph. - Drawing: fixed-point traceSealedPath (walls no unit crosses at any slant) and forEachTileInPolygon/fillPolygon, which split shared edges exactly, all in integers. - Walls: firstRegionLeak. Maze's validateWorld now rebuilds the design and proves no reachable ground joins two cells except through open passages. - GeneratorControls: GeneratorControl::choice, a named-option control shown by name in the lobby dropdown and the editor's selector (Number gains labels); the registry rejects malformed choices. Hexagons sit 1.5x the cell size apart so their slanted doorways keep the 9-tile minimum. Homes pick their wheat, wood and stone strips by distance from the shore, which fits slanted walls and selects exactly the old tiles in squares. Square mazes are tile-for-tile identical to revision 11 wherever the cell size divides the map: checked against the committed generator built separately, at the defaults on 128/256/512 with 2/4/8 colonies and in 160 random settings. Where it doesn't divide, channels are now the same width on both sides of a wall instead of one tile wider on one side. Maze moves to revision 12; its golden rows are regenerated. 600 random requests across both shapes and all warps: 381 generate with no failures and 219 are rejected with a message. 240 maps at warp 50-100 keep every home connected with water thickened by 6 tiles (passages at least 9 wide). A 512 map with 8 colonies at warp 100 generates in 0.15 s. This commit also carries the shared modules the walled-arena work extracted first, which the maze builds on: Walls, Territories, Homes, placeTower, and the Drawing, Sketch, Planting, Geometry and Wedge additions, with City states and Stone highlands migrated onto them byte-identically (their golden rows are unchanged). The Carousel, Amphitheatre and Switchbacks generators themselves are not included. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LuF1wZFUthYpXhAZGY6ME9 --- data/texts.ar.txt | 8 + data/texts.br.txt | 8 + data/texts.ca.txt | 8 + data/texts.cz.txt | 8 + data/texts.de.txt | 8 + data/texts.dk.txt | 8 + data/texts.en.txt | 8 + data/texts.eo.txt | 8 + data/texts.es.txt | 8 + data/texts.eu.txt | 8 + data/texts.fa.txt | 8 + data/texts.fi.txt | 8 + data/texts.fr.txt | 8 + data/texts.gr.txt | 8 + data/texts.hu.txt | 8 + data/texts.id.txt | 8 + data/texts.it.txt | 8 + data/texts.ja.txt | 8 + data/texts.keys.txt | 4 + data/texts.ko.txt | 8 + data/texts.nl.txt | 8 + data/texts.pl.txt | 8 + data/texts.pt.txt | 8 + data/texts.ro.txt | 8 + data/texts.ru.txt | 8 + data/texts.si.txt | 8 + data/texts.sk.txt | 8 + data/texts.sr.txt | 8 + data/texts.sv.txt | 8 + data/texts.tr.txt | 8 + data/texts.uk.txt | 8 + data/texts.vi.txt | 8 + data/texts.zh-cn.txt | 8 + data/texts.zh-tw.txt | 8 + docs/map-generators/ADDING_A_GENERATOR.md | 2 + .../GAME_RULES_FOR_MAP_DESIGN.md | 7 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 117 ++- libgag/include/GUINumber.h | 3 + libgag/src/GUINumber.cpp | 13 +- src/CustomGameScreen.cpp | 3 +- src/NewMapScreen.cpp | 5 +- src/SConscript | 3 + src/map/generator/core/GeneratorControls.cpp | 15 + src/map/generator/core/GeneratorControls.h | 10 + src/map/generator/core/GeneratorRegistry.cpp | 16 + .../generators/CityStatesGenerator.cpp | 160 +--- .../generator/generators/MazeGenerator.cpp | 890 ++++++++---------- src/map/generator/generators/MazeGenerator.h | 2 +- .../generators/StoneHighlandsGenerator.cpp | 54 +- src/map/generator/shared/Drawing.cpp | 80 ++ src/map/generator/shared/Drawing.h | 162 ++++ src/map/generator/shared/Geometry.h | 17 + src/map/generator/shared/GraphMaze.cpp | 187 ++++ src/map/generator/shared/GraphMaze.h | 48 + src/map/generator/shared/Homes.h | 142 +++ src/map/generator/shared/Planting.cpp | 39 +- src/map/generator/shared/Planting.h | 45 + src/map/generator/shared/Settlements.cpp | 44 + src/map/generator/shared/Settlements.h | 9 + src/map/generator/shared/Sketch.cpp | 20 + src/map/generator/shared/Sketch.h | 50 + src/map/generator/shared/Territories.h | 240 +++++ src/map/generator/shared/Tessellation.cpp | 418 ++++++++ src/map/generator/shared/Tessellation.h | 123 +++ src/map/generator/shared/Walls.cpp | 229 +++++ src/map/generator/shared/Walls.h | 133 +++ src/map/generator/shared/Wedge.h | 14 + test/MapGeneratorDefaultsTest.cpp | 16 + test/MapGeneratorStudy.cpp | 13 +- test/MapGeneratorToolkitChecks.h | 532 ++++++++++- test/map-generator-golden.txt | 16 +- 71 files changed, 3432 insertions(+), 713 deletions(-) create mode 100644 src/map/generator/shared/GraphMaze.cpp create mode 100644 src/map/generator/shared/GraphMaze.h create mode 100644 src/map/generator/shared/Homes.h create mode 100644 src/map/generator/shared/Territories.h create mode 100644 src/map/generator/shared/Tessellation.cpp create mode 100644 src/map/generator/shared/Tessellation.h create mode 100644 src/map/generator/shared/Walls.cpp create mode 100644 src/map/generator/shared/Walls.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 35133fa75..6b8f3a169 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2014,3 +2014,11 @@ OpenGL غير متوفر في هذا الإصدار. مستعمرات على الساحلين [Winding belt] حزام متعرج +[Cell shape] +شكل الخلايا +[Squares] +مربعات +[Hexagons] +سداسيات +[Warp] +التواء diff --git a/data/texts.br.txt b/data/texts.br.txt index dd225c1cd..a1cb4ee27 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -2012,3 +2012,11 @@ Pedra no pomar Colônias em ambas as costas [Winding belt] Faixa sinuosa +[Cell shape] +Stumm ar c'hellig +[Squares] +Karrezioù +[Hexagons] +C'hwec'hkornegoù +[Warp] +Distummañ diff --git a/data/texts.ca.txt b/data/texts.ca.txt index b1b838c95..46209e26b 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2024,3 +2024,11 @@ Pedra a l'hort Colònies a les dues costes [Winding belt] Cinturó sinuós +[Cell shape] +Forma de les cel·les +[Squares] +Quadrats +[Hexagons] +Hexàgons +[Warp] +Deformació diff --git a/data/texts.cz.txt b/data/texts.cz.txt index f92401fd2..906324e64 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2016,3 +2016,11 @@ Kamení v sadu Kolonie na obou pobřežích [Winding belt] Zvlněný pás +[Cell shape] +Tvar buněk +[Squares] +Čtverce +[Hexagons] +Šestiúhelníky +[Warp] +Deformace diff --git a/data/texts.de.txt b/data/texts.de.txt index 2ae7f1c44..fa0e83066 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2016,3 +2016,11 @@ Stein im Obstgarten Kolonien an beiden Küsten [Winding belt] Gewundener Gürtel +[Cell shape] +Zellenform +[Squares] +Quadrate +[Hexagons] +Sechsecke +[Warp] +Verzerrung diff --git a/data/texts.dk.txt b/data/texts.dk.txt index cff4e98e7..ac0e83681 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2092,3 +2092,11 @@ Sten i frugtplantagen Kolonier på begge kyster [Winding belt] Bugtet bælte +[Cell shape] +Celleform +[Squares] +Kvadrater +[Hexagons] +Sekskanter +[Warp] +Forvrængning diff --git a/data/texts.en.txt b/data/texts.en.txt index 64f04532e..2424bdb22 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2004,3 +2004,11 @@ Stone in the orchard Colonies on both coasts [Winding belt] Winding belt +[Cell shape] +Cell shape +[Squares] +Squares +[Hexagons] +Hexagons +[Warp] +Warp diff --git a/data/texts.eo.txt b/data/texts.eo.txt index c107f9cf8..726744812 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2014,3 +2014,11 @@ Fosaĵo Kolonioj ĉe ambaŭ marbordoj [Winding belt] Serpentuma zono +[Cell shape] +Formo de ĉeloj +[Squares] +Kvadratoj +[Hexagons] +Sesanguloj +[Warp] +Deformado diff --git a/data/texts.es.txt b/data/texts.es.txt index acc06d804..97b012e82 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2016,3 +2016,11 @@ Piedra en el huerto Colonias en ambas costas [Winding belt] Cinturón sinuoso +[Cell shape] +Forma de las celdas +[Squares] +Cuadrados +[Hexagons] +Hexágonos +[Warp] +Deformación diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 3e1941011..9cc3485ac 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2026,3 +2026,11 @@ Harria fruta-baratzean Koloniak bi kostaldeetan [Winding belt] Gerriko bihurgunetsua +[Cell shape] +Gelaxken forma +[Squares] +Karratuak +[Hexagons] +Hexagonoak +[Warp] +Deformazioa diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 421b0182c..0736f41e3 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2014,3 +2014,11 @@ OpenGL در این بیلد موجود نیست. مستعمره‌ها در هر دو ساحل [Winding belt] کمربند پرپیچ‌وخم +[Cell shape] +شکل خانه‌ها +[Squares] +مربع‌ها +[Hexagons] +شش‌ضلعی‌ها +[Warp] +پیچش diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 063e4ccf0..f89250f08 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2016,3 +2016,11 @@ Kiveä hedelmätarhassa Siirtokuntia molemmilla rannikoilla [Winding belt] Mutkitteleva vyöhyke +[Cell shape] +Solujen muoto +[Squares] +Neliöt +[Hexagons] +Kuusikulmiot +[Warp] +Vääristymä diff --git a/data/texts.fr.txt b/data/texts.fr.txt index b6f95fa36..f3886e811 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2026,3 +2026,11 @@ Pierre dans le verger Colonies sur les deux côtes [Winding belt] Ceinture sinueuse +[Cell shape] +Forme des cellules +[Squares] +Carrés +[Hexagons] +Hexagones +[Warp] +Déformation diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 8ac46fca4..ea89c6d82 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2086,3 +2086,11 @@ Cortex Αποικίες και στις δύο ακτές [Winding belt] Ελικοειδής ζώνη +[Cell shape] +Σχήμα κελιών +[Squares] +Τετράγωνα +[Hexagons] +Εξάγωνα +[Warp] +Παραμόρφωση diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 95030b18a..032ad31f1 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2016,3 +2016,11 @@ Kő a gyümölcsösben Kolóniák mindkét parton [Winding belt] Kanyargó öv +[Cell shape] +Cellák alakja +[Squares] +Négyzetek +[Hexagons] +Hatszögek +[Warp] +Torzítás diff --git a/data/texts.id.txt b/data/texts.id.txt index d26014c30..9fee893bf 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2012,3 +2012,11 @@ Batu di kebun buah Koloni di kedua pantai [Winding belt] Sabuk berkelok +[Cell shape] +Bentuk sel +[Squares] +Persegi +[Hexagons] +Segi enam +[Warp] +Distorsi diff --git a/data/texts.it.txt b/data/texts.it.txt index 88b65e29b..8e7764e45 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2078,3 +2078,11 @@ Roccia nel frutteto Colonie su entrambe le coste [Winding belt] Fascia sinuosa +[Cell shape] +Forma delle celle +[Squares] +Quadrati +[Hexagons] +Esagoni +[Warp] +Deformazione diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 4afa3c9d5..859135c05 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2012,3 +2012,11 @@ Globulation 2 のカーソルを表示します。 両岸のコロニー [Winding belt] 曲がりくねった帯 +[Cell shape] +セルの形 +[Squares] +正方形 +[Hexagons] +六角形 +[Warp] +ゆがみ diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 2c02496d1..e9b2a5d34 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1000,3 +1000,7 @@ [Stone in the orchard] [Colonies on both coasts] [Winding belt] +[Cell shape] +[Squares] +[Hexagons] +[Warp] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 08ac29ecf..d1f3b899d 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2012,3 +2012,11 @@ Globulation 2 커서를 표시합니다. 양쪽 해안의 식민지 [Winding belt] 구불구불한 띠 +[Cell shape] +칸 모양 +[Squares] +정사각형 +[Hexagons] +육각형 +[Warp] +뒤틀림 diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 7f7fef7a9..1e0455a1e 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2040,3 +2040,11 @@ Steen in de boomgaard Kolonies aan beide kusten [Winding belt] Kronkelende gordel +[Cell shape] +Celvorm +[Squares] +Vierkanten +[Hexagons] +Zeshoeken +[Warp] +Vervorming diff --git a/data/texts.pl.txt b/data/texts.pl.txt index b5221264c..13af94743 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2016,3 +2016,11 @@ Kamień w sadzie Kolonie na obu wybrzeżach [Winding belt] Kręty pas +[Cell shape] +Kształt komórek +[Squares] +Kwadraty +[Hexagons] +Sześciokąty +[Warp] +Zniekształcenie diff --git a/data/texts.pt.txt b/data/texts.pt.txt index bbd1f3652..341e0618a 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2022,3 +2022,11 @@ Pedra no pomar Colónias em ambas as costas [Winding belt] Faixa sinuosa +[Cell shape] +Forma das células +[Squares] +Quadrados +[Hexagons] +Hexágonos +[Warp] +Deformação diff --git a/data/texts.ro.txt b/data/texts.ro.txt index fe4d7f3ae..d221fbb81 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2016,3 +2016,11 @@ Piatră în livadă Colonii pe ambele coaste [Winding belt] Centură șerpuită +[Cell shape] +Forma celulelor +[Squares] +Pătrate +[Hexagons] +Hexagoane +[Warp] +Deformare diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 24c91f186..ad83e60b5 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2014,3 +2014,11 @@ OpenGL недоступен в этой сборке. Колонии на обоих берегах [Winding belt] Извилистый пояс +[Cell shape] +Форма ячеек +[Squares] +Квадраты +[Hexagons] +Шестиугольники +[Warp] +Искажение diff --git a/data/texts.si.txt b/data/texts.si.txt index 339a7a358..64b82480f 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2016,3 +2016,11 @@ Kamenje v sadovnjaku Kolonije na obeh obalah [Winding belt] Vijugast pas +[Cell shape] +Oblika celic +[Squares] +Kvadrati +[Hexagons] +Šestkotniki +[Warp] +Popačenje diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 1bb7fc190..22a185d51 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2024,3 +2024,11 @@ Kameň v sade Kolónie na oboch pobrežiach [Winding belt] Zvlnený pás +[Cell shape] +Tvar buniek +[Squares] +Štvorce +[Hexagons] +Šesťuholníky +[Warp] +Deformácia diff --git a/data/texts.sr.txt b/data/texts.sr.txt index ef651d0aa..0bda460af 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2016,3 +2016,11 @@ OpenGL није доступан у овој верзији. Колоније на обе обале [Winding belt] Кривудав појас +[Cell shape] +Облик ћелија +[Squares] +Квадрати +[Hexagons] +Шестоуглови +[Warp] +Изобличење diff --git a/data/texts.sv.txt b/data/texts.sv.txt index e5cb57614..16c68e3e4 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2026,3 +2026,11 @@ Sten i fruktträdgården Kolonier på båda kusterna [Winding belt] Slingrande bälte +[Cell shape] +Cellform +[Squares] +Kvadrater +[Hexagons] +Hexagoner +[Warp] +Förvrängning diff --git a/data/texts.tr.txt b/data/texts.tr.txt index cd2ddfa28..a8a54fa99 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2024,3 +2024,11 @@ Meyve bahçesinde taş Her iki kıyıda koloniler [Winding belt] Kıvrımlı kuşak +[Cell shape] +Hücre şekli +[Squares] +Kareler +[Hexagons] +Altıgenler +[Warp] +Bükülme diff --git a/data/texts.uk.txt b/data/texts.uk.txt index b5735b56b..34464abd2 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2012,3 +2012,11 @@ OpenGL недоступний у цій збірці. Колонії на обох узбережжях [Winding belt] Звивистий пояс +[Cell shape] +Форма клітинок +[Squares] +Квадрати +[Hexagons] +Шестикутники +[Warp] +Викривлення diff --git a/data/texts.vi.txt b/data/texts.vi.txt index dd6de9ff3..4266bae21 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2012,3 +2012,11 @@ Hào nước Thuộc địa ở cả hai bờ [Winding belt] Vành đai uốn lượn +[Cell shape] +Hình dạng ô +[Squares] +Hình vuông +[Hexagons] +Lục giác +[Warp] +Biến dạng diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 50bb216cb..2fbc0ee3f 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2012,3 +2012,11 @@ OpenGL 在此版本中不可用。 两岸都有殖民地 [Winding belt] 蜿蜒的环带 +[Cell shape] +格子形状 +[Squares] +正方形 +[Hexagons] +六边形 +[Warp] +扭曲 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 6de79c70f..312a7d12c 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2088,3 +2088,11 @@ OpenGL 在此版本中不可用。 兩岸都有殖民地 [Winding belt] 蜿蜒的環帶 +[Cell shape] +格子形狀 +[Squares] +正方形 +[Hexagons] +六邊形 +[Warp] +扭曲 diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index eab05d0d7..c8a5f640c 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -25,6 +25,8 @@ Each `GeneratorControl` defines `id`, translation label, minimum, maximum, step, A control's `kind` is `GeneratorControl::Kind::Range` unless it is an on/off switch. Declare a switch with `GeneratorControl::toggle(id, label, on, group)`: it stores 0 or 1, the lobby draws it as a checkbox row (click it, or press Space or Return while it has focus) and the editor as a check button, and the registry rejects a toggle with any other domain, step, allowed values, power-of-two formatting or terrain weight. Every 0/1 option is a toggle. A switch should change how a map feels without breaking its invariants: the map must still generate and pass the generator's own `validateWorld` in both states. +Declare a choice between named options with `GeneratorControl::choice(id, label, {"First", "Second"}, defaultIndex, group)`: it stores the option's index (0, 1, 2...), the lobby's dropdown and the editor's selector show the translated names instead of numbers, and the registry rejects a choice whose values aren't exactly 0 to the last name or that has an empty name, a step, power-of-two formatting or terrain weight. Add every name to the translation catalogs like any label. Use a choice when the options are kinds rather than amounts (Maze's cell shape). + Declare resource amounts with `GeneratorControl::percentage(id, label, maximum)`: 0 to `maximum` (300 unless given) in steps of 25, defaulting to 100, in the Resources group. The percentage scales the numbers that already decide that resource's amount — densities, clump sizes and counts, noise or fertility thresholds — and 100 must reproduce the unscaled map exactly: use `MapGeneration::scaledCount()` and `scaledShare()` from `shared/Resources.h`, which return their input unchanged at 100, and draw nothing from a random stream at 100 that the unscaled code didn't. Decide deliberately which placements are fairness guarantees (starter kits, reachability backstops, 1:1 guaranteed wheat and wood) and leave those unscaled, so every setting stays playable. Keep existing control ids and meanings; a generator that already exposes explicit amounts (Maze) keeps them. Check the top of each range as carefully as the bottom. Resources block ground units and buildings alike, so an amount well above the default can wall a colony into a pocket with nowhere to build, or cover the building sites a start search was looking at — a map that still passes `validateWorld` and is still unplayable. `MapGeneration::openCrampedStarts()` clears the resource tiles nearest such a colony, ring by ring, until it can reach 16 tiles where a 4x4 building fits within 24 steps; call it at non-default amounts only (the height-field generators go through `openStartsBuriedByAmounts()`) and re-run `guaranteeStartingResources()` afterwards, since the clearing can take the nearest crop with the wall. If a generator picks its start sites relative to a scaled field, make that search widen rather than fail when the field covers everything it wanted. `MapGeneratorStudy`'s `tuning` line reports the worst colony's build sites and crop distances, which is how to see both problems. diff --git a/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md index 0e016f06d..eebf34065 100644 --- a/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md +++ b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md @@ -31,6 +31,13 @@ comments can say "because grass may not touch water" and a reader can check it h closes it until it is cleared, which is why generators keep lanes and roads clear and run `openRoad` after placing deposits. +- **Towers shoot over walls.** A defence tower is 2x2 and scans square rings round its footprint's + top-left tile out to its range, 5, 7 or 9 tiles by level, with no line of sight + (`Building::findBestTarget`, `BuildingTypesDefence.cpp`). A wall stops walking but not shooting, so + a wall's thickness decides whether towers on either side can reach each other (`towerReach`). +- **Players can plug gaps.** Players build stone walls, so a narrow gate, ramp or trail can be sealed + by whoever holds it. How wide a map's doors are decides whether a colony can shut itself in. + ## The rules of resources | Resource | Used for | Where it can be | How it grows | diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index bd1b49112..6286e57e3 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -21,16 +21,21 @@ and Terrain take a `Game` because placing buildings and units needs its mutation |---|---| | `Grid` | `Torus` wrap and offset arithmetic; `floodFrom`/`stepsFrom`, the one eight-connected breadth-first flood (with a step limit and the visit order for callers that need it); `walkableTiles` and `groundUnitTiles` passability masks; `unitTilesByTeam` and `firstColonyCutOff` | | `Topology` | `connectedRegions` component labelling with explicit wrap and neighbour policy; `regionAdjacency` and `graphDistances` over sparse labels | -| `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `Stretch`, which places a layout designed in a circle on a map's shorter side onto a rectangular map as an ellipse touching all four sides (`toFill`, `apply`, `undo`, `heading`), exactly the identity on a square map; `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid | -| `Drawing` | Drawing on the torus: `strokePath`, a thick path of points each with its own half width, rasterized through the wrap; `tracePath`, a path traced one tile thick with no gaps (a sand road); `bezierPath`, a quadratic curve as such a path; `polarPoint`; `forEachTileInShape` and `fillShape`, a `RadialShape` filled at any centre and turned to any heading within its bounding box, optionally stretched into an oval; `stretchPath`, a designed path placed on the map by a `Stretch` with its half widths kept in tiles; `bentPath`, a tapered path leaving a point along a heading and bowing sideways; `pathClearance` and `pathBounds`, the water between two stroked paths (optionally ignoring a branch's root) and a cheap bounding circle; `growBranches`, a tree grown level by level by forking in two at every tip to a `ForkStyle`, every branch offered to a caller's `accept` before it is kept and a refused one retried once at half length | -| `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; given a `Stretch` it measures every cell in the round design frame, so the same design fills a rectangular map; `Blob`, a stretched, turned rough disc in that frame | -| `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `sprinkleSand`, decorative sand patches on inland grass following a caller's noise, kept a strip of grass away from every beach; `writeUndermap` | +| `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `Stretch`, which places a layout designed in a circle on a map's shorter side onto a rectangular map as an ellipse touching all four sides (`toFill`, `apply`, `undo`, `heading`), exactly the identity on a square map; `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid; `AxisFrame`, a frame along a heading from an origin (`at(along, across)` and `project`), in which a colony's trail, gates or towers are designed once | +| `Drawing` | Drawing on the torus: `strokePath`, a thick path of points each with its own half width, rasterized through the wrap; `tracePath`, a path traced one tile thick with no gaps (a sand road); `bezierPath`, a quadratic curve as such a path; `polarPoint`; `forEachTileInShape` and `fillShape`, a `RadialShape` filled at any centre and turned to any heading within its bounding box, optionally stretched into an oval; `stretchPath`, a designed path placed on the map by a `Stretch` with its half widths kept in tiles; `bentPath`, a tapered path leaving a point along a heading and bowing sideways; `pathClearance` and `pathBounds`, the water between two stroked paths (optionally ignoring a branch's root) and a cheap bounding circle; `growBranches`, a tree grown level by level by forking in two at every tip to a `ForkStyle`, every branch offered to a caller's `accept` before it is kept and a refused one retried once at half length; `arcPath`, a circular arc as a stroke; `zigzagPath`, a switchback trail in an `AxisFrame` with each leg's straight run returned apart from its turns; `ringWithGates`, every tile of a ring visited with the gate it lies in (a wall with ramps, a moat with bridges); exact fixed-point geometry for designs that must rasterize the same everywhere: `SubtilePoint` (16 units a tile), `sealedSegmentTiles` and `traceSealedPath` (a line no unit can cross even diagonally, at any slant), `forEachTileInPolygon` and `fillPolygon` (tile centres inside a polygon, shared edges split exactly, through the wrap) | +| `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; given a `Stretch` it measures every cell in the round design frame, so the same design fills a rectangular map; `Blob`, a stretched, turned rough disc in that frame; `WedgeField`, periodic noise sampled in the wedge frame so a layer planted by it comes out the same in every wedge | +| `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `sprinkleSand`, decorative sand patches on inland grass following a caller's noise, kept a strip of grass away from every beach; `writeUndermap`; `growWater`, one body of water grown to an exact tile count by a caller's key (Stone highlands' ponds, Amphitheatre's bays); `keepRoadInland` and `roadTiles`, a sand road kept off every beach and the tiles its vertices spoil | | `LatticeNoise` | `PeriodicNoise`, value noise that tiles the torus exactly and samples anywhere, with `periodicNoise`/`fractalNoise` (integer fields per tile, octaves) and `torusNoise` (four octaves in [-1, 1]) sampled from it, plus `percentile` | | `HeightMap`, `Noise` | Perlin noise faded across the wrap, and the stamped height fields the height-field generators shape | -| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band; optionally only its best-growing share, shared out equally between the colonies' wedges), `algaeGrowthChance` (each water tile's chance of passing the engine's algae growth test, which needs water within 15 tiles and solid sand within 30 at a reflected offset), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region) | +| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band; optionally only its best-growing share, shared out equally between the colonies' wedges), `algaeGrowthChance` (each water tile's chance of passing the engine's algae growth test, which needs water within 15 tiles and solid sand within 30 at a reflected offset), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region), `plantRound` and `plantOrchard` (a clump, or groves of the three fruits, at the same point round a circle at every colony's angle); `seedAlgae` can also share its clumps between labelled groups of water such as private bays | | `Resources` | The ambient layer and its fairness guards: `scaledCount`/`scaledShare`, `placeResourceClump`, `setScaledResource`, `scatterResources`, `guaranteeStartingResources`, `openCrampedStarts` | | `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared; `cheapestWalk`, the same search by any step cost (Everglades' fords, Symmetric arena's causeway routes) | -| `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics | +| `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics; `placeTower`, a completed, stocked defence tower of a chosen level at the allowed footprint nearest a designed point | +| `Walls` | Walls a design builds and the checks that prove they hold: `seaVertices` and `seaMargin` (the land a swimmer can stand on), `sealCoasts` (stone on every grass tile touching that margin, so a coast is sealed; City states, Carousel, Switchbacks), `labelBorders` (a wall between labelled regions no unit can cross even diagonally, one to three tiles thick; Stone highlands' ridges, Amphitheatre's borders), `designedStone` (a designed band's stone and any gaps the beach pass left), `reachesWithShut`, `pieceLeak` and `seaEntry` (with the doors shut, does any part reach another, or the sea reach inside), `towerReach` (how close a tower on some ground comes to shooting at a target: towers scan square rings with no line of sight), `walkSpread` (every colony's walk to its own target, and how uneven they are) and `firstRegionLeak` (reachable ground where two labelled regions meet that a design says must not; Maze's walls) | +| `Tessellation` | Polygon tilings of the torus with no terrain attached: `squareTessellation` and `hexTessellation` (cells, the corners they share and edges as identities, exact across the wrap even two cells wide), `edgeEnds`, `centreAcross`, `outline`, `transform` (the lattice's translations and mirrors), `labelTiles` (every tile's cell), `shortestEdgeSteps` and `centreClearance`; `warpLimit` and `warpCorners`, corners moved at random into irregular polygons that still tile, never closing the gap between walls that share no corner below a minimum (Maze) | +| `GraphMaze` | A maze carved through a tessellation's cell graph: `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | +| `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic; `smoothLabels`, a majority filter that trims spurs off borders; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | +| `Homes` | Homes built alike: `stampPondHome`, `pondHomeAnchor` and `plantPondHomeKit` (a round home island facing out from the middle, its swarm towards its door, one or two ponds kept clear of the coast, and a kit round the first pond; Carousel, Switchbacks), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | | `Pipeline` | The stages round the others: `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | | `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | @@ -38,8 +43,8 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `GenerationContext` | Named `std::mt19937` streams, `bounded` draws and `shuffle` | | `legacy/Regions`, `legacy/Distances`, `legacy/StartingPositions` | The older area-grid toolkit: point dispersion (`splitUpPoints`, `splitUpArea`, `divideUpArea`), the legacy distance encoding, `divideUpPlayerLands` and the boot-tile placers `placeStarts`/`placeArchipelagoStarts`. Concrete islands, Isles, Contested commons and the height-field generators still build on it; nothing new should | -Controls and validation live in `core/`: `GeneratorControls` declares discrete, stepped and -power-of-two option domains once for the UI and the catalog; `GeneratorDefinition::validateRequest` +Controls and validation live in `core/`: `GeneratorControls` declares discrete, stepped, +power-of-two and named-choice option domains once for the UI and the catalog; `GeneratorDefinition::validateRequest` is a pure check run before generation, and `validateWorld` runs after the structural checks against the finished terrain. @@ -322,50 +327,66 @@ A maze in the pen-and-paper sense, built to be lived in: grass passages that col build out into, separated by thin stone-and-water walls, with every colony in its own cul-de-sac. -- **Grid.** The map is tiled into cells of about `cell-size` tiles on the map's own torus. - Boundaries are chosen so the cells tile the map exactly (neighbouring cells differ in pitch by - at most one tile), so the maze wraps across the seam like any other boundary. At least two - cells are needed in each direction. -- **Maze and homes.** Homes are chosen first, by farthest-point spreading on the torus that only - accepts a cell if every home still has a non-home neighbour and the non-home cells stay - connected. The pattern depends only on the grid, so `validateRequest` checks exactly how many - colonies fit; each map then places it at a random offset and mirror image. A recursive - backtracker carves a spanning tree of passages over the non-home cells, and each home is - attached by exactly one passage, so every colony starts in a genuine dead end. `loopiness` knocks through extra walls between non-home cells only, so homes +- **Cells.** The map is tiled into cells on its own torus (`Tessellation`). With `cell-shape` + Squares (the default), cells are about `cell-size` tiles across, with boundaries chosen so the + cells tile the map exactly (neighbouring cells differ in pitch by at most one tile). With + Hexagons, pointy-topped hexagons in offset rows sit about one and a half `cell-size` apart + (`kHexPitchPercent`), stretched a little so an even number of rows wraps; a hexagon's slanted + doorways are shorter than a square's, so they need the extra room. Either way the maze wraps + across the seam like any other boundary, and at least two cells are needed in each direction. +- **Warp.** `warp` (0 by default) moves every corner of the tiling at random, up to that share of + what keeps every cell whole (`warpCorners`), so squares and hexagons become irregular polygons + that still tile. It runs after the maze is carved, so it knows which edges are walls: walls that + share no corner (and the ponds below) never come closer than the narrowest passage allows, and + no wall comes closer to a cell's centre than half that, so a warped maze keeps every guarantee + of an unwarped one. At small cells there is less room to spare and warp is gentler. +- **Maze and homes.** Homes are chosen first, by farthest-point spreading between cell centres + that only accepts a cell if every home still has a non-home neighbour and the non-home cells + stay connected (`spreadPockets`). The pattern depends only on the tiling, so `validateRequest` + checks exactly how many colonies fit; each map then places it at a random translation and mirror + image of the lattice. A recursive backtracker carves a spanning tree of passages over the + non-home cells, and each home is attached by exactly one passage, so every colony starts in a + genuine dead end. `loopiness` knocks through extra walls between non-home cells only, so homes stay cul-de-sacs. -- **Passages.** Every cell is a grass chamber and every open boundary a band of the same width - joining two chambers, so a run of passage reads as one continuous strip of buildable, - farmable grass. Passage width isn't a control of its own: passages fill whatever the narrowest - cell leaves once its walls and channels are taken out, and stay odd so they centre on a tile. -- **Roads.** A three-tile sand road runs down the middle of every open passage, centre to centre, - so every cell is linked to the rest of the maze by ground that can never be closed: - `Map::incResource` only seeds a resource on its own terrain and buildings need pure grass, so - nothing grows over a road or is built on one. At a home the road stops against the swarm's - footprint. Shore distances for the resource scatter ignore the road. With `sand-roads` off - (on by default), passages are grass from shore to shore, open to farmland and buildings. -- **Walls.** Every closed boundary has a stone spine covering the whole boundary line from corner - to corner, with `channel-width` all-water tiles on either side (default 2). Perpendicular walls - share their corner tile, so a boundary can only be crossed — on foot or swimming — where it's - open. STONE only places on a pure-grass tile, which needs grass at all four undermap corners - (`Map::regenerateMap`), so each spine sits on a two-wide grass core inside a sand ring. Terrain - is stamped directly rather than through `Map::controlSand()`, whose in-place raster pass shifts - shorelines unevenly. +- **Terrain from one distance field.** Only walls are drawn. Every closed edge gets a stone spine, + a sealed line (`traceSealedPath`) from its corner tile to the next, so walls meeting at a corner + share its tile and none can be slipped between at any angle. The undermap is then designed from + the steps to the nearest spine corner: one step out stays land, the next `channel-width` + 1 are + water, and everything further out is grass, which `layBeaches` edges with sand. In tiles that + is the spine, two sandy flank tiles, `channel-width` all-water tiles (default 2), a two-tile + shore and then passage; STONE only places on a pure-grass tile, and the spine's own corners are + grass. A corner where every edge is open gets a pond as wide as a wall's water instead, so a + junction of passages still reads as one. Passages are simply the ground no wall comes near, so + every cell is a grass chamber and every doorway as wide as its walls allow. - **No walking along a wall.** A wall's sandy flanks are walkable land, so they are always kept at least one all-water tile from every passage's shore — a unit can't step across a tile it can't - stand on. From a cell's centre, a passage's grass therefore reaches - `floor(pitch / 2) - 5 - channelWidth` tiles; `validateRequest` rejects settings that would - leave a passage narrower than 9 tiles. -- **Resources.** Every home starts identical: fixed 1:1 wheat and wood banking the dead end's side - shores, a compact stone deposit at its back wall, and a clear square around the swarm. Outside - the homes, clumps of wheat, wood and stone (densities per 256 shore tiles) are scattered along - every passage's shores, never more than three tiles in, so each passage keeps a clear lane down - its middle however the maze turns. Fruit is treasure: every dead end that isn't a home gets one - compact patch of `fruit` tiles near its far end, with fruit types dealt round-robin so every - kind is somewhere in the maze. With `dead-end-treasure` off (on by default) the same fruit is - scattered along the passages' shores instead. Algae is seeded along the channels. + stand on. A doorway between two walls is therefore `shortest edge / 2 - 5 - channelWidth` tiles + either side of its middle; `validateRequest` rejects settings that would leave a passage + narrower than 9 tiles. +- **Roads.** A three-tile sand road runs down every open passage, from each cell's centre through + the doorway's middle to the next centre, traced as a sealed line so its pure-sand tiles always + share a side. Every cell is linked to the rest of the maze by ground that can never be closed: + `Map::incResource` only seeds a resource on its own terrain and buildings need pure grass, so + nothing grows over a road or is built on one. Roads only turn grass to sand, never water, and + at a home the road stops against the swarm's footprint. Shore distances for the resource + scatter ignore the road. With `sand-roads` off (on by default), passages are grass from shore + to shore, open to farmland and buildings. +- **Resources.** Every home starts identical: fixed 1:1 wheat and wood on the tiles within three + of the shore to the left and right of the home's door, growing forward from its back wall; a + compact stone deposit at the back wall's centre; and a clear square around the swarm. Positions + in a cell are measured along and across its exit in eighths of a tile, so any cell shape sorts + its tiles the same way on every platform. Outside the homes, clumps of wheat, wood and stone + (densities per 256 shore tiles) are scattered along every passage's shores, never more than + three tiles in, so each passage keeps a clear lane down its middle however the maze turns. + Fruit is treasure: every dead end that isn't a home gets one compact patch of `fruit` tiles near + its far end, with fruit types dealt round-robin so every kind is somewhere in the maze. With + `dead-end-treasure` off (on by default) the same fruit is scattered along the passages' shores + instead. Algae is seeded along the channels. - **Checked, not assumed.** `validateWorld` floods walkable tiles (water, buildings and every - resource, including wall spines, block it) from colony 0's workers, and fails the candidate if - any colony isn't reached. + resource, including wall spines, block it) from colony 0's workers and fails the candidate if + any colony isn't reached. It then rebuilds the maze from the request and checks the walls hold + (`firstRegionLeak`): no ground the colonies can reach joins two cells except where the open + edges round a shared corner connect them. ## Watershed diff --git a/libgag/include/GUINumber.h b/libgag/include/GUINumber.h index f3a1f9c20..22e478743 100644 --- a/libgag/include/GUINumber.h +++ b/libgag/include/GUINumber.h @@ -20,6 +20,8 @@ namespace GAGGUI Sint32 nth; Sint32 m; std::vector numbers; + // Text shown in place of each number; empty to show the number itself. + std::vector labels; std::string font; // cache, recomputed at least on paint @@ -37,6 +39,7 @@ namespace GAGGUI virtual void paint(void); void add(int number); + void add(int number, const std::string &label); void clear(void); void setNth(int nth); void set(int number); diff --git a/libgag/src/GUINumber.cpp b/libgag/src/GUINumber.cpp index 247253d5b..afa048f50 100644 --- a/libgag/src/GUINumber.cpp +++ b/libgag/src/GUINumber.cpp @@ -185,7 +185,10 @@ namespace GAGGUI { // We center the string std::stringstream s; - s << numbers[nth]; + if (nth < (int)labels.size() && !labels[nth].empty()) + s << labels[nth]; + else + s << numbers[nth]; int tw=fontPtr->getStringWidth(s.str().c_str()); parent->getSurface()->drawString(x+m+(w-2*m-tw)/2, y+dy, fontPtr, s.str().c_str()); @@ -199,13 +202,21 @@ namespace GAGGUI } void Number::add(int number) + { + add(number, ""); + } + + void Number::add(int number, const std::string &label) { numbers.push_back(number); + labels.resize(numbers.size() - 1); + labels.push_back(label); } void Number::clear(void) { numbers.clear(); + labels.clear(); } void Number::setNth(int nth) diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 1fa6b4efe..6bfee0cf1 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -986,7 +986,8 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) ui.text(x + 4, yy + 7, tr(c.label), "little", 110, true); std::vector options; for (int v : c.values()) - options.push_back(std::to_string(c.displayValue(v))); + options.push_back(c.isChoice() ? tr(c.valueLabel(v)) + : std::to_string(c.displayValue(v))); ui.dropdown(id, {x + 112, yy, leftW - 128, 28}, options, c.indexOf(c.get(g)), [this, c, changed](int i) { diff --git a/src/NewMapScreen.cpp b/src/NewMapScreen.cpp index c158459ca..cac1b81ab 100644 --- a/src/NewMapScreen.cpp +++ b/src/NewMapScreen.cpp @@ -52,7 +52,10 @@ NewMapScreen::NewMapScreen(const GeneratorRegistry ®istry) : registry(registr { number = new Number(x, y, 114, 18, A, A, 18, "menu"); for (int value : c.values()) - number->add(c.displayValue(value)); + if (c.isChoice()) + number->add(value, tr(c.valueLabel(value))); + else + number->add(c.displayValue(value)); addWidget(number); } auto *label = new Text(x + 120, y, A, A, "standard", tr(c.label)); diff --git a/src/SConscript b/src/SConscript index f8cf23f8f..81b015091 100644 --- a/src/SConscript +++ b/src/SConscript @@ -207,6 +207,9 @@ map/generator/shared/BalancedStarts.cpp map/generator/shared/Drawing.cpp map/generator/shared/Geometry.cpp map/generator/shared/Grid.cpp +map/generator/shared/Walls.cpp +map/generator/shared/Tessellation.cpp +map/generator/shared/GraphMaze.cpp map/generator/shared/LatticeNoise.cpp map/generator/shared/Pipeline.cpp map/generator/shared/Planting.cpp diff --git a/src/map/generator/core/GeneratorControls.cpp b/src/map/generator/core/GeneratorControls.cpp index 21fe80984..e1b8bf76c 100644 --- a/src/map/generator/core/GeneratorControls.cpp +++ b/src/map/generator/core/GeneratorControls.cpp @@ -16,6 +16,21 @@ GeneratorControl GeneratorControl::percentage(std::string id, const char *label, { return {std::move(id), label, 0, maximum, 25, 100, ControlGroup::Resources}; } +GeneratorControl GeneratorControl::choice(std::string id, const char *label, + std::vector names, int defaultIndex, + ControlGroup group) +{ + const int last = int(names.size()) - 1; + GeneratorControl c{std::move(id), label, 0, last, 1, defaultIndex, group}; + for (int i = 0; i <= last; ++i) + c.allowedValues.push_back(i); + c.valueLabels = std::move(names); + return c; +} +const char *GeneratorControl::valueLabel(int value) const +{ + return value >= 0 && value < int(valueLabels.size()) ? valueLabels[value] : nullptr; +} int GeneratorControl::normalize(int v) const { if (!allowedValues.empty()) diff --git a/src/map/generator/core/GeneratorControls.h b/src/map/generator/core/GeneratorControls.h index 46d0edade..b94ca6f30 100644 --- a/src/map/generator/core/GeneratorControls.h +++ b/src/map/generator/core/GeneratorControls.h @@ -32,11 +32,21 @@ struct GeneratorControl // Optional ordered domain, e.g. {4, 8, 16}; values are stored literally. std::vector allowedValues; Kind kind = Kind::Range; + // A choice's name for each of its values 0, 1, 2... in order, translated where shown. Empty for + // a number. + std::vector valueLabels; static GeneratorControl toggle(std::string id, const char *label, bool on, ControlGroup group = ControlGroup::Layout); // A resource amount as a percentage of the generator's own default, which is 100. static GeneratorControl percentage(std::string id, const char *label, int maximum = 300); + // One of several named options, stored as its index: a cell shape, say, rather than a number. + static GeneratorControl choice(std::string id, const char *label, + std::vector names, int defaultIndex, + ControlGroup group = ControlGroup::Layout); bool isToggle() const { return kind == Kind::Toggle; } + bool isChoice() const { return !valueLabels.empty(); } + // The untranslated name a choice shows for `value`, or nullptr for a number. + const char *valueLabel(int value) const; std::vector values() const; int indexOf(int value) const; int valueAt(int index) const; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index fa1cf6f8f..6b6dffe60 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -24,6 +24,21 @@ #include #include #include +namespace +{ +// A choice names each of its values 0, 1, 2... in order, shown as a dropdown rather than a number. +bool validChoice(const GeneratorControl &c) +{ + if (c.isToggle() || c.powerOfTwo || c.terrainWeight || c.minimum != 0 || c.step != 1 || + c.allowedValues.size() != c.valueLabels.size() || + c.maximum != int(c.valueLabels.size()) - 1) + return false; + for (size_t i = 0; i < c.valueLabels.size(); ++i) + if (c.allowedValues[i] != int(i) || !c.valueLabels[i] || !*c.valueLabels[i]) + return false; + return true; +} +} // namespace GeneratorRegistry::GeneratorRegistry(std::vector values) : definitions(std::move(values)) { @@ -48,6 +63,7 @@ GeneratorRegistry::GeneratorRegistry(std::vector values) (c.powerOfTwo && (c.minimum < 0 || c.maximum > 30)) || (c.isToggle() && (c.minimum != 0 || c.maximum != 1 || c.step != 1 || c.powerOfTwo || c.terrainWeight || !c.allowedValues.empty())) || + (c.isChoice() && !validChoice(c)) || c.normalize(c.defaultValue) != c.defaultValue || !controls.insert(c.id).second) throw std::invalid_argument("Invalid generator control"); } diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 0482cb430..3b53a0724 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -7,6 +7,7 @@ #include "Drawing.h" #include "Geometry.h" #include "HeightMap.h" +#include "Homes.h" #include "Pipeline.h" #include "Planting.h" #include "Resources.h" @@ -14,6 +15,7 @@ #include "Settlements.h" #include "Sketch.h" #include "Unit.h" +#include "Walls.h" #include "Wedge.h" #include #include @@ -869,10 +871,8 @@ void planSandRoads(Layout &L, const Features &f) if (!clear) continue; L.sandRoad[i] = 1; - for (int dy = -1; dy <= 0; ++dy) - for (int dx = -1; dx <= 0; ++dx) - L.roadTile[t.at(x + dx, y + dy)] = 1; } + L.roadTile = roadTiles(t, L.sandRoad); } Layout design(const GenerationRequest &request, GenerationContext &context) @@ -933,43 +933,24 @@ std::vector heartTiles(const Map &map, const Layout &L) return heart; } -// The ground a unit landing from the sea can reach without crossing solid grass: every land tile -// with a sea vertex in the box its four corners touch, and every beach joined to those - a lake's -// beach too, where it meets the sea's. Stone cannot stand on any of it. +// The ground a unit landing from the sea can reach without crossing solid grass (seaMargin): the +// sea is every water vertex outside the homes' lakes and the commons' own water, and a sand road is +// not a beach, so it must not carry the margin inland. std::vector seaMargin(const Map &map, const Layout &L) { const Torus &t = L.t; const int n = t.w * t.h; - std::vector sea(n, 0), margin(n, 0), beach(n, 0); + std::vector sea(n, 0); for (int i = 0; i < n; ++i) sea[i] = map.getUMTerrain(i % t.w, i / t.w) == WATER && !L.lake[i] && L.region[i] != Commons; - for (int i = 0; i < n; ++i) - { - const int x = i % t.w, y = i / t.w; - if (map.isWater(x, y)) - continue; - // A sand road is not a beach: it must not carry the sea's margin inland. - beach[i] = map.getTerrainType(x, y) != GRASS && !L.roadTile[i]; - for (int dy = -1; dy <= 2 && !margin[i]; ++dy) - for (int dx = -1; dx <= 2; ++dx) - if (sea[t.at(x + dx, y + dy)]) - { - margin[i] = 1; - break; - } - } - const std::vector joined = stepsFrom(t, margin, beach); - for (int i = 0; i < n; ++i) - if (joined[i] >= 0) - margin[i] = 1; - return margin; + return MapGeneration::seaMargin(map, t, sea, L.roadTile); } // The design's stone, once the terrain is laid: every solid-grass tile of a causeway's shoulders // outside its road, the ridges and the crag, and round every home a wall on the solid-grass tiles -// that touch the sea's margin. Every step off the beach lands on the wall, so a home is sealed but -// for its road. Rebuilt the same way by validateWorld. +// that touch the sea's margin (sealCoasts). Every step off the beach lands on the wall, so a home +// is sealed but for its road. Rebuilt the same way by validateWorld. std::vector stoneTiles(const Map &map, const Layout &L) { const Torus &t = L.t; @@ -977,26 +958,13 @@ std::vector stoneTiles(const Map &map, const Layout &L) std::vector stone(n, 0); if (!L.g.walls) return stone; - const std::vector margin = seaMargin(map, L); + std::vector wallable(n, 0); for (int i = 0; i < n; ++i) - { - if (L.road[i] || map.getTerrainType(i % t.w, i / t.w) != GRASS) - continue; - if (L.strip[i] || L.ridge[i]) - { + wallable[i] = L.homeOf[i] >= 0 && !L.road[i]; + stone = sealCoasts(map, t, seaMargin(map, L), wallable); + for (int i = 0; i < n; ++i) + if ((L.strip[i] || L.ridge[i]) && !L.road[i] && map.getTerrainType(i % t.w, i / t.w) == GRASS) stone[i] = 1; - continue; - } - if (L.homeOf[i] < 0) - continue; - for (int dy = -1; dy <= 1 && !stone[i]; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (margin[t.at(i % t.w + dx, i / t.w + dy)]) - { - stone[i] = 1; - break; - } - } return stone; } @@ -1114,53 +1082,22 @@ void furnishHomes(Map &map, const Layout &L, GenerationContext &context, const C frame.at(0.6 * reach, flank * (reach + 1), 14), frame.at(reach + 8, 0, 12), kHomeWheat, kHomeWood, 2}; plantKit(map, t, context, kit, eligible); - // Ambient farmland on the home's fertile ground, in patches, then outcrops and a grove. - std::vector ground; - std::vector> farm; - std::vector levels; - for (int i = 0; i < n; ++i) - if (eligible(i)) + // Ambient farmland on the home's fertile ground, in patches, then outcrops and a grove: 4% of + // its ground ambient wheat and 2% wood on top of the kit, one outcrop per 2500 tiles and a + // single grove - self-sufficient but not rich, so the commons is worth the trip. The patch + // field has 12-tile cells. + furnishGround( + map, t, context, fertility, eligible, + [&](int i) { return patch(i % t.w, i / t.w); }, + [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }, + [&](int area) { - ground.push_back(i); - if (fertility.at(i % t.w, i / t.w) > 0) - levels.push_back(patch(i % t.w, i / t.w)); - } - float cut = 0; - if (!levels.empty()) - { - // The patch field (12-tile cells) cut at its 45th percentile keeps 55% of the fertile - // ground as candidate farmland, in patches with gaps to walk and build in. - std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, - levels.end()); - cut = levels[levels.size() * 45 / 100]; - } - for (int i : ground) - { - const std::uint32_t f = fertility.at(i % t.w, i / t.w); - if (f > 0 && patch(i % t.w, i / t.w) >= cut) - farm.push_back({-double(f), i}); - } - std::stable_sort(farm.begin(), farm.end()); - std::vector chosen; - for (const auto &entry : farm) - chosen.push_back(entry.second); - const int area = int(ground.size()); - const auto bySplit = [&](int i) { return split.uiLevel(i % t.w, i / t.w, 2048); }; - // A home: 4% of its ground ambient wheat and 2% wood on top of the kit, one outcrop per - // 2500 tiles and a single grove: self-sufficient but not rich, so the commons is worth the - // trip. - plantFields(map, t, chosen, int(scaledCount(area * 4 / 100, o.wheat)), - int(scaledCount(area * 2 / 100, o.wood)), bySplit); - scatterClumps(context, t, ground, int(scaledCount(std::max(1, area / 2500), o.stone)), - "city-home-stone", eligible, - [&](MapGeneratorPoint p) { placeResourceClump(map, context, p, STONE, 1); }); - scatterClumps(context, t, ground, int(scaledCount(1, o.fruit)), "city-home-fruit", - eligible, - [&](MapGeneratorPoint p) { - placeResourceClump(map, context, p, - CHERRY + int(context.bounded("city-home-fruit", 3)), - 1); - }); + return GroundAmounts{int(scaledCount(area * 4 / 100, o.wheat)), + int(scaledCount(area * 2 / 100, o.wood)), + int(scaledCount(std::max(1, area / 2500), o.stone)), + int(scaledCount(1, o.fruit))}; + }, + "city-home-stone", "city-home-fruit"); } } @@ -1448,7 +1385,6 @@ std::string validateWorld(const Game &game, const GenerationContext &context) return walk.error; const std::vector> &workers = walk.workers; const std::vector &fromFirst = walk.steps; - const std::vector open = walkableTiles(map); const std::vector heart = heartTiles(map, L); bool heartReached = false; for (int i = 0; i < n && !heartReached; ++i) @@ -1460,13 +1396,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (L.g.walls) { const std::vector margin = seaMargin(map, L); - std::vector beach(n, 0), inland(n, 0); + std::vector beach(n, 0); for (int i = 0; i < n; ++i) - { - inland[i] = walkable(i) && !L.road[i]; - beach[i] = inland[i] && margin[i]; - } - const std::vector landed = stepsFrom(t, beach, inland); + beach[i] = walkable(i) && !L.road[i] && margin[i]; + const std::vector landed = reachesWithShut(map, t, beach, L.road); for (int team = 0; team < teams; ++team) for (int i : workers[team]) if (landed[i] >= 0) @@ -1474,32 +1407,25 @@ std::string validateWorld(const Game &game, const GenerationContext &context) "'s home can be entered from the sea."; } // Shut every causeway: no home may reach the commons or another home any other way. - std::vector shut(n, 0); - for (int i = 0; i < n; ++i) - shut[i] = walkable(i) && !L.causeway[i]; - int shortest = INT_MAX, longest = 0; for (int team = 0; team < teams; ++team) { - std::vector from(n, 0); - for (int i : workers[team]) - from[i] = 1; - const std::vector inside = stepsFrom(t, from, shut); + const std::vector inside = reachesWithShut(map, t, tileMask(t, workers[team]), L.causeway); for (int i = 0; i < n; ++i) if (inside[i] >= 0 && (L.region[i] == Commons || (L.homeOf[i] >= 0 && L.homeOf[i] != team))) return "Colony " + std::to_string(team) + " can leave its home without its causeway, at " + where(i) + "."; - // And every colony's walk to its landing on the commons, over its own causeway. - const std::vector steps = stepsFrom(t, from, open); - const int landing = steps[L.homes[team].landing]; - if (landing < 0) - return "Colony " + std::to_string(team) + " cannot reach its causeway landing."; - shortest = std::min(shortest, landing); - longest = std::max(longest, landing); } - if (longest - shortest > std::max(kLandingSpread, longest / 3)) + // And every colony's walk to its landing on the commons, over its own causeway. + std::vector landings; + for (int team = 0; team < teams; ++team) + landings.push_back(L.homes[team].landing); + const WalkSpread spread = walkSpread(map, t, workers, landings); + if (spread.unreached >= 0) + return "Colony " + std::to_string(spread.unreached) + " cannot reach its causeway landing."; + if (spread.tooUneven(kLandingSpread)) return "The colonies' walks to their landings differ by " + - std::to_string(longest - shortest) + " steps."; + std::to_string(spread.longest - spread.shortest) + " steps."; return ""; } } // namespace diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index d8b7f9023..8f0d3d91a 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -1,29 +1,41 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "MazeGenerator.h" +#include "Drawing.h" #include "Game.h" #include "GenerationContext.h" +#include "GraphMaze.h" #include "Grid.h" #include "Pipeline.h" #include "Resources.h" #include "Settlements.h" +#include "Sketch.h" +#include "Tessellation.h" #include "Unit.h" +#include "Walls.h" #include +#include #include +#include #include #include #include using namespace MapGeneration; -// A maze in the pen-and-paper sense, built to be lived in. The world is tiled into square cells -// on the map's own torus; a recursive backtracker carves a spanning tree of passages between -// them; every boundary the tree leaves closed becomes a wall - a line of stone in a narrow water -// channel. Passages fill their cells with grass, so colonies farm and build their way out into -// the maze, and every colony starts in its own cul-de-sac. +// A maze in the pen-and-paper sense, built to be lived in. The world is tiled into cells on the +// map's own torus - squares or hexagons, their corners optionally warped into irregular polygons +// (Tessellation.h) - and a recursive backtracker carves a spanning tree of passages between them +// (GraphMaze.h). Every edge the tree leaves closed becomes a wall: a line of stone in a narrow water +// channel. Passages fill their cells with grass, so colonies farm and build their way out into the +// maze, and every colony starts in its own cul-de-sac. // -// Terrain is stamped directly rather than through Map::controlSand(), whose in-place raster pass -// shifts shorelines unevenly. Each tile's terrain comes from its four undermap corners -// (Map::regenerateMap), so a pure-grass tile needs grass at all four, and grass must never touch -// water; every grass area here is therefore stamped inside a one-tile sand ring. +// Terrain is designed in a TerrainSketch, one undermap corner at a time. Each tile's terrain comes +// from its four undermap corners (Map::regenerateMap), so the whole map is drawn from one distance +// field: steps from the corners of every wall's stone line. Walking out from a wall, the corners one +// step out stay land, the next channel-width + 1 are water and everything further is land again; +// layBeaches turns the land beside that water to sand. In tiles, that is the stone spine, two sandy +// flank tiles, channel-width all-water tiles, a two-tile shore and then grass. A corner no wall +// reaches gets a pond as wide as a wall's water instead. Nothing else is ever drawn, so a passage +// is simply ground no wall comes near, and its shape follows the cells'. // // WHY A MAZE PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Walls are stone, // which can never be cleared, flanked by water, which ground units cannot cross until they swim, @@ -33,33 +45,33 @@ using namespace MapGeneration; // defended door can be flanked. Farmland lines the passages' shores, next to channel water where // wheat and wood regrow, and sand roads keep that growth from ever sealing a passage. Fruit // sits at the ends of dead ends, off every through route, so fetching it is a deliberate raid. +// Hexagons give every junction three ways on instead of two or three, so their mazes wind more. // -// THE SIZES AT THE DEFAULTS (256x256, cell size 32, channel width 2): an 8x8 grid of 64 cells; -// each half-cell of 16 tiles is 9 of passage, 5 of sand ring, flank and spine, and 2 of channel, -// so passages are 19 tiles wide and two passages are 14 tiles apart across a wall. +// THE SIZES AT THE DEFAULTS (256x256, square cells of 32, channel width 2): an 8x8 grid of 64 +// cells; each half-cell of 16 tiles is 9 of passage, 5 of shore, flank and spine, and 2 of channel, +// so passages are 19 tiles wide and two passages are 14 tiles apart across a wall. Hexagons at cell +// size 32 are about 48 tiles apart (kHexPitchPercent), which keeps their slanted doorways as wide as +// a small base. namespace { -enum Direction -{ - East, - South, - West, - North -}; +// The undermap corners one step from a wall's stone line stay land (its flank); the channel's +// water starts one step further out. +constexpr int kFlankSteps = 1; -// Walking out from a cell's centre towards a wall: the passage's grass, then two walkable tiles of -// its sand ring, then channel-width all-water tiles, then the wall - two walkable tiles of flank -// and the stone spine on the boundary itself. Any tile with a non-water corner is walkable, so -// without that water a unit could step onto a wall's flank and follow the wall around the maze; a -// single water tile is enough, since a unit can't step across a tile it can't stand on. The ring, -// flank and spine account for this many tiles of every half-cell. +// Half a doorway's width is taken by the wall at each end: the spine, two tiles of flank, +// channel-width tiles of water and two tiles of shore, measured in tiles along the edge. The spine, +// flank and shore account for this many tiles of every half-edge. constexpr int kWallFootprint = 5; -// Half the width of the narrowest passage allowed: a 9-wide chamber still seats the 4x4 swarm -// with room for its workers. +// Half the width of the narrowest passage allowed: a 9-wide doorway still passes a column of +// workers and seats the 4x4 swarm with room to spare. constexpr int kMinimumPassageHalf = 4; +// Hexagons sit this share of the cell size apart. A hexagon's edge is only about 0.58 of its pitch, +// so at the square's spacing its doorways would come out too narrow to pass the size check. +constexpr int kHexPitchPercent = 150; + // Every home starts identical: fixed tile counts rather than densities, so the start doesn't // depend on the resource controls, with wheat and wood 1:1 as for any guaranteed placement. constexpr int kHomeFarmland = 32; @@ -69,289 +81,168 @@ constexpr int kHomeStone = 12; // the middle of every passage however the maze turns, so no deposit can seal a route. constexpr int kShoreBand = 3; -// The cell lattice. Boundaries are integer tile coordinates chosen so the cells tile the map -// exactly, which lets the maze wrap across the torus seam like any other boundary; when -// cell-size doesn't divide the map, neighbouring cells differ in pitch by at most one tile. -struct MazeGrid -{ - int width, height, columns, rows; - std::vector xs, ys; - - int cells() const { return columns * rows; } - int column(int cell) const { return cell % columns; } - int row(int cell) const { return cell / columns; } - int centerX(int cell) const { return (xs[column(cell)] + xs[column(cell) + 1]) / 2; } - int centerY(int cell) const { return (ys[row(cell)] + ys[row(cell) + 1]) / 2; } - int minimumPitch() const { return std::min(width / columns, height / rows); } - int cellAt(int x, int y) const - { - const int c = int(std::upper_bound(xs.begin(), xs.end(), x) - xs.begin()) - 1; - const int r = int(std::upper_bound(ys.begin(), ys.end(), y) - ys.begin()) - 1; - return std::min(r, rows - 1) * columns + std::min(c, columns - 1); - } - - int neighbour(int cell, int direction) const - { - const int c = column(cell), r = row(cell); - switch (direction) - { - case East: - return r * columns + (c + 1) % columns; - case South: - return ((r + 1) % rows) * columns + c; - case West: - return r * columns + (c + columns - 1) % columns; - default: - return ((r + rows - 1) % rows) * columns + c; - } - } - // Each undirected edge is stored once, owned by its west or north cell. - int edgeId(int cell, int direction) const - { - switch (direction) - { - case East: - return 2 * cell; - case South: - return 2 * cell + 1; - case West: - return 2 * neighbour(cell, West); - default: - return 2 * neighbour(cell, North) + 1; - } - } -}; +// Positions inside a cell are measured along its exit and across it in eighths of a tile, so ties +// between tiles are broken exactly the same way on every platform. +constexpr int kFrame = 8; -MazeGrid mazeGrid(int width, int height, int cellSize) +Tessellation latticeFor(const MazeOptions &o, int width, int height) { - MazeGrid g{width, height, width / cellSize, height / cellSize, {}, {}}; - for (int i = 0; i <= g.columns; ++i) - g.xs.push_back(i * width / std::max(1, g.columns)); - for (int i = 0; i <= g.rows; ++i) - g.ys.push_back(i * height / std::max(1, g.rows)); - return g; + if (o.cellShape == int(Tessellation::Shape::Hexagon)) + return hexTessellation(width, height, o.cellSize * kHexPitchPercent / 100); + return squareTessellation(width, height, o.cellSize); } -// Passages are as wide as the narrowest cell allows once the wall and its channels are taken -// out, and odd so they centre on a tile. -int passageHalf(const MazeGrid &g, int channelWidth) +// The walls at both ends of the shortest edge leave this much doorway between them. +int passageHalf(const Tessellation &g, int channelWidth) { - return g.minimumPitch() / 2 - kWallFootprint - channelWidth; + return shortestEdgeSteps(g) / 2 - kWallFootprint - channelWidth; } -// Whether a set of home cells leaves a valid layout: every home has a non-home neighbour to open -// its one passage into, and the non-home cells form a single connected region for the maze to -// span. -bool homesFit(const MazeGrid &g, const std::vector &isHome) +// What the settings alone decide, before anything random: the tiling and where the homes go. +struct MazeLayout { - int start = -1, freeCells = 0; - for (int cell = 0; cell < g.cells(); ++cell) - { - if (!isHome[cell]) - { - ++freeCells; - start = cell; - continue; - } - bool opening = false; - for (int d = 0; d < 4; ++d) - opening |= !isHome[g.neighbour(cell, d)]; - if (!opening) - return false; - } - if (!freeCells) - return false; - std::vector seen(g.cells(), 0); - std::vector stack{start}; - seen[start] = 1; - int reached = 1; - while (!stack.empty()) - { - const int cell = stack.back(); - stack.pop_back(); - for (int d = 0; d < 4; ++d) - { - const int next = g.neighbour(cell, d); - if (!isHome[next] && !seen[next]) - { - seen[next] = 1; - ++reached; - stack.push_back(next); - } - } - } - return reached == freeCells; -} + Tessellation g; + int half = 0; + std::vector homes; + std::string failure; +}; -// The home cells as a pattern fixed by the grid alone, so validation checks exactly what -// generation will do: farthest-point spreading on the torus, taking at each step the farthest -// cell that still leaves a valid layout. Empty if the grid can't hold every colony. -std::vector homePattern(const MazeGrid &g, int teams) +MazeLayout mazeLayout(const MazeOptions &o, int width, int height, int teams) { - auto distance = [&](int a, int b) - { - int dx = std::abs(g.column(a) - g.column(b)), dy = std::abs(g.row(a) - g.row(b)); - dx = std::min(dx, g.columns - dx); - dy = std::min(dy, g.rows - dy); - return dx * dx + dy * dy; - }; - std::vector isHome(g.cells(), 0); - std::vector homes, nearest(g.cells(), 0); - for (int team = 0; team < teams; ++team) - { - std::vector order; - for (int cell = 0; cell < g.cells(); ++cell) - if (!isHome[cell]) - order.push_back(cell); - std::stable_sort(order.begin(), order.end(), - [&](int a, int b) { return nearest[a] > nearest[b]; }); - int chosen = -1; - for (int cell : order) - { - isHome[cell] = 1; - if (homesFit(g, isHome)) - { - chosen = cell; - break; - } - isHome[cell] = 0; - } - if (chosen < 0) - return {}; - homes.push_back(chosen); - for (int cell = 0; cell < g.cells(); ++cell) - nearest[cell] = homes.size() == 1 ? distance(cell, chosen) - : std::min(nearest[cell], distance(cell, chosen)); - } - return homes; + MazeLayout L; + L.g = latticeFor(o, width, height); + L.half = passageHalf(L.g, o.channelWidth); + if (L.g.columns < 2 || L.g.rows < 2) + L.failure = "The maze needs at least two cells across and down; use a bigger map or smaller " + "cells."; + else if (L.half < kMinimumPassageHalf) + L.failure = "Channels this wide leave too little grass in each cell; use narrower channels " + "or bigger cells."; + else if (int((L.homes = spreadPockets(L.g, teams)).size()) != teams) + L.failure = "The maze has too few cul-de-sacs for this many colonies; use a bigger map or " + "smaller cells."; + return L; } std::string validate(const GenerationRequest &r) { - const MazeOptions o(r); - const MazeGrid g = mazeGrid(1 << r.wDec, 1 << r.hDec, o.cellSize); - if (g.columns < 2 || g.rows < 2) - return "The maze needs at least two cells across and down; use a bigger map or smaller " - "cells."; - if (passageHalf(g, o.channelWidth) < kMinimumPassageHalf) - return "Channels this wide leave too little grass in each cell; use narrower channels or " - "bigger cells."; - if (int(homePattern(g, r.nbTeams).size()) != r.nbTeams) - return "The maze has too few cul-de-sacs for this many colonies; use a bigger map or " - "smaller " - "cells."; - return ""; + return mazeLayout(MazeOptions(r), 1 << r.wDec, 1 << r.hDec, r.nbTeams).failure; } -// The home pattern at a random offset and mirror image on the torus - a translation or reflection -// of a valid layout is still valid - dealt to the colonies in random order. -std::vector chooseHomes(const MazeGrid &g, GenerationContext &context, int teams) +// The whole maze as drawn from the seed: the layout's home pattern at a random symmetry of the +// tiling, dealt to the colonies in random order; the carved passages; and the warped cells with +// every tile's cell. A pure function of the request, so validateWorld can build it again. +struct MazeDesign { - std::vector homes = homePattern(g, teams); - const int offsetX = context.bounded("maze", g.columns); - const int offsetY = context.bounded("maze", g.rows); - const bool mirrorX = context.bounded("maze", 2), mirrorY = context.bounded("maze", 2); - for (int &home : homes) - { - const int c = mirrorX ? (g.columns - g.column(home)) % g.columns : g.column(home); - const int r = mirrorY ? (g.rows - g.row(home)) % g.rows : g.row(home); - home = ((r + offsetY) % g.rows) * g.columns + (c + offsetX) % g.columns; - } - context.shuffle(homes.begin(), homes.end(), "maze"); - return homes; -} + MazeLayout layout; + std::vector isHome, open; + std::vector exitEdge, deadEnds, labels; + std::string failure; +}; -// Recursive backtracker (iterative, explicit stack) over the non-home cells: long winding -// passages with comparatively few, deep dead ends - the classic hand-drawn maze texture. -bool carveSpanningTree(const MazeGrid &g, GenerationContext &context, - const std::vector &isHome, std::vector &open) +MazeDesign designMaze(const GenerationRequest &request, GenerationContext &context) { - std::vector free; - for (int cell = 0; cell < g.cells(); ++cell) - if (!isHome[cell]) - free.push_back(cell); - if (free.empty()) - return false; - std::vector visited(isHome); - std::vector stack{free[context.bounded("maze", free.size())]}; - visited[stack.back()] = 1; - while (!stack.empty()) + const MazeOptions o(request); + MazeDesign d; + d.layout = mazeLayout(o, 1 << request.wDec, 1 << request.hDec, request.nbTeams); + MazeLayout &L = d.layout; + Tessellation &g = L.g; + if (!L.failure.empty()) { - const int cell = stack.back(); - int choices[4], count = 0; - for (int d = 0; d < 4; ++d) - if (!visited[g.neighbour(cell, d)]) - choices[count++] = d; - if (!count) - { - stack.pop_back(); - continue; - } - const int d = choices[context.bounded("maze", count)]; - const int next = g.neighbour(cell, d); - open[g.edgeId(cell, d)] = 1; - visited[next] = 1; - stack.push_back(next); + d.failure = L.failure; + return d; } - return std::all_of(visited.begin(), visited.end(), [](unsigned char v) { return v != 0; }); -} - -// Loopiness knocks through extra walls between non-home cells, giving routes to flank along; -// a home is never touched, so it stays a cul-de-sac. -void addLoops(const MazeGrid &g, GenerationContext &context, - const std::vector &isHome, int loopiness, - std::vector &open) -{ - std::vector closed; - int freeCells = 0; - for (int cell = 0; cell < g.cells(); ++cell) + // A translation or reflection of a valid layout is still valid. + const int dColumns = int(context.bounded("maze", g.columns)); + const int dRows = int(context.bounded("maze", g.rows)); + const bool mirrorColumns = context.bounded("maze", 2), mirrorRows = context.bounded("maze", 2); + for (int &home : L.homes) + home = g.transform(home, dColumns, dRows, mirrorColumns, mirrorRows); + context.shuffle(L.homes.begin(), L.homes.end(), "maze"); + + d.isHome.assign(g.cells.size(), 0); + for (int home : L.homes) + d.isHome[home] = 1; + d.open.assign(g.edges.size(), 0); + if (!carveSpanningTree(g, context, "maze", d.isHome, d.open)) { - if (isHome[cell]) - continue; - ++freeCells; - for (int d : {East, South}) - if (!open[g.edgeId(cell, d)] && !isHome[g.neighbour(cell, d)]) - closed.push_back(g.edgeId(cell, d)); + d.failure = "the non-home cells did not form one connected maze"; + return d; } - context.shuffle(closed.begin(), closed.end(), "maze"); - const size_t extra = std::min(closed.size(), size_t(freeCells * loopiness / 100)); - for (size_t i = 0; i < extra; ++i) - open[closed[i]] = 1; + const std::vector doors = openPocketDoors(g, context, "maze", L.homes, d.isHome, d.open); + openLoops(g, context, "maze", d.isHome, o.loopiness, d.open); + d.deadEnds = deadEnds(g, d.open, d.isHome, d.exitEdge); + for (size_t k = 0; k < L.homes.size(); ++k) + d.exitEdge[L.homes[k]] = doors[k]; + + // Warping never brings two walls (or ponds) that share no corner closer than the narrowest + // passage allows, nor a wall closer to a cell's centre than half that, so the validation above + // still holds for the warped cells: walls meeting at a corner may close up, passages may not. + const int passageSteps = 2 * (kWallFootprint + o.channelWidth + kMinimumPassageHalf); + std::vector walls(g.edges.size(), 0); + for (size_t edge = 0; edge < g.edges.size(); ++edge) + walls[edge] = !d.open[edge]; + warpCorners(g, warpLimit(g) * o.warp / 100, walls, passageSteps, passageSteps / 2, context, + "maze-warp"); + d.labels = labelTiles(g); + if (d.labels.empty()) + d.failure = "the maze's cells did not tile the map"; + return d; } -void fillUndermap(Map &map, int x0, int y0, int w, int h, TerrainType t) +// Measurements of a cell's tiles from its centre, along its exit (u, positive towards the door) +// and across it (v, positive to the door's right), in kFrame units. +struct CellTile { - for (int dy = 0; dy < h; ++dy) - for (int dx = 0; dx < w; ++dx) - map.setUMTerrain(map.normalizeX(x0 + dx), map.normalizeY(y0 + dy), t); -} - -// An inclusive rectangle of pure-grass tiles. Stamping one takes undermap grass on -// [x0, x1 + 1] x [y0, y1 + 1] inside undermap sand one line further out. -struct TileRect + int x, y, u, v; +}; +struct CellFrame { - int x0, y0, x1, y1; + double fx, fy; + int cx, cy; + CellTile measure(const Torus &t, int x, int y) const + { + const int ox = t.offsetX(cx, x), oy = t.offsetY(cy, y); + return {x, y, int(std::lround((ox * fx + oy * fy) * kFrame)), + int(std::lround((ox * -fy + oy * fx) * kFrame))}; + } }; - -void stampRing(Map &map, const TileRect &r) +CellFrame frameFor(const Tessellation &g, int cell, int exitEdge) { - fillUndermap(map, r.x0 - 1, r.y0 - 1, r.x1 - r.x0 + 4, r.y1 - r.y0 + 4, SAND); + const SubtilePoint centre = g.cells[cell].centre; + const auto ends = g.edgeEnds(exitEdge, cell); + const double dx = double(ends.first.x + ends.second.x) / 2 - centre.x; + const double dy = double(ends.first.y + ends.second.y) / 2 - centre.y; + const double length = std::hypot(dx, dy); + return {dx / length, dy / length, g.centreTileX(cell), g.centreTileY(cell)}; } -void stampCore(Map &map, const TileRect &r) + +// Places the first `count` tiles of a region in the order `before` ranks them. +template +void fillInOrder(Map &map, std::vector region, int count, int resourceType, Order before) { - fillUndermap(map, r.x0, r.y0, r.x1 - r.x0 + 2, r.y1 - r.y0 + 2, GRASS); + std::stable_sort(region.begin(), region.end(), before); + for (int i = 0; i < count && i < int(region.size()); ++i) + map.setResource(region[i].x, region[i].y, resourceType, 1); } -// One wall per closed boundary: a stone spine covering every tile of the boundary line from -// corner to corner inclusive, so perpendicular walls share their corner tile and nothing can slip -// between them. As a tile rectangle one tile wide, its pure-grass core is exactly the spine, -// which is what lets STONE be placed on every tile of it. -TileRect wallFor(const MazeGrid &g, int cell, int direction) +// A cell's free grass tiles - no deposit on them, which also leaves out the walls' stone - in row +// order, measured in its exit's frame; `back` is how far they reach behind the centre and `side` +// how far across, both in kFrame units. +std::vector grassOfCell(const Map &map, const Torus &t, const MazeDesign &d, int cell, + const CellFrame &frame, int &back, int &side) { - const int c = g.column(cell), r = g.row(cell); - if (direction == East) - return {g.xs[c + 1], g.ys[r], g.xs[c + 1], g.ys[r + 1]}; - return {g.xs[c], g.ys[r + 1], g.xs[c + 1], g.ys[r + 1]}; + std::vector tiles; + back = side = 0; + for (int i = 0; i < t.size(); ++i) + if (d.labels[i] == cell && map.isGrass(i % t.w, i / t.w) && + !map.isResource(i % t.w, i / t.w)) + { + tiles.push_back(frame.measure(t, i % t.w, i / t.w)); + back = std::max(back, -tiles.back().u); + side = std::max(side, std::abs(tiles.back().v)); + } + return tiles; } // Distance in 8-neighbour steps from every tile to the nearest shore - a tile that isn't pure @@ -366,57 +257,47 @@ std::vector shoreDepth(const Map &map, const std::vector &on return stepsFrom(t, shore); } -struct CellTile -{ - int x, y, u, v; // u: towards the cell's exit; v: to the exit's right -}; - -// Places the first `count` tiles of a region in the order `before` ranks them. -template -void fillInOrder(Map &map, std::vector region, int count, int resourceType, Order before) -{ - std::stable_sort(region.begin(), region.end(), before); - for (int i = 0; i < count && i < int(region.size()); ++i) - map.setResource(region[i].x, region[i].y, resourceType, 1); -} - // A home's own cell, seen from inside looking out through its exit: wheat banks the left shore // and wood the right, both growing forward from the dead end's back wall; stone is a compact -// deposit at the back wall's centre (a line along it would read as a second wall). The middle of -// the passage and a square around the swarm stay clear, so workers always walk straight out. -void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) +// deposit at the back wall's centre (a line along it would read as a second wall). Only tiles +// within kShoreBand of a shore take anything (`depth`, from shoreDepth), so the middle of the +// passage stays clear whatever the cell's shape, and so does a square around the swarm, so workers +// always walk straight out. The back wall is the shore behind the centre: its three rows of tiles, +// less the side shores at their ends. +void furnishHome(Map &map, const Torus &t, const MazeDesign &d, const std::vector &depth, + int cell) { - static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; - const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; - const int cx = g.centerX(cell), cy = g.centerY(cell); - const int lane = std::max(1, half - kShoreBand); - std::vector left, right, back; - const int c = g.column(cell), r = g.row(cell); - for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) - for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) - { - if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID || - map.getGroundUnit(x, y) != NOGUID) - continue; - const int ox = x - cx, oy = y - cy; - const int u = ox * fx + oy * fy, v = ox * rx + oy * ry; - // A 9x9 square round the swarm stays clear: its 4x4 footprint plus the ring its first - // workers step out into. - if (std::abs(u) <= 4 && std::abs(v) <= 4) - continue; - if (v < -lane) - left.push_back({x, y, u, v}); - else if (v > lane) - right.push_back({x, y, u, v}); - else if (u <= -half + 2) - back.push_back({x, y, u, v}); - } + const CellFrame frame = frameFor(d.layout.g, cell, d.exitEdge[cell]); + int back, side; + const std::vector grass = grassOfCell(map, t, d, cell, frame, back, side); + int backWidth = 0; + for (const CellTile &c : grass) + if (c.u <= -back + 2 * kFrame) + backWidth = std::max(backWidth, std::abs(c.v)); + const int lane = std::max(1 * kFrame, backWidth - kShoreBand * kFrame); + std::vector left, right, rear; + for (const CellTile &c : grass) + { + const int reach = depth[t.at(c.x, c.y)]; + if (reach < 1 || reach > kShoreBand || map.getBuilding(c.x, c.y) != NOGBID || + map.getGroundUnit(c.x, c.y) != NOGUID) + continue; + // A 9x9 square round the swarm stays clear: its 4x4 footprint plus the ring its first + // workers step out into. + if (std::abs(c.u) <= 4 * kFrame && std::abs(c.v) <= 4 * kFrame) + continue; + if (c.u <= -back + 2 * kFrame && std::abs(c.v) <= lane) + rear.push_back(c); + else if (c.v < 0) + left.push_back(c); + else if (c.v > 0) + right.push_back(c); + } const auto shoreFromBack = [](const CellTile &a, const CellTile &b) { return a.u != b.u ? a.u < b.u : std::abs(a.v) > std::abs(b.v); }; - const auto nearBackCentre = [half](const CellTile &a, const CellTile &b) + const auto nearBackCentre = [back](const CellTile &a, const CellTile &b) { - const int da = std::max(a.u + half, std::abs(a.v)), - db = std::max(b.u + half, std::abs(b.v)); + const int da = std::max(a.u + back, std::abs(a.v)), db = std::max(b.u + back, std::abs(b.v)); if (da != db) return da < db; if (std::abs(a.v) != std::abs(b.v)) @@ -425,27 +306,18 @@ void furnishHome(Map &map, const MazeGrid &g, int cell, int exit, int half) }; fillInOrder(map, left, kHomeFarmland, CORN, shoreFromBack); fillInOrder(map, right, kHomeFarmland, WOOD, shoreFromBack); - fillInOrder(map, back, kHomeStone, STONE, nearBackCentre); + fillInOrder(map, rear, kHomeStone, STONE, nearBackCentre); } // A treasure: a compact patch of one fruit at the far end of a dead end, so colonies have to // go and take it rather than passing it on the way somewhere else. It sits a few tiles back // from the end wall, so its front always faces the passage's clear lane. -void placeTreasure(Map &map, const MazeGrid &g, int cell, int exit, int half, int size, - int fruitType) +void placeTreasure(Map &map, const Torus &t, const MazeDesign &d, int cell, int size, int fruitType) { - static const int forward[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; - const int fx = forward[exit][0], fy = forward[exit][1], rx = -fy, ry = fx; - const int cx = g.centerX(cell), cy = g.centerY(cell), anchor = -half + 3; - std::vector tiles; - const int c = g.column(cell), r = g.row(cell); - for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) - for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) - if (map.isGrass(x, y) && !map.isResource(x, y)) - { - const int ox = x - cx, oy = y - cy; - tiles.push_back({x, y, ox * fx + oy * fy, ox * rx + oy * ry}); - } + const CellFrame frame = frameFor(d.layout.g, cell, d.exitEdge[cell]); + int back, side; + std::vector tiles = grassOfCell(map, t, d, cell, frame, back, side); + const int anchor = -back + 3 * kFrame; fillInOrder(map, tiles, size, fruitType, [anchor](const CellTile &a, const CellTile &b) { @@ -463,21 +335,20 @@ void placeTreasure(Map &map, const MazeGrid &g, int cell, int exit, int half, in // grown over free shore tiles. Only tiles within kShoreBand of a shore are eligible, which is // what keeps each passage's middle clear. Densities are per 256 shore tiles; fruitTiles of fruit, // when the dead ends hold no treasure, follow in small clumps of each kind in turn. -void scatterThroughMaze(Map &map, GenerationContext &context, const MazeGrid &g, - const std::vector &isHome, - const std::vector &onRoad, int half, const MazeOptions &o, +void scatterThroughMaze(Map &map, GenerationContext &context, const MazeDesign &d, + const std::vector &onRoad, const MazeOptions &o, int fruitTiles) { const int w = map.getW(), h = map.getH(); const std::vector depth = shoreDepth(map, onRoad); - const int band = std::min(kShoreBand, half - 1); + const int band = std::min(kShoreBand, d.layout.half - 1); std::vector free(size_t(w) * h, 0); std::vector pool; for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { const size_t i = size_t(y) * w + x; - if (depth[i] >= 1 && depth[i] <= band && !isHome[g.cellAt(x, y)] && + if (depth[i] >= 1 && depth[i] <= band && !d.isHome[d.labels[i]] && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID && map.getGroundUnit(x, y) == NOGUID) { @@ -564,194 +435,239 @@ void seedAlgae(Map &map, GenerationContext &context, int algae) water[context.bounded("resources", water.size())], ALGA, 1); } +// Each closed edge's stone line, from corner tile to corner tile inclusive, so walls meeting at a +// corner share its tile and nothing can slip between them at any angle. +std::vector wallSpines(const Torus &t, const MazeDesign &d) +{ + const Tessellation &g = d.layout.g; + std::vector spine(size_t(t.size()), 0); + for (int edge = 0; edge < int(g.edges.size()); ++edge) + if (!d.open[edge]) + { + const auto ends = g.edgeEnds(edge, g.edges[edge].cells[0]); + traceSealedPath(spine, t, {ends.first, ends.second}); + } + return spine; +} + +// The tile of every corner where all the edges meeting are open. No wall reaches it, but the +// passages round it stay apart all the same: its pond keeps the corner's chambers from merging into +// one open plaza, so a junction of passages still reads as one. +std::vector openCorners(const Torus &t, const MazeDesign &d) +{ + const Tessellation &g = d.layout.g; + std::vector walled(g.corners.size(), 0), ponds(size_t(t.size()), 0); + for (int edge = 0; edge < int(g.edges.size()); ++edge) + if (!d.open[edge]) + walled[g.edges[edge].corners[0]] = walled[g.edges[edge].corners[1]] = 1; + for (size_t corner = 0; corner < g.corners.size(); ++corner) + if (!walled[corner]) + ponds[t.at(int(subtileTile(g.corners[corner].x)), int(subtileTile(g.corners[corner].y)))] = + 1; + return ponds; +} + +// The undermap corners of every tile in `tiles`. +std::vector cornersOf(const Torus &t, const std::vector &tiles) +{ + std::vector corners(tiles.size(), 0); + for (int i = 0; i < t.size(); ++i) + if (tiles[i]) + { + const int x = i % t.w, y = i / t.w; + corners[i] = corners[t.at(x + 1, y)] = corners[t.at(x, y + 1)] = + corners[t.at(x + 1, y + 1)] = 1; + } + return corners; +} + +// A sand road down every open edge, from each cell's centre through the edge's middle to the +// next centre: undermap sand on the corners of every tile a sealed line passes, so each road tile is +// pure sand and consecutive ones share a side. At a home it stops against the swarm's 4x4 +// footprint (the settlement is anchored on the centre), so the swarm still stands on grass. Only +// grass corners take sand, so a road can never reach into a channel. Returns the tiles the road +// touches, which the shore scatter doesn't count as shore. +std::vector layRoads(TerrainSketch &sketch, const Torus &t, const MazeDesign &d) +{ + const Tessellation &g = d.layout.g; + std::vector path(size_t(t.size()), 0); + for (int edge = 0; edge < int(g.edges.size()); ++edge) + if (d.open[edge]) + { + const int a = g.edges[edge].cells[0]; + const auto ends = g.edgeEnds(edge, a); + const SubtilePoint middle{(ends.first.x + ends.second.x) / 2, + (ends.first.y + ends.second.y) / 2}; + traceSealedPath(path, t, {g.cells[a].centre, middle, g.centreAcross(edge, a)}); + } + std::vector road = cornersOf(t, path); + for (int cell = 0; cell < g.cellCount(); ++cell) + if (d.isHome[cell]) + for (int dy = -2; dy <= 2; ++dy) + for (int dx = -2; dx <= 2; ++dx) + road[t.at(g.centreTileX(cell) + dx, g.centreTileY(cell) + dy)] = 0; + std::vector onRoad(size_t(t.size()), 0); + for (int i = 0; i < t.size(); ++i) + if (road[i] && sketch[i] == GRASS) + { + sketch[i] = SAND; + const int x = i % t.w, y = i / t.w; + onRoad[i] = onRoad[t.at(x - 1, y)] = onRoad[t.at(x, y - 1)] = + onRoad[t.at(x - 1, y - 1)] = 1; + } + return onRoad; +} + bool generate(Game &game, GenerationContext &context) { context.stage = "maze layout"; const MazeOptions o(context.request); Map &map = game.map; - const MazeGrid g = mazeGrid(map.getW(), map.getH(), o.cellSize); const int teams = context.request.nbTeams; - const int half = passageHalf(g, o.channelWidth); map.makeHomogenMap(WATER); for (int i = 0; i < teams; ++i) game.addTeam(); - if (g.columns < 2 || g.rows < 2 || half < kMinimumPassageHalf) + const MazeDesign d = designMaze(context.request, context); + if (!d.failure.empty()) { - context.detail = "the maze grid is too small for these settings"; + context.detail = d.failure; return false; } + const Tessellation &g = d.layout.g; + const std::vector &homes = d.layout.homes; - const std::vector homes = chooseHomes(g, context, teams); - if (int(homes.size()) != teams) - { - context.detail = "the maze grid has too few cul-de-sacs for every colony"; - return false; - } - std::vector isHome(g.cells(), 0); - for (int home : homes) - isHome[home] = 1; - std::vector open(2 * g.cells(), 0); - if (!carveSpanningTree(g, context, isHome, open)) - { - context.detail = "the non-home cells did not form one connected maze"; - return false; - } - std::vector exitOf(g.cells(), -1); - for (int home : homes) - { - int choices[4], count = 0; - for (int d = 0; d < 4; ++d) - if (!isHome[g.neighbour(home, d)]) - choices[count++] = d; - const int d = choices[context.bounded("maze", count)]; - open[g.edgeId(home, d)] = 1; - exitOf[home] = d; - } - addLoops(g, context, isHome, o.loopiness, open); - std::vector deadEnds; - for (int cell = 0; cell < g.cells(); ++cell) - { - if (isHome[cell]) - continue; - int degree = 0, exit = -1; - for (int d = 0; d < 4; ++d) - if (open[g.edgeId(cell, d)]) - { - ++degree; - exit = d; - } - if (degree == 1) - { - deadEnds.push_back(cell); - exitOf[cell] = exit; - } - } - - // Every cell is a chamber as wide as a passage, and every open boundary a band of that width - // joining two chambers, so a run of passage reads as one continuous strip of grass. context.stage = "maze terrain"; - std::vector passages, walls, roads; - for (int cell = 0; cell < g.cells(); ++cell) - { - const int cx = g.centerX(cell), cy = g.centerY(cell); - passages.push_back({cx - half, cy - half, cx + half, cy + half}); - for (int d : {East, South}) - { - if (!open[g.edgeId(cell, d)]) - { - walls.push_back(wallFor(g, cell, d)); - continue; - } - // A road runs centre to centre, three tiles wide and one tile past each centre so roads - // meeting at a cell overlap. At a home it stops against the swarm's 4x4 footprint (the - // settlement is anchored on the centre), so the swarm still stands on grass. - const int next = g.neighbour(cell, d); - if (d == East) - { - const int farX = g.centerX(next) < cx ? g.centerX(next) + g.width : g.centerX(next); - passages.push_back({cx - half, cy - half, farX + half, cy + half}); - roads.push_back({isHome[cell] ? cx + 2 : cx - 1, cy - 1, - isHome[next] ? farX - 3 : farX + 1, cy + 1}); - } - else - { - const int farY = - g.centerY(next) < cy ? g.centerY(next) + g.height : g.centerY(next); - passages.push_back({cx - half, cy - half, cx + half, farY + half}); - roads.push_back({cx - 1, isHome[cell] ? cy + 2 : cy - 1, cx + 1, - isHome[next] ? farY - 3 : farY + 1}); - } - } - } - // All sand before any grass, so a grass core is never overwritten by a neighbouring ring. - for (const TileRect &r : passages) - stampRing(map, r); - for (const TileRect &r : walls) - stampRing(map, r); - for (const TileRect &r : passages) - stampCore(map, r); - for (const TileRect &r : walls) - stampCore(map, r); - // Then the roads, cut through the passages' grass: a sand road links every cell to the maze - // and can never be closed. Map::incResource only seeds a resource on its own terrain and - // buildings need pure grass, so nothing can grow over a road tile or be built on one. A road's - // tile rectangle takes undermap sand on (x0, x1] x (y0, y1], giving each of its tiles at least - // one sand corner and leaving every tile around it untouched. - // Without roads, passages are grass from shore to shore. - std::vector onRoad(size_t(map.getW()) * map.getH(), 0); - for (const TileRect &r : roads) + const Torus t(map); + const std::vector spine = wallSpines(t, d); + const std::vector fromWall = stepsFrom(t, cornersOf(t, spine)); + const std::vector fromPond = stepsFrom(t, cornersOf(t, openCorners(t, d))); + const int channelEdge = kFlankSteps + 1 + o.channelWidth; + TerrainSketch sketch(size_t(t.size()), GRASS); + for (int i = 0; i < t.size(); ++i) + if ((fromWall[i] > kFlankSteps && fromWall[i] <= channelEdge) || + (fromPond[i] >= 0 && fromPond[i] <= channelEdge)) + sketch[i] = WATER; + layBeaches(sketch, t); + // Without roads, passages are grass from shore to shore. Map::incResource only seeds a resource + // on its own terrain and buildings need pure grass, so nothing can grow over a road tile or be + // built on one: a road links every cell to the maze and can never be closed. + const std::vector onRoad = + o.sandRoads ? layRoads(sketch, t, d) : std::vector(size_t(t.size()), 0); + writeUndermap(map, sketch); + const DesignedStone stone = designedStone(map, t, spine); + if (stone.gaps) { - if (!o.sandRoads) - break; - fillUndermap(map, r.x0 + 1, r.y0 + 1, r.x1 - r.x0, r.y1 - r.y0, SAND); - for (int y = r.y0; y <= r.y1; ++y) - for (int x = r.x0; x <= r.x1; ++x) - onRoad[size_t(map.normalizeY(y)) * map.getW() + map.normalizeX(x)] = 1; + context.detail = "a wall spine tile is not solid grass"; + return false; } - map.rebuildTerrain(); - for (const TileRect &r : walls) - for (int y = r.y0; y <= r.y1; ++y) - for (int x = r.x0; x <= r.x1; ++x) - { - const int nx = map.normalizeX(x), ny = map.normalizeY(y); - if (map.getTerrainType(nx, ny) != GRASS) - { - context.detail = "a wall spine tile is not solid grass"; - return false; - } - map.setResource(nx, ny, STONE, 1); - } + for (int i = 0; i < t.size(); ++i) + if (stone.stone[i]) + map.setResource(i % t.w, i / t.w, STONE, 1); const auto chamber = [&](int team) { - const int cell = homes[team], c = g.column(cell), r = g.row(cell); - std::vector home(size_t(map.getW()) * map.getH(), 0); - for (int y = g.ys[r]; y < g.ys[r + 1]; ++y) - for (int x = g.xs[c]; x < g.xs[c + 1]; ++x) - if (!map.isWater(x, y)) - home[size_t(y) * map.getW() + x] = 1; + std::vector home(size_t(t.size()), 0); + for (int i = 0; i < t.size(); ++i) + home[i] = d.labels[i] == homes[team] && !map.isWater(i % t.w, i / t.w); return home; }; // placeSettlement measures from the footprint's top-left tile; this centres the 4x4 swarm. const auto centre = [&](int team) - { return MapGeneratorPoint(g.centerX(homes[team]) - 2, g.centerY(homes[team]) - 2); }; + { return MapGeneratorPoint(g.centreTileX(homes[team]) - 2, g.centreTileY(homes[team]) - 2); }; if (!settleColonies(game, context, "starts", chamber, centre)) return false; context.stage = "maze resources"; + const std::vector depth = shoreDepth(map, onRoad); for (int home : homes) - furnishHome(map, g, home, exitOf[home], half); + furnishHome(map, t, d, depth, home); // Fruit is treasure: one patch at the far end of every dead end that isn't a home, with fruit // types dealt round-robin so every kind is somewhere in the maze and a colony has to go and // fight for the ones it lacks. Placed before the shore scatter, which works around them. // Without treasure the same fruit is scattered along the passages' shores instead. + std::vector deadEnds = d.deadEnds; if (o.fruit > 0 && o.treasure) { context.shuffle(deadEnds.begin(), deadEnds.end(), "resources"); const int firstType = context.bounded("resources", 3); for (size_t i = 0; i < deadEnds.size(); ++i) - placeTreasure(map, g, deadEnds[i], exitOf[deadEnds[i]], half, o.fruit, - CHERRY + int((firstType + i) % 3)); + placeTreasure(map, t, d, deadEnds[i], o.fruit, CHERRY + int((firstType + i) % 3)); } - scatterThroughMaze(map, context, g, isHome, onRoad, half, o, + scatterThroughMaze(map, context, d, onRoad, o, o.treasure ? 0 : int(deadEnds.size()) * o.fruit); seedAlgae(map, context, o.algae); return true; } -// The maze's defining guarantee, checked on the finished world rather than trusted: every -// colony can walk to every other one. Water, buildings and every resource (including the wall -// spines) block the flood; units don't, since they move. +// Which cells may share ground: two cells round a corner are joined when the open edges at that +// corner connect them, which includes any two cells across an open edge. +std::vector joinedCells(const MazeDesign &d) +{ + const Tessellation &g = d.layout.g; + const int n = g.cellCount(); + std::vector> cornerCells(g.corners.size()); + for (int cell = 0; cell < n; ++cell) + for (int corner : g.cells[cell].corners) + cornerCells[corner].push_back(cell); + std::vector joined(size_t(n) * n, 0); + std::vector parent(n); + std::iota(parent.begin(), parent.end(), 0); + const auto find = [&](int c) + { + while (parent[c] != c) + c = parent[c] = parent[parent[c]]; + return c; + }; + for (size_t corner = 0; corner < g.corners.size(); ++corner) + { + for (int cell : cornerCells[corner]) + parent[cell] = cell; + for (int cell : cornerCells[corner]) + for (int edge : g.cells[cell].edges) + if (d.open[edge] && (g.edges[edge].corners[0] == int(corner) || + g.edges[edge].corners[1] == int(corner))) + parent[find(cell)] = find(g.other(edge, cell)); + for (int a : cornerCells[corner]) + for (int b : cornerCells[corner]) + if (find(a) == find(b)) + joined[size_t(a) * n + b] = 1; + } + return joined; +} + +// The maze's defining guarantees, checked on the finished world rather than trusted: every +// colony can walk to every other one, and the walls hold - no ground a colony can reach joins two +// cells except through the passages the maze opened. Water, buildings and every resource +// (including the wall spines) block the walk; units don't, since they move. std::string validateWorld(const Game &game, const GenerationContext &context) { - return walkFromFirstColony(game.map, context.request.nbTeams, "the maze", "through the maze") - .error; + const ColonyWalk walk = + walkFromFirstColony(game.map, context.request.nbTeams, "the maze", "through the maze"); + if (!walk.error.empty()) + return walk.error; + GenerationContext replay(context.request); + const MazeDesign d = designMaze(context.request, replay); + if (!d.failure.empty()) + return "The maze could not be rebuilt: " + d.failure; + const Torus t(game.map); + std::vector reached(size_t(t.size()), 0); + for (int i = 0; i < t.size(); ++i) + reached[i] = walk.steps[i] >= 0; + const std::vector joined = joinedCells(d); + const int n = d.layout.g.cellCount(); + const RegionLeak leak = firstRegionLeak(t, reached, d.labels, [&](int a, int b) + { return joined[size_t(a) * n + b] != 0; }); + if (leak.tile >= 0) + return "A maze wall leaks at (" + std::to_string(leak.tile % t.w) + ", " + + std::to_string(leak.tile / t.w) + ")."; + return ""; } } // namespace MazeOptions::MazeOptions(const GenerationRequest &r) - : cellSize(r.option("cell-size")), channelWidth(r.option("channel-width")), - loopiness(r.option("loopiness")), corn(r.option("wheat")), wood(r.option("wood")), + : cellShape(r.option("cell-shape")), cellSize(r.option("cell-size")), + channelWidth(r.option("channel-width")), loopiness(r.option("loopiness")), + warp(r.option("warp")), corn(r.option("wheat")), wood(r.option("wood")), stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")), sandRoads(r.option("sand-roads") != 0), treasure(r.option("dead-end-treasure") != 0) { @@ -762,9 +678,10 @@ GeneratorDefinition mazeDefinition() return {"maze", 11, "Maze", - 11, + 12, false, - {{"cell-size", + {GeneratorControl::choice("cell-shape", "Cell shape", {"Squares", "Hexagons"}, 0), + {"cell-size", "Cell size", 24, 48, @@ -774,8 +691,13 @@ GeneratorDefinition mazeDefinition() false, false, {24, 32, 40, 48}}, - // Cells snap to 24, 32, 40 or 48 tiles; the default 32 gives 64 cells on a 256 map, - // enough corridors to feel like a maze without passages narrower than a small base. + // Cells snap to 24, 32, 40 or 48 tiles; the default 32 gives 64 square cells on a 256 + // map, enough corridors to feel like a maze without passages narrower than a small base. + // Hexagons sit half as far apart again (kHexPitchPercent). + // Warp moves the cells' corners by up to this share of what keeps every cell whole, so + // squares and hexagons become irregular polygons; doorways never narrow past the + // smallest a passage may be, so at small cells warp is gentler. + {"warp", "Warp", 0, 100, 25, 0, ControlGroup::Layout}, // Open water on each side of a wall's stone line; passages widen to fill the rest. {"channel-width", "Channel width", 1, 6, 1, 2, ControlGroup::Layout}, {"loopiness", diff --git a/src/map/generator/generators/MazeGenerator.h b/src/map/generator/generators/MazeGenerator.h index cc1bb83b8..1bd2fd812 100644 --- a/src/map/generator/generators/MazeGenerator.h +++ b/src/map/generator/generators/MazeGenerator.h @@ -4,7 +4,7 @@ #include "GeneratorDefinition.h" struct MazeOptions { - int cellSize, channelWidth, loopiness; + int cellShape, cellSize, channelWidth, loopiness, warp; int corn, wood, stone, algae, fruit; bool sandRoads, treasure; explicit MazeOptions(const GenerationRequest &r); diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 98433de48..962e8cb65 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -8,8 +8,10 @@ #include "Pipeline.h" #include "Resources.h" #include "Settlements.h" +#include "Sketch.h" #include "Topology.h" #include "Unit.h" +#include "Walls.h" #include #include #include @@ -507,13 +509,12 @@ bool chooseHomes(Layout &L, GenerationContext &context, int teams) return true; } -// Grows one pond from its seed, always taking the lowest point of the depth field next, so it -// fills its hollow the way water would. Distance from the seed counts as depth too, which keeps -// the outline round rather than following the noise lattice into points. +// Grows one pond from its seed (growWater), always taking the lowest point of the depth field next, +// so it fills its hollow the way water would. Distance from the seed counts as depth too, which +// keeps the outline round rather than following the noise lattice into points. void growPond(Layout &L, const std::vector &depth, const std::vector &eligible, int eligibleStamp, std::vector &queued, int queuedStamp, int seed, int target) { - using Entry = std::pair; const int sx = seed % L.t.w, sy = seed / L.t.w; const auto key = [&](int tile) { @@ -523,29 +524,9 @@ void growPond(Layout &L, const std::vector &depth, const std::vector & // distance only stops it running off along a trough. return depth[tile] + int(std::lround(d * 1500)); }; - std::priority_queue, std::greater> frontier; - frontier.push({key(seed), seed}); - queued[seed] = queuedStamp; - const auto &steps = kCardinalSteps; - int grown = 0; - while (!frontier.empty() && grown < target) - { - const int tile = frontier.top().second; - frontier.pop(); - if (L.pond[tile]) - continue; - L.pond[tile] = 1; - ++grown; - for (const auto &s : steps) - { - const int next = L.t.at(tile % L.t.w + s[0], tile / L.t.w + s[1]); - if (eligible[next] == eligibleStamp && !L.pond[next] && queued[next] != queuedStamp) - { - queued[next] = queuedStamp; - frontier.push({key(next), next}); - } - } - } + growWater( + L.t, L.pond, seed, target, [&](int tile) { return eligible[tile] == eligibleStamp; }, key, + queued, queuedStamp); } void placePonds(Layout &L, GenerationContext &context, int spacing, int pondSize) @@ -697,22 +678,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const std::vector sites = scatterSites(t, o.valleySize, context); const std::vector label = labelCells(t, sites, o.valleySize, context); - // A tile is ridge when any of its eight neighbours has a lower label. A tile no ridge was - // painted on then touches only its own label or ridge, so no unit can step, even diagonally, - // from one cell into another. - L.ridge.assign(n, 0); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - const int i = y * t.w + x; - for (int dy = -1; dy <= 1 && !L.ridge[i]; ++dy) - for (int dx = -1; dx <= 1; ++dx) - if (label[t.at(x + dx, y + dy)] < label[i]) - { - L.ridge[i] = 1; - break; - } - } + // A tile is ridge when any of its eight neighbours has a lower label (labelBorders), so no unit + // can step, even diagonally, from one cell into another. + L.ridge = labelBorders(t, label); // Stretches of ridgeline two tiles thick, where a noise field says so: 45% of the ridge at the // default stone amount, which scales that share. More stone never opens a gap. const std::vector thickness = diff --git a/src/map/generator/shared/Drawing.cpp b/src/map/generator/shared/Drawing.cpp index a0fa0b7c7..cd9328b4e 100644 --- a/src/map/generator/shared/Drawing.cpp +++ b/src/map/generator/shared/Drawing.cpp @@ -75,6 +75,60 @@ void tracePath(std::vector &mask, const Torus &t, } } +std::vector> sealedSegmentTiles(SubtilePoint a, SubtilePoint b) +{ + // A grid walk (Amanatides and Woo) in integers: from the tile holding `a`, step into whichever + // neighbouring column or row the segment reaches first, comparing the distances to the next + // boundaries by cross-multiplication. A tie is a corner, where both tiles beside it are taken. + long long x = subtileTile(a.x), y = subtileTile(a.y); + const long long endX = subtileTile(b.x), endY = subtileTile(b.y); + const long long dx = b.x - a.x, dy = b.y - a.y; + const int sx = dx > 0 ? 1 : -1, sy = dy > 0 ? 1 : -1; + const long long adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; + std::vector> tiles{{x, y}}; + const long long limit = (endX > x ? endX - x : x - endX) + (endY > y ? endY - y : y - endY); + for (long long step = 0; step < limit && (x != endX || y != endY); ++step) + { + // Distance along each axis to the boundary the segment crosses next, in subtile units. + const long long toX = adx ? (sx > 0 ? (x + 1) * kSubtile - a.x : a.x - x * kSubtile) : 0; + const long long toY = ady ? (sy > 0 ? (y + 1) * kSubtile - a.y : a.y - y * kSubtile) : 0; + // Parameters toX / adx and toY / ady; an axis the segment never moves along never wins. + const bool xFirst = x != endX && (y == endY || !ady || (adx && toX * ady < toY * adx)); + const bool yFirst = y != endY && (x == endX || !adx || (ady && toY * adx < toX * ady)); + if (xFirst) + x += sx; + else if (yFirst) + y += sy; + else + { + tiles.push_back({x + sx, y}); + tiles.push_back({x, y + sy}); + x += sx; + y += sy; + ++step; + } + tiles.push_back({x, y}); + } + return tiles; +} + +void traceSealedPath(std::vector &mask, const Torus &t, + const std::vector &points, unsigned char value, bool closed) +{ + const size_t n = points.size(); + if (n == 1) + mask[t.at(int(subtileTile(points[0].x)), int(subtileTile(points[0].y)))] = value; + for (size_t k = 0; k + 1 < n || (closed && n > 2 && k < n); ++k) + for (const auto &tile : sealedSegmentTiles(points[k], points[(k + 1) % n])) + mask[t.at(int(tile.first), int(tile.second))] = value; +} + +void fillPolygon(std::vector &mask, const Torus &t, + const std::vector &outline, unsigned char value) +{ + forEachTileInPolygon(t, outline, [&](int tile) { mask[tile] = value; }); +} + std::vector bezierPath(ShapePoint from, ShapePoint control, ShapePoint to, double fromHalfWidth, double toHalfWidth, int segments) { @@ -182,4 +236,30 @@ PathBounds pathBounds(const std::vector &path) std::max(bounds.radius, std::hypot(p.x - bounds.x, p.y - bounds.y) + p.halfWidth); return bounds; } + +Zigzag zigzagPath(const AxisFrame &frame, double start, double firstLeg, double pitch, int legs, + double span, double finish, double halfWidth) +{ + Zigzag zigzag; + double side = span; + const auto point = [&](double along, double across) + { + const ShapePoint p = frame.at(along, across); + zigzag.path.push_back({p.x, p.y, halfWidth}); + }; + point(start, side); + for (int j = 0; j < legs; ++j) + { + const double along = firstLeg - j * pitch; + point(along, side); + const ShapePoint a = frame.at(along, side - (side > 0 ? halfWidth : -halfWidth)); + side = -side; + point(along, side); + const ShapePoint b = frame.at(along, side - (side > 0 ? halfWidth : -halfWidth)); + zigzag.legs.push_back({{a.x, a.y, halfWidth}, {b.x, b.y, halfWidth}}); + } + point(finish, side); + zigzag.finishAcross = side; + return zigzag; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Drawing.h b/src/map/generator/shared/Drawing.h index 2aa943cad..1d3248b60 100644 --- a/src/map/generator/shared/Drawing.h +++ b/src/map/generator/shared/Drawing.h @@ -4,6 +4,7 @@ #include "Grid.h" #include #include +#include #include namespace MapGeneration { @@ -204,6 +205,68 @@ inline void fillShape(std::vector &mask, const Torus &t, double c t, cx, cy, shape, turn, [&](int i, double, double) { mask[i] = value; }, stretch); } +/// Points along the arc `radius` tiles round (cx, cy) from angle `from` to angle `to` (radians, +/// either way round), about `step` tiles apart along the arc, all with the same half width: a +/// corridor that follows a circle, or a ring drawn a piece at a time. +inline std::vector arcPath(double cx, double cy, double radius, double from, double to, + double halfWidth, double step = 3) +{ + const int segments = + std::max(1, int(std::ceil(std::abs(to - from) * radius / std::max(0.5, step)))); + std::vector path; + path.reserve(segments + 1); + for (int i = 0; i <= segments; ++i) + { + const double a = from + (to - from) * i / segments; + path.push_back({cx + radius * std::cos(a), cy + radius * std::sin(a), halfWidth}); + } + return path; +} + +/// A zigzag in a frame, the trail a switchback climbs: it starts at `start` along the frame on the +/// left side (`span` across), runs down that side to the first leg at `firstLeg`, crosses to the +/// other side, turns down that side by `pitch`, crosses back, and so on for `legs` legs, then runs +/// along whichever side it finished on to `finish`. `legs` holds each leg's straight run on its own, +/// short of the turns at its ends by the path's half width, so a caller can tell leg from turn. +/// `pitch` is signed: positive steps the legs back towards the frame's origin. +struct Zigzag +{ + std::vector path; + std::vector> legs; + double finishAcross = 0; // the side the path finishes on +}; +Zigzag zigzagPath(const AxisFrame &, double start, double firstLeg, double pitch, int legs, double span, + double finish, double halfWidth); + +/// Visits every tile within `halfWidth` of the circle of `radius` round (cx, cy), through the wrap: +/// `visit(tile, gate)` with the index into `gates` of the gate the tile lies in - a gap reaching +/// `gateHalfWidth` tiles either side of the ring's point at that angle (radians), measured along the +/// ring - or -1 for the ring itself. A wall of stone round an arena with ramps through it, or a moat +/// with bridges over it. +template +void ringWithGates(const Torus &t, double cx, double cy, double radius, double halfWidth, + const std::vector &gates, double gateHalfWidth, Visit visit) +{ + const int reach = int(std::ceil(radius + halfWidth)) + 1; + const int x0 = int(std::lround(cx)), y0 = int(std::lround(cy)); + for (int y = y0 - reach; y <= y0 + reach; ++y) + for (int x = x0 - reach; x <= x0 + reach; ++x) + { + const double dx = x - cx, dy = y - cy, r = std::hypot(dx, dy); + if (std::abs(r - radius) >= halfWidth) + continue; + const double angle = std::atan2(dy, dx); + int gate = -1; + for (size_t g = 0; g < gates.size() && gate < 0; ++g) + { + const double turn = std::remainder(angle - gates[g], 2 * kPi); + if (std::abs(turn) * r <= gateHalfWidth) + gate = int(g); + } + visit(t.at(x, y), gate); + } +} + /// A path laid out round (cx, cy) placed on the map by `stretch`: its points move, its half widths /// stay in tiles. inline std::vector stretchPath(const std::vector &path, double cx, @@ -218,4 +281,103 @@ inline std::vector stretchPath(const std::vector &path } return placed; } + +/// Exact geometry for designs whose rasterization must agree with itself to the tile: lines and +/// polygons on a fixed-point grid of kSubtile units per tile, computed in integers, so two polygons +/// that share an edge split its tiles between them the same way on every platform and on both +/// sides of the wrap. A tile (x, y) spans [x, x + 1) * kSubtile on each axis; subtileCentre is the +/// point in its middle. +constexpr int kSubtile = 16; +struct SubtilePoint +{ + long long x = 0, y = 0; + bool operator==(const SubtilePoint &o) const { return x == o.x && y == o.y; } +}; +inline SubtilePoint subtileCentre(int tileX, int tileY) +{ + return {tileX * (long long)kSubtile + kSubtile / 2, tileY * (long long)kSubtile + kSubtile / 2}; +} +/// The tile a fixed-point position lies in, before wrapping. +inline long long subtileTile(long long v) +{ + return v >= 0 ? v / kSubtile : -((-v + kSubtile - 1) / kSubtile); +} + +/// Every tile the segment from `a` to `b` passes through, end tiles included, as unwrapped tile +/// coordinates in order from `a`. Where the segment crosses a tile corner exactly, both tiles beside +/// the corner are listed as well, so wherever two tiles of the line touch only at a corner, the two +/// tiles across that corner are on the line too: no unit can step across it, even diagonally, +/// however the segment slants. +std::vector> sealedSegmentTiles(SubtilePoint a, SubtilePoint b); + +/// Sets `value` on every tile of sealedSegmentTiles along each segment of `points`, through the +/// wrap: a wall line that seals at any angle. A closed path joins its last point back to its first. +void traceSealedPath(std::vector &mask, const Torus &, + const std::vector &points, unsigned char value = 1, + bool closed = false); + +/// Visits each tile whose centre lies inside the polygon `outline` (any simple polygon, either +/// winding, coordinates unwrapped) with its wrapped tile index. A centre exactly on an edge +/// belongs to the side to its right, or below for a horizontal edge, so polygons that tile the +/// plane with shared corners cover every tile exactly once between them. +template +void forEachTileInPolygon(const Torus &t, const std::vector &outline, Visit visit); + +/// Sets `value` on every tile forEachTileInPolygon visits. +void fillPolygon(std::vector &mask, const Torus &, + const std::vector &outline, unsigned char value = 1); + +template +void forEachTileInPolygon(const Torus &t, const std::vector &outline, Visit visit) +{ + const size_t n = outline.size(); + if (n < 3) + return; + long long top = outline[0].y, bottom = outline[0].y; + for (const SubtilePoint &p : outline) + { + top = std::min(top, p.y); + bottom = std::max(bottom, p.y); + } + // A row's centre line y = row * kSubtile + kSubtile / 2 meets an edge when it lies in the edge's + // half-open span [lower y, upper y); the crossing's x is kept as a fraction, num / den. + struct Crossing + { + long long num, den; + }; + std::vector crossings; + for (long long row = subtileTile(top); row <= subtileTile(bottom); ++row) + { + const long long yc = row * kSubtile + kSubtile / 2; + crossings.clear(); + for (size_t k = 0; k < n; ++k) + { + SubtilePoint a = outline[k], b = outline[(k + 1) % n]; + if (a.y == b.y) + continue; + if (a.y > b.y) + std::swap(a, b); + if (yc < a.y || yc >= b.y) + continue; + crossings.push_back({a.x * (b.y - a.y) + (yc - a.y) * (b.x - a.x), b.y - a.y}); + } + std::sort(crossings.begin(), crossings.end(), [](const Crossing &p, const Crossing &q) + { return p.num * q.den < q.num * p.den; }); + for (size_t k = 0; k + 1 < crossings.size(); k += 2) + { + // Tiles whose centre xc satisfies left <= xc < right, compared exactly. + const Crossing &left = crossings[k], &right = crossings[k + 1]; + auto firstCentreAtOrAfter = [](const Crossing &c) + { + // Smallest column whose centre column * kSubtile + kSubtile / 2 >= num / den. + const long long shifted = c.num - (long long)(kSubtile / 2) * c.den; + const long long span = (long long)kSubtile * c.den; + return shifted >= 0 ? (shifted + span - 1) / span : -((-shifted) / span); + }; + const long long from = firstCentreAtOrAfter(left), to = firstCentreAtOrAfter(right); + for (long long column = from; column < to; ++column) + visit(t.at(int(column), int(row))); + } + } +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Geometry.h b/src/map/generator/shared/Geometry.h index c7eef2059..a5f64fb01 100644 --- a/src/map/generator/shared/Geometry.h +++ b/src/map/generator/shared/Geometry.h @@ -49,6 +49,23 @@ struct Stretch /// The larger of the two scales: how much farther any design distance may reach on the map. double longest() const { return std::max(sx, sy); } }; +/// A frame laid along a heading from an origin: `at(along, across)` is the point `along` tiles down +/// the heading (radians) and `across` tiles to its left, and `project` turns a map offset from the +/// origin back into (along, across). A colony's axis out from the map's middle is the usual frame: +/// a trail, a gate or a tower designed once in it lands the same way round every colony. +struct AxisFrame +{ + double x, y, angle; + ShapePoint at(double along, double across) const + { + return {x + along * std::cos(angle) - across * std::sin(angle), + y + along * std::sin(angle) + across * std::cos(angle)}; + } + ShapePoint project(double dx, double dy) const + { + return {dx * std::cos(angle) + dy * std::sin(angle), -dx * std::sin(angle) + dy * std::cos(angle)}; + } +}; // Area-preserving stretch and rotation. Geometry stays independent of terrain and wrapping. class ShapeTransform { diff --git a/src/map/generator/shared/GraphMaze.cpp b/src/map/generator/shared/GraphMaze.cpp new file mode 100644 index 000000000..4c06e784c --- /dev/null +++ b/src/map/generator/shared/GraphMaze.cpp @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "GraphMaze.h" +#include "GenerationContext.h" +#include +namespace MapGeneration +{ +bool pocketsFit(const Tessellation &g, const std::vector &pocket) +{ + int start = -1, freeCells = 0; + for (int cell = 0; cell < g.cellCount(); ++cell) + { + if (!pocket[cell]) + { + ++freeCells; + start = cell; + continue; + } + bool door = false; + for (int edge : g.cells[cell].edges) + door = door || !pocket[g.other(edge, cell)]; + if (!door) + return false; + } + if (!freeCells) + return false; + std::vector seen(g.cells.size(), 0); + std::vector stack{start}; + seen[start] = 1; + int reached = 1; + while (!stack.empty()) + { + const int cell = stack.back(); + stack.pop_back(); + for (int edge : g.cells[cell].edges) + { + const int next = g.other(edge, cell); + if (!pocket[next] && !seen[next]) + { + seen[next] = 1; + ++reached; + stack.push_back(next); + } + } + } + return reached == freeCells; +} + +std::vector spreadPockets(const Tessellation &g, int count) +{ + std::vector isPocket(g.cells.size(), 0); + std::vector pockets; + std::vector nearest(g.cells.size(), 0); + for (int k = 0; k < count; ++k) + { + std::vector order; + for (int cell = 0; cell < g.cellCount(); ++cell) + if (!isPocket[cell]) + order.push_back(cell); + std::stable_sort(order.begin(), order.end(), + [&](int a, int b) { return nearest[a] > nearest[b]; }); + int chosen = -1; + for (int cell : order) + { + isPocket[cell] = 1; + if (pocketsFit(g, isPocket)) + { + chosen = cell; + break; + } + isPocket[cell] = 0; + } + if (chosen < 0) + return {}; + pockets.push_back(chosen); + for (int cell = 0; cell < g.cellCount(); ++cell) + nearest[cell] = pockets.size() == 1 ? g.distance2(cell, chosen) + : std::min(nearest[cell], g.distance2(cell, chosen)); + } + return pockets; +} + +bool carveSpanningTree(const Tessellation &g, GenerationContext &context, + const std::string &stream, const std::vector &blocked, + std::vector &open) +{ + std::vector free; + for (int cell = 0; cell < g.cellCount(); ++cell) + if (!blocked[cell]) + free.push_back(cell); + if (free.empty()) + return false; + std::vector visited(blocked); + std::vector stack{free[context.bounded(stream, std::uint32_t(free.size()))]}; + visited[stack.back()] = 1; + std::vector choices; + while (!stack.empty()) + { + const int cell = stack.back(); + choices.clear(); + for (int edge : g.cells[cell].edges) + if (!visited[g.other(edge, cell)]) + choices.push_back(edge); + if (choices.empty()) + { + stack.pop_back(); + continue; + } + const int edge = choices[context.bounded(stream, std::uint32_t(choices.size()))]; + const int next = g.other(edge, cell); + open[edge] = 1; + visited[next] = 1; + stack.push_back(next); + } + return std::all_of(visited.begin(), visited.end(), [](unsigned char v) { return v != 0; }); +} + +std::vector openPocketDoors(const Tessellation &g, GenerationContext &context, + const std::string &stream, const std::vector &pockets, + const std::vector &isPocket, + std::vector &open) +{ + std::vector doors; + std::vector choices; + for (int pocket : pockets) + { + choices.clear(); + for (int edge : g.cells[pocket].edges) + if (!isPocket[g.other(edge, pocket)]) + choices.push_back(edge); + if (choices.empty()) + { + doors.push_back(-1); + continue; + } + const int edge = choices[context.bounded(stream, std::uint32_t(choices.size()))]; + open[edge] = 1; + doors.push_back(edge); + } + return doors; +} + +void openLoops(const Tessellation &g, GenerationContext &context, const std::string &stream, + const std::vector &blocked, int percent, + std::vector &open) +{ + std::vector closed; + int freeCells = 0; + for (int cell = 0; cell < g.cellCount(); ++cell) + { + if (blocked[cell]) + continue; + ++freeCells; + for (int edge : g.cells[cell].edges) + if (g.edges[edge].cells[0] == cell && !open[edge] && !blocked[g.other(edge, cell)]) + closed.push_back(edge); + } + context.shuffle(closed.begin(), closed.end(), stream); + const size_t extra = std::min(closed.size(), size_t(freeCells * percent / 100)); + for (size_t i = 0; i < extra; ++i) + open[closed[i]] = 1; +} + +std::vector deadEnds(const Tessellation &g, const std::vector &open, + const std::vector &blocked, std::vector &exitEdge) +{ + std::vector ends; + exitEdge.assign(g.cells.size(), -1); + for (int cell = 0; cell < g.cellCount(); ++cell) + { + if (blocked[cell]) + continue; + int degree = 0, exit = -1; + for (int edge : g.cells[cell].edges) + if (open[edge]) + { + ++degree; + exit = edge; + } + if (degree == 1) + { + ends.push_back(cell); + exitEdge[cell] = exit; + } + } + return ends; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/GraphMaze.h b/src/map/generator/shared/GraphMaze.h new file mode 100644 index 000000000..f92a2f8b8 --- /dev/null +++ b/src/map/generator/shared/GraphMaze.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Tessellation.h" +#include +#include +struct GenerationContext; +namespace MapGeneration +{ +// A maze carved through a Tessellation's cells: which edges are open passages, which stay walls. +// Every function here works on the cell graph alone, so the same maze logic runs on squares, +// hexagons or warped cells. Some cells can be set aside as pockets - colonies' homes - that the +// tree never passes through, each opened later by a single door so it is a cul-de-sac. `open` holds +// one flag per edge. + +/// Whether a set of pocket cells leaves a layout a maze can be carved in: every pocket borders at +/// least one other cell to put its door in, and the other cells are all joined together. +bool pocketsFit(const Tessellation &, const std::vector &pocket); + +/// `count` pocket cells spread as far apart as the torus allows, as a pattern fixed by the tiling +/// alone, so a validator can check exactly what generation will use: farthest-point spreading +/// between cell centres from cell 0, taking at each step the farthest cell that still leaves +/// pocketsFit (the lowest index on a tie). Empty if the tiling can't hold them. +std::vector spreadPockets(const Tessellation &, int count); + +/// Opens a spanning tree of the cells not `blocked` with a recursive backtracker from a random +/// start: long winding passages with comparatively few, deep dead ends. Returns whether every such +/// cell was reached. +bool carveSpanningTree(const Tessellation &, GenerationContext &, const std::string &stream, + const std::vector &blocked, std::vector &open); + +/// Opens one door into each pocket, through a random edge onto a cell that isn't a pocket, and +/// returns each pocket's door edge in the order given. +std::vector openPocketDoors(const Tessellation &, GenerationContext &, const std::string &stream, + const std::vector &pockets, + const std::vector &isPocket, + std::vector &open); + +/// Opens extra walls between cells that aren't `blocked`: `percent` of those cells' count, chosen +/// at random from the closed edges between them, giving second routes to flank along. +void openLoops(const Tessellation &, GenerationContext &, const std::string &stream, + const std::vector &blocked, int percent, + std::vector &open); + +/// The cells, other than `blocked` ones, with exactly one open edge, and that edge for each (by +/// cell index; -1 elsewhere). +std::vector deadEnds(const Tessellation &, const std::vector &open, + const std::vector &blocked, std::vector &exitEdge); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Homes.h b/src/map/generator/shared/Homes.h new file mode 100644 index 000000000..34df5edd7 --- /dev/null +++ b/src/map/generator/shared/Homes.h @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Drawing.h" +#include "FertilityField.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Planting.h" +#include "Resources.h" +#include +#include +#include +#include +namespace MapGeneration +{ +// Homes that stand alone in the sea, as a round island turned to face out from the map's middle +// with its door on the inner side: the swarm stands in from the middle towards the door, one pond +// lies out behind it and a second, if any, on one flank. Every pond keeps a gap of land to the home's +// coast, so its beach never meets the sea's. Carousel and Switchbacks both build their homes this way. + +/// The farthest a pond home's first pond lies from its middle beyond its own radius, and the swarm from +/// its middle: a short walk apart however big the home, so the kit round the pond is near the swarm. +constexpr double kPondHomeReach = 8; + +/// How a pond home is proportioned: its radius, its ponds' radius, the land kept between a pond and +/// the coast, and how many ponds (0 to 2). +struct PondHomeStyle +{ + double radius, pondRadius, lakeSeaGap; + int ponds; +}; + +/// Stamps a pond home: `visit(tile)` for every tile of the `home` outline centred at `centre` and +/// turned to `axis` (the heading out from the map's middle), and its ponds' outlines into `pondMask`, +/// turned the same way. Returns the first pond's centre, where the kit is planted. +template +ShapePoint stampPondHome(const Torus &t, ShapePoint centre, double axis, const RadialShape &home, + const RadialShape &pond, const PondHomeStyle &style, + std::vector &pondMask, Visit visit) +{ + forEachTileInShape(t, centre.x, centre.y, home, axis, [&](int i, double, double) { visit(i); }); + // The first pond lies out from the middle, as far out as the coast gap allows but never more than + // a short walk from the swarm, so a big home's kit still grows beside water; a second on the flank, + // as close. + const double along = std::min({0.4 * style.radius, style.radius - style.pondRadius - style.lakeSeaGap - 1, + style.pondRadius + kPondHomeReach}); + const double flank = std::min(0.55 * style.radius, style.pondRadius + kPondHomeReach + 4); + const double offsets[2][2] = {{along, 0}, {-0.05 * style.radius, -flank}}; + ShapePoint first = centre; + for (int p = 0; p < std::clamp(style.ponds, 0, 2); ++p) + { + const double px = centre.x + offsets[p][0] * std::cos(axis) - offsets[p][1] * std::sin(axis); + const double py = centre.y + offsets[p][0] * std::sin(axis) + offsets[p][1] * std::cos(axis); + if (p == 0) + first = {px, py}; + fillShape(pondMask, t, px, py, pond, axis); + } + return first; +} + +/// Where a pond home's swarm goes: in from the middle towards the door, as the top-left tile of its +/// 4x4 footprint, which is what placeSettlement measures from. +inline MapGeneratorPoint pondHomeAnchor(ShapePoint centre, double axis, double radius) +{ + const ShapePoint p = polarPoint(centre.x, centre.y, std::min(0.3 * radius, kPondHomeReach), axis + kPi); + return MapGeneratorPoint(int(std::lround(p.x)) - 2, int(std::lround(p.y)) - 2); +} + +/// A pond home's starter kit: wheat and wood either side of its first pond on the swarm's side, where +/// they regrow, and a stone clump out behind the pond. +template +void plantPondHomeKit(Map &map, const Torus &t, GenerationContext &context, ShapePoint firstPond, + double axis, double pondRadius, int wheat, int wood, Eligible eligible) +{ + const KitFrame frame{int(std::lround(firstPond.x)), int(std::lround(firstPond.y)), axis}; + const double reach = pondRadius * 1.2 + 3; + const Kit kit{frame.at(-0.4 * reach, -reach, 12), frame.at(-0.4 * reach, reach, 12), + frame.at(reach + 3, 0, 10), wheat, wood, 2}; + plantKit(map, t, context, kit, eligible); +} + +/// What a stretch of ground carries (a home beyond its starter kit, a commons, a plaza), given how many +/// tiles it has. +struct GroundAmounts +{ + int wheat, wood, outcrops, groves; +}; + +/// A ground's ambient layer (a home's after its kit, a plaza's), planted on its `eligible` tiles: +/// farmland on the fertile ground in patches, then stone outcrops and fruit groves dropped at random. +/// +/// Farmland takes the fertile tiles whose `patchAt(tile)` is in the top 55% of the fertile ground's +/// (so it lies in patches with gaps to walk and build in), most fertile first, and deals them into +/// wheat and wood by `splitAt(tile)` (plantFields). `amountsFor(area)` sizes all four layers from the +/// ground's tile count. Outcrops are one-tile stone clumps from `stoneStream`; groves are one-tile +/// clumps of a fruit drawn from `fruitStream`. A generator that stamps one design into every wedge +/// samples `patchAt` and `splitAt` in the wedge frame, so every home gets the same patches. +template +void furnishGround(Map &map, const Torus &t, GenerationContext &context, + const Fertility::Field &fertility, Eligible eligible, PatchAt patchAt, + SplitAt splitAt, AmountsFor amountsFor, const char *stoneStream, + const char *fruitStream) +{ + const int n = t.w * t.h; + std::vector ground; + std::vector> farm; + std::vector levels; + for (int i = 0; i < n; ++i) + if (eligible(i)) + { + ground.push_back(i); + if (fertility.at(i % t.w, i / t.w) > 0) + levels.push_back(patchAt(i)); + } + float cut = 0; + if (!levels.empty()) + { + std::nth_element(levels.begin(), levels.begin() + levels.size() * 45 / 100, levels.end()); + cut = levels[levels.size() * 45 / 100]; + } + for (int i : ground) + { + const std::uint32_t f = fertility.at(i % t.w, i / t.w); + if (f > 0 && patchAt(i) >= cut) + farm.push_back({-double(f), i}); + } + std::stable_sort(farm.begin(), farm.end()); + std::vector chosen; + for (const auto &entry : farm) + chosen.push_back(entry.second); + const GroundAmounts amounts = amountsFor(int(ground.size())); + plantFields(map, t, chosen, amounts.wheat, amounts.wood, splitAt); + scatterClumps(context, t, ground, amounts.outcrops, stoneStream, eligible, + [&](MapGeneratorPoint p) { placeResourceClump(map, context, p, STONE, 1); }); + scatterClumps(context, t, ground, amounts.groves, fruitStream, eligible, + [&](MapGeneratorPoint p) + { + placeResourceClump(map, context, p, + CHERRY + int(context.bounded(fruitStream, 3)), 1); + }); +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Planting.cpp b/src/map/generator/shared/Planting.cpp index 24b295354..1e1d5ef31 100644 --- a/src/map/generator/shared/Planting.cpp +++ b/src/map/generator/shared/Planting.cpp @@ -94,8 +94,13 @@ std::vector algaeGrowthChance(const Map &map, const Torus &t) return chance; } -void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char *stream, - int algaePercent, const AlgaeBand &band, const WedgeFrame *wedges) +namespace +{ +// seedAlgae's work, with the clumps shared out between `groups` groups of water (one group when +// `groupAt` is null); a tile whose group is negative takes no clump. +template +void seedAlgaeIn(Map &map, GenerationContext &context, const Torus &t, const char *stream, + int algaePercent, const AlgaeBand &band, int groups, const GroupAt *groupAt) { const int n = t.w * t.h; std::vector water; @@ -124,7 +129,7 @@ void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char // The count follows the whole band. With a best share, the clumps then go only on the water // in it where algae regrows most readily, so the same amount of algae sits where it lasts. const int clumps = scaledCount(int(water.size()) / band.tilesPerClump, algaePercent); - if (band.bestShare <= 0 && !wedges) + if (band.bestShare <= 0 && !groupAt) { for (int clump = 0; clump < clumps; ++clump) placeResourceClump(map, context, water[context.bounded(stream, water.size())], ALGA, @@ -133,13 +138,13 @@ void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char } // Only the band's own tiles are measured: the test costs 961 lookups a tile. const AlgaeGrowth growth(map, t); - const int groups = wedges ? wedges->teams : 1; std::vector>> byGroup(groups); for (const MapGeneratorPoint &p : water) { const int i = p.y * t.w + p.x; - byGroup[wedges ? wedges->cell(p.x, p.y).k : 0].push_back( - {band.bestShare > 0 ? -growth.at(i) : 0.0, i}); + const int group = groupAt ? (*groupAt)(p.x, p.y) : 0; + if (group >= 0 && group < groups) + byGroup[group].push_back({band.bestShare > 0 ? -growth.at(i) : 0.0, i}); } for (auto &group : byGroup) { @@ -155,6 +160,28 @@ void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char } } } +} // namespace + +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char *stream, + int algaePercent, const AlgaeBand &band, const WedgeFrame *wedges) +{ + if (!wedges) + { + const auto none = [](int, int) { return 0; }; + seedAlgaeIn(map, context, t, stream, algaePercent, band, 1, + band.bestShare > 0 ? &none : nullptr); + return; + } + const auto wedgeOf = [&](int x, int y) { return wedges->cell(x, y).k; }; + seedAlgaeIn(map, context, t, stream, algaePercent, band, wedges->teams, &wedgeOf); +} + +void seedAlgae(Map &map, GenerationContext &context, const Torus &t, const char *stream, + int algaePercent, const AlgaeBand &band, const std::vector &groupOf, int groups) +{ + const auto groupAt = [&](int x, int y) { return groupOf[y * t.w + x]; }; + seedAlgaeIn(map, context, t, stream, algaePercent, band, std::max(1, groups), &groupAt); +} void stockIslands(Map &map, GenerationContext &context, const std::vector &islands, const char *stream) diff --git a/src/map/generator/shared/Planting.h b/src/map/generator/shared/Planting.h index 95ef8293c..08774d04e 100644 --- a/src/map/generator/shared/Planting.h +++ b/src/map/generator/shared/Planting.h @@ -137,6 +137,46 @@ void plantFields(Map &map, const Torus &t, std::vector tiles, int wheat, in map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheatShare ? CORN : WOOD, 1); } +/// Clumps round a circle, the same at every angle: at each of `angles` (radians) round (cx, cy), a +/// clump of `type` and `clumpRadius` grown from the eligible tile nearest the point on the circle of +/// `radius`, searched within `within` tiles. A prize or an outcrop designed once per colony lands the +/// same way at every colony's angle. Returns how many were placed. +template +int plantRound(Map &map, const Torus &t, GenerationContext &context, double cx, double cy, double radius, + const std::vector &angles, int type, int clumpRadius, int within, Eligible eligible) +{ + int planted = 0; + for (const double a : angles) + { + const int seed = seedNear(t, int(std::lround(cx + radius * std::cos(a))), + int(std::lround(cy + radius * std::sin(a))), within, eligible); + if (seed < 0) + continue; + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), type, clumpRadius); + ++planted; + } + return planted; +} + +/// An orchard of the three fruits round a circle: at each of `angles` (radians) round (cx, cy), a grove +/// of cherries, one of oranges and one of prunes, `spacing` tiles apart along the circle of `radius`, +/// each a clump of `clumpRadius` grown from the eligible tile nearest its point within `within`. Every +/// colony finds the same three fruits at its own angle, which is how a shared prize stays fair. +/// Returns how many groves were planted. +template +int plantOrchard(Map &map, const Torus &t, GenerationContext &context, double cx, double cy, + double radius, const std::vector &angles, double spacing, int within, + int clumpRadius, Eligible eligible) +{ + int planted = 0; + for (const double angle : angles) + for (int fruit = 0; fruit < 3; ++fruit) + planted += plantRound(map, t, context, cx, cy, radius, + {angle + (fruit - 1) * spacing / std::max(1.0, radius)}, CHERRY + fruit, + clumpRadius, within, eligible); + return planted; +} + /// `count` clumps dropped on random tiles of `ground`: for each, up to `attempts` tiles are drawn /// from `stream` and the first `eligible` one gets `place(point)`, which places the clump and may /// draw its type and size from the same stream. Returns how many were placed. @@ -206,6 +246,11 @@ struct AlgaeBand /// one best share taken over the whole map would favour some wedges' water over others'. void seedAlgae(Map &, GenerationContext &, const Torus &, const char *stream, int algaePercent, const AlgaeBand &, const WedgeFrame *wedges = nullptr); +/// The same, with the clumps shared out equally between labelled groups of water that are not wedges +/// (every colony's own bay, say): `groupOf` gives each tile's group, 0 to `groups` - 1, or -1 for +/// water that belongs to none and takes no clump. +void seedAlgae(Map &, GenerationContext &, const Torus &, const char *stream, int algaePercent, + const AlgaeBand &, const std::vector &groupOf, int groups); /// Each island carries one themed prize at its middle: stone, a fruit or wheat, so finding one /// feels like a distinct find rather than an interchangeable resource dump. diff --git a/src/map/generator/shared/Settlements.cpp b/src/map/generator/shared/Settlements.cpp index 4da54cc92..c4afa4736 100644 --- a/src/map/generator/shared/Settlements.cpp +++ b/src/map/generator/shared/Settlements.cpp @@ -5,6 +5,7 @@ #include "GenerationContext.h" #include "GlobalContainer.h" #include "Unit.h" +#include #include namespace MapGeneration { @@ -123,4 +124,47 @@ bool placeSettlement(Game &game, GenerationContext &context, int team, game.teams[team]->createLists(); return true; } + +int placeTower(Game &game, int team, int level, double x, double y, int within, + const std::vector &allowed) +{ + const int type = globalContainer->buildingsTypes.getTypeNum("defencetower", level, false); + const BuildingType *tower = globalContainer->buildingsTypes.get(type); + if (!tower || team < 0 || team >= game.teamsCount() || !game.teams[team]) + return -1; + const Map &map = game.map; + const int w = map.getW(), h = map.getH(); + const int cx = int(std::lround(x)), cy = int(std::lround(y)); + int best = -1; + double nearest = std::numeric_limits::max(); + for (int dy = -within; dy <= within; ++dy) + for (int dx = -within; dx <= within; ++dx) + { + const int px = map.normalizeX(cx + dx), py = map.normalizeY(cy + dy); + bool fits = true; + for (int fy = 0; fy < tower->height && fits; ++fy) + for (int fx = 0; fx < tower->width && fits; ++fx) + fits = allowed[map.normalizeY(py + fy) * w + map.normalizeX(px + fx)] != 0; + if (!fits || !game.checkRoomForBuilding(px, py, tower, team, false)) + continue; + const double mx = cx + dx + tower->width / 2.0 - x, my = cy + dy + tower->height / 2.0 - y; + const double d = mx * mx + my * my; + if (d < nearest) + { + nearest = d; + best = py * w + px; + } + } + (void)h; + if (best < 0) + return -1; + Building *building = game.addBuilding(best % w, best / w, type, team, 1, 0); + if (!building) + return -1; + building->bullets = tower->maxBullets; + // The colony's lists were built when its swarm went down; the tower joins its turrets the way + // Team::createLists would have taken it in. + game.teams[team]->turrets.push_back(building); + return best; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Settlements.h b/src/map/generator/shared/Settlements.h index 1a59ade89..f3fe401ea 100644 --- a/src/map/generator/shared/Settlements.h +++ b/src/map/generator/shared/Settlements.h @@ -9,4 +9,13 @@ namespace MapGeneration // Mutates only the disposable candidate; any failure requires discarding it. bool placeSettlement(Game &, GenerationContext &, int team, const std::vector &home, MapGeneratorPoint preferredAnchor, const std::string &stream = "settlements"); + +/// A defence tower a colony starts with: the completed tower of `level` (0 to 2, shooting 5, 7 or 9 +/// tiles) whose 2x2 footprint lies wholly on `allowed` ground the tower can stand on, nearest the +/// point (x, y) within `within` tiles of it, stocked with as many bullets as it holds so it defends +/// from the first tick. Nearest is measured from the footprint's middle, and ties go to the first +/// footprint found in row order, so a site designed in a wedge lands the same way for every colony. +/// Returns the footprint's top-left tile, or -1 when no footprint fits. +int placeTower(Game &, int team, int level, double x, double y, int within, + const std::vector &allowed); } // namespace MapGeneration diff --git a/src/map/generator/shared/Sketch.cpp b/src/map/generator/shared/Sketch.cpp index a39dbaa89..80ca2945e 100644 --- a/src/map/generator/shared/Sketch.cpp +++ b/src/map/generator/shared/Sketch.cpp @@ -94,4 +94,24 @@ std::vector raiseIslands(TerrainSketch &terrain, const Torus &t, Generat } return islands; } + +void keepRoadInland(std::vector &road, const Torus &t, const std::vector &water, + int gap) +{ + const std::vector shore = stepsFrom(t, water); + for (int i = 0; i < t.size(); ++i) + if (road[i] && shore[i] >= 0 && shore[i] < gap) + road[i] = 0; +} + +std::vector roadTiles(const Torus &t, const std::vector &road) +{ + std::vector tiles(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + if (road[i]) + for (int dy = -1; dy <= 0; ++dy) + for (int dx = -1; dx <= 0; ++dx) + tiles[t.at(i % t.w + dx, i / t.w + dy)] = 1; + return tiles; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Sketch.h b/src/map/generator/shared/Sketch.h index d76020e91..7c78397f1 100644 --- a/src/map/generator/shared/Sketch.h +++ b/src/map/generator/shared/Sketch.h @@ -4,6 +4,8 @@ #include "TerrainType.h" #include #include +#include +#include #include #include class Map; @@ -51,6 +53,54 @@ void sprinkleSand(TerrainSketch &sketch, const Torus &t, const std::vector &road, const Torus &, const std::vector &water, + int gap); + +/// The tiles a sand road spoils for building and deposits: a tile takes its terrain from its four +/// corners, so every tile touching one of the road's vertices is no longer pure grass. +std::vector roadTiles(const Torus &, const std::vector &road); + +/// Grows one body of water to exactly `target` tiles from its seed, always taking the frontier tile +/// with the lowest `key(tile)` next, so it fills a hollow the way water would: a key of depth plus +/// distance from the seed keeps the outline round while the depth shapes it. The frontier moves +/// over the four cardinal neighbours onto tiles `eligible(tile)` allows, and never onto a tile +/// already in `water`; the seed itself is taken whatever `eligible` says. `queued` is scratch the +/// caller owns and `stamp` a value no earlier call used, so many bodies can share one buffer +/// without clearing it. Returns how many tiles it added; fewer than `target` only when the +/// eligible ground ran out. +template +int growWater(const Torus &t, std::vector &water, int seed, int target, + Eligible eligible, Key key, std::vector &queued, int stamp) +{ + using Entry = std::pair; + std::priority_queue, std::greater> frontier; + frontier.push({key(seed), seed}); + queued[seed] = stamp; + int grown = 0; + while (!frontier.empty() && grown < target) + { + const int tile = frontier.top().second; + frontier.pop(); + if (water[tile]) + continue; + water[tile] = 1; + ++grown; + for (const auto &s : kCardinalSteps) + { + const int next = t.at(tile % t.w + s[0], tile / t.w + s[1]); + if (eligible(next) && !water[next] && queued[next] != stamp) + { + queued[next] = stamp; + frontier.push({key(next), next}); + } + } + } + return grown; +} + /// A small island raised out in open water: its centre, how far its outline can reach, and /// every tile it covers. struct Island diff --git a/src/map/generator/shared/Territories.h b/src/map/generator/shared/Territories.h new file mode 100644 index 000000000..c4151cb75 --- /dev/null +++ b/src/map/generator/shared/Territories.h @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include "Sketch.h" +#include "Topology.h" +#include +#include +#include +#include +#include +#include +#include +namespace MapGeneration +{ +/// Ground shared out between claimants: each tile's territory (-1 for ground none claimed) and each +/// territory's size in tiles. +struct Territories +{ + std::vector labels; + std::vector areas; + int smallest() const + { + int least = areas.empty() ? 0 : areas[0]; + for (int a : areas) + least = std::min(least, a); + return least; + } + int largest() const + { + int most = 0; + for (int a : areas) + most = std::max(most, a); + return most; + } +}; + +/// Shares the `eligible` ground out into territories of equal area, one per list of seed tiles, +/// grown together across the torus's wrap. +/// +/// Every territory first takes its seeds (a seed already taken by an earlier territory stays +/// there). Then, one tile at a time, the territory that is smallest so far - the lowest-numbered +/// one on a tie - takes the cheapest eligible tile beside its ground, where a tile costs its steps +/// from that territory's seeds times 1000 plus `cost(tile)`. Taking only tiles beside its own ground +/// over the four cardinal neighbours keeps every territory in one piece; the step cost keeps it +/// compact, and a noise field in `cost` makes the borders between territories wander. A territory +/// with nothing left beside it stops growing and the others carry on, so the areas come out equal +/// to the tile unless some territory is boxed in. Eligible ground none could reach stays -1. +/// +/// Growth draws nothing at random: the result is a function of the ground, the seeds and the cost +/// alone, with ties broken by tile index, so a validator can grow the same territories again. +/// +/// Unlike wedges round the centre, which are equal only on a round design, this shares out a +/// square map's corners and the ground across its wrap as fairly as the middle, for any number of +/// claimants. +template +Territories growTerritories(const Torus &t, const std::vector &eligible, + const std::vector> &seeds, Cost cost) +{ + const int n = t.w * t.h, claimants = int(seeds.size()); + Territories result; + result.labels.assign(n, -1); + result.areas.assign(claimants, 0); + using Entry = std::pair; + using Frontier = std::priority_queue, std::greater>; + std::vector frontier(claimants); + std::vector> steps(claimants); + std::vector> queued(claimants); + const auto offer = [&](int k, int tile) + { + for (const auto &s : kCardinalSteps) + { + const int next = t.at(tile % t.w + s[0], tile / t.w + s[1]); + if (eligible[next] && result.labels[next] < 0 && !queued[k][next] && steps[k][next] >= 0) + { + queued[k][next] = 1; + frontier[k].push({std::int64_t(steps[k][next]) * 1000 + cost(next), next}); + } + } + }; + for (int k = 0; k < claimants; ++k) + { + steps[k] = stepsFrom(t, tileMask(t, seeds[k]), eligible); + queued[k].assign(n, 0); + for (int tile : seeds[k]) + if (eligible[tile] && result.labels[tile] < 0) + { + result.labels[tile] = k; + ++result.areas[k]; + } + } + for (int k = 0; k < claimants; ++k) + for (int tile : seeds[k]) + if (result.labels[tile] == k) + offer(k, tile); + std::vector growing(claimants, 1); + for (;;) + { + int k = -1; + for (int c = 0; c < claimants; ++c) + if (growing[c] && (k < 0 || result.areas[c] < result.areas[k])) + k = c; + if (k < 0) + break; + int taken = -1; + while (!frontier[k].empty() && taken < 0) + { + const int tile = frontier[k].top().second; + frontier[k].pop(); + if (result.labels[tile] < 0) + taken = tile; + } + if (taken < 0) + { + growing[k] = 0; + continue; + } + result.labels[taken] = k; + ++result.areas[k]; + offer(k, taken); + } + return result; +} +/// Trims the spurs a race for the cheapest tile leaves along territories' borders: for `passes` +/// passes, every labelled tile six or more of whose eight neighbours share another label takes that +/// label, all at once per pass. Tiles of `keep` (a territory's frontage) never change. Areas move by a +/// few tiles each pass, so a caller that needs them equal measures them again afterwards. +inline void smoothLabels(const Torus &t, std::vector &labels, int passes, + const std::vector &keep) +{ + for (int pass = 0; pass < passes; ++pass) + { + std::vector smoothed = labels; + for (int i = 0; i < t.size(); ++i) + { + if (labels[i] < 0 || keep[i]) + continue; + int best = labels[i], bestCount = 0; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int label = labels[t.at(i % t.w + dx, i / t.w + dy)]; + if ((!dx && !dy) || label < 0 || label == labels[i]) + continue; + int count = 0; + for (int ey = -1; ey <= 1; ++ey) + for (int ex = -1; ex <= 1; ++ex) + count += (ex || ey) && labels[t.at(i % t.w + ex, i / t.w + ey)] == label; + if (count > bestCount) + { + bestCount = count; + best = label; + } + } + if (bestCount >= 6) + smoothed[i] = best; + } + labels.swap(smoothed); + } +} + +/// The ground `sources` cannot walk to: every tile of `ground` a flood from them over `ground` does not +/// reach. Land a lake or a wall has closed off, which a design usually fills rather than leaves as a +/// pocket nobody can use. +inline std::vector strandedGround(const Torus &t, const std::vector &sources, + const std::vector &ground) +{ + const std::vector reach = stepsFrom(t, tileMask(t, sources), ground); + std::vector stranded(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + stranded[i] = ground[i] && reach[i] < 0; + return stranded; +} + +/// A lake of exactly `target` tiles at the far end of a region: `depth` gives every tile of the +/// region its steps from the region's way in (-1 where not in it), and `room` its steps from anything +/// the lake must keep clear of; only tiles at least `gap` of room away may be water. The seed is deep +/// and roomy - the tile with the most depth plus twice its room, room counted only up to what a round +/// lake of the target needs - in a stretch of such tiles big enough for the whole lake, and the lake +/// grows from it by distance from the seed, pulled towards the far end by depth and roughened by +/// `noiseAt(tile)` (0 to 1). Every region given the same target gets a lake of the same size, which +/// is how a private lake stays fair when the regions are not the same shape. Adds the lake to +/// `water`; returns its size, 0 when no stretch of the region had room for it. +template +int growFarLake(const Torus &t, std::vector &water, const std::vector &depth, + const std::vector &room, int gap, int target, NoiseAt noiseAt, + std::vector &queued, int stamp) +{ + const int n = t.size(); + std::vector roomy(n, 0); + for (int i = 0; i < n; ++i) + roomy[i] = depth[i] >= 0 && room[i] >= gap && !water[i]; + const std::vector stretch = connectedRegions(roomy, t.w, t.h, true); + std::vector stretchSize; + for (int label : stretch) + if (label >= 0) + { + if (label >= int(stretchSize.size())) + stretchSize.resize(label + 1, 0); + ++stretchSize[label]; + } + const int enough = gap + int(std::ceil(std::sqrt(target / 3.14159265358979))); + const auto score = [&](int i) { return depth[i] + 2 * std::min(room[i], enough); }; + int seed = -1; + for (int i = 0; i < n; ++i) + if (roomy[i] && stretchSize[stretch[i]] >= target && (seed < 0 || score(i) > score(seed))) + seed = i; + if (seed < 0) + return 0; + const int far = depth[seed]; + return growWater( + t, water, seed, target, [&](int i) { return roomy[i] != 0; }, + [&](int i) + { + const double d = std::sqrt(double(t.dist2(seed % t.w, seed / t.w, i % t.w, i / t.w))); + return std::int64_t(d * 1000) + (far - depth[i]) * 250LL + std::int64_t(noiseAt(i) * 2500); + }, + queued, stamp); +} + +/// The site in a region the same number of steps from its way in as every other region's: among the +/// tiles whose `depth` is within `spread` of `target` (trying 0, then 1, up to `spread`), the one with +/// the most `room` (steps from anything a site must keep clear of), at least `minimumRoom`; ties go to +/// the first in row order. -1 when no tile qualifies. A swarm placed this way walks as far to the way +/// out as every other colony's, whatever shape its region is. +inline int siteAtDepth(const std::vector &depth, const std::vector &room, int target, + int minimumRoom, int spread) +{ + for (int within = 0; within <= spread; ++within) + { + int best = -1; + for (size_t i = 0; i < depth.size(); ++i) + if (depth[i] >= 0 && std::abs(depth[i] - target) <= within && room[i] >= minimumRoom && + (best < 0 || room[i] > room[best])) + best = int(i); + if (best >= 0) + return best; + } + return -1; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Tessellation.cpp b/src/map/generator/shared/Tessellation.cpp new file mode 100644 index 000000000..b1c1afc83 --- /dev/null +++ b/src/map/generator/shared/Tessellation.cpp @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Tessellation.h" +#include "GenerationContext.h" +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +long long floorDiv(long long a, long long b) +{ + return a >= 0 ? a / b : -((-a + b - 1) / b); +} +int wrapIndex(int v, int n) +{ + return ((v % n) + n) % n; +} +// Distance from `p` to the line through `a` and `b`, in subtile units. +double lineDistance(SubtilePoint p, SubtilePoint a, SubtilePoint b) +{ + const long long ux = b.x - a.x, uy = b.y - a.y; + const long long cross = ux * (p.y - a.y) - uy * (p.x - a.x); + const double length = std::sqrt(double(ux * ux + uy * uy)); + return length > 0 ? std::abs(double(cross)) / length : 0; +} +int edgeSteps(SubtilePoint a, SubtilePoint b) +{ + const long long dx = std::llabs(subtileTile(a.x) - subtileTile(b.x)); + const long long dy = std::llabs(subtileTile(a.y) - subtileTile(b.y)); + return int(std::max(dx, dy)); +} +} // namespace + +int Tessellation::cellAt(int column, int row) const +{ + return wrapIndex(row, rows) * columns + wrapIndex(column, columns); +} + +SubtilePoint Tessellation::imageNear(SubtilePoint p, SubtilePoint reference) const +{ + const long long w = (long long)t.w * kSubtile, h = (long long)t.h * kSubtile; + p.x -= floorDiv(p.x - reference.x + w / 2, w) * w; + p.y -= floorDiv(p.y - reference.y + h / 2, h) * h; + return p; +} + +std::vector Tessellation::outline(int cell) const +{ + const Cell &c = cells[cell]; + std::vector points; + for (size_t k = 0; k < c.corners.size(); ++k) + points.push_back({corners[c.corners[k]].x + c.cornerShifts[k].x, + corners[c.corners[k]].y + c.cornerShifts[k].y}); + return points; +} + +std::pair Tessellation::edgeEnds(int edge, int cell) const +{ + const Cell &c = cells[cell]; + const size_t n = c.edges.size(); + const size_t k = size_t(std::find(c.edges.begin(), c.edges.end(), edge) - c.edges.begin()); + const auto at = [&](size_t i) -> SubtilePoint + { + return {corners[c.corners[i]].x + c.cornerShifts[i].x, + corners[c.corners[i]].y + c.cornerShifts[i].y}; + }; + return {at(k % n), at((k + 1) % n)}; +} + +SubtilePoint Tessellation::centreAcross(int edge, int cell) const +{ + const int next = other(edge, cell); + // The same edge seen from both sides runs in opposite directions; the difference between its + // two copies is the shift that carries the neighbour beside this cell. + const SubtilePoint here = edgeEnds(edge, cell).first, there = edgeEnds(edge, next).second; + return {cells[next].centre.x + here.x - there.x, cells[next].centre.y + here.y - there.y}; +} + +RegionGraph Tessellation::neighbours() const +{ + RegionGraph graph(cells.size()); + for (int cell = 0; cell < cellCount(); ++cell) + for (int edge : cells[cell].edges) + graph[cell].push_back(other(edge, cell)); + return graph; +} + +long long Tessellation::distance2(int a, int b) const +{ + const SubtilePoint q = imageNear(cells[b].centre, cells[a].centre); + const long long dx = q.x - cells[a].centre.x, dy = q.y - cells[a].centre.y; + return dx * dx + dy * dy; +} + +int Tessellation::transform(int cell, int dColumns, int dRows, bool mirrorColumns, + bool mirrorRows) const +{ + int c = cells[cell].column, r = cells[cell].row; + if (shape == Shape::Square) + { + c = mirrorColumns ? (columns - c) % columns : c; + r = mirrorRows ? (rows - r) % rows : r; + return ((r + dRows) % rows) * columns + (c + dColumns) % columns; + } + // Offset rows: an odd row sits half a cell right, so mirroring across columns takes an odd + // row's cell c to -c - 1 rather than -c, and moving down an odd number of rows moves the column + // by half a cell, rounded by the row's parity. The row count is even, so mirroring rows keeps + // every row's parity. + if (mirrorColumns) + c = (r & 1) ? -c - 1 : -c; + if (mirrorRows) + r = (rows - r) % rows; + const int column = c + dColumns + int(floorDiv((r & 1) + dRows, 2)); + return cellAt(column, r + dRows); +} + +Tessellation squareTessellation(int width, int height, int cellSize) +{ + Tessellation g; + g.t = Torus(width, height); + g.shape = Tessellation::Shape::Square; + g.columns = std::max(1, width / std::max(1, cellSize)); + g.rows = std::max(1, height / std::max(1, cellSize)); + std::vector xs, ys; + for (int i = 0; i <= g.columns; ++i) + xs.push_back(i * width / g.columns); + for (int i = 0; i <= g.rows; ++i) + ys.push_back(i * height / g.rows); + // A corner is owned by the cell to its lower right, so corner (c, r) has the cell's index. + for (int r = 0; r < g.rows; ++r) + for (int c = 0; c < g.columns; ++c) + g.corners.push_back(subtileCentre(xs[c], ys[r])); + for (int r = 0; r < g.rows; ++r) + for (int c = 0; c < g.columns; ++c) + { + const int self = g.cellAt(c, r); + Tessellation::Cell cell; + cell.column = c; + cell.row = r; + cell.centre = subtileCentre((xs[c] + xs[c + 1]) / 2, (ys[r] + ys[r + 1]) / 2); + // Clockwise from the top right corner, so the edges run east, south, west, north. + cell.corners = {g.cellAt(c + 1, r), g.cellAt(c + 1, r + 1), g.cellAt(c, r + 1), self}; + const auto shift = [&](int column, int row) -> SubtilePoint + { + return {(long long)(column / g.columns) * width * kSubtile, + (long long)(row / g.rows) * height * kSubtile}; + }; + cell.cornerShifts = {shift(c + 1, r), shift(c + 1, r + 1), shift(c, r + 1), shift(c, r)}; + cell.edges = {2 * self, 2 * self + 1, 2 * g.cellAt(c - 1, r), + 2 * g.cellAt(c, r - 1) + 1}; + g.cells.push_back(cell); + } + for (int self = 0; self < g.cellCount(); ++self) + { + const auto &cell = g.cells[self]; + g.edges.push_back({{self, g.cellAt(cell.column + 1, cell.row)}, + {cell.corners[0], cell.corners[1]}}); + g.edges.push_back({{self, g.cellAt(cell.column, cell.row + 1)}, + {cell.corners[1], cell.corners[2]}}); + } + return g; +} + +Tessellation hexTessellation(int width, int height, int pitch) +{ + Tessellation g; + g.t = Torus(width, height); + g.shape = Tessellation::Shape::Hexagon; + pitch = std::max(1, pitch); + g.columns = std::max(2, width / pitch); + // A regular hexagon's rows are sqrt(3) / 2 of its pitch apart; round to the nearest even count. + const long long half = (10000LL * height * g.columns + 17320LL * width / 2) / (17320LL * width); + g.rows = 2 * int(std::max(1LL, half)); + const long long w = (long long)width * kSubtile, h = (long long)height * kSubtile; + // Positions as exact fractions of the map: x in halves of a column, y in thirds of a row. + const auto at = [&](long long halfColumns, long long thirdRows) -> SubtilePoint + { return {floorDiv(halfColumns * w, 2LL * g.columns), floorDiv(thirdRows * h, 3LL * g.rows)}; }; + // Each cell owns its top corner (index 2 * cell) and its upper right corner (2 * cell + 1). + for (int r = 0; r < g.rows; ++r) + for (int c = 0; c < g.columns; ++c) + { + const int p = r & 1; + g.corners.push_back(at(2 * c + p, 3 * r - 2)); + g.corners.push_back(at(2 * c + p + 1, 3 * r - 1)); + } + for (int r = 0; r < g.rows; ++r) + for (int c = 0; c < g.columns; ++c) + { + const int p = r & 1, self = g.cellAt(c, r); + const int east = g.cellAt(c + 1, r), west = g.cellAt(c - 1, r); + const int northEast = g.cellAt(c + p, r - 1), northWest = g.cellAt(c + p - 1, r - 1); + const int southEast = g.cellAt(c + p, r + 1), southWest = g.cellAt(c + p - 1, r + 1); + Tessellation::Cell cell; + cell.column = c; + cell.row = r; + cell.centre = at(2 * c + p, 3 * r); + // Clockwise from the upper right corner: lower right, bottom, lower left, upper left, + // top; so the edges run east, south-east, south-west, west, north-west, north-east. + cell.corners = {2 * self + 1, 2 * southEast, 2 * southWest + 1, + 2 * southWest, 2 * west + 1, 2 * self}; + // Each corner is stored with the cell that owns it; a neighbour past the seam owns the + // copy one map size away. + const auto shift = [&](int column, int row) -> SubtilePoint + { return {floorDiv(column, g.columns) * w, floorDiv(row, g.rows) * h}; }; + cell.cornerShifts = {shift(c, r), shift(c + p, r + 1), shift(c + p - 1, r + 1), + shift(c + p - 1, r + 1), shift(c - 1, r), shift(c, r)}; + cell.edges = {3 * self, 3 * self + 1, 3 * self + 2, + 3 * west, 3 * northWest + 1, 3 * northEast + 2}; + g.cells.push_back(cell); + const int owned[3] = {east, southEast, southWest}; + for (int k = 0; k < 3; ++k) + g.edges.push_back({{self, owned[k]}, {cell.corners[k], cell.corners[k + 1]}}); + } + return g; +} + +int shortestEdgeSteps(const Tessellation &g) +{ + int shortest = INT_MAX; + for (int edge = 0; edge < int(g.edges.size()); ++edge) + { + const auto ends = g.edgeEnds(edge, g.edges[edge].cells[0]); + shortest = std::min(shortest, edgeSteps(ends.first, ends.second)); + } + return shortest; +} + +int centreClearance(const Tessellation &g) +{ + double least = 1e18; + for (int cell = 0; cell < g.cellCount(); ++cell) + { + const std::vector points = g.outline(cell); + for (size_t k = 0; k < points.size(); ++k) + least = std::min(least, lineDistance(g.cells[cell].centre, points[k], + points[(k + 1) % points.size()])); + } + return int(std::floor(least / kSubtile)); +} + +int warpLimit(const Tessellation &g) +{ + double least = 1e18; + for (int cell = 0; cell < g.cellCount(); ++cell) + { + const std::vector points = g.outline(cell); + const size_t n = points.size(); + for (size_t k = 0; k < n; ++k) + for (size_t e = 0; e < n; ++e) + if (e != k && (e + 1) % n != k) + least = std::min(least, lineDistance(points[k], points[e], points[(e + 1) % n])); + } + // Three corners move at once (this one and the far edge's two ends), each by up to sqrt(2) times + // its reach on one axis. + return std::max(0, int(std::floor(least / (3 * std::sqrt(2.0)))) - 1); +} + +void warpCorners(Tessellation &g, int reach, const std::vector &walls, + int minimumGap, int minimumClearance, GenerationContext &context, + const std::string &stream) +{ + reach = std::min(reach, warpLimit(g)); + if (reach <= 0) + return; + const Torus &t = g.t; + std::vector> cornerCells(g.corners.size()), cornerWalls(g.corners.size()); + for (int cell = 0; cell < g.cellCount(); ++cell) + for (int corner : g.cells[cell].corners) + cornerCells[corner].push_back(cell); + for (int edge = 0; edge < int(g.edges.size()); ++edge) + if (walls[edge]) + for (int corner : g.edges[edge].corners) + cornerWalls[corner].push_back(edge); + + // The obstacles passages run between: every wall, and every corner no wall reaches. Each is kept + // as the tiles its line (or point) covers, with a bounding box for cheap rejections. + struct Obstacle + { + int edge, corner; + std::vector corners; + std::vector> tiles; + long long cx, cy, radius; + }; + std::vector obstacles; + std::vector> cornerObstacles(g.corners.size()); + for (int edge = 0; edge < int(g.edges.size()); ++edge) + if (walls[edge]) + obstacles.push_back({edge, -1, {g.edges[edge].corners[0], g.edges[edge].corners[1]}}); + for (int corner = 0; corner < int(g.corners.size()); ++corner) + if (cornerWalls[corner].empty()) + obstacles.push_back({-1, corner, {corner}}); + const auto trace = [&](Obstacle &o) + { + std::vector> raw; + if (o.edge >= 0) + { + const auto ends = g.edgeEnds(o.edge, g.edges[o.edge].cells[0]); + raw = sealedSegmentTiles(ends.first, ends.second); + } + else + raw = {{subtileTile(g.corners[o.corner].x), subtileTile(g.corners[o.corner].y)}}; + long long x0 = raw[0].first, x1 = x0, y0 = raw[0].second, y1 = y0; + o.tiles.clear(); + for (const auto &tile : raw) + { + x0 = std::min(x0, tile.first); + x1 = std::max(x1, tile.first); + y0 = std::min(y0, tile.second); + y1 = std::max(y1, tile.second); + o.tiles.push_back({t.x(int(tile.first)), t.y(int(tile.second))}); + } + o.cx = (x0 + x1) / 2; + o.cy = (y0 + y1) / 2; + o.radius = std::max(x1 - x0, y1 - y0) / 2 + 1; + }; + // Steps between two obstacles, the short way round; anything at least `enough` apart by their + // boxes alone reports `enough`. + const auto gap = [&](const Obstacle &a, const Obstacle &b, int enough) + { + const long long apart = + t.chebyshev(int(a.cx), int(a.cy), int(b.cx), int(b.cy)) - a.radius - b.radius; + if (apart >= enough) + return enough; + int least = enough; + for (const auto &p : a.tiles) + for (const auto &q : b.tiles) + least = std::min(least, t.chebyshev(p.first, p.second, q.first, q.second)); + return least; + }; + for (Obstacle &o : obstacles) + { + trace(o); + for (int corner : o.corners) + cornerObstacles[corner].push_back(int(&o - obstacles.data())); + } + // Pairs that could come within minimumGap once both move, and the gap each must keep: its + // unwarped gap, or minimumGap if that is less. Obstacles sharing a corner meet there by design. + const int drift = 2 * (reach / kSubtile + 2); + std::vector>> near(obstacles.size()); + for (size_t a = 0; a < obstacles.size(); ++a) + for (size_t b = a + 1; b < obstacles.size(); ++b) + { + bool touching = false; + for (int corner : obstacles[a].corners) + touching = touching || std::count(obstacles[b].corners.begin(), + obstacles[b].corners.end(), corner); + if (touching) + continue; + const int apart = gap(obstacles[a], obstacles[b], minimumGap + drift); + if (apart >= minimumGap + drift) + continue; + const int floor = std::min(apart, minimumGap); + near[a].push_back({int(b), floor}); + near[b].push_back({int(a), floor}); + } + + const auto clearanceOf = [&](int cell) + { + const std::vector points = g.outline(cell); + double least = 1e18; + for (size_t k = 0; k < points.size(); ++k) + least = std::min(least, lineDistance(g.cells[cell].centre, points[k], + points[(k + 1) % points.size()])); + return least; + }; + std::vector clearanceFloor(g.cells.size()); + for (int cell = 0; cell < g.cellCount(); ++cell) + clearanceFloor[cell] = std::min(clearanceOf(cell), double(minimumClearance) * kSubtile); + + for (size_t corner = 0; corner < g.corners.size(); ++corner) + { + const int dx = int(context.bounded(stream, std::uint32_t(2 * reach + 1))) - reach; + const int dy = int(context.bounded(stream, std::uint32_t(2 * reach + 1))) - reach; + const SubtilePoint home = g.corners[corner]; + for (int share = 1; share <= 16; share *= 2) + { + g.corners[corner] = {home.x + dx / share, home.y + dy / share}; + for (int o : cornerObstacles[corner]) + trace(obstacles[o]); + bool fits = true; + for (int cell : cornerCells[corner]) + fits = fits && clearanceOf(cell) >= clearanceFloor[cell]; + for (size_t k = 0; fits && k < cornerObstacles[corner].size(); ++k) + { + const int o = cornerObstacles[corner][k]; + for (const auto &pair : near[o]) + if (gap(obstacles[o], obstacles[pair.first], pair.second) < pair.second) + { + fits = false; + break; + } + } + if (fits) + break; + g.corners[corner] = home; + for (int o : cornerObstacles[corner]) + trace(obstacles[o]); + } + } +} + +std::vector labelTiles(const Tessellation &g) +{ + std::vector labels(size_t(g.t.size()), -1); + bool overlap = false; + for (int cell = 0; cell < g.cellCount(); ++cell) + forEachTileInPolygon(g.t, g.outline(cell), + [&](int tile) + { + overlap = overlap || labels[tile] >= 0; + labels[tile] = cell; + }); + if (overlap || std::find(labels.begin(), labels.end(), -1) != labels.end()) + return {}; + return labels; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Tessellation.h b/src/map/generator/shared/Tessellation.h new file mode 100644 index 000000000..9f2b38bb7 --- /dev/null +++ b/src/map/generator/shared/Tessellation.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Drawing.h" +#include "Grid.h" +#include "Topology.h" +#include +#include +struct GenerationContext; +namespace MapGeneration +{ +/// A polygon tiling of the torus: cells, the corners they share and the edges between them, with +/// no terrain attached. Positions are exact fixed-point (Drawing.h's SubtilePoint), so a tiling +/// labels the same tiles on every platform. The lattice wraps exactly, so an edge across the seam +/// is an edge like any other, and every cell lists its corners and edges in the same turning order. +/// +/// Edges are identities, not pairs of cells: on a torus two cells wide a cell meets the same +/// neighbour across two different edges, and each is its own wall or opening. +struct Tessellation +{ + enum class Shape + { + Square = 0, + Hexagon = 1 + }; + struct Cell + { + int column, row; + SubtilePoint centre; + /// Clockwise on the map (y grows downwards); edges[k] joins corners[k] and corners[k + 1]. + std::vector corners, edges; + /// For each corner, the whole map sizes (in subtile units) to add to its stored position to + /// get the copy that belongs to this cell. Fixed by the lattice, so it stays right however + /// the corners are warped, and exact even on a torus two cells across, where a corner's + /// copies on either side are equally near. + std::vector cornerShifts; + }; + struct Edge + { + /// The cell that owns the edge, then the cell across it. + int cells[2]; + /// The ends, in the owner's clockwise order. + int corners[2]; + }; + + Torus t{0, 0}; + Shape shape = Shape::Square; + int columns = 0, rows = 0; + /// Each corner's position. A corner shared across the wrap is stored once; a cell's + /// cornerShifts place the copy that belongs to it. + std::vector corners; + std::vector cells; + std::vector edges; + + int cellCount() const { return int(cells.size()); } + int cellAt(int column, int row) const; + int other(int edge, int cell) const + { + return edges[edge].cells[0] == cell ? edges[edge].cells[1] : edges[edge].cells[0]; + } + /// The copy of `p`, shifted by whole map sizes, nearest `reference`. + SubtilePoint imageNear(SubtilePoint p, SubtilePoint reference) const; + /// A cell's corners as one unwrapped polygon round its centre. + std::vector outline(int cell) const; + /// An edge's two ends as they lie on `cell`'s outline, in that cell's clockwise order. `cell` + /// must be one of the edge's two cells. + std::pair edgeEnds(int edge, int cell) const; + /// The other cell's centre across `edge`, placed beside `cell` rather than wherever it is + /// stored. + SubtilePoint centreAcross(int edge, int cell) const; + /// The tile holding a cell's centre. + int centreTileX(int cell) const { return int(subtileTile(cells[cell].centre.x)); } + int centreTileY(int cell) const { return int(subtileTile(cells[cell].centre.y)); } + /// Per cell, the cell across each of its edges, in edge order. + RegionGraph neighbours() const; + /// Squared distance between two cells' centres, the short way round, in subtile units. + long long distance2(int a, int b) const; + /// The cell a lattice symmetry takes `cell` to: mirrored across columns and/or rows, then moved + /// by whole columns and rows. Any such move maps the tiling onto itself, edges onto edges. + int transform(int cell, int dColumns, int dRows, bool mirrorColumns, bool mirrorRows) const; +}; + +/// Square cells, columns = width / cellSize across and rows = height / cellSize down. Boundaries +/// fall on whole tiles, i * width / columns, so when cellSize doesn't divide the map neighbouring +/// cells differ in pitch by at most a tile. Corners sit on tile centres: the tile holding a corner +/// is the tile two walls meet on. +Tessellation squareTessellation(int width, int height, int cellSize); + +/// Pointy-topped hexagons in offset rows, odd rows shifted half a cell right: at least `pitch` tiles +/// between neighbouring centres across a row (width / pitch columns, like squareTessellation, and +/// never fewer than two), with an even row count so the offset wraps. The pitches stretch to +/// whatever tiles the map exactly, so a hexagon may be a little taller or squatter than regular. +Tessellation hexTessellation(int width, int height, int pitch); + +/// The shortest edge of the tiling, in whole tiles between the tiles its two corners lie in, taking +/// the larger of the two axes (the steps a unit takes along it). +int shortestEdgeSteps(const Tessellation &); + +/// Least distance, in tiles rounded down, from any cell's centre to the line through any of its +/// edges. +int centreClearance(const Tessellation &); + +/// How far warpCorners may move a corner along each axis, in subtile units, so no cell can fold +/// over or lose its centre however the moves combine: a third of the smallest distance from a +/// corner to an edge of one of its cells that it isn't on, over the square root of two. +int warpLimit(const Tessellation &); + +/// Moves every corner by up to `reach` subtile units on each axis (never past warpLimit), drawn from +/// `stream`, so the cells become irregular polygons that still tile the torus. +/// +/// The warp keeps room for passages between the obstacles a design builds on the tiling: every edge +/// marked in `walls`, and every corner no wall reaches (where a design leaves a pond or post). A move +/// is taken only while every two obstacles that share no corner stay at least min(their unwarped gap, +/// minimumGap) steps apart, tile to tile, and every cell's centre stays at least min(unwarped, +/// minimumClearance) tiles from the lines of its edges; otherwise it is halved, down to no move at +/// all. Every corner costs exactly two draws whatever happens, and a reach of 0 draws nothing. +void warpCorners(Tessellation &, int reach, const std::vector &walls, int minimumGap, + int minimumClearance, GenerationContext &, const std::string &stream); + +/// Each tile's cell: the cell whose outline contains the tile's centre (forEachTileInPolygon's rule), +/// so every tile belongs to exactly one cell. Empty if some tile was left out or taken twice, which a +/// valid tiling never does. +std::vector labelTiles(const Tessellation &); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Walls.cpp b/src/map/generator/shared/Walls.cpp new file mode 100644 index 000000000..bb89ba9ab --- /dev/null +++ b/src/map/generator/shared/Walls.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Walls.h" +#include "Map.h" +#include "TerrainType.h" +#include +namespace MapGeneration +{ +std::vector seaMargin(const Map &map, const Torus &t, + const std::vector &sea, + const std::vector ¬Beach) +{ + const int n = t.w * t.h; + std::vector margin(n, 0), beach(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + if (map.isWater(x, y)) + continue; + beach[i] = map.getTerrainType(x, y) != GRASS && !notBeach[i]; + // A tile's corners are undermap vertices (x, y) to (x + 1, y + 1); the beach pass reaches a + // vertex one step further, so a sea vertex anywhere in the box one wider decides the tile. + for (int dy = -1; dy <= 2 && !margin[i]; ++dy) + for (int dx = -1; dx <= 2; ++dx) + if (sea[t.at(x + dx, y + dy)]) + { + margin[i] = 1; + break; + } + } + const std::vector joined = stepsFrom(t, margin, beach); + for (int i = 0; i < n; ++i) + if (joined[i] >= 0) + margin[i] = 1; + return margin; +} + +std::vector seaVertices(const Map &map, const Torus &t, const std::vector &lakes) +{ + std::vector sea(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + sea[i] = map.getUMTerrain(i % t.w, i / t.w) == WATER && !lakes[i]; + return sea; +} + +int seaEntry(const Map &map, const Torus &t, const std::vector &margin, + const std::vector &inside) +{ + const std::vector open = walkableTiles(map); + std::vector beach(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + beach[i] = open[i] && margin[i]; + const std::vector landed = stepsFrom(t, beach, open); + for (int i = 0; i < t.size(); ++i) + if (landed[i] >= 0 && !margin[i] && inside[i]) + return i; + return -1; +} + +std::vector sealCoasts(const Map &map, const Torus &t, + const std::vector &margin, + const std::vector &wallable) +{ + const int n = t.w * t.h; + std::vector stone(n, 0); + for (int i = 0; i < n; ++i) + { + if (!wallable[i] || map.getTerrainType(i % t.w, i / t.w) != GRASS) + continue; + for (int dy = -1; dy <= 1 && !stone[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + if (margin[t.at(i % t.w + dx, i / t.w + dy)]) + { + stone[i] = 1; + break; + } + } + return stone; +} + +std::vector labelBorders(const Torus &t, const std::vector &labels) +{ + const int n = t.w * t.h; + std::vector wall(n, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (labels[i] < 0) + continue; + for (int dy = -1; dy <= 1 && !wall[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int other = labels[t.at(x + dx, y + dy)]; + if (other >= 0 && other < labels[i]) + { + wall[i] = 1; + break; + } + } + } + return wall; +} + +std::vector labelBorders(const Torus &t, const std::vector &labels, int thickness) +{ + std::vector wall = labelBorders(t, labels); + for (int pass = 1; pass < std::clamp(thickness, 1, 3); ++pass) + { + std::vector thicker = wall; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (labels[i] < 0 || wall[i]) + continue; + for (int dy = -1; dy <= 1 && !thicker[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int j = t.at(x + dx, y + dy); + // The second pass walls the other region's side of the border; the third + // thickens either side. + if (wall[j] && labels[j] >= 0 && (pass > 1 || labels[j] != labels[i])) + { + thicker[i] = 1; + break; + } + } + } + wall.swap(thicker); + } + return wall; +} + +DesignedStone designedStone(const Map &map, const Torus &t, const std::vector &wall) +{ + const int n = t.w * t.h; + DesignedStone result; + result.stone.assign(n, 0); + for (int i = 0; i < n; ++i) + { + if (!wall[i]) + continue; + if (map.getTerrainType(i % t.w, i / t.w) == GRASS) + result.stone[i] = 1; + else if (result.gaps++ == 0) + result.firstGap = i; + } + return result; +} + +std::vector reachesWithShut(const Map &map, const Torus &t, + const std::vector &from, + const std::vector &shut) +{ + std::vector open = walkableTiles(map); + for (size_t i = 0; i < open.size(); ++i) + if (shut[i]) + open[i] = 0; + return stepsFrom(t, from, open); +} + +int pieceLeak(const Map &map, const Torus &t, const std::vector &piece, + const std::vector &shut) +{ + const int n = t.w * t.h; + std::vector open = walkableTiles(map); + for (int i = 0; i < n; ++i) + if (shut[i]) + open[i] = 0; + int pieces = 0; + for (int p : piece) + pieces = std::max(pieces, p + 1); + for (int p = 0; p < pieces; ++p) + { + std::vector from(n, 0); + for (int i = 0; i < n; ++i) + from[i] = piece[i] == p && open[i]; + const std::vector reach = stepsFrom(t, from, open); + for (int i = 0; i < n; ++i) + if (reach[i] >= 0 && piece[i] >= 0 && piece[i] != p) + return i; + } + return -1; +} + +int towerReach(const Torus &t, const std::vector &buildable, + const std::vector &target) +{ + // With every tile open, the eight-connected flood counts Chebyshev steps. + const std::vector distance = stepsFrom(t, target); + int best = INT_MAX; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (distance[i] < 0 || distance[i] >= best) + continue; + bool fits = true; + for (int dy = 0; dy < kTowerFootprint && fits; ++dy) + for (int dx = 0; dx < kTowerFootprint && fits; ++dx) + fits = buildable[t.at(x + dx, y + dy)]; + if (fits) + best = distance[i]; + } + return best; +} + +WalkSpread walkSpread(const Map &map, const Torus &t, const std::vector> &workers, + const std::vector &targets) +{ + WalkSpread spread; + const std::vector open = walkableTiles(map); + for (size_t team = 0; team < workers.size(); ++team) + { + const std::vector steps = stepsFrom(t, tileMask(t, workers[team]), open); + const int walk = steps[targets[team]]; + spread.steps.push_back(walk); + if (walk < 0) + { + if (spread.unreached < 0) + spread.unreached = int(team); + continue; + } + spread.shortest = std::min(spread.shortest, walk); + spread.longest = std::max(spread.longest, walk); + } + return spread; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Walls.h b/src/map/generator/shared/Walls.h new file mode 100644 index 000000000..899a42956 --- /dev/null +++ b/src/map/generator/shared/Walls.h @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include +#include +#include +class Map; +namespace MapGeneration +{ +// Walls a generator designs and the checks that prove they hold. Stone can never be cleared, so a +// wall of it is permanent; but stone only stands on pure grass and grass may never touch water, so +// a wall is always a band of grass tiles given stone, never the water's edge itself. These are the +// pieces every walled map needs: which land a swimmer can stand on, a wall round a coast that keeps +// such a swimmer on the beach, a wall between labelled regions that no unit can cross even +// diagonally, and the walks and tower ranges that a validator checks the result against. + +/// The land a unit landing from the sea can reach without crossing solid grass: every land tile +/// with a `sea` undermap vertex in the box its four corners touch, and every beach (land that is +/// not pure grass) joined to one. `sea` marks undermap vertices, so a generator decides which +/// water is sea and which (a home's lake) is not. `notBeach` marks sand that must not carry the +/// margin inland, such as a sand road. +std::vector seaMargin(const Map &, const Torus &, const std::vector &sea, + const std::vector ¬Beach); + +/// The sea for seaMargin: every water vertex of the map except those of `lakes`, the inland water a +/// generator keeps apart from the sea. +std::vector seaVertices(const Map &, const Torus &, const std::vector &lakes); + +/// Whether a sealed coast holds: the first tile of `inside` (land a swimmer must not get onto) that a +/// unit walking from the margin's walkable tiles reaches, or -1 when none is. Margin tiles themselves +/// never count. +int seaEntry(const Map &, const Torus &, const std::vector &margin, + const std::vector &inside); + +/// The wall that seals a coast: stone on every pure-grass tile of `wallable` that touches the +/// margin, diagonals included. Every step off the beach then lands on stone, so a swimmer can land +/// and walk the sand lane outside it but never get in. Set these tiles to stone in index order. +std::vector sealCoasts(const Map &, const Torus &, + const std::vector &margin, + const std::vector &wallable); + +/// A wall between labelled regions: a tile is wall when any of its eight neighbours has a lower +/// label. A tile left off the wall then touches only its own label or wall, so no unit can step, +/// even diagonally, from one region into another, and the wall is one tile thick. Negative labels +/// (sea, or ground no region claimed) take no part: they never make a wall and are never walled. +std::vector labelBorders(const Torus &, const std::vector &labels); +/// The same wall `thickness` tiles thick (1 to 3): the second tile on the other region's side of the +/// border, the third back on the first's. +std::vector labelBorders(const Torus &, const std::vector &labels, int thickness); + +/// A designed band of stone, once the terrain is laid: the tiles of `wall` that are pure grass and +/// so can take stone, and how many were not (the beach pass reached them, or the design put water or +/// sand there) - any such tile is a gap in the wall, which a design should refuse rather than ship. +struct DesignedStone +{ + std::vector stone; + int gaps = 0; + int firstGap = -1; +}; +DesignedStone designedStone(const Map &, const Torus &, const std::vector &wall); + +/// Steps from `from` over the land a ground unit can walk (walkableTiles: no water, deposit or +/// building) with the `shut` tiles closed as well: -1 wherever nothing was reached. The question +/// every sealing check asks - with the gates shut, can this ground reach that ground? +std::vector reachesWithShut(const Map &, const Torus &, const std::vector &from, + const std::vector &shut); + +/// The check that a map's parts stay apart with their doors shut: floods each part's walkable tiles +/// (a `piece` label of 0 or more) over walkable land with the `shut` tiles closed, and returns the +/// first tile of a different part any flood reaches, or -1 when every part keeps to itself. +int pieceLeak(const Map &, const Torus &, const std::vector &piece, + const std::vector &shut); + +/// The size of a defence tower's footprint, and the range of each of its three levels +/// (BuildingTypesDefence.cpp). A tower scans square rings round its footprint's top-left tile with +/// no line of sight (Building::findBestTarget), so a wall stops walking but not shooting. +constexpr int kTowerFootprint = 2; +constexpr int kTowerRange[3] = {5, 7, 9}; + +/// How close a tower built on `buildable` can come to shooting at `target`: the least Chebyshev +/// distance, across the wrap, from the top-left tile of any 2x2 footprint wholly on `buildable` to +/// any tile of `target`. INT_MAX when no footprint fits or there is no target. +int towerReach(const Torus &, const std::vector &buildable, + const std::vector &target); + +/// Each colony's walk from its workers to a target tile of its own, over walkable land: the +/// check that every colony is as far from the place the map sends it as every other. +struct WalkSpread +{ + std::vector steps; // per colony; -1 where the target was not reached + int shortest = INT_MAX, longest = 0; + int unreached = -1; // the first colony that could not reach its target + /// Whether the walks differ by more than `allowed` steps or a third of the longest walk. + bool tooUneven(int allowed) const + { + return longest - shortest > std::max(allowed, longest / 3); + } +}; +WalkSpread walkSpread(const Map &, const Torus &, const std::vector> &workers, + const std::vector &targets); + +/// Where one labelled region leaks into another it shouldn't: the first two neighbouring tiles, +/// diagonals included, both in `reached` (the ground units can actually walk to, such as a flood +/// from the colonies) whose labels differ and `allowed(a, b)` refuses. A design whose walls are +/// meant to leave only certain regions joined proves it with this. `tile` is -1 when nothing leaks. +struct RegionLeak +{ + int tile = -1, neighbour = -1; +}; +template +RegionLeak firstRegionLeak(const Torus &t, const std::vector &reached, + const std::vector &labels, Allowed allowed) +{ + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (!reached[i] || labels[i] < 0) + continue; + for (int dy = 0; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if (dy == 0 && dx <= 0) + continue; + const int j = t.at(x + dx, y + dy); + if (reached[j] && labels[j] >= 0 && labels[j] != labels[i] && + !allowed(labels[i], labels[j])) + return {i, j}; + } + } + return {}; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Wedge.h b/src/map/generator/shared/Wedge.h index 2693da1f5..9f5dc05b1 100644 --- a/src/map/generator/shared/Wedge.h +++ b/src/map/generator/shared/Wedge.h @@ -2,6 +2,7 @@ #pragma once #include "Geometry.h" #include "Grid.h" +#include "LatticeNoise.h" #include #include namespace MapGeneration @@ -83,4 +84,17 @@ struct Blob return std::hypot(px, py) < shape.radiusAt(std::atan2(py, px)); } }; +/// A noise field every wedge sees alike: periodic noise sampled at a tile's arc offset and radius in +/// the wedge frame rather than at its map position, so a layer planted by it (fields in patches, a +/// wheat/wood split) comes out the same in every colony's wedge. +struct WedgeField +{ + WedgeFrame frame; + PeriodicNoise noise; + double operator()(int tile) const + { + const WedgeFrame::Cell c = frame.cell(tile % frame.t.w, tile / frame.t.w); + return noise.at(c.s, c.d); + } +}; } // namespace MapGeneration diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 06ed2c623..12b99004e 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -220,6 +220,7 @@ class MapGeneratorDefaultsTest false, {4, 8, 16}}, {"test-gap", "Channel width", 1, 5, 2, 3, ControlGroup::Layout}, + GeneratorControl::choice("test-shape", "Cell shape", {"Squares", "Hexagons"}, 0), GeneratorControl::toggle("test-switch", "Lake connects to fjords", false)}, [](Game &game, GenerationContext &context) { @@ -261,6 +262,18 @@ class MapGeneratorDefaultsTest assert(!rejected(on)); const auto amount = GeneratorControl::percentage("amount", "Fruit"); assert(!amount.isToggle() && amount.defaultValue == 100 && !rejected(amount)); + // A choice stores the index of its named option and is shown by name. + const auto shape = GeneratorControl::choice("shape", "Cell shape", + {"Squares", "Hexagons"}, 1); + assert(shape.isChoice() && !shape.isToggle() && shape.defaultValue == 1 && + shape.values() == std::vector({0, 1}) && + std::string(shape.valueLabel(1)) == "Hexagons" && !shape.valueLabel(2) && + shape.normalize(5) == 1 && !rejected(shape)); + auto unnamed = shape; + unnamed.valueLabels.pop_back(); + auto blank = shape; + blank.valueLabels[0] = ""; + assert(rejected(unnamed) && rejected(blank) && !on.isChoice() && !on.valueLabel(0)); std::vector broken(6, on); broken[0].maximum = 2; broken[1].minimum = broken[1].defaultValue = 1; @@ -279,10 +292,13 @@ class MapGeneratorDefaultsTest edit(screen, "Island size", 16); edit(screen, "Channel width", 5); edit(screen, "Lake connects to fjords", 1); + assert(screen.descriptor.option("test-shape") == 0); + edit(screen, "Cell shape", 1); select(screen, D::eRIVER); select(screen, 101); assert(screen.descriptor.option("test-cell") == 16 && screen.descriptor.option("test-gap") == 5 && + screen.descriptor.option("test-shape") == 1 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); assert(registry.selectionIndex(101) == 21); diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index f5e1721ab..71303ae8c 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -351,13 +351,22 @@ int main(int argc, char **argv) "%s{\"id\":\"%s\",\"label\":\"%s\",\"kind\":\"%s\",\"min\":%d,\"max\":%d," "\"step\":%d,\"default\":%d," "\"group\":%d,\"powerOfTwo\":%s,\"values\":[", - i ? "," : "", c.id.c_str(), c.label, c.isToggle() ? "toggle" : "range", + i ? "," : "", c.id.c_str(), c.label, + c.isToggle() ? "toggle" : c.isChoice() ? "choice" : "range", c.minimum, c.maximum, c.step, c.defaultValue, int(c.group), c.powerOfTwo ? "true" : "false"); const auto domain = c.values(); for (size_t j = 0; j < domain.size(); ++j) std::printf("%s%d", j ? "," : "", domain[j]); - std::printf("]}"); + std::printf("]"); + if (c.isChoice()) + { + std::printf(",\"labels\":["); + for (size_t j = 0; j < domain.size(); ++j) + std::printf("%s\"%s\"", j ? "," : "", c.valueLabel(domain[j])); + std::printf("]"); + } + std::printf("}"); } std::printf("]}%s\n", m == methods.back() ? "" : ","); } diff --git a/test/MapGeneratorToolkitChecks.h b/test/MapGeneratorToolkitChecks.h index 1b99e7437..84955b2f1 100644 --- a/test/MapGeneratorToolkitChecks.h +++ b/test/MapGeneratorToolkitChecks.h @@ -6,6 +6,7 @@ // globals loaded: building and resource types. #include "BalancedStarts.h" #include "Drawing.h" +#include "GraphMaze.h" #include "Game.h" #include "GenerationContext.h" #include "Geometry.h" @@ -18,6 +19,10 @@ #include "Settlements.h" #include "Sketch.h" #include "StartQuality.h" +#include "Tessellation.h" +#include "Territories.h" +#include "Walls.h" +#include "Homes.h" #include "Wedge.h" #include #include @@ -848,6 +853,524 @@ inline void layerChecks() [](MapGeneratorPoint) { assert(false); }) == 0); } +// Walls: a sealed coast keeps anything landing on the beach out; labelBorders leaves no diagonal +// step between regions and ignores unlabelled ground; designedStone reports tiles that cannot hold +// stone; towerReach measures from a footprint's top-left tile; growWater stops at its target or +// its ground; arcPath runs round its circle. +inline void wallChecks() +{ + Game game(nullptr); + grassMap(game, 5, 5); + Map &map = game.map; + const Torus t(map); + TerrainSketch sketch(t.size(), WATER); + for (int y = 6; y < 26; ++y) + for (int x = 6; x < 26; ++x) + sketch[t.at(x, y)] = GRASS; + layBeaches(sketch, t); + writeUndermap(map, sketch); + std::vector sea(t.size(), 0), none(t.size(), 0), all(t.size(), 1); + for (int i = 0; i < t.size(); ++i) + sea[i] = map.getUMTerrain(i % t.w, i / t.w) == WATER; + const std::vector margin = seaMargin(map, t, sea, none); + const std::vector stone = sealCoasts(map, t, margin, all); + assert(std::count(stone.begin(), stone.end(), 1) > 40 && !margin[t.at(16, 16)]); + for (int i = 0; i < t.size(); ++i) + if (stone[i]) + { + assert(!margin[i] && map.getTerrainType(i % t.w, i / t.w) == GRASS); + map.setResource(i % t.w, i / t.w, STONE, 1); + } + std::vector beach(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + beach[i] = margin[i] && !map.isWater(i % t.w, i / t.w); + assert(std::count(beach.begin(), beach.end(), 1) > 0); + const std::vector landed = reachesWithShut(map, t, beach, none); + assert(landed[t.at(16, 16)] < 0); + std::vector middle(t.size(), 0); + middle[t.at(16, 16)] = 1; + assert(reachesWithShut(map, t, middle, none)[t.at(12, 12)] >= 0); + std::vector row(t.size(), 0); + for (int x = 0; x < t.w; ++x) + row[t.at(x, 14)] = 1; + assert(reachesWithShut(map, t, middle, row)[t.at(12, 12)] < 0); + std::vector onBeach(t.size(), 0); + onBeach[t.at(6, 6)] = onBeach[t.at(16, 16)] = 1; + const DesignedStone designed = designedStone(map, t, onBeach); + assert(designed.gaps == 1 && designed.firstGap == t.at(6, 6) && designed.stone[t.at(16, 16)]); + + const Torus square(16, 16); + std::vector labels(square.size(), 0); + for (int y = 0; y < square.h; ++y) + for (int x = 8; x < 16; ++x) + labels[square.at(x, y)] = x == 12 ? -1 : 1; + const std::vector border = labelBorders(square, labels); + for (int y = 0; y < square.h; ++y) + for (int x = 0; x < square.w; ++x) + { + const int i = square.at(x, y); + assert(border[i] == (x == 8 || x == 15)); + if (labels[i] != 0 || border[i]) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int j = square.at(x + dx, y + dy); + assert(labels[j] != 1 || border[j]); + } + } + + const Torus strip(32, 8); + std::vector buildable(strip.size(), 0), target(strip.size(), 0); + for (int y = 0; y < strip.h; ++y) + { + for (int x = 0; x < 4; ++x) + buildable[strip.at(x, y)] = 1; + target[strip.at(9, y)] = 1; + } + assert(towerReach(strip, buildable, target) == 7); + std::fill(buildable.begin(), buildable.end(), 0); + for (int y = 0; y < strip.h; ++y) + buildable[strip.at(2, y)] = 1; + assert(towerReach(strip, buildable, target) == INT_MAX); + + std::vector water(square.size(), 0); + std::vector queued(square.size(), 0); + const auto flat = [](int tile) { return tile; }; + assert(growWater(square, water, square.at(8, 8), 20, [](int) { return true; }, flat, queued, + 1) == 20 && + std::count(water.begin(), water.end(), 1) == 20); + std::fill(water.begin(), water.end(), 0); + const auto smallRoom = [&](int tile) { return tile % square.w < 3 && tile / square.w < 2; }; + assert(growWater(square, water, 0, 20, smallRoom, flat, queued, 2) == 6); + + const std::vector arc = arcPath(10, 10, 10, 0, kPi / 2, 2, 3); + assert(std::abs(arc.front().x - 20) < 1e-9 && std::abs(arc.back().y - 20) < 1e-9); + for (size_t k = 1; k < arc.size(); ++k) + { + assert(std::abs(std::hypot(arc[k].x - 10, arc[k].y - 10) - 10) < 1e-9); + assert(std::hypot(arc[k].x - arc[k - 1].x, arc[k].y - arc[k - 1].y) <= 3); + } +} + +// growTerritories: equal areas to the tile on an open torus, each territory in one piece, the same +// result grown twice, and a boxed-in territory that stops while the rest share out the remainder. +inline void territoryChecks() +{ + const Torus t(30, 30); + std::vector eligible(t.size(), 1); + const std::vector> seeds = {{t.at(5, 5)}, {t.at(20, 8)}, {t.at(12, 22)}}; + const auto noise = [&](int tile) { return (tile * 2654435761u) % 700; }; + const Territories grown = growTerritories(t, eligible, seeds, noise); + assert(grown.areas == std::vector({300, 300, 300})); + assert(grown.labels == growTerritories(t, eligible, seeds, noise).labels); + for (int k = 0; k < 3; ++k) + { + std::vector mine(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + mine[i] = grown.labels[i] == k; + // Four-connected inside its own ground: flood cardinally by hand. + std::vector stack{seeds[k][0]}; + std::vector seen(t.size(), 0); + seen[seeds[k][0]] = 1; + int reached = 0; + while (!stack.empty()) + { + const int i = stack.back(); + stack.pop_back(); + ++reached; + for (const auto &s : kCardinalSteps) + { + const int j = t.at(i % t.w + s[0], i / t.w + s[1]); + if (mine[j] && !seen[j]) + { + seen[j] = 1; + stack.push_back(j); + } + } + } + assert(reached == 300); + } + // A 3x3 room walled off round colony 0's seed. + for (int y = 3; y <= 7; ++y) + for (int x = 3; x <= 7; ++x) + if (x == 3 || x == 7 || y == 3 || y == 7) + eligible[t.at(x, y)] = 0; + const Territories boxed = growTerritories(t, eligible, seeds, noise); + assert(boxed.areas[0] == 9 && std::abs(boxed.areas[1] - boxed.areas[2]) <= 1 && + boxed.areas[1] + boxed.areas[2] == 900 - 16 - 9); + assert(boxed.labels[t.at(3, 3)] == -1 && boxed.smallest() == 9); +} + +// The arena primitives: ringWithGates leaves exact gaps; zigzagPath alternates sides and keeps its +// legs short of their turns; AxisFrame projects back what it placed; keepRoadInland and roadTiles; +// labelBorders thickens; seaEntry finds a hole in a sealed coast; plantRound and plantOrchard place +// the same at every angle; growFarLake grows a lake of the target at the far end of a region; +// siteAtDepth, smoothLabels and strandedGround; placeTower stocks a tower on allowed ground. +inline void arenaChecks() +{ + const Torus t(64, 64); + std::vector gateOf(t.size(), -2); + ringWithGates(t, 32, 32, 20, 1.2, {0.0, kPi}, 3, + [&](int i, int gate) { gateOf[i] = gate; }); + assert(gateOf[t.at(52, 32)] == 0 && gateOf[t.at(12, 32)] == 1 && gateOf[t.at(32, 52)] == -1); + assert(gateOf[t.at(32, 32)] == -2 && gateOf[t.at(52, 36)] == -1); + + const AxisFrame frame{32, 32, kPi / 2}; + const ShapePoint p = frame.at(10, 4); + assert(std::abs(p.x - 28) < 1e-9 && std::abs(p.y - 42) < 1e-9); + const ShapePoint back = frame.project(p.x - 32, p.y - 32); + assert(std::abs(back.x - 10) < 1e-9 && std::abs(back.y - 4) < 1e-9); + const Zigzag zigzag = zigzagPath(AxisFrame{0, 0, 0}, 40, 30, 8, 3, 6, 5, 2); + assert(zigzag.legs.size() == 3 && zigzag.path.size() == 8 && zigzag.finishAcross == -6); + assert(zigzag.path.front().x == 40 && zigzag.path.front().y == 6 && zigzag.path.back().x == 5); + assert(zigzag.legs[1].front().x == 22 && zigzag.legs[1].front().y == -4 && zigzag.legs[1].back().y == 4); + + std::vector road(t.size(), 0), water(t.size(), 0); + for (int x = 0; x < t.w; ++x) + road[t.at(x, 10)] = 1; + for (int y = 0; y < t.h; ++y) + water[t.at(0, y)] = 1; + keepRoadInland(road, t, water, 3); + assert(!road[t.at(2, 10)] && road[t.at(3, 10)] && !road[t.at(62, 10)]); + const std::vector spoiled = roadTiles(t, road); + assert(spoiled[t.at(3, 9)] && spoiled[t.at(2, 10)] && !spoiled[t.at(3, 11)]); + + std::vector labels(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + labels[i] = i % t.w < 32 ? 0 : 1; + const std::vector thin = labelBorders(t, labels), thick = labelBorders(t, labels, 2); + assert(std::count(thick.begin(), thick.end(), 1) == 2 * std::count(thin.begin(), thin.end(), 1)); + assert(thick[t.at(31, 5)] && thick[t.at(32, 5)] && !thin[t.at(31, 5)]); + + // Stranded ground and depth sites. + std::vector ground(t.size(), 1); + for (int y = 0; y < t.h; ++y) + ground[t.at(20, y)] = 0; + const std::vector stranded = strandedGround(t, {t.at(5, 5)}, ground); + assert(!stranded[t.at(19, 5)] && !stranded[t.at(21, 5)] && !stranded[t.at(20, 5)]); + for (int y = 0; y < t.h; ++y) + ground[t.at(40, y)] = 0; + const std::vector cut = strandedGround(t, {t.at(5, 5)}, ground); + assert(cut[t.at(30, 5)] && !cut[t.at(50, 5)]); + std::vector depth(t.size(), -1), room(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + { + depth[i] = i % t.w; + room[i] = std::min(i / t.w, t.h - 1 - i / t.w); + } + const int site = siteAtDepth(depth, room, 12, 5, 0); + assert(site % t.w == 12 && room[site] == 31); + assert(siteAtDepth(depth, room, 12, 40, 3) == -1); + + // A lake at the far end of a strip: the same size whatever the seed, and never within the gap. + std::vector lake(t.size(), 0); + std::vector queued(t.size(), 0); + std::vector stripDepth(t.size(), -1); + for (int y = 10; y < 30; ++y) + for (int x = 0; x < 60; ++x) + stripDepth[t.at(x, y)] = x; + std::vector edge(t.size(), 1); + for (int y = 10; y < 30; ++y) + for (int x = 0; x < 60; ++x) + edge[t.at(x, y)] = 0; + const std::vector stripRoom = stepsFrom(t, edge); + assert(growFarLake(t, lake, stripDepth, stripRoom, 3, 60, [](int) { return 0.0; }, queued, 1) == 60); + int far = 0, near = 0; + for (int i = 0; i < t.size(); ++i) + if (lake[i]) + { + assert(stripRoom[i] >= 3); + (i % t.w > 30 ? far : near)++; + } + assert(far == 60 && near == 0); + assert(growFarLake(t, lake, stripDepth, stripRoom, 12, 60, [](int) { return 0.0; }, queued, 2) == 0); + + std::vector jagged(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + jagged[i] = i % t.w < 32 ? 0 : 1; + jagged[t.at(20, 20)] = 1; + std::vector keep(t.size(), 0); + smoothLabels(t, jagged, 1, keep); + assert(jagged[t.at(20, 20)] == 0 && jagged[t.at(40, 20)] == 1); + + // Clumps round a circle and a stocked tower, on a real map with a sealed island. + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + GenerationRequest request; + request.seed = 5; + GenerationContext context(request); + const auto anywhere = [&](int i) { return clearGround(map, i % t.w, i / t.w); }; + assert(plantRound(map, t, context, 32, 32, 10, {0.0, kPi / 2, kPi}, STONE, 0, 2, anywhere) == 3); + assert(map.getResource(42, 32).type == STONE && map.getResource(22, 32).type == STONE); + assert(plantOrchard(map, t, context, 32, 32, 20, {0.0}, 4, 2, 0, anywhere) == 3); + assert(map.getResource(52, 32).type == CHERRY + 1); + + game.addTeam(); + std::vector allowed(t.size(), 0); + for (int y = 4; y < 10; ++y) + for (int x = 4; x < 10; ++x) + allowed[t.at(x, y)] = 1; + const int tower = placeTower(game, 0, 1, 3, 3, 4, allowed); + assert(tower == t.at(4, 4)); + assert(map.getBuilding(4, 4) != NOGBID && map.getBuilding(5, 5) != NOGBID); + assert(game.teams[0]->turrets.size() == 1 && game.teams[0]->turrets.front()->bullets > 0); + std::vector none(t.size(), 0); + assert(placeTower(game, 0, 1, 30, 30, 4, none) == -1); + + // seaEntry finds the one grass gap in a walled coast. + TerrainSketch sketch(t.size(), WATER); + for (int y = 20; y < 44; ++y) + for (int x = 20; x < 44; ++x) + sketch[t.at(x, y)] = GRASS; + layBeaches(sketch, t); + writeUndermap(map, sketch); + const std::vector lakes(t.size(), 0), inside(t.size(), 1); + const std::vector margin = seaMargin(map, t, seaVertices(map, t, lakes), lakes); + std::vector island(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + island[i] = !map.isWater(i % t.w, i / t.w); + std::vector wall = sealCoasts(map, t, margin, island); + for (int i = 0; i < t.size(); ++i) + map.setNoResource(i % t.w, i / t.w, 1); + int gap = -1; + for (int i = 0; i < t.size(); ++i) + if (wall[i]) + { + if (gap < 0 && i % t.w == 32) + gap = i; + else + map.setResource(i % t.w, i / t.w, STONE, 1); + } + assert(gap >= 0 && seaEntry(map, t, margin, island) >= 0); + map.setResource(gap % t.w, gap / t.w, STONE, 1); + assert(seaEntry(map, t, margin, island) == -1); + (void)inside; +} + +// sealedSegmentTiles steps one side at a time from end tile to end tile, taking both tiles at an +// exact corner; forEachTileInPolygon takes the tiles whose centres lie inside, and polygons sharing +// edges - across the wrap too - split the tiles between them exactly. +inline void subtileRasterChecks() +{ + const auto sealed = [](SubtilePoint a, SubtilePoint b) + { + const auto tiles = sealedSegmentTiles(a, b); + assert(tiles.front().first == subtileTile(a.x) && tiles.front().second == subtileTile(a.y)); + assert(tiles.back().first == subtileTile(b.x) && tiles.back().second == subtileTile(b.y)); + const std::set> line(tiles.begin(), tiles.end()); + for (size_t k = 1; k < tiles.size(); ++k) + { + const long long dx = tiles[k].first - tiles[k - 1].first, + dy = tiles[k].second - tiles[k - 1].second; + assert(std::llabs(dx) <= 1 && std::llabs(dy) <= 1 && (dx || dy)); + if (dx && dy) + assert(line.count({tiles[k - 1].first + dx, tiles[k - 1].second}) && + line.count({tiles[k - 1].first, tiles[k - 1].second + dy})); + } + return tiles; + }; + assert(sealed(subtileCentre(2, 5), subtileCentre(9, 5)).size() == 8); + assert(sealed(subtileCentre(3, 3), subtileCentre(3, -4)).size() == 8); + // A diagonal through tile corners exactly: every step takes both tiles beside the corner. + assert(sealed({0, 0}, {5 * kSubtile, 5 * kSubtile}).size() == 16); + for (int k = 0; k < 40; ++k) + sealed({k * 7 - 90, k * 13 - 50}, {300 - k * 11, k * 5 + 17}); + + const Torus t(24, 16); + std::vector mask(t.size(), 0); + fillPolygon(mask, t, + {{2 * kSubtile, 3 * kSubtile}, + {5 * kSubtile, 3 * kSubtile}, + {5 * kSubtile, 7 * kSubtile}, + {2 * kSubtile, 7 * kSubtile}}); + assert(std::count(mask.begin(), mask.end(), 1) == 12 && mask[t.at(2, 3)] && mask[t.at(4, 6)] && + !mask[t.at(5, 3)] && !mask[t.at(2, 7)]); + // Two triangles either side of a slanted cut through a strip that wraps the seam. + std::vector cover(t.size(), 0); + const SubtilePoint a{-70, 2 * kSubtile + 3}, b{19 * kSubtile + 5, 2 * kSubtile + 3}, + c{19 * kSubtile + 5, 12 * kSubtile - 9}, d{-70, 12 * kSubtile - 9}; + forEachTileInPolygon(t, {a, b, c}, [&](int tile) { ++cover[tile]; }); + forEachTileInPolygon(t, {a, c, d}, [&](int tile) { ++cover[tile]; }); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const long long cx = x * kSubtile + kSubtile / 2, cy = y * kSubtile + kSubtile / 2; + const bool inside = cy >= a.y && cy < d.y && + ((cx >= a.x && cx < b.x) || (cx - t.w * kSubtile >= a.x && + cx - t.w * kSubtile < b.x)); + assert(cover[t.at(x, y)] == (inside ? 1 : 0)); + } +} + +// A tiling's cells cover every tile exactly once, edges join their cells from both sides with the +// same two corners, the counts obey Euler's formula for a torus, lattice symmetries keep neighbours +// neighbours, and warping keeps the tiling whole and the walls' passages open, the same way twice. +inline void tessellationChecks() +{ + const auto contract = [](const Tessellation &g, int degree) + { + assert(int(g.corners.size()) - int(g.edges.size()) + g.cellCount() == 0); + const std::vector labels = labelTiles(g); + assert(int(labels.size()) == g.t.size()); + std::vector area(g.cells.size(), 0); + for (int label : labels) + ++area[label]; + for (int cell = 0; cell < g.cellCount(); ++cell) + { + assert(area[cell] > 0 && int(g.cells[cell].edges.size()) == degree); + assert(labels[g.t.at(g.centreTileX(cell), g.centreTileY(cell))] == cell); + for (int edge : g.cells[cell].edges) + { + const int next = g.other(edge, cell); + const auto &back = g.cells[next].edges; + assert(std::find(back.begin(), back.end(), edge) != back.end()); + // Seen from the other side the edge runs backwards, one whole map shift away. + const auto here = g.edgeEnds(edge, cell), there = g.edgeEnds(edge, next); + const long long sx = here.first.x - there.second.x, sy = here.first.y - there.second.y; + assert(sx % (g.t.w * kSubtile) == 0 && sy % (g.t.h * kSubtile) == 0); + assert(here.second.x - there.first.x == sx && here.second.y - there.first.y == sy); + const SubtilePoint across = g.centreAcross(edge, cell); + assert((across.x - g.cells[next].centre.x) == sx && + (across.y - g.cells[next].centre.y) == sy); + } + } + const RegionGraph graph = g.neighbours(); + for (int s = 0; s < 8; ++s) + for (int cell = 0; cell < g.cellCount(); ++cell) + { + const auto move = [&](int c) + { return g.transform(c, s * 3 % g.columns, s % g.rows, s & 1, (s >> 1) & 1); }; + const int image = move(cell); + for (int next : graph[cell]) + assert(std::count(graph[image].begin(), graph[image].end(), move(next)) > 0); + } + return labels; + }; + const Tessellation squares = squareTessellation(64, 48, 16); + assert(squares.columns == 4 && squares.rows == 3); + const std::vector squareLabels = contract(squares, 4); + assert(squareLabels[squares.t.at(15, 0)] == 0 && squareLabels[squares.t.at(16, 0)] == 1); + contract(squareTessellation(64, 64, 20), 4); // uneven pitches, 3 cells of 21 or 22 tiles + contract(squareTessellation(32, 32, 16), 4); // two cells across: every neighbour twice + const Tessellation hexes = hexTessellation(128, 128, 30); + assert(hexes.columns == 4 && hexes.rows % 2 == 0); + contract(hexes, 6); + contract(hexTessellation(64, 64, 32), 6); + contract(hexTessellation(256, 128, 48), 6); + + GenerationRequest request; + request.seed = 9; + for (const Tessellation &lattice : {squares, hexes}) + { + Tessellation first = lattice, second = lattice; + std::vector walls(lattice.edges.size(), 0); + for (size_t edge = 0; edge < walls.size(); ++edge) + walls[edge] = edge % 3 != 0; + const int limit = warpLimit(lattice); + assert(limit > 0); + GenerationContext one(request), two(request), untouched(request); + warpCorners(first, limit, walls, 6, 3, one, "warp"); + warpCorners(second, limit, walls, 6, 3, two, "warp"); + assert(first.corners.size() == lattice.corners.size()); + bool moved = false; + for (size_t k = 0; k < first.corners.size(); ++k) + { + assert(first.corners[k] == second.corners[k]); + moved = moved || !(first.corners[k] == lattice.corners[k]); + } + assert(moved); + contract(first, lattice.shape == Tessellation::Shape::Square ? 4 : 6); + // No move at all draws nothing. + Tessellation still = lattice; + warpCorners(still, 0, walls, 6, 3, untouched, "warp"); + GenerationContext fresh(request); + assert(untouched.bounded("warp", 1000) == fresh.bounded("warp", 1000)); + // Walls that share no corner keep their unwarped gap or 6 steps, whichever is less. + for (size_t e = 0; e < walls.size(); ++e) + for (size_t f = e + 1; f < walls.size(); ++f) + { + if (!walls[e] || !walls[f]) + continue; + const auto &ce = lattice.edges[e].corners, &cf = lattice.edges[f].corners; + if (ce[0] == cf[0] || ce[0] == cf[1] || ce[1] == cf[0] || ce[1] == cf[1]) + continue; + const auto steps = [&](const Tessellation &g) + { + const auto p = g.edgeEnds(int(e), g.edges[e].cells[0]); + const auto q = g.edgeEnds(int(f), g.edges[f].cells[0]); + int least = INT_MAX; + for (const auto &u : sealedSegmentTiles(p.first, p.second)) + for (const auto &v : sealedSegmentTiles(q.first, q.second)) + least = std::min(least, g.t.chebyshev(int(u.first), int(u.second), + int(v.first), int(v.second))); + return least; + }; + assert(steps(first) >= std::min(steps(lattice), 6)); + } + } +} + +// The maze toolkit on a small tiling: pockets that fit, a spanning tree through the rest, one door +// per pocket, loops that never touch a pocket, dead ends with their one exit; and firstRegionLeak +// finds ground shared by regions that aren't allowed to share it. +inline void graphMazeChecks() +{ + for (const Tessellation &g : {squareTessellation(96, 96, 16), hexTessellation(128, 128, 30)}) + { + const int n = g.cellCount(); + std::vector surrounded(g.cells.size(), 0); + for (int edge : g.cells[0].edges) + surrounded[g.other(edge, 0)] = 1; + surrounded[0] = 1; + assert(!pocketsFit(g, surrounded)); + const std::vector pockets = spreadPockets(g, 3); + assert(pockets.size() == 3 && spreadPockets(g, 3) == pockets); + std::vector isPocket(g.cells.size(), 0); + for (int pocket : pockets) + isPocket[pocket] = 1; + assert(pocketsFit(g, isPocket)); + + GenerationRequest request; + request.seed = 4; + GenerationContext context(request); + std::vector open(g.edges.size(), 0); + assert(carveSpanningTree(g, context, "maze", isPocket, open)); + assert(std::count(open.begin(), open.end(), 1) == n - 3 - 1); + const std::vector doors = openPocketDoors(g, context, "maze", pockets, isPocket, open); + for (size_t k = 0; k < pockets.size(); ++k) + { + int degree = 0; + for (int edge : g.cells[pockets[k]].edges) + degree += open[edge]; + assert(degree == 1 && open[doors[k]] && !isPocket[g.other(doors[k], pockets[k])]); + } + openLoops(g, context, "maze", isPocket, 50, open); + assert(std::count(open.begin(), open.end(), 1) == n - 1 + (n - 3) * 50 / 100); + std::vector exits; + for (int cell : deadEnds(g, open, isPocket, exits)) + { + int degree = 0; + for (int edge : g.cells[cell].edges) + degree += open[edge]; + assert(!isPocket[cell] && degree == 1 && open[exits[cell]]); + } + } + + const Torus t(8, 8); + std::vector labels(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + labels[i] = i % t.w < 4 ? 0 : i % t.w == 4 ? -1 : 1; + std::vector reached(t.size(), 1); + const RegionLeak leak = firstRegionLeak(t, reached, labels, [](int, int) { return false; }); + assert(leak.tile >= 0 && labels[leak.tile] != labels[leak.neighbour]); + assert(firstRegionLeak(t, reached, labels, [](int, int) { return true; }).tile < 0); + for (int i = 0; i < t.size(); ++i) + reached[i] = labels[i] == 1; + assert(firstRegionLeak(t, reached, labels, [](int, int) { return false; }).tile < 0); +} + inline void toolkitChecks() { floodChecks(); @@ -865,8 +1388,15 @@ inline void toolkitChecks() stretchChecks(); dressingChecks(); layerChecks(); + wallChecks(); + territoryChecks(); + arenaChecks(); + subtileRasterChecks(); + tessellationChecks(); + graphMazeChecks(); puts("PASS shared toolkit: floods, sketch, planting, roads, settlements, balanced starts, " "scatter, lattice noise, wedge frame, shuffle, drawing, branches, stretch, sand patches, " - "algae growth, fields and clumps"); + "algae growth, fields and clumps, walls and tower reach, territories, arena primitives, " + "sealed lines and polygons, tessellations and warp, graph mazes and region leaks"); } } // namespace ToolkitChecks diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 4227f3784..bcf5e6d34 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -80,14 +80,14 @@ macos-arm64 9 2 8 8 4 3 ok 5877899263698877100 macos-arm64 9 2 8 8 8 1 ok 4204618854271989380 macos-arm64 9 2 9 8 4 1 ok 13364805290811868144 macos-arm64 9 2 9 9 4 1 ok 17370525382688177735 -macos-arm64 11 11 7 7 4 1 ok 7099819568130086231 -macos-arm64 11 11 8 8 2 1 ok 6302098072410457647 -macos-arm64 11 11 8 8 4 1 ok 35825363079579543 -macos-arm64 11 11 8 8 4 2 ok 4919401921601200783 -macos-arm64 11 11 8 8 4 3 ok 4565939337693971842 -macos-arm64 11 11 8 8 8 1 ok 906849352031588290 -macos-arm64 11 11 9 8 4 1 ok 7686433018839407442 -macos-arm64 11 11 9 9 4 1 ok 1888712339707412248 +macos-arm64 11 12 7 7 4 1 ok 13998387927847893421 +macos-arm64 11 12 8 8 2 1 ok 1580814293182404478 +macos-arm64 11 12 8 8 4 1 ok 9436694036498979113 +macos-arm64 11 12 8 8 4 2 ok 6039395870650123087 +macos-arm64 11 12 8 8 4 3 ok 17321560833037622047 +macos-arm64 11 12 8 8 8 1 ok 13476391256386289913 +macos-arm64 11 12 9 8 4 1 ok 11214128330290450603 +macos-arm64 11 12 9 9 4 1 ok 2809958562561781155 macos-arm64 12 11 7 7 4 1 ok 7388571519173228055 macos-arm64 12 11 8 8 2 1 ok 15699276765293966482 macos-arm64 12 11 8 8 4 1 ok 11621882014804934120 From 79761d59d54485ec3703cca15cf2539462fcc22a Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 14:12:43 -0400 Subject: [PATCH 112/136] Add Carousel, Amphitheatre and Switchbacks walled-arena generators Three arena maps built on the shared toolkit, with starting towers: - Carousel (22): a ring of walled homes. Each colony's 3-tile corridor leads to a small court pressed against the next colony's home, and a 3-tile spoke runs from the court to the plaza. Single lines of stone part every piece, with no water channels. Towers in each home stand against the wall and shell the neighbour's elbow. - Switchbacks (24): homes reach a central plateau only up zigzag trails through stone mountains, with towers on the inner side of the 2-tile leg walls. - Amphitheatre (23): equal-area walled territories, each with two inland seas, round a ringed arena. New shared pieces: - Farmland: rows at the widths that yield most for their angle, land shared by equal yield, building plots, sand bridges across the water rows, and a sand cap ring so crops stay in the rows. - Towers: siting by coverage, optionally against a wall; keeping designed routes open; even counts per colony. - Territories and Walls: fillToNearest, labelBorders with doors, and pieceLeak counting crops and buildings as passable. Homes on these maps have no stone clump and no ponds where farms are their water. The plaza and plateau carry only fruit. The towers' level and count per colony (default 3) are lobby controls. Existing generators' golden rows are unchanged; the lobby sweep has 0 failing combinations. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 52 + data/texts.br.txt | 52 + data/texts.ca.txt | 52 + data/texts.cz.txt | 52 + data/texts.de.txt | 52 + data/texts.dk.txt | 52 + data/texts.en.txt | 54 + data/texts.eo.txt | 52 + data/texts.es.txt | 52 + data/texts.eu.txt | 52 + data/texts.fa.txt | 52 + data/texts.fi.txt | 52 + data/texts.fr.txt | 52 + data/texts.gr.txt | 52 + data/texts.hu.txt | 52 + data/texts.id.txt | 52 + data/texts.it.txt | 52 + data/texts.ja.txt | 52 + data/texts.keys.txt | 27 + data/texts.ko.txt | 52 + data/texts.nl.txt | 52 + data/texts.pl.txt | 52 + data/texts.pt.txt | 52 + data/texts.ro.txt | 52 + data/texts.ru.txt | 52 + data/texts.si.txt | 52 + data/texts.sk.txt | 52 + data/texts.sr.txt | 52 + data/texts.sv.txt | 52 + data/texts.tr.txt | 52 + data/texts.uk.txt | 52 + data/texts.vi.txt | 52 + data/texts.zh-cn.txt | 52 + data/texts.zh-tw.txt | 52 + .../GAME_RULES_FOR_MAP_DESIGN.md | 10 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 147 ++- src/SConscript | 5 + src/map/generator/core/GeneratorRegistry.cpp | 5 +- .../generators/AmphitheatreGenerator.cpp | 709 +++++++++++ .../generators/AmphitheatreGenerator.h | 11 + .../generators/CarouselGenerator.cpp | 1050 +++++++++++++++++ .../generator/generators/CarouselGenerator.h | 12 + .../generators/SwitchbacksGenerator.cpp | 826 +++++++++++++ .../generators/SwitchbacksGenerator.h | 12 + src/map/generator/shared/Farmland.cpp | 348 ++++++ src/map/generator/shared/Farmland.h | 164 +++ src/map/generator/shared/Homes.h | 4 +- src/map/generator/shared/Planting.h | 6 +- src/map/generator/shared/Settlements.cpp | 4 +- src/map/generator/shared/Settlements.h | 6 +- src/map/generator/shared/Territories.h | 192 ++- src/map/generator/shared/Towers.cpp | 237 ++++ src/map/generator/shared/Towers.h | 80 ++ src/map/generator/shared/Walls.cpp | 23 +- src/map/generator/shared/Walls.h | 38 +- test/MapGeneratorDefaultsTest.cpp | 2 +- test/MapGeneratorToolkitChecks.h | 195 ++- test/map-generator-golden.txt | 24 + tools/farm_row_fit.py | 60 + 59 files changed, 5859 insertions(+), 56 deletions(-) create mode 100644 src/map/generator/generators/AmphitheatreGenerator.cpp create mode 100644 src/map/generator/generators/AmphitheatreGenerator.h create mode 100644 src/map/generator/generators/CarouselGenerator.cpp create mode 100644 src/map/generator/generators/CarouselGenerator.h create mode 100644 src/map/generator/generators/SwitchbacksGenerator.cpp create mode 100644 src/map/generator/generators/SwitchbacksGenerator.h create mode 100644 src/map/generator/shared/Farmland.cpp create mode 100644 src/map/generator/shared/Farmland.h create mode 100644 src/map/generator/shared/Towers.cpp create mode 100644 src/map/generator/shared/Towers.h create mode 100644 tools/farm_row_fit.py diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 6b8f3a169..0fc49b091 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1806,6 +1806,8 @@ OpenGL غير متوفر في هذا الإصدار. حجم الغرفة [Corridor width] عرض الممر +[Spoke width] +عرض الأشعة [Loopiness] كثافة الحلقات [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1932,6 +1934,56 @@ OpenGL غير متوفر في هذا الإصدار. يحتاج المرجان إلى مستعمرة واحدة على الأقل. [Coral needs a map at least 128 tiles across.] يحتاج المرجان إلى خريطة بعرض 128 مربعًا على الأقل. +[Carousel] +الدوّامة +[Amphitheatre] +المدرج +[Switchbacks] +الطرق المتعرجة +[Court size] +حجم الساحات +[Court wall] +جدار الساحات +[Plaza size] +حجم الميدان +[Starting tower level] +مستوى الأبراج الأولية +[Towers per colony] +الأبراج لكل مستعمرة +[Rings] +الحلقات +[Ramp width] +عرض المنحدرات +[Pit size] +حجم الحفرة +[Terrace width] +عرض المدرجات +[Border roughness] +تعرّج الحدود +[Bay size] +حجم الخلجان +[Border wall] +الجدار الحدودي +[Plateau size] +حجم الهضبة +[Trail width] +عرض الدرب +[Wall between legs] +الجدار بين المنعطفات +[Farm building plots] +قطع بناء في المزارع +[Carousels need at least two colonies.] +تحتاج الدوّامة إلى مستعمرتين على الأقل. +[Switchbacks need at least two colonies.] +تحتاج الطرق المتعرجة إلى مستعمرتين على الأقل. +[An amphitheatre needs at least two colonies.] +يحتاج المدرج إلى مستعمرتين على الأقل. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +لا تتسع هذه الخريطة للدوّامة؛ استخدم خريطة أكبر أو مواطن أصغر أو مستعمرات أقل. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +لا تتسع هذه الخريطة للطرق المتعرجة؛ استخدم خريطة أكبر أو هضبة أصغر أو دروبًا أضيق أو مستعمرات أقل. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +لا تتسع هذه الخريطة للمدرج؛ استخدم خريطة أكبر أو حلقات أقل أو مدرجات أضيق أو مستعمرات أقل. [Spider web] شبكة العنكبوت [Spokes per colony] diff --git a/data/texts.br.txt b/data/texts.br.txt index a1cb4ee27..9d4f437aa 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1804,6 +1804,8 @@ Tamanho da célula Tamanho da sala [Corridor width] Largura do corredor +[Spoke width] +Ledander ar skinoù [Loopiness] Densidade de ciclos [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1930,6 +1932,56 @@ Ledander ar skourroù Ezhomm en deus ar c'houral eus un drevadenn da vihanañ. [Coral needs a map at least 128 tiles across.] Ezhomm en deus ar c'houral eus ur gartenn 128 karrezenn ledan da vihanañ. +[Carousel] +Karrousel +[Amphitheatre] +Amfiteatr +[Switchbacks] +Troioù-kamm +[Court size] +Ment ar porzhioù +[Court wall] +Moger ar porzhioù +[Plaza size] +Ment ar blasenn +[Starting tower level] +Live an tourioù kentañ +[Towers per colony] +Tourioù dre drevadenn +[Rings] +Kelc'hioù +[Ramp width] +Ledander ar pantennoù +[Pit size] +Ment ar poull +[Terrace width] +Ledander ar bazennoù +[Border roughness] +Direizhder an harzoù +[Bay size] +Ment ar baeoù +[Border wall] +Moger an harzoù +[Plateau size] +Ment ar plaen uhel +[Trail width] +Ledander an hent +[Wall between legs] +Moger etre an troioù +[Farm building plots] +Leurioù-savadur er fermoù +[Carousels need at least two colonies.] +Ezhomm en deus ar c'harrousel eus div drevadenn da vihanañ. +[Switchbacks need at least two colonies.] +Ezhomm o deus an troioù-kamm eus div drevadenn da vihanañ. +[An amphitheatre needs at least two colonies.] +Ezhomm en deus an amfiteatr eus div drevadenn da vihanañ. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +N'eo ket mat ar c'harrousel evit ar gartenn-mañ; implijit ur gartenn vrasoc'h, diazezoù bihanoc'h pe nebeutoc'h a drevadennoù. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +N'eo ket mat an troioù-kamm evit ar gartenn-mañ; implijit ur gartenn vrasoc'h, ur plaen uhel bihanoc'h, hentoù strishoc'h pe nebeutoc'h a drevadennoù. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +N'eo ket mat an amfiteatr evit ar gartenn-mañ; implijit ur gartenn vrasoc'h, nebeutoc'h a gelc'hioù, pazennoù strishoc'h pe nebeutoc'h a drevadennoù. [Spider web] Gwiad-kevnid [Spokes per colony] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 46209e26b..5dff90df0 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1816,6 +1816,8 @@ Mida de la cel·la Mida de la sala [Corridor width] Amplada del passadís +[Spoke width] +Amplada dels radis [Loopiness] Densitat de bucles [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1942,6 +1944,56 @@ Amplada de les branques El corall necessita almenys una colònia. [Coral needs a map at least 128 tiles across.] El corall necessita un mapa d'almenys 128 caselles d'amplada. +[Carousel] +Carrusel +[Amphitheatre] +Amfiteatre +[Switchbacks] +Ziga-zagues +[Court size] +Mida dels patis +[Court wall] +Mur dels patis +[Plaza size] +Mida de la plaça +[Starting tower level] +Nivell de les torres inicials +[Towers per colony] +Torres per colònia +[Rings] +Anells +[Ramp width] +Amplada de les rampes +[Pit size] +Mida del fossat +[Terrace width] +Amplada de les graderies +[Border roughness] +Irregularitat de les fronteres +[Bay size] +Mida de les badies +[Border wall] +Mur fronterer +[Plateau size] +Mida de l'altiplà +[Trail width] +Amplada del camí +[Wall between legs] +Mur entre trams +[Farm building plots] +Solars a les granges +[Carousels need at least two colonies.] +El carrusel necessita almenys dues colònies. +[Switchbacks need at least two colonies.] +Els ziga-zagues necessiten almenys dues colònies. +[An amphitheatre needs at least two colonies.] +L'amfiteatre necessita almenys dues colònies. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +El carrusel no cap en aquest mapa; useu un mapa més gran, bases més petites o menys colònies. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Els ziga-zagues no caben en aquest mapa; useu un mapa més gran, un altiplà més petit, camins més estrets o menys colònies. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +L'amfiteatre no cap en aquest mapa; useu un mapa més gran, menys anells, graderies més estretes o menys colònies. [Spider web] Teranyina [Spokes per colony] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 906324e64..8da285feb 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1808,6 +1808,8 @@ Velikost buňky Velikost místnosti [Corridor width] Šířka chodby +[Spoke width] +Šířka paprsků [Loopiness] Propojenost [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Větvení Korál potřebuje alespoň jednu kolonii. [Coral needs a map at least 128 tiles across.] Korál potřebuje mapu širokou alespoň 128 polí. +[Carousel] +Kolotoč +[Amphitheatre] +Amfiteátr +[Switchbacks] +Serpentiny +[Court size] +Velikost nádvoří +[Court wall] +Zeď nádvoří +[Plaza size] +Velikost náměstí +[Starting tower level] +Úroveň počátečních věží +[Towers per colony] +Věže na kolonii +[Rings] +Prstence +[Ramp width] +Šířka ramp +[Pit size] +Velikost jámy +[Terrace width] +Šířka teras +[Border roughness] +Členitost hranic +[Bay size] +Velikost zátok +[Border wall] +Hraniční zeď +[Plateau size] +Velikost náhorní plošiny +[Trail width] +Šířka stezky +[Wall between legs] +Zeď mezi serpentinami +[Farm building plots] +Stavební parcely na farmách +[Carousels need at least two colonies.] +Kolotoč potřebuje alespoň dvě kolonie. +[Switchbacks need at least two colonies.] +Serpentiny potřebují alespoň dvě kolonie. +[An amphitheatre needs at least two colonies.] +Amfiteátr potřebuje alespoň dvě kolonie. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Kolotoč se na tuto mapu nevejde; použijte větší mapu, menší domovy nebo méně kolonií. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Serpentiny se na tuto mapu nevejdou; použijte větší mapu, menší náhorní plošinu, užší stezky nebo méně kolonií. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteátr se na tuto mapu nevejde; použijte větší mapu, méně prstenců, užší terasy nebo méně kolonií. [Spider web] Pavučina [Spokes per colony] diff --git a/data/texts.de.txt b/data/texts.de.txt index fa0e83066..e6429fb75 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1808,6 +1808,8 @@ Zellengröße Raumgröße [Corridor width] Korridorbreite +[Spoke width] +Speichenbreite [Loopiness] Vernetzungsgrad [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Astbreite Korallen brauchen mindestens eine Kolonie. [Coral needs a map at least 128 tiles across.] Korallen brauchen eine Karte von mindestens 128 Feldern Breite. +[Carousel] +Karussell +[Amphitheatre] +Amphitheater +[Switchbacks] +Serpentinen +[Court size] +Hofgröße +[Court wall] +Hofmauer +[Plaza size] +Platzgröße +[Starting tower level] +Stufe der Starttürme +[Towers per colony] +Türme pro Kolonie +[Rings] +Ringe +[Ramp width] +Rampenbreite +[Pit size] +Arenagröße +[Terrace width] +Terrassenbreite +[Border roughness] +Grenzverlauf +[Bay size] +Buchtgröße +[Border wall] +Grenzmauer +[Plateau size] +Plateaugröße +[Trail width] +Pfadbreite +[Wall between legs] +Mauer zwischen Kehren +[Farm building plots] +Bauplätze in den Farmen +[Carousels need at least two colonies.] +Ein Karussell braucht mindestens zwei Kolonien. +[Switchbacks need at least two colonies.] +Serpentinen brauchen mindestens zwei Kolonien. +[An amphitheatre needs at least two colonies.] +Ein Amphitheater braucht mindestens zwei Kolonien. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Das Karussell passt nicht auf diese Karte; wähle eine größere Karte, kleinere Heimaten oder weniger Kolonien. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Die Serpentinen passen nicht auf diese Karte; wähle eine größere Karte, ein kleineres Plateau, schmalere Pfade oder weniger Kolonien. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Das Amphitheater passt nicht auf diese Karte; wähle eine größere Karte, weniger Ringe, schmalere Terrassen oder weniger Kolonien. [Spider web] Spinnennetz [Spokes per colony] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index ac0e83681..fdf8bf114 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1884,6 +1884,8 @@ Cellestørrelse Rumstørrelse [Corridor width] Korridorbredde +[Spoke width] +Egernes bredde [Loopiness] Løkketæthed [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -2010,6 +2012,56 @@ Grenbredde Koral kræver mindst én koloni. [Coral needs a map at least 128 tiles across.] Koral kræver et kort på mindst 128 felter i bredden. +[Carousel] +Karrusel +[Amphitheatre] +Amfiteater +[Switchbacks] +Hårnålesving +[Court size] +Gårdenes størrelse +[Court wall] +Gårdmur +[Plaza size] +Pladsens størrelse +[Starting tower level] +Niveau for starttårne +[Towers per colony] +Tårne pr. koloni +[Rings] +Ringe +[Ramp width] +Rampebredde +[Pit size] +Arenaens størrelse +[Terrace width] +Terrassebredde +[Border roughness] +Grænsernes ujævnhed +[Bay size] +Bugternes størrelse +[Border wall] +Grænsemur +[Plateau size] +Plateauets størrelse +[Trail width] +Stibredde +[Wall between legs] +Mur mellem sving +[Farm building plots] +Byggegrunde på gårdene +[Carousels need at least two colonies.] +En karrusel kræver mindst to kolonier. +[Switchbacks need at least two colonies.] +Hårnålesving kræver mindst to kolonier. +[An amphitheatre needs at least two colonies.] +Et amfiteater kræver mindst to kolonier. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Karrusellen passer ikke til dette kort; brug et større kort, mindre hjem eller færre kolonier. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Hårnålesvingene passer ikke til dette kort; brug et større kort, et mindre plateau, smallere stier eller færre kolonier. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteatret passer ikke til dette kort; brug et større kort, færre ringe, smallere terrasser eller færre kolonier. [Spider web] Spindelvæv [Spokes per colony] diff --git a/data/texts.en.txt b/data/texts.en.txt index 2424bdb22..ca00f3ade 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1922,6 +1922,60 @@ Branch width Coral needs at least one colony. [Coral needs a map at least 128 tiles across.] Coral needs a map at least 128 tiles across. +[Carousel] +Carousel +[Amphitheatre] +Amphitheatre +[Switchbacks] +Switchbacks +[Corridor width] +Corridor width +[Spoke width] +Spoke width +[Court size] +Court size +[Court wall] +Court wall +[Plaza size] +Plaza size +[Starting tower level] +Starting tower level +[Towers per colony] +Towers per colony +[Rings] +Rings +[Ramp width] +Ramp width +[Pit size] +Pit size +[Terrace width] +Terrace width +[Border roughness] +Border roughness +[Bay size] +Bay size +[Border wall] +Border wall +[Plateau size] +Plateau size +[Trail width] +Trail width +[Wall between legs] +Wall between legs +[Farm building plots] +Farm building plots +[Carousels need at least two colonies.] +Carousels need at least two colonies. +[Switchbacks need at least two colonies.] +Switchbacks need at least two colonies. +[An amphitheatre needs at least two colonies.] +An amphitheatre needs at least two colonies. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies. [Spider webs need at least one colony.] Spider webs need at least one colony. [Spiral] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 726744812..f81f87350 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1806,6 +1806,8 @@ Grandeco de ĉelo Grandeco de ĉambro [Corridor width] Larĝo de koridoro +[Spoke width] +Larĝo de la radioj [Loopiness] Bukleco [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1932,6 +1934,56 @@ Larĝo de branĉoj Koralo bezonas almenaŭ unu kolonion. [Coral needs a map at least 128 tiles across.] Koralo bezonas mapon almenaŭ 128 kahelojn larĝan. +[Carousel] +Karuselo +[Amphitheatre] +Amfiteatro +[Switchbacks] +Serpentenoj +[Court size] +Grando de kortoj +[Court wall] +Muro de kortoj +[Plaza size] +Grando de la placo +[Starting tower level] +Nivelo de la komencaj turoj +[Towers per colony] +Turoj po kolonio +[Rings] +Ringoj +[Ramp width] +Larĝo de deklivirejoj +[Pit size] +Grando de la areno +[Terrace width] +Larĝo de terasoj +[Border roughness] +Malglateco de limoj +[Bay size] +Grando de golfetoj +[Border wall] +Lima muro +[Plateau size] +Grando de la altebenaĵo +[Trail width] +Larĝo de la pado +[Wall between legs] +Muro inter kurboj +[Farm building plots] +Konstruejoj en la bienoj +[Carousels need at least two colonies.] +Karuselo bezonas almenaŭ du koloniojn. +[Switchbacks need at least two colonies.] +Serpentenoj bezonas almenaŭ du koloniojn. +[An amphitheatre needs at least two colonies.] +Amfiteatro bezonas almenaŭ du koloniojn. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +La karuselo ne konvenas al ĉi tiu mapo; uzu pli grandan mapon, pli malgrandajn hejmojn aŭ malpli da kolonioj. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +La serpentenoj ne konvenas al ĉi tiu mapo; uzu pli grandan mapon, pli malgrandan altebenaĵon, pli mallarĝajn padojn aŭ malpli da kolonioj. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +La amfiteatro ne konvenas al ĉi tiu mapo; uzu pli grandan mapon, malpli da ringoj, pli mallarĝajn terasojn aŭ malpli da kolonioj. [Spider web] Araneaĵo [Spokes per colony] diff --git a/data/texts.es.txt b/data/texts.es.txt index 97b012e82..f72941dd2 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1808,6 +1808,8 @@ Tamaño de la celda Tamaño de la sala [Corridor width] Anchura del pasillo +[Spoke width] +Ancho de los radios [Loopiness] Densidad de bucles [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Ancho de las ramas El coral necesita al menos una colonia. [Coral needs a map at least 128 tiles across.] El coral necesita un mapa de al menos 128 casillas de ancho. +[Carousel] +Carrusel +[Amphitheatre] +Anfiteatro +[Switchbacks] +Zigzags +[Court size] +Tamaño de los patios +[Court wall] +Muro de los patios +[Plaza size] +Tamaño de la plaza +[Starting tower level] +Nivel de las torres iniciales +[Towers per colony] +Torres por colonia +[Rings] +Anillos +[Ramp width] +Ancho de las rampas +[Pit size] +Tamaño del foso +[Terrace width] +Ancho de las gradas +[Border roughness] +Irregularidad de las fronteras +[Bay size] +Tamaño de las bahías +[Border wall] +Muro fronterizo +[Plateau size] +Tamaño de la meseta +[Trail width] +Ancho del sendero +[Wall between legs] +Muro entre tramos +[Farm building plots] +Solares en las granjas +[Carousels need at least two colonies.] +El carrusel necesita al menos dos colonias. +[Switchbacks need at least two colonies.] +Los zigzags necesitan al menos dos colonias. +[An amphitheatre needs at least two colonies.] +El anfiteatro necesita al menos dos colonias. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +El carrusel no cabe en este mapa; usa un mapa más grande, bases más pequeñas o menos colonias. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Los zigzags no caben en este mapa; usa un mapa más grande, una meseta más pequeña, senderos más estrechos o menos colonias. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +El anfiteatro no cabe en este mapa; usa un mapa más grande, menos anillos, gradas más estrechas o menos colonias. [Spider web] Telaraña [Spokes per colony] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 9cc3485ac..084da1ad6 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1818,6 +1818,8 @@ Gelaxkaren tamaina Gelaren tamaina [Corridor width] Korridorearen zabalera +[Spoke width] +Erradioen zabalera [Loopiness] Begizta-maila [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1944,6 +1946,56 @@ Adarren zabalera Koraleak gutxienez kolonia bat behar du. [Coral needs a map at least 128 tiles across.] Koraleak gutxienez 128 lauki zabaleko mapa behar du. +[Carousel] +Karrusela +[Amphitheatre] +Anfiteatroa +[Switchbacks] +Sigi-sagak +[Court size] +Patioen tamaina +[Court wall] +Patioko horma +[Plaza size] +Plazaren tamaina +[Starting tower level] +Hasierako dorreen maila +[Towers per colony] +Dorreak koloniako +[Rings] +Eraztunak +[Ramp width] +Arrapalen zabalera +[Pit size] +Zuloaren tamaina +[Terrace width] +Harmailen zabalera +[Border roughness] +Mugen zimurtasuna +[Bay size] +Badien tamaina +[Border wall] +Mugako horma +[Plateau size] +Goi-lautadaren tamaina +[Trail width] +Bidearen zabalera +[Wall between legs] +Bihurguneen arteko horma +[Farm building plots] +Eraikitzeko lursailak baserrietan +[Carousels need at least two colonies.] +Karruselak gutxienez bi kolonia behar ditu. +[Switchbacks need at least two colonies.] +Sigi-sagek gutxienez bi kolonia behar dituzte. +[An amphitheatre needs at least two colonies.] +Anfiteatroak gutxienez bi kolonia behar ditu. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Karrusela ez da mapa honetan sartzen; erabili mapa handiagoa, etxe txikiagoak edo kolonia gutxiago. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Sigi-sagak ez dira mapa honetan sartzen; erabili mapa handiagoa, goi-lautada txikiagoa, bide estuagoak edo kolonia gutxiago. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Anfiteatroa ez da mapa honetan sartzen; erabili mapa handiagoa, eraztun gutxiago, harmaila estuagoak edo kolonia gutxiago. [Spider web] Armiarma-sarea [Spokes per colony] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 0736f41e3..39130a295 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1806,6 +1806,8 @@ OpenGL در این بیلد موجود نیست. اندازهٔ اتاق [Corridor width] پهنای راهرو +[Spoke width] +پهنای پره‌ها [Loopiness] میزان حلقه‌دار بودن [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1932,6 +1934,56 @@ OpenGL در این بیلد موجود نیست. مرجان دست‌کم به یک مستعمره نیاز دارد. [Coral needs a map at least 128 tiles across.] مرجان به نقشه‌ای با پهنای دست‌کم ۱۲۸ خانه نیاز دارد. +[Carousel] +چرخ‌وفلک +[Amphitheatre] +آمفی‌تئاتر +[Switchbacks] +پیچ‌وتاب‌ها +[Court size] +اندازه حیاط‌ها +[Court wall] +دیوار حیاط‌ها +[Plaza size] +اندازه میدان +[Starting tower level] +سطح برج‌های آغازین +[Towers per colony] +برج برای هر مستعمره +[Rings] +حلقه‌ها +[Ramp width] +پهنای سطح‌های شیب‌دار +[Pit size] +اندازه گودال +[Terrace width] +پهنای سکوها +[Border roughness] +ناهمواری مرزها +[Bay size] +اندازه خلیج‌ها +[Border wall] +دیوار مرزی +[Plateau size] +اندازه فلات +[Trail width] +پهنای کوره‌راه +[Wall between legs] +دیوار میان پیچ‌ها +[Farm building plots] +قطعه‌های ساخت در مزرعه‌ها +[Carousels need at least two colonies.] +چرخ‌وفلک دست‌کم به دو مستعمره نیاز دارد. +[Switchbacks need at least two colonies.] +پیچ‌وتاب‌ها دست‌کم به دو مستعمره نیاز دارند. +[An amphitheatre needs at least two colonies.] +آمفی‌تئاتر دست‌کم به دو مستعمره نیاز دارد. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +چرخ‌وفلک در این نقشه جا نمی‌شود؛ از نقشهٔ بزرگ‌تر، پایگاه‌های کوچک‌تر یا مستعمره‌های کمتر استفاده کنید. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +پیچ‌وتاب‌ها در این نقشه جا نمی‌شوند؛ از نقشهٔ بزرگ‌تر، فلات کوچک‌تر، کوره‌راه‌های باریک‌تر یا مستعمره‌های کمتر استفاده کنید. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +آمفی‌تئاتر در این نقشه جا نمی‌شود؛ از نقشهٔ بزرگ‌تر، حلقه‌های کمتر، سکوهای باریک‌تر یا مستعمره‌های کمتر استفاده کنید. [Spider web] تار عنکبوت [Spokes per colony] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index f89250f08..020959f5c 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1808,6 +1808,8 @@ Solun koko Huoneen koko [Corridor width] Käytävän leveys +[Spoke width] +Säteiden leveys [Loopiness] Silmukoiden määrä [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Oksien leveys Koralli tarvitsee vähintään yhden siirtokunnan. [Coral needs a map at least 128 tiles across.] Koralli tarvitsee vähintään 128 ruutua leveän kartan. +[Carousel] +Karuselli +[Amphitheatre] +Amfiteatteri +[Switchbacks] +Serpentiinit +[Court size] +Pihojen koko +[Court wall] +Pihan muuri +[Plaza size] +Aukion koko +[Starting tower level] +Aloitustornien taso +[Towers per colony] +Torneja siirtokuntaa kohden +[Rings] +Renkaat +[Ramp width] +Ramppien leveys +[Pit size] +Areenan koko +[Terrace width] +Terassien leveys +[Border roughness] +Rajojen rosoisuus +[Bay size] +Lahtien koko +[Border wall] +Rajamuuri +[Plateau size] +Ylätasangon koko +[Trail width] +Polun leveys +[Wall between legs] +Muuri mutkien välissä +[Farm building plots] +Rakennuspaikat tiloilla +[Carousels need at least two colonies.] +Karuselli tarvitsee vähintään kaksi siirtokuntaa. +[Switchbacks need at least two colonies.] +Serpentiinit tarvitsevat vähintään kaksi siirtokuntaa. +[An amphitheatre needs at least two colonies.] +Amfiteatteri tarvitsee vähintään kaksi siirtokuntaa. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Karuselli ei mahdu tälle kartalle; käytä suurempaa karttaa, pienempiä kotialueita tai vähemmän siirtokuntia. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Serpentiinit eivät mahdu tälle kartalle; käytä suurempaa karttaa, pienempää ylätasankoa, kapeampia polkuja tai vähemmän siirtokuntia. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteatteri ei mahdu tälle kartalle; käytä suurempaa karttaa, vähemmän renkaita, kapeampia terasseja tai vähemmän siirtokuntia. [Spider web] Hämähäkinseitti [Spokes per colony] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index f3886e811..143f6c69f 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1818,6 +1818,8 @@ Taille des cellules Taille des salles [Corridor width] Largeur des couloirs +[Spoke width] +Largeur des rayons [Loopiness] Taux de boucles [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1944,6 +1946,56 @@ Largeur des branches Le corail nécessite au moins une colonie. [Coral needs a map at least 128 tiles across.] Le corail nécessite une carte d'au moins 128 cases de large. +[Carousel] +Carrousel +[Amphitheatre] +Amphithéâtre +[Switchbacks] +Lacets +[Court size] +Taille des cours +[Court wall] +Mur des cours +[Plaza size] +Taille de la place +[Starting tower level] +Niveau des tours de départ +[Towers per colony] +Tours par colonie +[Rings] +Anneaux +[Ramp width] +Largeur des rampes +[Pit size] +Taille de l'arène +[Terrace width] +Largeur des gradins +[Border roughness] +Irrégularité des frontières +[Bay size] +Taille des baies +[Border wall] +Mur des frontières +[Plateau size] +Taille du plateau +[Trail width] +Largeur du sentier +[Wall between legs] +Mur entre les lacets +[Farm building plots] +Parcelles à bâtir dans les fermes +[Carousels need at least two colonies.] +Le carrousel nécessite au moins deux colonies. +[Switchbacks need at least two colonies.] +Les lacets nécessitent au moins deux colonies. +[An amphitheatre needs at least two colonies.] +L'amphithéâtre nécessite au moins deux colonies. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Le carrousel ne tient pas sur cette carte ; utilisez une carte plus grande, des bases plus petites ou moins de colonies. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Les lacets ne tiennent pas sur cette carte ; utilisez une carte plus grande, un plateau plus petit, des sentiers plus étroits ou moins de colonies. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +L'amphithéâtre ne tient pas sur cette carte ; utilisez une carte plus grande, moins d'anneaux, des gradins plus étroits ou moins de colonies. [Spider web] Toile d'araignée [Spokes per colony] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index ea89c6d82..fe616c8c9 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1878,6 +1878,8 @@ Cortex Μέγεθος δωματίου [Corridor width] Πλάτος διαδρόμου +[Spoke width] +Πλάτος ακτίνων [Loopiness] Βαθμός διασύνδεσης [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -2004,6 +2006,56 @@ Cortex Το κοράλλι χρειάζεται τουλάχιστον μία αποικία. [Coral needs a map at least 128 tiles across.] Το κοράλλι χρειάζεται χάρτη πλάτους τουλάχιστον 128 τετραγώνων. +[Carousel] +Καρουζέλ +[Amphitheatre] +Αμφιθέατρο +[Switchbacks] +Φουρκέτες +[Court size] +Μέγεθος αυλών +[Court wall] +Τείχος αυλών +[Plaza size] +Μέγεθος πλατείας +[Starting tower level] +Επίπεδο αρχικών πύργων +[Towers per colony] +Πύργοι ανά αποικία +[Rings] +Δακτύλιοι +[Ramp width] +Πλάτος ραμπών +[Pit size] +Μέγεθος αρένας +[Terrace width] +Πλάτος κερκίδων +[Border roughness] +Ανωμαλία συνόρων +[Bay size] +Μέγεθος κόλπων +[Border wall] +Συνοριακό τείχος +[Plateau size] +Μέγεθος οροπεδίου +[Trail width] +Πλάτος μονοπατιού +[Wall between legs] +Τείχος ανάμεσα στις στροφές +[Farm building plots] +Οικόπεδα στα αγροκτήματα +[Carousels need at least two colonies.] +Το καρουζέλ χρειάζεται τουλάχιστον δύο αποικίες. +[Switchbacks need at least two colonies.] +Οι φουρκέτες χρειάζονται τουλάχιστον δύο αποικίες. +[An amphitheatre needs at least two colonies.] +Το αμφιθέατρο χρειάζεται τουλάχιστον δύο αποικίες. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Το καρουζέλ δεν χωρά σε αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη, μικρότερες βάσεις ή λιγότερες αποικίες. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Οι φουρκέτες δεν χωρούν σε αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη, μικρότερο οροπέδιο, στενότερα μονοπάτια ή λιγότερες αποικίες. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Το αμφιθέατρο δεν χωρά σε αυτόν τον χάρτη· χρησιμοποιήστε μεγαλύτερο χάρτη, λιγότερους δακτυλίους, στενότερες κερκίδες ή λιγότερες αποικίες. [Spider web] Ιστός αράχνης [Spokes per colony] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 032ad31f1..4a0ca9d0d 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1808,6 +1808,8 @@ Cella mérete Szoba mérete [Corridor width] Folyosó szélessége +[Spoke width] +Küllők szélessége [Loopiness] Huroksűrűség [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Elágazás A korallhoz legalább egy kolónia kell. [Coral needs a map at least 128 tiles across.] A korallhoz legalább 128 mező széles térkép kell. +[Carousel] +Körhinta +[Amphitheatre] +Amfiteátrum +[Switchbacks] +Szerpentinek +[Court size] +Udvarok mérete +[Court wall] +Udvarfal +[Plaza size] +Tér mérete +[Starting tower level] +Kezdő tornyok szintje +[Towers per colony] +Tornyok kolóniánként +[Rings] +Gyűrűk +[Ramp width] +Rámpák szélessége +[Pit size] +Aréna mérete +[Terrace width] +Teraszok szélessége +[Border roughness] +Határok szabálytalansága +[Bay size] +Öblök mérete +[Border wall] +Határfal +[Plateau size] +Fennsík mérete +[Trail width] +Ösvény szélessége +[Wall between legs] +Fal a kanyarok között +[Farm building plots] +Építési telkek a farmokon +[Carousels need at least two colonies.] +A körhintához legalább két kolónia kell. +[Switchbacks need at least two colonies.] +A szerpentinekhez legalább két kolónia kell. +[An amphitheatre needs at least two colonies.] +Az amfiteátrumhoz legalább két kolónia kell. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +A körhinta nem fér el ezen a térképen; használj nagyobb térképet, kisebb otthonokat vagy kevesebb kolóniát. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +A szerpentinek nem férnek el ezen a térképen; használj nagyobb térképet, kisebb fennsíkot, keskenyebb ösvényeket vagy kevesebb kolóniát. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Az amfiteátrum nem fér el ezen a térképen; használj nagyobb térképet, kevesebb gyűrűt, keskenyebb teraszokat vagy kevesebb kolóniát. [Spider web] Pókháló [Spokes per colony] diff --git a/data/texts.id.txt b/data/texts.id.txt index 9fee893bf..d27b0a9fe 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1804,6 +1804,8 @@ Ukuran sel Ukuran ruangan [Corridor width] Lebar koridor +[Spoke width] +Lebar jari-jari [Loopiness] Kepadatan jalur melingkar [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1930,6 +1932,56 @@ Lebar cabang Karang memerlukan setidaknya satu koloni. [Coral needs a map at least 128 tiles across.] Karang memerlukan peta dengan lebar setidaknya 128 petak. +[Carousel] +Komidi putar +[Amphitheatre] +Amfiteater +[Switchbacks] +Jalan berkelok +[Court size] +Ukuran halaman +[Court wall] +Tembok halaman +[Plaza size] +Ukuran alun-alun +[Starting tower level] +Tingkat menara awal +[Towers per colony] +Menara per koloni +[Rings] +Cincin +[Ramp width] +Lebar tanjakan +[Pit size] +Ukuran arena +[Terrace width] +Lebar teras +[Border roughness] +Kekasaran batas +[Bay size] +Ukuran teluk +[Border wall] +Tembok batas +[Plateau size] +Ukuran dataran tinggi +[Trail width] +Lebar jalan setapak +[Wall between legs] +Tembok antar tikungan +[Farm building plots] +Petak bangunan di ladang +[Carousels need at least two colonies.] +Komidi putar memerlukan setidaknya dua koloni. +[Switchbacks need at least two colonies.] +Jalan berkelok memerlukan setidaknya dua koloni. +[An amphitheatre needs at least two colonies.] +Amfiteater memerlukan setidaknya dua koloni. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Komidi putar tidak muat di peta ini; gunakan peta lebih besar, markas lebih kecil, atau lebih sedikit koloni. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Jalan berkelok tidak muat di peta ini; gunakan peta lebih besar, dataran tinggi lebih kecil, jalan setapak lebih sempit, atau lebih sedikit koloni. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteater tidak muat di peta ini; gunakan peta lebih besar, lebih sedikit cincin, teras lebih sempit, atau lebih sedikit koloni. [Spider web] Jaring laba-laba [Spokes per colony] diff --git a/data/texts.it.txt b/data/texts.it.txt index 8e7764e45..ec5d61772 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1870,6 +1870,8 @@ Dimensione delle celle Dimensione delle stanze [Corridor width] Larghezza dei corridoi +[Spoke width] +Larghezza dei raggi [Loopiness] Densità dei cicli [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1996,6 +1998,56 @@ Larghezza dei rami Il corallo richiede almeno una colonia. [Coral needs a map at least 128 tiles across.] Il corallo richiede una mappa larga almeno 128 caselle. +[Carousel] +Giostra +[Amphitheatre] +Anfiteatro +[Switchbacks] +Tornanti +[Court size] +Dimensione dei cortili +[Court wall] +Muro dei cortili +[Plaza size] +Dimensione della piazza +[Starting tower level] +Livello delle torri iniziali +[Towers per colony] +Torri per colonia +[Rings] +Anelli +[Ramp width] +Larghezza delle rampe +[Pit size] +Dimensione della fossa +[Terrace width] +Larghezza delle gradinate +[Border roughness] +Irregolarità dei confini +[Bay size] +Dimensione delle baie +[Border wall] +Muro di confine +[Plateau size] +Dimensione dell'altopiano +[Trail width] +Larghezza del sentiero +[Wall between legs] +Muro tra i tornanti +[Farm building plots] +Lotti edificabili nelle fattorie +[Carousels need at least two colonies.] +La giostra richiede almeno due colonie. +[Switchbacks need at least two colonies.] +I tornanti richiedono almeno due colonie. +[An amphitheatre needs at least two colonies.] +L'anfiteatro richiede almeno due colonie. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +La giostra non entra in questa mappa; usa una mappa più grande, basi più piccole o meno colonie. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +I tornanti non entrano in questa mappa; usa una mappa più grande, un altopiano più piccolo, sentieri più stretti o meno colonie. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +L'anfiteatro non entra in questa mappa; usa una mappa più grande, meno anelli, gradinate più strette o meno colonie. [Spider web] Ragnatela [Spokes per colony] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 859135c05..d4b31217b 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1804,6 +1804,8 @@ Globulation 2 のカーソルを表示します。 部屋の大きさ [Corridor width] 通路の幅 +[Spoke width] +スポークの幅 [Loopiness] ループの多さ [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1930,6 +1932,56 @@ Globulation 2 のカーソルを表示します。 サンゴには少なくとも1つのコロニーが必要です。 [Coral needs a map at least 128 tiles across.] サンゴには幅128マス以上のマップが必要です。 +[Carousel] +回転木馬 +[Amphitheatre] +円形闘技場 +[Switchbacks] +つづら折り +[Court size] +中庭の大きさ +[Court wall] +中庭の壁 +[Plaza size] +広場の大きさ +[Starting tower level] +初期タワーのレベル +[Towers per colony] +コロニーごとのタワー数 +[Rings] +環の数 +[Ramp width] +斜路の幅 +[Pit size] +闘技場の大きさ +[Terrace width] +段の幅 +[Border roughness] +境界の起伏 +[Bay size] +入り江の大きさ +[Border wall] +境界の壁 +[Plateau size] +台地の大きさ +[Trail width] +山道の幅 +[Wall between legs] +折り返しの間の壁 +[Farm building plots] +農場の建設用地 +[Carousels need at least two colonies.] +回転木馬には少なくとも2つのコロニーが必要です。 +[Switchbacks need at least two colonies.] +つづら折りには少なくとも2つのコロニーが必要です。 +[An amphitheatre needs at least two colonies.] +円形闘技場には少なくとも2つのコロニーが必要です。 +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +回転木馬はこの地図に収まりません。より大きな地図、より小さな本拠地、またはより少ないコロニーにしてください。 +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +つづら折りはこの地図に収まりません。より大きな地図、より小さな台地、より狭い山道、またはより少ないコロニーにしてください。 +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +円形闘技場はこの地図に収まりません。より大きな地図、より少ない環、より狭い段、またはより少ないコロニーにしてください。 [Spider web] クモの巣 [Spokes per colony] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index e9b2a5d34..a17498252 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -959,6 +959,33 @@ [Branch width] [Coral needs at least one colony.] [Coral needs a map at least 128 tiles across.] +[Carousel] +[Amphitheatre] +[Switchbacks] +[Corridor width] +[Spoke width] +[Court size] +[Court wall] +[Plaza size] +[Starting tower level] +[Towers per colony] +[Rings] +[Ramp width] +[Pit size] +[Terrace width] +[Border roughness] +[Bay size] +[Border wall] +[Plateau size] +[Trail width] +[Wall between legs] +[Farm building plots] +[Carousels need at least two colonies.] +[Switchbacks need at least two colonies.] +[An amphitheatre needs at least two colonies.] +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] [Spider webs need at least one colony.] [Spiral] [Dew drops] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index d1f3b899d..9e1d07a66 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1804,6 +1804,8 @@ Globulation 2 커서를 표시합니다. 방 크기 [Corridor width] 통로 너비 +[Spoke width] +바큇살 너비 [Loopiness] 순환로 밀도 [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1930,6 +1932,56 @@ Globulation 2 커서를 표시합니다. 산호에는 식민지가 하나 이상 필요합니다. [Coral needs a map at least 128 tiles across.] 산호에는 너비가 128칸 이상인 지도가 필요합니다. +[Carousel] +회전목마 +[Amphitheatre] +원형 경기장 +[Switchbacks] +굽잇길 +[Court size] +안뜰 크기 +[Court wall] +안뜰 벽 +[Plaza size] +광장 크기 +[Starting tower level] +시작 탑 레벨 +[Towers per colony] +식민지당 탑 수 +[Rings] +고리 수 +[Ramp width] +경사로 너비 +[Pit size] +경기장 크기 +[Terrace width] +계단식 단 너비 +[Border roughness] +경계의 굴곡 +[Bay size] +만 크기 +[Border wall] +경계 벽 +[Plateau size] +고원 크기 +[Trail width] +산길 너비 +[Wall between legs] +굽이 사이 벽 +[Farm building plots] +농장 건설 부지 +[Carousels need at least two colonies.] +회전목마에는 식민지가 두 개 이상 필요합니다. +[Switchbacks need at least two colonies.] +굽잇길에는 식민지가 두 개 이상 필요합니다. +[An amphitheatre needs at least two colonies.] +원형 경기장에는 식민지가 두 개 이상 필요합니다. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +회전목마가 이 지도에 맞지 않습니다. 더 큰 지도, 더 작은 본거지 또는 더 적은 식민지를 사용하세요. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +굽잇길이 이 지도에 맞지 않습니다. 더 큰 지도, 더 작은 고원, 더 좁은 산길 또는 더 적은 식민지를 사용하세요. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +원형 경기장이 이 지도에 맞지 않습니다. 더 큰 지도, 더 적은 고리, 더 좁은 단 또는 더 적은 식민지를 사용하세요. [Spider web] 거미줄 [Spokes per colony] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 1e0455a1e..54ab87504 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1832,6 +1832,8 @@ Celgrootte Kamergrootte [Corridor width] Gangbreedte +[Spoke width] +Spaakbreedte [Loopiness] Aantal lussen [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1958,6 +1960,56 @@ Takbreedte Koraal heeft minstens één kolonie nodig. [Coral needs a map at least 128 tiles across.] Koraal heeft een kaart van minstens 128 tegels breed nodig. +[Carousel] +Draaimolen +[Amphitheatre] +Amfitheater +[Switchbacks] +Haarspeldbochten +[Court size] +Grootte van de binnenplaatsen +[Court wall] +Muur van de binnenplaatsen +[Plaza size] +Grootte van het plein +[Starting tower level] +Niveau van de starttorens +[Towers per colony] +Torens per kolonie +[Rings] +Ringen +[Ramp width] +Hellingbreedte +[Pit size] +Grootte van de arena +[Terrace width] +Terrasbreedte +[Border roughness] +Grillige grenzen +[Bay size] +Grootte van de baaien +[Border wall] +Grensmuur +[Plateau size] +Grootte van het plateau +[Trail width] +Padbreedte +[Wall between legs] +Muur tussen bochten +[Farm building plots] +Bouwkavels in de boerderijen +[Carousels need at least two colonies.] +Een draaimolen heeft minstens twee kolonies nodig. +[Switchbacks need at least two colonies.] +Haarspeldbochten hebben minstens twee kolonies nodig. +[An amphitheatre needs at least two colonies.] +Een amfitheater heeft minstens twee kolonies nodig. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +De draaimolen past niet op deze kaart; gebruik een grotere kaart, kleinere thuisbases of minder kolonies. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +De haarspeldbochten passen niet op deze kaart; gebruik een grotere kaart, een kleiner plateau, smallere paden of minder kolonies. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Het amfitheater past niet op deze kaart; gebruik een grotere kaart, minder ringen, smallere terrassen of minder kolonies. [Spider web] Spinnenweb [Spokes per colony] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 13af94743..b9f8cc678 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1808,6 +1808,8 @@ Rozmiar komórki Rozmiar pomieszczenia [Corridor width] Szerokość korytarza +[Spoke width] +Szerokość szprych [Loopiness] Liczba pętli [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Szerokość gałęzi Koralowiec wymaga co najmniej jednej kolonii. [Coral needs a map at least 128 tiles across.] Koralowiec wymaga mapy o szerokości co najmniej 128 pól. +[Carousel] +Karuzela +[Amphitheatre] +Amfiteatr +[Switchbacks] +Serpentyny +[Court size] +Rozmiar dziedzińców +[Court wall] +Mur dziedzińców +[Plaza size] +Rozmiar placu +[Starting tower level] +Poziom wież startowych +[Towers per colony] +Wieże na kolonię +[Rings] +Pierścienie +[Ramp width] +Szerokość ramp +[Pit size] +Rozmiar areny +[Terrace width] +Szerokość tarasów +[Border roughness] +Nieregularność granic +[Bay size] +Rozmiar zatok +[Border wall] +Mur graniczny +[Plateau size] +Rozmiar płaskowyżu +[Trail width] +Szerokość ścieżki +[Wall between legs] +Mur między zakrętami +[Farm building plots] +Działki budowlane na farmach +[Carousels need at least two colonies.] +Karuzela wymaga co najmniej dwóch kolonii. +[Switchbacks need at least two colonies.] +Serpentyny wymagają co najmniej dwóch kolonii. +[An amphitheatre needs at least two colonies.] +Amfiteatr wymaga co najmniej dwóch kolonii. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Karuzela nie mieści się na tej mapie; użyj większej mapy, mniejszych baz lub mniejszej liczby kolonii. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Serpentyny nie mieszczą się na tej mapie; użyj większej mapy, mniejszego płaskowyżu, węższych ścieżek lub mniejszej liczby kolonii. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteatr nie mieści się na tej mapie; użyj większej mapy, mniejszej liczby pierścieni, węższych tarasów lub mniejszej liczby kolonii. [Spider web] Pajęczyna [Spokes per colony] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 341e0618a..9adbe3f69 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1814,6 +1814,8 @@ Tamanho da célula Tamanho da sala [Corridor width] Largura do corredor +[Spoke width] +Largura dos raios [Loopiness] Densidade de ciclos [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1940,6 +1942,56 @@ Largura dos ramos O coral precisa de pelo menos uma colónia. [Coral needs a map at least 128 tiles across.] O coral precisa de um mapa com pelo menos 128 casas de largura. +[Carousel] +Carrossel +[Amphitheatre] +Anfiteatro +[Switchbacks] +Ziguezagues +[Court size] +Tamanho dos pátios +[Court wall] +Muro dos pátios +[Plaza size] +Tamanho da praça +[Starting tower level] +Nível das torres iniciais +[Towers per colony] +Torres por colónia +[Rings] +Anéis +[Ramp width] +Largura das rampas +[Pit size] +Tamanho da arena +[Terrace width] +Largura das bancadas +[Border roughness] +Irregularidade das fronteiras +[Bay size] +Tamanho das baías +[Border wall] +Muro de fronteira +[Plateau size] +Tamanho do planalto +[Trail width] +Largura do trilho +[Wall between legs] +Muro entre troços +[Farm building plots] +Lotes para construir nas quintas +[Carousels need at least two colonies.] +O carrossel precisa de pelo menos duas colónias. +[Switchbacks need at least two colonies.] +Os ziguezagues precisam de pelo menos duas colónias. +[An amphitheatre needs at least two colonies.] +O anfiteatro precisa de pelo menos duas colónias. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +O carrossel não cabe neste mapa; use um mapa maior, bases menores ou menos colónias. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Os ziguezagues não cabem neste mapa; use um mapa maior, um planalto menor, trilhos mais estreitos ou menos colónias. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +O anfiteatro não cabe neste mapa; use um mapa maior, menos anéis, bancadas mais estreitas ou menos colónias. [Spider web] Teia de aranha [Spokes per colony] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index d221fbb81..8db9ced3b 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1808,6 +1808,8 @@ Mărimea celulei Mărimea camerei [Corridor width] Lățimea coridorului +[Spoke width] +Lățimea spițelor [Loopiness] Densitatea buclelor [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Lățimea ramurilor Coralul are nevoie de cel puțin o colonie. [Coral needs a map at least 128 tiles across.] Coralul are nevoie de o hartă lată de cel puțin 128 de pătrate. +[Carousel] +Carusel +[Amphitheatre] +Amfiteatru +[Switchbacks] +Serpentine +[Court size] +Mărimea curților +[Court wall] +Zidul curților +[Plaza size] +Mărimea pieței +[Starting tower level] +Nivelul turnurilor de start +[Towers per colony] +Turnuri per colonie +[Rings] +Inele +[Ramp width] +Lățimea rampelor +[Pit size] +Mărimea arenei +[Terrace width] +Lățimea teraselor +[Border roughness] +Neregularitatea granițelor +[Bay size] +Mărimea golfurilor +[Border wall] +Zid de graniță +[Plateau size] +Mărimea platoului +[Trail width] +Lățimea potecii +[Wall between legs] +Zid între serpentine +[Farm building plots] +Loturi de construcție în ferme +[Carousels need at least two colonies.] +Caruselul are nevoie de cel puțin două colonii. +[Switchbacks need at least two colonies.] +Serpentinele au nevoie de cel puțin două colonii. +[An amphitheatre needs at least two colonies.] +Amfiteatrul are nevoie de cel puțin două colonii. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Caruselul nu încape pe această hartă; folosește o hartă mai mare, baze mai mici sau mai puține colonii. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Serpentinele nu încap pe această hartă; folosește o hartă mai mare, un platou mai mic, poteci mai înguste sau mai puține colonii. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteatrul nu încape pe această hartă; folosește o hartă mai mare, mai puține inele, terase mai înguste sau mai puține colonii. [Spider web] Pânză de păianjen [Spokes per colony] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index ad83e60b5..744cec4b4 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1806,6 +1806,8 @@ OpenGL недоступен в этой сборке. Размер комнаты [Corridor width] Ширина коридора +[Spoke width] +Ширина спиц [Loopiness] Количество петель [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1932,6 +1934,56 @@ OpenGL недоступен в этой сборке. Для коралла нужна хотя бы одна колония. [Coral needs a map at least 128 tiles across.] Для коралла нужна карта шириной не менее 128 клеток. +[Carousel] +Карусель +[Amphitheatre] +Амфитеатр +[Switchbacks] +Серпантин +[Court size] +Размер дворов +[Court wall] +Стена дворов +[Plaza size] +Размер площади +[Starting tower level] +Уровень начальных башен +[Towers per colony] +Башен на колонию +[Rings] +Кольца +[Ramp width] +Ширина пандусов +[Pit size] +Размер арены +[Terrace width] +Ширина террас +[Border roughness] +Изрезанность границ +[Bay size] +Размер заливов +[Border wall] +Пограничная стена +[Plateau size] +Размер плато +[Trail width] +Ширина тропы +[Wall between legs] +Стена между витками +[Farm building plots] +Участки под застройку на фермах +[Carousels need at least two colonies.] +Для карусели нужны хотя бы две колонии. +[Switchbacks need at least two colonies.] +Для серпантина нужны хотя бы две колонии. +[An amphitheatre needs at least two colonies.] +Для амфитеатра нужны хотя бы две колонии. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Карусель не помещается на этой карте; используйте карту побольше, базы поменьше или меньше колоний. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Серпантин не помещается на этой карте; используйте карту побольше, плато поменьше, тропы поуже или меньше колоний. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Амфитеатр не помещается на этой карте; используйте карту побольше, меньше колец, террасы поуже или меньше колоний. [Spider web] Паутина [Spokes per colony] diff --git a/data/texts.si.txt b/data/texts.si.txt index 64b82480f..63d489c2a 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1808,6 +1808,8 @@ Velikost celice Velikost sobe [Corridor width] Širina hodnika +[Spoke width] +Širina prečk [Loopiness] Gostota zank [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ Razvejanost Korala potrebuje vsaj eno kolonijo. [Coral needs a map at least 128 tiles across.] Korala potrebuje zemljevid, širok vsaj 128 polj. +[Carousel] +Vrtiljak +[Amphitheatre] +Amfiteater +[Switchbacks] +Serpentine +[Court size] +Velikost dvorišč +[Court wall] +Zid dvorišč +[Plaza size] +Velikost trga +[Starting tower level] +Raven začetnih stolpov +[Towers per colony] +Stolpi na kolonijo +[Rings] +Obroči +[Ramp width] +Širina klančin +[Pit size] +Velikost jame +[Terrace width] +Širina teras +[Border roughness] +Razgibanost meja +[Bay size] +Velikost zalivov +[Border wall] +Mejni zid +[Plateau size] +Velikost planote +[Trail width] +Širina poti +[Wall between legs] +Zid med ovinki +[Farm building plots] +Gradbene parcele na kmetijah +[Carousels need at least two colonies.] +Vrtiljak potrebuje vsaj dve koloniji. +[Switchbacks need at least two colonies.] +Serpentine potrebujejo vsaj dve koloniji. +[An amphitheatre needs at least two colonies.] +Amfiteater potrebuje vsaj dve koloniji. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Vrtiljak ne gre na ta zemljevid; uporabite večji zemljevid, manjše domove ali manj kolonij. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Serpentine ne gredo na ta zemljevid; uporabite večji zemljevid, manjšo planoto, ožje poti ali manj kolonij. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteater ne gre na ta zemljevid; uporabite večji zemljevid, manj obročev, ožje terase ali manj kolonij. [Spider web] Pajčevina [Spokes per colony] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 22a185d51..5b11d1abe 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1816,6 +1816,8 @@ Veľkosť bunky Veľkosť miestnosti [Corridor width] Šírka chodby +[Spoke width] +Šírka lúčov [Loopiness] Hustota slučiek [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1942,6 +1944,56 @@ Vetvenie Koral potrebuje aspoň jednu kolóniu. [Coral needs a map at least 128 tiles across.] Koral potrebuje mapu širokú aspoň 128 polí. +[Carousel] +Kolotoč +[Amphitheatre] +Amfiteáter +[Switchbacks] +Serpentíny +[Court size] +Veľkosť nádvorí +[Court wall] +Múr nádvorí +[Plaza size] +Veľkosť námestia +[Starting tower level] +Úroveň počiatočných veží +[Towers per colony] +Veže na kolóniu +[Rings] +Prstence +[Ramp width] +Šírka rámp +[Pit size] +Veľkosť jamy +[Terrace width] +Šírka terás +[Border roughness] +Členitosť hraníc +[Bay size] +Veľkosť zálivov +[Border wall] +Hraničný múr +[Plateau size] +Veľkosť náhornej plošiny +[Trail width] +Šírka chodníka +[Wall between legs] +Múr medzi serpentínami +[Farm building plots] +Stavebné pozemky na farmách +[Carousels need at least two colonies.] +Kolotoč potrebuje aspoň dve kolónie. +[Switchbacks need at least two colonies.] +Serpentíny potrebujú aspoň dve kolónie. +[An amphitheatre needs at least two colonies.] +Amfiteáter potrebuje aspoň dve kolónie. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Kolotoč sa na túto mapu nezmestí; použite väčšiu mapu, menšie domovy alebo menej kolónií. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Serpentíny sa na túto mapu nezmestia; použite väčšiu mapu, menšiu náhornú plošinu, užšie chodníky alebo menej kolónií. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteáter sa na túto mapu nezmestí; použite väčšiu mapu, menej prstencov, užšie terasy alebo menej kolónií. [Spider web] Pavučina [Spokes per colony] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 0bda460af..4278cf799 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1808,6 +1808,8 @@ OpenGL није доступан у овој верзији. Величина собе [Corridor width] Ширина ходника +[Spoke width] +Ширина жбица [Loopiness] Учесталост петљи [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1934,6 +1936,56 @@ OpenGL није доступан у овој верзији. Коралу је потребна бар једна колонија. [Coral needs a map at least 128 tiles across.] Коралу је потребна мапа широка бар 128 поља. +[Carousel] +Вртешка +[Amphitheatre] +Амфитеатар +[Switchbacks] +Серпентине +[Court size] +Величина дворишта +[Court wall] +Зид дворишта +[Plaza size] +Величина трга +[Starting tower level] +Ниво почетних кула +[Towers per colony] +Куле по колонији +[Rings] +Прстенови +[Ramp width] +Ширина рампи +[Pit size] +Величина јаме +[Terrace width] +Ширина тераса +[Border roughness] +Разуђеност граница +[Bay size] +Величина залива +[Border wall] +Гранични зид +[Plateau size] +Величина висоравни +[Trail width] +Ширина стазе +[Wall between legs] +Зид између кривина +[Farm building plots] +Грађевинске парцеле на фармама +[Carousels need at least two colonies.] +Вртешци су потребне бар две колоније. +[Switchbacks need at least two colonies.] +Серпентинама су потребне бар две колоније. +[An amphitheatre needs at least two colonies.] +Амфитеатру су потребне бар две колоније. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Вртешка не стаје на ову мапу; користите већу мапу, мање базе или мање колонија. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Серпентине не стају на ову мапу; користите већу мапу, мању висораван, уже стазе или мање колонија. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Амфитеатар не стаје на ову мапу; користите већу мапу, мање прстенова, уже терасе или мање колонија. [Spider web] Паукова мрежа [Spokes per colony] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 16c68e3e4..e3aaa496b 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1818,6 +1818,8 @@ Cellstorlek Rumsstorlek [Corridor width] Korridorbredd +[Spoke width] +Ekrarnas bredd [Loopiness] Antal slingor [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1944,6 +1946,56 @@ Grenbredd Korall behöver minst en koloni. [Coral needs a map at least 128 tiles across.] Korall behöver en karta som är minst 128 rutor bred. +[Carousel] +Karusell +[Amphitheatre] +Amfiteater +[Switchbacks] +Serpentiner +[Court size] +Gårdarnas storlek +[Court wall] +Gårdsmur +[Plaza size] +Torgets storlek +[Starting tower level] +Starttornens nivå +[Towers per colony] +Torn per koloni +[Rings] +Ringar +[Ramp width] +Rampbredd +[Pit size] +Arenans storlek +[Terrace width] +Terrassbredd +[Border roughness] +Gränsernas ojämnhet +[Bay size] +Vikarnas storlek +[Border wall] +Gränsmur +[Plateau size] +Platåns storlek +[Trail width] +Stigbredd +[Wall between legs] +Mur mellan kurvor +[Farm building plots] +Byggtomter på gårdarna +[Carousels need at least two colonies.] +En karusell behöver minst två kolonier. +[Switchbacks need at least two colonies.] +Serpentiner behöver minst två kolonier. +[An amphitheatre needs at least two colonies.] +En amfiteater behöver minst två kolonier. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Karusellen får inte plats på den här kartan; använd en större karta, mindre hem eller färre kolonier. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Serpentinerna får inte plats på den här kartan; använd en större karta, en mindre platå, smalare stigar eller färre kolonier. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteatern får inte plats på den här kartan; använd en större karta, färre ringar, smalare terrasser eller färre kolonier. [Spider web] Spindelnät [Spokes per colony] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index a8a54fa99..3574ffb14 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1816,6 +1816,8 @@ Hücre boyutu Oda boyutu [Corridor width] Koridor genişliği +[Spoke width] +Tel genişliği [Loopiness] Döngü yoğunluğu [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1942,6 +1944,56 @@ Dal genişliği Mercan en az bir koloni gerektirir. [Coral needs a map at least 128 tiles across.] Mercan en az 128 kare genişliğinde bir harita gerektirir. +[Carousel] +Atlıkarınca +[Amphitheatre] +Amfiteatr +[Switchbacks] +Virajlar +[Court size] +Avlu boyutu +[Court wall] +Avlu duvarı +[Plaza size] +Meydan boyutu +[Starting tower level] +Başlangıç kulelerinin seviyesi +[Towers per colony] +Koloni başına kule +[Rings] +Halkalar +[Ramp width] +Rampa genişliği +[Pit size] +Arena boyutu +[Terrace width] +Teras genişliği +[Border roughness] +Sınır düzensizliği +[Bay size] +Koy boyutu +[Border wall] +Sınır duvarı +[Plateau size] +Plato boyutu +[Trail width] +Patika genişliği +[Wall between legs] +Virajlar arası duvar +[Farm building plots] +Çiftliklerde yapı arsaları +[Carousels need at least two colonies.] +Atlıkarınca en az iki koloni gerektirir. +[Switchbacks need at least two colonies.] +Virajlar en az iki koloni gerektirir. +[An amphitheatre needs at least two colonies.] +Amfiteatr en az iki koloni gerektirir. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Atlıkarınca bu haritaya sığmıyor; daha büyük bir harita, daha küçük üsler veya daha az koloni kullanın. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Virajlar bu haritaya sığmıyor; daha büyük bir harita, daha küçük bir plato, daha dar patikalar veya daha az koloni kullanın. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Amfiteatr bu haritaya sığmıyor; daha büyük bir harita, daha az halka, daha dar teraslar veya daha az koloni kullanın. [Spider web] Örümcek ağı [Spokes per colony] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 34464abd2..e2bc81d99 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1804,6 +1804,8 @@ OpenGL недоступний у цій збірці. Розмір кімнати [Corridor width] Ширина коридору +[Spoke width] +Ширина спиць [Loopiness] Кількість петель [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1930,6 +1932,56 @@ OpenGL недоступний у цій збірці. Для корала потрібна принаймні одна колонія. [Coral needs a map at least 128 tiles across.] Для корала потрібна мапа завширшки щонайменше 128 клітинок. +[Carousel] +Карусель +[Amphitheatre] +Амфітеатр +[Switchbacks] +Серпантин +[Court size] +Розмір дворів +[Court wall] +Стіна дворів +[Plaza size] +Розмір площі +[Starting tower level] +Рівень початкових веж +[Towers per colony] +Веж на колонію +[Rings] +Кільця +[Ramp width] +Ширина пандусів +[Pit size] +Розмір арени +[Terrace width] +Ширина терас +[Border roughness] +Порізаність кордонів +[Bay size] +Розмір заток +[Border wall] +Прикордонна стіна +[Plateau size] +Розмір плато +[Trail width] +Ширина стежки +[Wall between legs] +Стіна між витками +[Farm building plots] +Ділянки під забудову на фермах +[Carousels need at least two colonies.] +Для каруселі потрібні принаймні дві колонії. +[Switchbacks need at least two colonies.] +Для серпантину потрібні принаймні дві колонії. +[An amphitheatre needs at least two colonies.] +Для амфітеатру потрібні принаймні дві колонії. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Карусель не вміщується на цій мапі; використайте більшу мапу, менші бази або менше колоній. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Серпантин не вміщується на цій мапі; використайте більшу мапу, менше плато, вужчі стежки або менше колоній. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Амфітеатр не вміщується на цій мапі; використайте більшу мапу, менше кілець, вужчі тераси або менше колоній. [Spider web] Павутина [Spokes per colony] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 4266bae21..f118baa46 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1804,6 +1804,8 @@ Kích thước ô Kích thước phòng [Corridor width] Độ rộng hành lang +[Spoke width] +Độ rộng nan hoa [Loopiness] Mật độ vòng lặp [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1930,6 +1932,56 @@ Phân nhánh San hô cần ít nhất một thuộc địa. [Coral needs a map at least 128 tiles across.] San hô cần bản đồ rộng ít nhất 128 ô. +[Carousel] +Vòng quay ngựa gỗ +[Amphitheatre] +Đấu trường +[Switchbacks] +Đường đèo +[Court size] +Kích thước sân +[Court wall] +Tường sân +[Plaza size] +Kích thước quảng trường +[Starting tower level] +Cấp tháp ban đầu +[Towers per colony] +Số tháp mỗi thuộc địa +[Rings] +Số vòng +[Ramp width] +Độ rộng dốc +[Pit size] +Kích thước hố +[Terrace width] +Độ rộng bậc +[Border roughness] +Độ gồ ghề của biên giới +[Bay size] +Kích thước vịnh +[Border wall] +Tường biên giới +[Plateau size] +Kích thước cao nguyên +[Trail width] +Độ rộng lối mòn +[Wall between legs] +Tường giữa các khúc +[Farm building plots] +Ô đất xây dựng trong nông trại +[Carousels need at least two colonies.] +Vòng quay ngựa gỗ cần ít nhất hai thuộc địa. +[Switchbacks need at least two colonies.] +Đường đèo cần ít nhất hai thuộc địa. +[An amphitheatre needs at least two colonies.] +Đấu trường cần ít nhất hai thuộc địa. +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +Vòng quay ngựa gỗ không vừa bản đồ này; hãy dùng bản đồ lớn hơn, căn cứ nhỏ hơn hoặc ít thuộc địa hơn. +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +Đường đèo không vừa bản đồ này; hãy dùng bản đồ lớn hơn, cao nguyên nhỏ hơn, lối mòn hẹp hơn hoặc ít thuộc địa hơn. +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +Đấu trường không vừa bản đồ này; hãy dùng bản đồ lớn hơn, ít vòng hơn, bậc hẹp hơn hoặc ít thuộc địa hơn. [Spider web] Mạng nhện [Spokes per colony] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 2fbc0ee3f..d0a8a5749 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1804,6 +1804,8 @@ OpenGL 在此版本中不可用。 房间大小 [Corridor width] 通道宽度 +[Spoke width] +辐条宽度 [Loopiness] 回环程度 [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1930,6 +1932,56 @@ OpenGL 在此版本中不可用。 珊瑚至少需要一个殖民地。 [Coral needs a map at least 128 tiles across.] 珊瑚需要宽度至少为 128 格的地图。 +[Carousel] +旋转木马 +[Amphitheatre] +圆形剧场 +[Switchbacks] +之字山路 +[Court size] +庭院大小 +[Court wall] +庭院围墙 +[Plaza size] +广场大小 +[Starting tower level] +初始塔等级 +[Towers per colony] +每个殖民地的塔数 +[Rings] +环数 +[Ramp width] +坡道宽度 +[Pit size] +竞技场大小 +[Terrace width] +台阶宽度 +[Border roughness] +边界起伏 +[Bay size] +海湾大小 +[Border wall] +边界围墙 +[Plateau size] +高原大小 +[Trail width] +山路宽度 +[Wall between legs] +弯道之间的墙 +[Farm building plots] +农场建筑用地 +[Carousels need at least two colonies.] +旋转木马至少需要两个殖民地。 +[Switchbacks need at least two colonies.] +之字山路至少需要两个殖民地。 +[An amphitheatre needs at least two colonies.] +圆形剧场至少需要两个殖民地。 +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +此地图放不下旋转木马;请使用更大的地图、更小的基地或更少的殖民地。 +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +此地图放不下之字山路;请使用更大的地图、更小的高原、更窄的山路或更少的殖民地。 +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +此地图放不下圆形剧场;请使用更大的地图、更少的环、更窄的台阶或更少的殖民地。 [Spider web] 蜘蛛网 [Spokes per colony] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 312a7d12c..6ba3634d7 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1880,6 +1880,8 @@ OpenGL 在此版本中不可用。 房間大小 [Corridor width] 通道寬度 +[Spoke width] +輻條寬度 [Loopiness] 迴路密度 [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -2006,6 +2008,56 @@ OpenGL 在此版本中不可用。 珊瑚至少需要一個殖民地。 [Coral needs a map at least 128 tiles across.] 珊瑚需要寬度至少為 128 格的地圖。 +[Carousel] +旋轉木馬 +[Amphitheatre] +圓形劇場 +[Switchbacks] +之字山路 +[Court size] +庭院大小 +[Court wall] +庭院圍牆 +[Plaza size] +廣場大小 +[Starting tower level] +初始塔等級 +[Towers per colony] +每個殖民地的塔數 +[Rings] +環數 +[Ramp width] +坡道寬度 +[Pit size] +競技場大小 +[Terrace width] +台階寬度 +[Border roughness] +邊界起伏 +[Bay size] +海灣大小 +[Border wall] +邊界圍牆 +[Plateau size] +高原大小 +[Trail width] +山路寬度 +[Wall between legs] +彎道之間的牆 +[Farm building plots] +農場建築用地 +[Carousels need at least two colonies.] +旋轉木馬至少需要兩個殖民地。 +[Switchbacks need at least two colonies.] +之字山路至少需要兩個殖民地。 +[An amphitheatre needs at least two colonies.] +圓形劇場至少需要兩個殖民地。 +[The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] +此地圖放不下旋轉木馬;請使用更大的地圖、更小的基地或更少的殖民地。 +[The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] +此地圖放不下之字山路;請使用更大的地圖、更小的高原、更窄的山路或更少的殖民地。 +[The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] +此地圖放不下圓形劇場;請使用更大的地圖、更少的環、更窄的台階或更少的殖民地。 [Spider web] 蜘蛛網 [Spokes per colony] diff --git a/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md index eebf34065..d0e7b7e2a 100644 --- a/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md +++ b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md @@ -31,9 +31,9 @@ comments can say "because grass may not touch water" and a reader can check it h closes it until it is cleared, which is why generators keep lanes and roads clear and run `openRoad` after placing deposits. -- **Towers shoot over walls.** A defence tower is 2x2 and scans square rings round its footprint's - top-left tile out to its range, 5, 7 or 9 tiles by level, with no line of sight - (`Building::findBestTarget`, `BuildingTypesDefence.cpp`). A wall stops walking but not shooting, so +- **Towers shoot over walls.** A defence tower is 2x2 and scans square rings round its footprint out + to its range, 5, 7 or 9 tiles beyond every side by level, with no line of sight + (`Building::findBestTarget`, `BuildingUtils::turretScanTile`, `BuildingTypesDefence.cpp`). A wall stops walking but not shooting, so a wall's thickness decides whether towers on either side can reach each other (`towerReach`). - **Players can plug gaps.** Players build stone walls, so a narrow gate, ramp or trail can be sealed by whoever holds it. How wide a map's doors are decides whether a colony can shut itself in. @@ -53,6 +53,10 @@ The growth test lives in `Map::growResources` (`src/map/MapStep.cpp`), the resou Consequences a generator has to design around: +- **Farmland follows water, in rows.** A crop regrows when a probe up to 15 tiles away finds pure water + and the opposite probe finds no pure sand, so rows of crops between rows of water yield most at about + 10 tiles of crops and 8 of water along an axis, 12 and 9 on the diagonal, where stepped edges lose + more to the beach (`bestFarmRows`, `tools/farm_row_fit.py`). - **Farmland follows water.** Wheat and wood planted far from water never regrow; planted near water they spread until cleared, harvested or blocked. Land far from any water is a desert for the economy even when it is grass. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 6286e57e3..b16d1ca3c 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -27,15 +27,17 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `sprinkleSand`, decorative sand patches on inland grass following a caller's noise, kept a strip of grass away from every beach; `writeUndermap`; `growWater`, one body of water grown to an exact tile count by a caller's key (Stone highlands' ponds, Amphitheatre's bays); `keepRoadInland` and `roadTiles`, a sand road kept off every beach and the tiles its vertices spoil | | `LatticeNoise` | `PeriodicNoise`, value noise that tiles the torus exactly and samples anywhere, with `periodicNoise`/`fractalNoise` (integer fields per tile, octaves) and `torusNoise` (four octaves in [-1, 1]) sampled from it, plus `percentile` | | `HeightMap`, `Noise` | Perlin noise faded across the wrap, and the stamped height fields the height-field generators shape | -| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band; optionally only its best-growing share, shared out equally between the colonies' wedges), `algaeGrowthChance` (each water tile's chance of passing the engine's algae growth test, which needs water within 15 tiles and solid sand within 30 at a reflected offset), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region), `plantRound` and `plantOrchard` (a clump, or groves of the three fruits, at the same point round a circle at every colony's angle); `seedAlgae` can also share its clumps between labelled groups of water such as private bays | +| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds, or no stone for a home already walled in it), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band; optionally only its best-growing share, shared out equally between the colonies' wedges), `algaeGrowthChance` (each water tile's chance of passing the engine's algae growth test, which needs water within 15 tiles and solid sand within 30 at a reflected offset), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region), `plantRound` and `plantOrchard` (a clump, or groves of the three fruits, at the same point round a circle at every colony's angle); `seedAlgae` can also share its clumps between labelled groups of water such as private bays | | `Resources` | The ambient layer and its fairness guards: `scaledCount`/`scaledShare`, `placeResourceClump`, `setScaledResource`, `scatterResources`, `guaranteeStartingResources`, `openCrampedStarts` | | `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared; `cheapestWalk`, the same search by any step cost (Everglades' fords, Symmetric arena's causeway routes) | | `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics; `placeTower`, a completed, stocked defence tower of a chosen level at the allowed footprint nearest a designed point | -| `Walls` | Walls a design builds and the checks that prove they hold: `seaVertices` and `seaMargin` (the land a swimmer can stand on), `sealCoasts` (stone on every grass tile touching that margin, so a coast is sealed; City states, Carousel, Switchbacks), `labelBorders` (a wall between labelled regions no unit can cross even diagonally, one to three tiles thick; Stone highlands' ridges, Amphitheatre's borders), `designedStone` (a designed band's stone and any gaps the beach pass left), `reachesWithShut`, `pieceLeak` and `seaEntry` (with the doors shut, does any part reach another, or the sea reach inside), `towerReach` (how close a tower on some ground comes to shooting at a target: towers scan square rings with no line of sight), `walkSpread` (every colony's walk to its own target, and how uneven they are) and `firstRegionLeak` (reachable ground where two labelled regions meet that a design says must not; Maze's walls) | +| `Walls` | Walls a design builds and the checks that prove they hold: `seaVertices` and `seaMargin` (the land a swimmer can stand on), `sealCoasts` (stone on every grass tile touching that margin, so a coast is sealed; City states, Carousel, Switchbacks), `labelBorders` (a wall between labelled regions no unit can cross even diagonally, one to three tiles thick, or with doors a design leaves open; Stone highlands' ridges, Amphitheatre's borders, Carousel's walls), `designedStone` (a designed band's stone and any gaps the beach pass left), `reachesWithShut`, `pieceLeak` and `seaEntry` (with the doors shut, does any part reach another, or the sea reach inside), `towerReach` (how close a tower on some ground comes to shooting at a target: towers scan square rings with no line of sight), `walkSpread` (every colony's walk to its own target, and how uneven they are) and `firstRegionLeak` (reachable ground where two labelled regions meet that a design says must not; Maze's walls) | | `Tessellation` | Polygon tilings of the torus with no terrain attached: `squareTessellation` and `hexTessellation` (cells, the corners they share and edges as identities, exact across the wrap even two cells wide), `edgeEnds`, `centreAcross`, `outline`, `transform` (the lattice's translations and mirrors), `labelTiles` (every tile's cell), `shortestEdgeSteps` and `centreClearance`; `warpLimit` and `warpCorners`, corners moved at random into irregular polygons that still tile, never closing the gap between walls that share no corner below a minimum (Maze) | | `GraphMaze` | A maze carved through a tessellation's cell graph: `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | -| `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic; `smoothLabels`, a majority filter that trims spurs off borders; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | -| `Homes` | Homes built alike: `stampPondHome`, `pondHomeAnchor` and `plantPondHomeKit` (a round home island facing out from the middle, its swarm towards its door, one or two ponds kept clear of the coast, and a kit round the first pond; Carousel, Switchbacks), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | +| `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic, or shared by value when each claimant's ground has a worth per tile; `smoothLabels`, a majority filter over any radius that trims spurs or, at a radius of 3 or 4, straightens borders into curves; `separateTerritories`, a gap opened between neighbouring territories; `fillToNearest`, the reverse: unclaimed ground near labelled regions given to the nearest, so water between pieces becomes land; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `growLakeBeside`, one on either flank of a site, kept wholly to its side of the line from the way in; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | +| `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges across the water rows every so many tiles along them, so workers need not walk round a long row (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives, and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | +| `Towers` | Tower sites chosen for the ground they cover over the walls, other colonies' (offence) or their own (defence), each weighted: `chooseTowerSites` (best first, a colony at a time in turn, no stocked tower in range of another colony's tower or swarm, plus open 2x2 pads for players to build more), `raiseTowers`, `towerFootprints`, `evenTowerPlan` (every colony trimmed to the fewest sites any got) and `roomyGround` (no tower on a strip it could close); a tower in range of another colony's tower starts empty; a request can require every site to stand directly against a wall, as all three arena maps do | +| `Homes` | Homes built alike: `stampPondHome`, `pondHomeAnchor` and `plantPondHomeKit` (a round home island facing out from the middle, its swarm towards its door, any ponds (none, where farms are the water) kept clear of the coast, and a wheat and wood kit round the first pond, with no stone since the home is walled in stone; Carousel, Switchbacks), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | | `Pipeline` | The stages round the others: `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | | `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | @@ -50,8 +52,8 @@ against the finished terrain. ## The designed generator -Eleven generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, -City states, Tidal flats, Everglades, Spider web, Coral) follow one shape, and the six newest of them +Fourteen generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, +City states, Tidal flats, Everglades, Spider web, Coral, Carousel, Amphitheatre, Switchbacks) follow one shape, and the nine newest of them are little more than a sequence of shared stages: 1. `design(request, context)` computes the whole layout from the request and the context's @@ -96,6 +98,9 @@ reused after a generator is retired. | `everglades` | 19 | Everglades | Its own — see below | | `spider-web` | 20 | Spider web | Its own — see below | | `coral` | 21 | Coral | Its own — see below | +| `carousel` | 22 | Carousel | Its own — see below | +| `amphitheatre` | 23 | Amphitheatre | Its own — see below | +| `switchbacks` | 24 | Switchbacks | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -144,6 +149,9 @@ ambient layer but still leaves every colony a start. Maze keeps its explicit den | Everglades | The swamp's standing wood and wheat, its outcrops and groves, and the pools' algae; every home's kit is unscaled | None | | Spider web | The threads' standing wheat and wood, the share of knots carrying stone, whether the dew drops and the hub carry fruit and stone, and the shallows' algae; every pad's kit is unscaled | Spiral (on): off, the capture threads are closed rings; Sand roads (on): off, the threads are grass from shore to shore | | Coral | The branches' standing wheat and wood, the share of forks carrying stone, the tips' fruit groves and the shallows' algae; every pad's kit is unscaled | Sand roads (on): off, the branches are grass from shore to shore | +| Carousel | Every home's ambient fields, the farms' wheat and woodlots, the courts' and the plaza orchard's fruit, and the algae; every home's kit, the walls' stone and the starting towers are unscaled | Sand roads (on): off, the corridors and spokes are grass from wall to wall | +| Amphitheatre | Every territory's ambient fields and grove, the arena's groves and terrace outcrops, and the bays' algae; every home's kit, the walls' stone and the starting towers are unscaled | None | +| Switchbacks | Every home's ambient fields and grove, the farms' wheat and woodlots, the plateau's orchard, and the algae; every home's kit, the mountains' stone and the starting towers are unscaled | Sand roads (on): off, the trails are grass from wall to wall | `scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of @@ -759,6 +767,133 @@ further it is from home. map by `Stretch`, so on a rectangular map it fills the map as an ellipse; widths stay in tiles. Square maps are unchanged. +## Carousel + +A ring of walled homes in the sea, each with one door that leads clockwise. A colony's corridor +sweeps round the ring to a court pressed against the next colony's home, and a spoke runs from every +court in to the plaza. Court and home are parted by a thin wall of stone that no unit can cross but a +tower shoots over, so every colony besieges one neighbour from its court while the other besieges it. +Every home also reaches two walled wheat farms, one in towards the plaza and one out beyond the ring. +The pieces are parted by single lines of stone with no water beside them, so a siege tower stands +right against the wall it shoots across. + +- **Geometry.** Designed once in the wedge's frame and turned for every colony; round on a + rectangular map. The ring's outer coast is `kRingShare` of the half side (a little more on maps + under 512), leaving a band of sea round it for the outer farms. A home is `kHomeShare` of the half + side but never wider than a fifth of the ring's arc per colony. The court sits on the homes' ring + where its edge is `court-wall` tiles from the next home's rough outline, found by solving for the + chord; the corridor is an `arcPath` from the home's axis to the court, and the spoke, `spoke-width` + tiles wide (default 3, as the corridors' `corridor-width` is, so every lane is a tight laneway and the court at the elbow a small circle, `court-size` 30, about 3 tiles in radius), leans back from the court to the middle + of the wedge so it clears the next home. +- **Farms.** `growFarmFields` shares the sea inside the ring (each cell between a home, its corridor + and two spokes) and the sea outside it between the colonies' homes by equal yield for each colony's + row angle, so a colony whose rows fall on the diagonal gets more ground. Each farm grows straight + out of its home with no coast between, keeps three tiles from all other land (filled in later and walled), and is joined + by a broad neck. With `farm-plots` (on), every farm has a 10x4 grass plot ringed with sand in its + middle for a swarm or an inn. Once the gaps round it have filled in and + its walls stand, `layFarm` lays rows along the colony's axis at `bestFarmRows` widths over the whole + field, right up to its walls with three tiles of land kept round the water (six against open sea), with a sand bridge across the water rows every 16 tiles, and `plantFarm` plants wheat along the + water with one small woodlot. A set of farms (inner or outer) is laid only where every colony's + field has room. +- **Walls.** No water lies between the pieces. Once the farms are grown, the sea within + `kFillReach` (24) tiles of a home or farm fills in from the nearest one (`fillToNearest`), so the + lanes, courts and plaza keep exactly the width they were drawn; any sea left keeps a + `kSeaMargin` (3) strip of the nearest piece before its sealed coast. A one-tile line of stone then stands wherever two colonies' sides meet + (`labelBorders` with doors): a home with its farms is one side, a court with its lanes another, + and the plaza a third. The walls stand on the home's or farm's side, so no wall narrows a lane. Two borders stay open: a home's door onto its own corridor, and every spoke's way into the + plaza. The wall between a court and the next home is the land no more than `court-wall` tiles + (default 2) from both (`designedStone`). Whatever sea is left beyond the outermost pieces has its + coast sealed (`sealCoasts`), so nothing landing from the sea gets in; the farms' water rows are + inland water, not sea. Sand roads run down the corridors, through the courts, down the spokes and across the plaza to its pond, so the way in never overgrows; they stay + `kRoadSeaGap` from water (`keepRoadInland`) and never touch a wall tile. +- **Homes.** `stampPondHome` with no ponds, since its farms are its water; the swarm stands towards the corridor; wheat and + wood kits beside the swarm, with no stone clump since the walls are stone already; ambient fields by `furnishGround` with `WedgeField` noise. Nothing is planted + within six steps of a corridor or spoke, or on a tower pad. +- **Prizes.** One grove of one fruit per court; on the plaza only fruit, so nothing overgrows it: an + orchard of the three fruits between every two spokes' arrivals (`plantOrchard`). +- **Starting towers.** `starting-towers` (0 none, otherwise the level plus one; default 2, level 1) + and `tower-count` (0 to 12, default 3): the siege line. The towers and three open pads per colony stand in its own home, packed within six + tiles of the thin wall it shares with the previous colony's court, chosen for how much of that + colony's elbow - its court and its lanes within twelve tiles - they cover across the wall + (`chooseTowerSites` counting other colonies' elbows only). Every colony shells one neighbour's way + out while the other neighbour shells its own. Every site stands directly against stone (`TowerRequest::against`). At level 0, every site is an open pad. +- **Checked, not assumed.** `validateWorld` rebuilds the design and requires every designed stone, + every colony reachable, 95% of every farm's crop land and all of its plot a walk from its colony + (`farmReachable`), no land reachable from the sea (`seaEntry`), every home with its farms, + every court and the plaza apart with the corridors and spokes shut (`pieceLeak`), a level-1 tower + in every court within range of the next home (`towerReach`), and even walks to the plaza + (`walkSpread`). + +## Amphitheatre + +A sunken arena in the middle of the map, ringed by walled terraces, and all round it a walled +territory for every colony with two inland seas beside its home. The outermost wall's ramps face the +colonies, the next wall's stand between neighbours, and so on down to the pit, so every step inward +is a meeting at a known place. + +- **Arena.** `rings` walls `terrace-width` apart round a pit of `pit-size`, drawn with + `ringWithGates`; ramps `ramp-width` wide alternate between each colony's axis and the middle of + its wedge. +- **Territories.** The ground outside the outer wall is shared out by `growTerritories` from every + colony's frontage (its wedge's arc just outside the wall), with `territory-roughness` noise in the + cost, then smoothed by four passes of `smoothLabels` over a radius of four, so every border runs in + a clean curve. This handles a square map's corners, the wrap and odd colony counts, which wedges + cannot. Borders become stone (`labelBorders`, `border-wall` thick), and any unclaimed tile becomes + stone. The areas come out within a few percent of each other. +- **Homes.** Every swarm stands the same number of steps from its ramp (`siteAtDepth`), four tenths + of the way to the shallowest territory's far end; the wheat and wood kit (no stone clump: the territory is walled in stone) faces the nearest sea; + ambient fields by + `furnishGround`. +- **Inland seas.** `bay-size` percent of the smallest territory, split into two seas, one either side + of the home (`growLakeBeside`), each wholly on its side of the line from the ramp through the home + and ten tiles clear of it, seven from any wall. Where any territory lacks the room, every territory + gets one sea of the full size at its far end instead (`growFarLake`). Every colony's seas are the + same size; land a sea closes off becomes stone (`strandedGround`). There is no sea shared between + territories. +- **Prizes.** Orchards of the three fruits in the pit and on the innermost terrace, and an outcrop on + every terrace, the same at every colony's angle. +- **Starting towers.** `tower-count` towers (default 3, at `starting-towers` level) and three open + pads per colony in its territory, each directly against stone, away from its ramp, covering the most of its neighbours' territories over the border walls (`chooseTowerSites`). +- **Checked, not assumed.** Every designed stone present, every ramp walkable, territories within + `kAreaTolerance` percent, every colony's seas the same size, no territory reaching another or the + arena with the outer ramps shut, and even walks to the ramps and to the pit. + +## Switchbacks + +A plateau in the middle of the sea and a ring of homes on the rim, each joined to the plateau by a +mountain of stone with one zigzag trail through it. Walking the trail takes several times the +straight-line distance, but the walls between legs are a few tiles thick, so towers at either end +cover the nearest legs. Every home reaches a walled wheat farm on either flank. + +- **Geometry.** Designed in every colony's `AxisFrame` and turned round the centre; round on a + rectangular map. Homes stand on the rim; the mountain fills the ground between the plateau and the + home with as many legs as fit (`layLegs`): every wall, between legs and at both ends, exactly + `leg-wall` thick (default 2), and the trails widened to use up the rest, so the stone stays thin. + On a crowded ring the plateau grows until the mountains fit round it; a smaller map narrows the + trail and walls with the square root of its size. +- **Trail.** `zigzagPath` gives the trail and each leg's straight run; everything else in the + mountain is stone. A sand road runs down the trail's middle. +- **Farms.** On maps at least 200 tiles across their shorter side (a 128-tile side has no room + between the mountains), two fields per colony, growing out of either flank of the home, share all the open sea + between the colonies by equal yield for their row angles (`growFarmFields`), joined to the home with + no coast between, with a 10x4 building plot each under `farm-plots` (on); rows run across the axis at + `bestFarmRows` widths (`layFarm`) with a sand bridge across the water every 16 tiles, wheat with one woodlot (`plantFarm`). Neighbouring colonies' + farms meet across three tiles of water and two sealed coasts, within a range-7 tower's reach. +- **Walls.** Every coast sealed, farms included; mountains keep water between each other beyond + their innermost legs. +- **Homes and plateau.** Pond homes as in Carousel, with two ponds only on maps too small for farms; the plateau has a pond and only fruit, an + orchard of the three fruits between every two summits, so nothing overgrows it. +- **Starting towers.** `tower-count` towers (default 3) and four open pads per colony, all on its trail beside the + walkway down its middle, each against the inner side of a wall - the side towards the middle of the + map - so it shoots across the stone at the next leg up, where attackers coming down from the plateau + pass (`chooseTowerSites` counting the colony's own trail). A trail too narrow for a tower beside its + walkway gets fewer, the same for every colony (`evenTowerPlan`), and no site may close a trail + (`dropBlockingSites`). +- **Checked, not assumed.** Every designed stone present, every farm walkable from its colony + (`farmReachable`), no land reachable from the sea, homes and their farms and the plateau apart with the trails shut and with only the middle legs shut (so no + leg can be skipped), a level-1 tower at home reaching the first leg and one on the plateau reaching + the last, and even walks to the plateau. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort diff --git a/src/SConscript b/src/SConscript index 81b015091..9cb8b2a6d 100644 --- a/src/SConscript +++ b/src/SConscript @@ -195,6 +195,9 @@ map/generator/generators/ShatteredCoastGenerator.cpp map/generator/generators/StoneHighlandsGenerator.cpp map/generator/generators/SpiderWebGenerator.cpp map/generator/generators/CoralGenerator.cpp +map/generator/generators/AmphitheatreGenerator.cpp +map/generator/generators/CarouselGenerator.cpp +map/generator/generators/SwitchbacksGenerator.cpp map/generator/generators/SwampGenerator.cpp map/generator/generators/SymmetricArenaGenerator.cpp map/generator/generators/TidalFlatsGenerator.cpp @@ -208,6 +211,8 @@ map/generator/shared/Drawing.cpp map/generator/shared/Geometry.cpp map/generator/shared/Grid.cpp map/generator/shared/Walls.cpp +map/generator/shared/Farmland.cpp +map/generator/shared/Towers.cpp map/generator/shared/Tessellation.cpp map/generator/shared/GraphMaze.cpp map/generator/shared/LatticeNoise.cpp diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 6b6dffe60..d9c3a90cd 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -3,6 +3,9 @@ #include "CityStatesGenerator.h" #include "ConcreteIslandsGenerator.h" #include "ContestedCommonsGenerator.h" +#include "AmphitheatreGenerator.h" +#include "CarouselGenerator.h" +#include "SwitchbacksGenerator.h" #include "CoralGenerator.h" #include "CraterLakesGenerator.h" #include "EvergladesGenerator.h" @@ -112,7 +115,7 @@ const GeneratorRegistry &GeneratorRegistry::builtins() shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), stoneHighlandsDefinition(), symmetricArenaDefinition(), ringWorldDefinition(), cityStatesDefinition(), tidalFlatsDefinition(), evergladesDefinition(), spiderWebDefinition(), - coralDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), + coralDefinition(), carouselDefinition(), amphitheatreDefinition(), switchbacksDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), riverDefinition(), uniformDefinition()}); return registry; diff --git a/src/map/generator/generators/AmphitheatreGenerator.cpp b/src/map/generator/generators/AmphitheatreGenerator.cpp new file mode 100644 index 000000000..2f46b5688 --- /dev/null +++ b/src/map/generator/generators/AmphitheatreGenerator.cpp @@ -0,0 +1,709 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "AmphitheatreGenerator.h" +#include "Drawing.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Territories.h" +#include "Towers.h" +#include "Topology.h" +#include "Walls.h" +#include "Wedge.h" +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Amphitheatre: a sunken arena in the middle of the map, ringed by terraces, and all round it one +// walled territory for every colony. Concentric walls of stone step down from the territories to +// the pit, and each wall is broken only by ramps. The ramps through the outermost wall face the +// colonies, one each; the ramps through the next wall stand between neighbours, so two colonies +// share each; the next face the colonies again, and so on down. Every step towards the pit is a +// meeting at a known place: first alone, then with one neighbour, then with the other. The pit holds +// the orchard, the innermost terrace more fruit, and the territories everything a colony needs to +// live on. +// +// A territory is walled off from its neighbours along its whole border and has no other way out +// than its ramp, so a colony that wants anything beyond its own ground has to come down into the +// amphitheatre. There is no sea: each territory's water is a private bay at its far end, the ground +// deepest from its ramp, enclosed by its own land, so its algae and its regrowing fields are its +// own and no swimmer can use it to cross into another territory. +// +// The arena is designed round the centre and turned for every colony, like a wedge design. The +// territories cannot be: a square map's corners and the ground across its wrap belong to no wedge, +// and with an odd number of colonies the corners fall unevenly. So the territories are grown instead +// (growTerritories): every colony's frontage on the outer wall is its own arc of equal length, and +// from there the smallest territory always takes the next tile, until the whole of the ground outside +// the walls is shared out to the tile. Every bay is then grown to the same number of tiles, and every +// swarm stands the same number of steps from its ramp, so what the growth cannot make identical is +// measured and held equal instead. The design is a pure function of the request, so validateWorld +// rebuilds it and checks the finished world. +// +// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be +// cleared, so the walls and the ramps are permanent; a unit may step diagonally, which is why every +// border is walled where any neighbour has another label (labelBorders); water blocks walking until a +// colony swims, which is why a bay must lie wholly inside its territory; wheat and wood regrow only +// near water, which is why every territory's fields grow round its bay and the terraces are dry; and +// fruit lets an inn pull hungry enemy units across, which makes the pit's orchard the prize at the +// bottom of every ramp. +// +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a pit 15 tiles in radius, three walls 13 tiles +// apart with ramps 7 wide, so the outer wall stands 41 tiles out; four territories of about 11,000 +// tiles each, each with a bay of 1,500 tiles. +namespace +{ + +// Half the thickness of a ring wall, in tiles: stone wherever a tile is this close to the ring. +constexpr double kRingHalf = 1.2; +// A terrace is never narrower than this, whatever the control. +constexpr double kMinimumTerrace = 8; +// Ground kept outside the outer wall before the map's edge, per colony at least this many tiles. +constexpr int kMinimumTerritory = 2000; +// The majority filter that smooths the territories' borders into curves: passes, and the radius of the +// square each tile looks at. +constexpr int kSmoothingPasses = 4; +constexpr int kSmoothingRadius = 4; +// The territory control's noise: a border wanders by up to this many steps at 100. +constexpr double kRoughSteps = 24; +// A bay keeps this many steps from any wall, so its beach never reaches stone and its neighbours' +// bays are always parted by land. +constexpr int kBayWallGap = 7; +// A swarm needs this much room round its site, and its site may miss the common depth by this much. +constexpr int kSwarmRoom = 5; +constexpr int kSiteSpread = 3; +// A swarm stands this share of the way from its ramp to the far end of the shallowest territory. An +// inland sea beside it keeps this far from its site, and seeds no farther than this. +constexpr double kHomeDepthShare = 0.4; +constexpr int kLakeSiteGap = 10; +constexpr int kLakeReach = 60; +// A swarm stands at least this many steps from its ramp's mouth, and this many short of the nearest +// bay, measured from the ramp. +constexpr int kHomeRampDepth = 14; +// Every home starts identical: fixed wheat and wood facing its bay, a quarry behind, unscaled. +constexpr int kHomeWheat = 30; +constexpr int kHomeWood = 30; +// Ambient farmland on a territory, as percentages of its tiles at 100. +constexpr int kHomeWheatShare = 3; +constexpr int kHomeWoodShare = 2; +// Nothing is planted within this many steps of a ramp. +constexpr int kRampClearance = 5; +// Open 2x2 pads beside every colony's `tower-count` towers, and the spacing between sites. +constexpr int kTowerPads = 3; +constexpr int kTowerSpacing = 5; +// A tower keeps this many steps from water and stone on every side, so it never closes a passage. +constexpr int kTowerRoom = 4; +// The most the territories' areas may differ, in percent of the smallest: growth shares the ground +// out to the tile, and trimming the spurs off the borders moves a little of it. +constexpr int kAreaTolerance = 5; +// The most the colonies' walks to their ramps and to the pit may differ. +constexpr int kWalkSpread = 12; + +struct Geometry +{ + int teams = 0, half = 0, rings = 0, width = 0, height = 0; + double wedge = 0, rampHalf = 0, pitR = 0, terrace = 0, outer = 0; + std::vector ringR; + std::string failure; +}; + +Geometry geometryFor(const GenerationRequest &r) +{ + const AmphitheatreOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + g.width = 1 << r.wDec; + g.height = 1 << r.hDec; + g.half = std::min(g.width, g.height) / 2; + g.wedge = 2 * kPi / g.teams; + g.rings = std::clamp(o.rings, 1, 6); + g.rampHalf = o.rampWidth / 2.0; + g.pitR = g.half * o.pitSize / 100.0; + g.terrace = std::max(kMinimumTerrace, g.half * o.terraceWidth / 100.0); + for (int j = 0; j < g.rings; ++j) + g.ringR.push_back(g.pitR + j * g.terrace); + g.outer = g.ringR.back(); + const double inside = kPi * (g.outer + kRingHalf + 1) * (g.outer + kRingHalf + 1); + if (g.outer + kRingHalf + 8 > g.half) + g.failure = "The amphitheatre does not fit on this map: use fewer rings or narrower terraces."; + else if ((double(g.width) * g.height - inside) / g.teams < kMinimumTerritory) + g.failure = "Too many colonies for this map: their territories would be too small."; + else if (g.ringR[0] * g.wedge < 2 * g.rampHalf + 6) + g.failure = "Too many colonies for the pit: its ramps would run into each other."; + return g; +} + +struct Layout +{ + Torus t{1, 1}; + Geometry g; + double phase = 0, cx = 0, cy = 0; + // zone: -1 wall, 0 the pit, j the terrace outside ring j - 1, rings the ground outside. + std::vector zone, territory, depth; + // rampOf: the colony whose outer ramp a tile is (or -1); innerRamp marks every other ramp tile. + std::vector rampOf; + std::vector ring, innerRamp, border, unclaimed, bay, pocket; + std::vector> mouths; // each colony's tiles just outside its outer ramp + std::vector anchor, rampMiddle; // each colony's swarm centre and middle of its outer ramp + std::vector bayTiles, areas; + int pitMiddle = 0; + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const AmphitheatreOptions o(request); + Layout L; + L.t = Torus(1 << request.wDec, 1 << request.hDec); + const Torus &t = L.t; + const int n = t.size(); + L.g = geometryFor(request); + const Geometry &g = L.g; + if (!g.failure.empty()) + { + L.failure = g.failure; + return L; + } + const int teams = g.teams; + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("amphitheatre-phase", 3600) / 3600.0 * 2 * kPi; + L.zone.assign(n, g.rings); + L.rampOf.assign(n, -1); + L.ring.assign(n, 0); + L.innerRamp.assign(n, 0); + L.mouths.assign(teams, {}); + + // The arena: rings of stone with their ramps (ringWithGates). The outermost ring's ramps face the + // colonies, the next ring's stand between them, and so on inwards. + for (int i = 0; i < n; ++i) + { + const double r = std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); + for (int j = 0; j < g.rings && L.zone[i] == g.rings; ++j) + if (r < g.ringR[j] - kRingHalf) + L.zone[i] = j; + } + for (int j = 0; j < g.rings; ++j) + { + const bool outermost = j == g.rings - 1; + const double offset = (g.rings - 1 - j) % 2 == 0 ? 0 : g.wedge / 2; + std::vector ramps; + for (int k = 0; k < teams; ++k) + ramps.push_back(L.phase + g.wedge * k + offset); + ringWithGates(t, L.cx, L.cy, g.ringR[j], kRingHalf, ramps, g.rampHalf, + [&](int i, int ramp) + { + L.zone[i] = -1; + if (ramp < 0) + L.ring[i] = 1; + else if (outermost) + L.rampOf[i] = ramp; + else + L.innerRamp[i] = 1; + }); + } + // The outer ramps' mouths: the outside tiles touching them. + for (int i = 0; i < n; ++i) + { + if (L.zone[i] != g.rings) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int k = L.rampOf[t.at(i % t.w + dx, i / t.w + dy)]; + if (k >= 0 && (L.mouths[k].empty() || L.mouths[k].back() != i)) + L.mouths[k].push_back(i); + } + } + + // The territories (growTerritories): every colony's frontage is the ground just outside the outer + // wall across its own wedge, and from there the ground outside is shared out to the tile, then + // smoothed (smoothLabels) so the stone along every border runs in a clean curve. + const WedgeFrame wedges(t, L.phase - g.wedge / 2, teams); + std::vector outside(n, 0), frontage(n, 0); + std::vector> seeds(teams); + for (int i = 0; i < n; ++i) + { + outside[i] = L.zone[i] == g.rings; + const WedgeFrame::Cell cell = wedges.cell(i % t.w, i / t.w); + if (outside[i] && cell.d <= g.outer + kRingHalf + 2) + { + seeds[cell.k].push_back(i); + frontage[i] = 1; + } + } + const PeriodicNoise wander(t.w, t.h, std::max(16, g.half / 4), context.stream("amphitheatre-borders")); + const double swing = kRoughSteps * 1000 * o.roughness / 100.0; + L.territory = growTerritories(t, outside, seeds, + [&](int i) { return long(wander.at(i % t.w, i / t.w) * swing); }) + .labels; + smoothLabels(t, L.territory, kSmoothingPasses, frontage, kSmoothingRadius); + L.areas.assign(teams, 0); + for (int i = 0; i < n; ++i) + if (L.territory[i] >= 0) + ++L.areas[L.territory[i]]; + + // The borders: a wall where any neighbour belongs to another territory, as thick as the control + // says, and every tile no territory claimed. A ramp's mouth is never walled. + L.border = labelBorders(t, L.territory, o.borderWall); + L.unclaimed.assign(n, 0); + for (int i = 0; i < n; ++i) + L.unclaimed[i] = outside[i] && L.territory[i] < 0; + for (int k = 0; k < teams; ++k) + for (int i : L.mouths[k]) + L.border[i] = 0; + + // The homes (siteAtDepth): every swarm the same number of steps from its ramp's mouth, a share of + // the way to the shallowest territory's far end, as far as possible from any wall. + std::vector walls(n, 0); + for (int i = 0; i < n; ++i) + walls[i] = L.ring[i] || L.border[i] || L.unclaimed[i]; + const std::vector fromWalls = stepsFrom(t, walls); + L.depth.assign(n, -1); + int shallowest = INT_MAX; + for (int k = 0; k < teams; ++k) + { + std::vector ground(n, 0); + for (int i = 0; i < n; ++i) + ground[i] = L.territory[i] == k && !L.border[i]; + const std::vector depth = stepsFrom(t, tileMask(t, L.mouths[k]), ground); + int deepest = 0; + for (int i = 0; i < n; ++i) + if (ground[i]) + { + L.depth[i] = depth[i]; + deepest = std::max(deepest, depth[i]); + } + shallowest = std::min(shallowest, deepest); + } + const int home = std::max(kHomeRampDepth, int(kHomeDepthShare * shallowest)); + for (int k = 0; k < teams; ++k) + { + std::vector depth(n, -1); + for (int i = 0; i < n; ++i) + if (L.territory[i] == k && !walls[i]) + depth[i] = L.depth[i]; + const int site = siteAtDepth(depth, fromWalls, home, kSwarmRoom, kSiteSpread); + if (site < 0) + { + L.failure = "A territory has no room for its swarm."; + return L; + } + L.anchor.push_back(site); + // The middle of the colony's outer ramp. + const ShapePoint p = polarPoint(L.cx, L.cy, g.outer, L.phase + g.wedge * k); + L.rampMiddle.push_back(t.at(int(std::lround(p.x)), int(std::lround(p.y)))); + } + + // The inland seas: where every territory has the room, one either side of its home (growLakeBeside), + // each half the bay size; otherwise one at the far end of every territory (growFarLake). Every + // colony's seas are the same size, clear of every wall and of the home, and any land a sea closes off + // becomes stone (strandedGround). + const int total = int(std::lround(*std::min_element(L.areas.begin(), L.areas.end()) * o.baySize / 100.0)); + const PeriodicNoise shoreline(t.w, t.h, 8, context.stream("amphitheatre-bays")); + const auto noise = [&](int i) { return shoreline.at(i % t.w, i / t.w); }; + std::vector queued(n, 0); + for (const bool beside : {true, false}) + { + L.bay.assign(n, 0); + L.bayTiles.assign(teams, 0); + bool fits = true; + for (int k = 0; k < teams && fits; ++k) + { + std::vector depth(n, -1); + for (int i = 0; i < n; ++i) + if (L.territory[i] == k && !L.border[i]) + depth[i] = L.depth[i]; + if (beside) + { + const int site = L.anchor[k]; + const int mouth = L.rampMiddle[k]; + const double heading = std::atan2(t.offsetY(mouth / t.w, site / t.w), t.offsetX(mouth % t.w, site % t.w)); + for (const int side : {1, -1}) + { + const int grown = growLakeBeside(t, L.bay, depth, fromWalls, kBayWallGap, total / 2, site, heading, + side, kLakeSiteGap, kLakeReach, noise, queued, 2 * k + (side > 0) + 1); + fits = fits && grown == total / 2; + L.bayTiles[k] += grown; + } + } + else + { + L.bayTiles[k] = growFarLake(t, L.bay, depth, fromWalls, kBayWallGap, total, noise, queued, 100 + k); + fits = L.bayTiles[k] == total; + } + } + if (fits) + break; + if (!beside) + { + L.failure = "A territory's bay does not fit; use a smaller bay or fewer colonies."; + return L; + } + } + L.pocket.assign(n, 0); + for (int k = 0; k < teams; ++k) + { + std::vector ground(n, 0); + for (int i = 0; i < n; ++i) + ground[i] = L.territory[i] == k && !L.border[i] && !L.bay[i]; + const std::vector stranded = strandedGround(t, L.mouths[k], ground); + for (int i = 0; i < n; ++i) + if (stranded[i]) + L.pocket[i] = 1; + } + L.pitMiddle = t.at(int(L.cx), int(L.cy)); + return L; +} + +// The design's stone: every ring outside its ramps, every border and every unclaimed tile, and any +// land a bay closed off. Rings, borders and unclaimed ground all keep clear of the bays, so all of +// it lies on grass; a closed-off pocket may touch a beach, where its sand simply stays sand. +std::vector stoneTiles(const Map &map, const Layout &L) +{ + const int n = L.t.size(); + std::vector wall(n, 0), stone(n, 0); + for (int i = 0; i < n; ++i) + wall[i] = L.ring[i] || L.border[i] || L.unclaimed[i] || L.pocket[i]; + const DesignedStone designed = designedStone(map, L.t, wall); + return designed.stone; +} + +// The towers every colony starts with and the open pads beside them (chooseTowerSites): sites in a +// colony's territory, away from its ramp, that cover the most of neighbouring territories over the +// border walls, none in range of another colony's tower or swarm. With the control at 0 every site is +// an open pad. +TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &context, + const AmphitheatreOptions &o, const std::vector &stone) +{ + const Torus &t = L.t; + const int n = t.size(); + std::vector ramps(n, 0); + for (int i = 0; i < n; ++i) + ramps[i] = L.rampOf[i] >= 0 || L.innerRamp[i]; + const std::vector fromRamps = stepsFrom(t, ramps); + const std::vector swarms = swarmSurroundings(t, context, 0); + const std::vector reserved = swarmSurroundings(t, context); + std::vector owner(n, -1); + std::vector buildable(n, 0), target(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + owner[i] = L.territory[i]; + buildable[i] = map.isGrass(x, y) && !stone[i] && !reserved[i] && fromRamps[i] > kRampClearance && + !map.isResource(x, y); + target[i] = !map.isWater(x, y) && !stone[i]; + } + // No tower on a shore strip narrow enough for it to close; against the walls is where they belong. + std::vector land(n, 0); + for (int i = 0; i < n; ++i) + land[i] = !map.isWater(i % t.w, i / t.w); + const std::vector roomy = roomyGround(t, land, kTowerRoom); + for (int i = 0; i < n; ++i) + buildable[i] = buildable[i] && roomy[i]; + TowerRequest request; + request.range = kTowerRange[std::clamp(o.towers - 1, 0, 2)]; + request.towers = o.towers > 0 ? o.towerCount : 0; + request.pads = o.towers > 0 ? kTowerPads : o.towerCount + kTowerPads; + request.spacing = kTowerSpacing; + request.against = &stone; + return chooseTowerSites(t, owner, buildable, target, swarms, L.g.teams, request); +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "amphitheatre layout"; + const AmphitheatreOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(GRASS); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.size(); + const Geometry &g = L.g; + + context.stage = "amphitheatre terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + if (L.bay[i]) + terrain[i] = WATER; + layBeaches(terrain, t); + writeUndermap(map, terrain); + const std::vector stone = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + if (stone[i]) + map.setResource(i % t.w, i / t.w, STONE, 1); + + context.stage = "amphitheatre colonies"; + const auto home = [&](int team) + { + std::vector ground(n, 0); + for (int i = 0; i < n; ++i) + ground[i] = L.territory[i] == team && !stone[i] && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) + { return MapGeneratorPoint(L.anchor[team] % t.w - 2, L.anchor[team] / t.w - 2); }; + if (!settleColonies(game, context, "amphitheatre-starts", home, anchor)) + return false; + + context.stage = "amphitheatre towers"; + TowerPlan towers = planTowers(map, L, context, o, stone); + // A crowded map may not seat every tower: every colony then keeps as many as the fewest got, as + // long as every colony has one. + if (evenTowerPlan(towers) < (o.towers > 0 && o.towerCount > 0 ? 1 : 0)) + { + context.detail = "a colony has no room for its towers"; + return false; + } + if (!raiseTowers(game, towers, std::clamp(o.towers - 1, 0, 2))) + { + context.detail = "a tower site no longer fits"; + return false; + } + const std::vector pads = towerFootprints(t, towers); + + context.stage = "amphitheatre resources"; + const std::vector reserved = swarmSurroundings(t, context); + std::vector ramps(n, 0); + for (int i = 0; i < n; ++i) + ramps[i] = L.rampOf[i] >= 0 || L.innerRamp[i]; + const std::vector fromRamps = stepsFrom(t, ramps); + const auto free = [&](int i) + { return !reserved[i] && !pads[i] && fromRamps[i] > kRampClearance && clearGround(map, i % t.w, i / t.w); }; + const Fertility::Field fertility = Fertility::forMap(map, false); + const PeriodicNoise patch(t.w, t.h, 12, context.stream("amphitheatre-patch")); + const PeriodicNoise split(t.w, t.h, 6, context.stream("amphitheatre-split")); + for (int k = 0; k < teams; ++k) + { + const auto eligible = [&](int i) { return L.territory[i] == k && free(i); }; + // The kit faces the bay: wheat and wood between the swarm and its shore, the quarry behind. + const int ax = L.anchor[k] % t.w, ay = L.anchor[k] / t.w; + int bayTile = -1; + for (int i = 0; i < n; ++i) + if (L.bay[i] && L.territory[i] == k && + (bayTile < 0 || t.dist2(ax, ay, i % t.w, i / t.w) < t.dist2(ax, ay, bayTile % t.w, bayTile / t.w))) + bayTile = i; + const double facing = + bayTile < 0 ? 0 : std::atan2(t.offsetY(ay, bayTile / t.w), t.offsetX(ax, bayTile % t.w)); + const KitFrame frame{ax, ay, facing}; + plantKit(map, t, context, + Kit{frame.at(9, -6, 12), frame.at(9, 6, 12), frame.at(-8, 0, 10), kHomeWheat, kHomeWood, -1}, + eligible); + furnishGround( + map, t, context, fertility, eligible, + [&](int i) { return patch.at(i % t.w, i / t.w); }, + [&](int i) { return split.at(i % t.w, i / t.w); }, + [&](int area) + { + return GroundAmounts{int(scaledCount(area * kHomeWheatShare / 100, o.wheat)), + int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, + int(scaledCount(1, o.fruit))}; + }, + "amphitheatre-home-stone", "amphitheatre-home-fruit"); + } + // The arena's prizes, the same in every wedge (plantOrchard, plantRound): groves of the three fruits + // on the innermost terrace and in the pit, a quarter-wedge round from the ramps, and a stone outcrop + // on every terrace. + const auto arena = [&](int zone) { return [&, zone](int i) { return L.zone[i] == zone && free(i); }; }; + std::vector quarter, back; + for (int k = 0; k < teams; ++k) + { + quarter.push_back(L.phase + g.wedge * k + g.wedge / 4); + back.push_back(L.phase + g.wedge * k - g.wedge / 4); + } + if (scaledCount(1, o.fruit) > 0) + { + plantOrchard(map, t, context, L.cx, L.cy, 0.55 * g.pitR, quarter, 5, 4, 1, arena(0)); + if (g.rings > 1) + plantOrchard(map, t, context, L.cx, L.cy, (g.ringR[0] + g.ringR[1]) / 2, quarter, 5, 4, 1, arena(1)); + } + for (int j = 1; j < g.rings && scaledCount(1, o.stone) > 0; ++j) + plantRound(map, t, context, L.cx, L.cy, (g.ringR[j - 1] + g.ringR[j]) / 2, back, STONE, 1, 4, arena(j)); + std::vector bayOf(n, -1); + for (int i = 0; i < n; ++i) + if (L.bay[i]) + bayOf[i] = L.territory[i]; + seedAlgae(map, context, t, "amphitheatre-algae", o.algae, AlgaeBand::shallows(1, 4), bayOf, teams); + secureStartingCrops(game, context, t, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, &stone); + + // Keep every colony's walk open, clearing only deposits on it: home to its ramp, ramp to the pit. + context.stage = "amphitheatre roads"; + const std::vector> workers = unitTilesByTeam(map, teams); + std::vector pit(n, 0); + for (int i = 0; i < n; ++i) + pit[i] = L.zone[i] == 0 && !stone[i] && + std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) < 0.4 * g.pitR; + for (int team = 0; team < teams; ++team) + { + if (workers[team].empty()) + continue; + std::vector ramp(n, 0); + std::vector rampTiles; + for (int i = 0; i < n; ++i) + if (L.rampOf[i] == team) + { + ramp[i] = 1; + rampTiles.push_back(i); + } + if (!openRoad(map, t, workers[team], ramp, &stone) || !openRoad(map, t, rampTiles, pit, &stone)) + { + context.detail = "colony " + std::to_string(team) + " has no walk to its ramp and the pit"; + return false; + } + } + return true; +} + +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 2) + return "An amphitheatre needs at least two colonies."; + // The geometry's own reason stays in the candidate's diagnostics; the player sees one message + // that says what to change. + return geometryFor(r).failure.empty() ? "" : "The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies."; +} + +// Checked on the finished world against the rebuilt design: every designed stone stands and every +// ramp is open; every colony walks to colony 0; the territories are equal to within a few percent and +// every bay has the same number of tiles; with the outer ramps shut, no territory reaches another or +// the arena; and the colonies' walks to their ramps and to the pit are even. +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "amphitheatre"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(), teams = context.request.nbTeams; + const auto where = [&](int i) + { return "(" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")"; }; + const std::vector stone = stoneTiles(map, L); + const std::vector open = walkableTiles(map); + std::vector area(teams, 0), water(teams, 0); + for (int i = 0; i < n; ++i) + { + if (stone[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "The stone at " + where(i) + " is missing."; + if ((L.rampOf[i] >= 0 || L.innerRamp[i]) && !open[i]) + return "The ramp at " + where(i) + " is blocked."; + if (L.territory[i] >= 0) + { + ++area[L.territory[i]]; + water[L.territory[i]] += map.isWater(i % t.w, i / t.w); + } + } + const int smallest = *std::min_element(area.begin(), area.end()); + const int largest = *std::max_element(area.begin(), area.end()); + if (largest - smallest > std::max(4, smallest * kAreaTolerance / 100)) + return "The territories differ in size by " + std::to_string(largest - smallest) + " tiles."; + // Each bay was grown to the same number of undermap corners; a tile is water only when all four of + // its corners are, so the tiles that read as water can differ a little with the outline, but every + // bay must be there. + for (int k = 0; k < teams; ++k) + if (L.bayTiles[k] != L.bayTiles[0] || water[k] == 0) + return "The bays differ in size."; + + const ColonyWalk walk = walkFromFirstColony(map, teams, "the amphitheatre", ""); + if (!walk.error.empty()) + return walk.error; + std::vector piece(n, -1); + std::vector outerRamps(n, 0); + for (int i = 0; i < n; ++i) + { + piece[i] = L.territory[i] >= 0 ? L.territory[i] : L.zone[i] >= 0 && L.zone[i] < g.rings ? teams : -1; + outerRamps[i] = L.rampOf[i] >= 0; + } + if (const int leak = pieceLeak(map, t, piece, outerRamps); leak >= 0) + return "With the outer ramps shut, " + where(leak) + " can still be reached from elsewhere."; + + const auto nearestOpen = [&](int tile) + { + const int s = seedNear(t, tile % t.w, tile / t.w, 4, [&](int i) { return open[i] != 0; }); + return s >= 0 ? s : tile; + }; + std::vector ramps, pits; + for (int k = 0; k < teams; ++k) + { + ramps.push_back(nearestOpen(L.rampMiddle[k])); + pits.push_back(nearestOpen(L.pitMiddle)); + } + for (const auto &[targets, what] : {std::pair{ramps, "ramps"}, std::pair{pits, "the pit"}}) + { + const WalkSpread spread = walkSpread(map, t, walk.workers, targets); + if (spread.unreached >= 0) + return "Colony " + std::to_string(spread.unreached) + " cannot walk to " + what + "."; + if (spread.tooUneven(kWalkSpread)) + return std::string("The colonies' walks to ") + what + " differ by " + + std::to_string(spread.longest - spread.shortest) + " steps."; + } + return ""; +} +} // namespace + +AmphitheatreOptions::AmphitheatreOptions(const GenerationRequest &r) + : rings(r.option("rings")), rampWidth(r.option("ramp-width")), pitSize(r.option("pit-size")), + terraceWidth(r.option("terrace-width")), roughness(r.option("territory-roughness")), + baySize(r.option("bay-size")), borderWall(r.option("border-wall")), + towers(r.option("starting-towers")), towerCount(r.option("tower-count")), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition amphitheatreDefinition() +{ + return {"amphitheatre", + 23, + "Amphitheatre", + 1, + false, + // Rings of wall; each ramp's width in tiles; the pit's radius and each terrace's width as + // shares of the half side; how far the territories' borders wander; each bay's size as a + // percentage of the smallest territory; the borders' thickness in tiles. + {{"rings", "Rings", 2, 4, 1, 3, ControlGroup::Layout}, + {"ramp-width", "Ramp width", 5, 11, 2, 7, ControlGroup::Terrain}, + {"pit-size", "Pit size", 8, 30, 2, 16, ControlGroup::Layout}, + {"terrace-width", "Terrace width", 6, 20, 1, 14, ControlGroup::Layout}, + {"territory-roughness", "Border roughness", 0, 100, 10, 50, ControlGroup::Terrain}, + {"bay-size", "Bay size", 6, 24, 2, 14, ControlGroup::Terrain}, + {"border-wall", "Border wall", 1, 3, 1, 2, ControlGroup::Terrain}, + // The towers every colony starts with, all against its walls: their level (0 for none, just + // open pads) and how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, + // Every territory's ambient fields and grove, the arena's fruit and outcrops, and the + // bays' algae; every home's kit and the walls' stone are unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/AmphitheatreGenerator.h b/src/map/generator/generators/AmphitheatreGenerator.h new file mode 100644 index 000000000..e5d9f0726 --- /dev/null +++ b/src/map/generator/generators/AmphitheatreGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct AmphitheatreOptions +{ + int rings, rampWidth, pitSize, terraceWidth, roughness, baySize, borderWall, towers, towerCount; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit AmphitheatreOptions(const GenerationRequest &r); +}; +GeneratorDefinition amphitheatreDefinition(); diff --git a/src/map/generator/generators/CarouselGenerator.cpp b/src/map/generator/generators/CarouselGenerator.cpp new file mode 100644 index 000000000..158fb685b --- /dev/null +++ b/src/map/generator/generators/CarouselGenerator.cpp @@ -0,0 +1,1050 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CarouselGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Drawing.h" +#include "Farmland.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Territories.h" +#include "Towers.h" +#include "Walls.h" +#include "Wedge.h" +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Carousel: a ring of walled homes in the sea, each with a single door that leads clockwise. A +// colony's door opens onto a corridor that sweeps round to a small court pressed against the next +// colony's home, and from every court a spoke runs in to the plaza in the middle of the map. The +// court and the home beside it are parted by nothing but a thin wall of stone: no unit can walk +// through it, but a tower shoots over it. Nothing but stone parts any two pieces: the sea between them +// fills in, and a single line of stone stands where they meet. So every +// colony besieges one neighbour from its court while the other neighbour besieges it, and the +// only way to walk into anyone's home is the long way round, through the plaza, the enemy's court +// and the corridor behind it. +// +// Every coast is sealed, as City states' homes are: stone stands on every solid-grass tile that +// touches the sea's margin, so a swimmer can land on a beach and walk its sand lane but never get +// inside. Swimming therefore opens nothing; the corridors, spokes and courts are the only routes +// for the whole game. Each home has no pond: its two farms, inland and clear of the sea, are its water +// algae and for fields that regrow. +// +// Everything is designed once in the wedge's frame (a home, its corridor, its court, the wall and +// the spoke, measured by angle round the centre and radius out from it) and turned round the +// centre for every colony, so every colony's ground is the same and the layout is fair for any +// colony count. Like City states it stays round on a rectangular map, in a circle on the shorter +// side, with sea filling the rest. The whole design is a pure function of the request, so +// validateWorld rebuilds it and checks the finished world. +// +// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be +// cleared, so the walls are permanent and a sealed coast stays sealed; a defence tower scans square +// rings round its footprint with no line of sight (Building::findBestTarget), so a wall two tiles +// thick stops walking but not shooting; grass may never touch water, so every coast wall stands one +// tile in from a sand lane, and the ponds keep well away from the sea so their beaches never join +// its margin; wheat and wood regrow only near water, which is why every home has its ponds; and +// fruit lets an inn pull hungry enemy units across, which makes the plaza's orchard worth the walk. +// +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): homes 26 tiles in radius 94 tiles out from the +// centre, corridors 3 wide sweeping about 50 degrees, courts 3 in radius, a wall 2 thick, spokes 3 +// wide and a plaza 31 in radius with a pond and an orchard. +namespace +{ + +// The homes' outer coast, as a share of the half side on a 512-tile map (a little more on smaller +// ones): the sea beyond it is where the outer farms grow. +constexpr double kRingShare = 0.7; +// ...but never less than this many tiles of the ring's outer coast per colony, nor more than this +// share of the half side. +constexpr double kArcPerColony = 110; +constexpr double kMaximumRingShare = 0.9; +// The farms: water kept between a farm and any other land, the neck joining it to its home, the land +// a farm keeps between its water rows and its coast, and the smallest farm worth having, as a share +// of the home's area. +constexpr int kFarmGap = 3; +// How far the ground between the pieces fills in from the homes and farms, and the margin of land any +// sea left beyond that keeps beside every piece. +constexpr int kFillReach = 24; +constexpr int kSeaMargin = 3; +constexpr double kNeckHalf = 8; +// The least share of a farm's crop land its colony must be able to walk to. +constexpr double kFarmReach = 0.95; +// How far apart, along a farm's rows, the sand bridges across its water rows stand. +constexpr int kFarmBridgeSpacing = 16; +// Land kept round a farm's water rows inside its walls, and the further margin kept from open sea. +constexpr int kFarmRim = 3; +constexpr int kFarmSeaRim = 3; +constexpr double kMinimumFarmShare = 0.5; +// A farm is a wheat farm: wheat on this share of its crop rows at 100, and a small woodlot of this +// share along one row. +constexpr int kFarmWheatShare = 45; +constexpr int kFarmWoodShare = 3; +// Towers: every colony starts with `tower-count` of them, and this many open 2x2 pads beside them for +// more, all sited against the walls (chooseTowerSites). +constexpr int kTowerPads = 3; +constexpr int kTowerSpacing = 2; +// A colony's elbow, which its neighbour's towers aim at: its court and its lanes this many tiles beyond +// the court's edge. The siege line: towers stand in a home no more than this many steps from the wall. +constexpr double kElbowReach = 12; +constexpr int kSiegeLine = 6; +// A home's radius at 100% of home size, as a share of the half side, but never across more than +// this share of the ring's arc per colony (as a half share: the diameter takes twice it), so crowded +// rings get smaller homes; its outline wobbles by up to this share. A home needs room for its ponds, +// their gap to the sea and this much more for the swarm. +constexpr double kHomeShare = 0.2; +constexpr double kHomeArcShare = 0.2; +constexpr double kMinimumHome = 14; +constexpr double kSwarmRoom = 4; +constexpr double kHomeRoughness = 0.12; +// A court's radius at 100% of court size, in tiles at 256 and never below the smallest court that +// seats a tower with room round it. +constexpr double kCourtRadius = 9; +constexpr double kMinimumCourt = 3; +// The wall between a court and the next home is a band this many court radii wide, so it reaches +// past the court's sides and the court's own coast wall never cuts in between its grass and the wall. +constexpr double kWallBand = 1.3; +// Ground kept between pieces that must not join: two colonies' lanes (a wall on one of them must not +// close it), and the plaza's clearance from the homes' ring. +constexpr double kPieceGap = 4; +// A lane passing another colony's home needs only a tile between them for the wall. +constexpr int kLaneHomeGap = 2; +// The corridor must run at least this far between its home and its court, or the court is simply +// part of the home. +constexpr double kMinimumCorridor = 10; +// A pond keeps this much land between its water and the home's coast, so its beach never meets the +// sea's margin (where it did, a swimmer could step from one beach to the other, round the wall). +constexpr double kLakeSeaGap = 5; +// A pond's radius as a share of its home's, clamped. +constexpr double kPondShare = 0.18; +constexpr double kMinimumPond = 2.5; +constexpr double kMaximumPond = 7; +// The plaza's pond as a share of the plaza. +constexpr double kPlazaPondShare = 0.28; +// Every home starts identical: fixed wheat and wood beside its first pond and a stone clump behind +// it, all unscaled, wheat and wood 1:1. +constexpr int kHomeWheat = 30; +constexpr int kHomeWood = 30; +// Ambient farmland on a home's ground, as percentages of their tiles at 100. +constexpr int kHomeWheatShare = 4; +constexpr int kHomeWoodShare = 2; +// Deposits keep this many steps from every corridor and spoke, so a door never grows shut. +constexpr int kDoorClearance = 6; +// A sand road vertex keeps this many steps from any water. +constexpr int kRoadSeaGap = 5; +// The most the colonies' walks to the plaza may differ. +constexpr int kWalkSpread = 12; +// A tower in a court must reach the next home at this level (0, 1 or 2): level 1 shoots 7 tiles. +constexpr int kSiegeTower = 1; + +struct Geometry +{ + int teams = 0, half = 0; + double wedge = 0, outer = 0; + double homeR = 0, homeReach = 0, homeRadius = 0; // radius, wobbled reach, centre's distance out + double corridorHalf = 0, spokeHalf = 0, courtR = 0, wall = 0, plazaR = 0, plazaPondR = 0, pondR = 0; + double sweep = 0; // from a home's axis to its court's + double courtRadius = 0; // the court centre's distance out: the homes' ring + int ponds = 1; + bool roads = true; + std::string failure; +}; + +// The court sits on the homes' ring, clockwise of its home, where its edge is `wall` tiles from the +// next home's edge; `toward` is how far the next home's outline reaches towards it. +double courtSweep(const Geometry &g, double toward) +{ + const double chord = toward + g.wall + g.courtR; + if (chord >= 2 * g.homeRadius) + return -1; + return g.wedge - 2 * std::asin(chord / (2 * g.homeRadius)); +} + +Geometry geometryFor(const GenerationRequest &r) +{ + const CarouselOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; + const double scale = g.half / 128.0; + g.wedge = 2 * kPi / g.teams; + // The ring of homes keeps well inside the map, leaving a band of sea round it for the outer farms; + // smaller maps give the ring more of their room, since homes cannot shrink with them. + const double ringShare = g.half >= 256 ? kRingShare : g.half >= 128 ? kRingShare + 0.06 : kRingShare + 0.18; + // A crowded ring grows until every colony has its arc, up to most of the map. + g.outer = std::min(kMaximumRingShare * g.half, + std::max(ringShare * g.half, g.teams * kArcPerColony / (2 * kPi))); + // A home is a share of the half side, but never wider than its share of the ring: with its + // centre at outer - reach, a reach of s * wedge * (outer - reach) solves to the bound below. + const double arcShare = kHomeArcShare * g.wedge; + const double reach = std::min(std::max(kMinimumHome, kHomeShare * g.half) * (1 + kHomeRoughness), + arcShare * g.outer / (1 + arcShare)); + g.homeReach = reach * o.homeSize / 100.0; + g.homeR = g.homeReach / (1 + kHomeRoughness); + g.homeRadius = g.outer - g.homeReach; + g.corridorHalf = o.corridorWidth / 2.0; + g.spokeHalf = o.spokeWidth / 2.0; + g.courtR = std::max(kMinimumCourt, kCourtRadius * std::sqrt(scale) * o.courtSize / 100.0); + g.courtR = std::max(g.courtR, g.corridorHalf + 0.5); + g.wall = o.courtWall; + // The plaza is a share of the half side, held clear of the homes' ring. + g.plazaR = std::min(g.half * o.plazaSize / 100.0, + g.homeRadius - g.homeReach - g.corridorHalf - 2 * kPieceGap); + g.plazaPondR = std::max(kMinimumPond, kPlazaPondShare * g.plazaR); + g.pondR = std::clamp(kPondShare * g.homeR, kMinimumPond, kMaximumPond); + // No ponds: every home's farms are its water. + g.ponds = 0; + g.roads = o.sandRoads; + g.sweep = courtSweep(g, g.homeReach); + // A home must still hold its swarm, its kit and its towers: at least what a home with a pond once needed. + if (g.homeR < g.pondR + kLakeSeaGap + kSwarmRoom) + g.failure = "Too many colonies for this map: the homes are too small."; + else if (g.sweep <= 0) + g.failure = "Too many colonies for this map: the homes do not fit round the ring."; + else if (g.homeRadius * g.sweep - g.homeReach - g.courtR < kMinimumCorridor) + g.failure = "Too many colonies for this map: no room for a corridor between a home and its court."; + else if (g.plazaR - g.plazaPondR < kLakeSeaGap + 3) + g.failure = "The plaza is too small for its pond on this map."; + return g; +} + +struct Layout +{ + Torus t{1, 1}; + Geometry g; + double phase = 0, cx = 0, cy = 0, sweep = 0; + // Per tile: which colony's home, court or path (corridor and spoke) it belongs to, -1 for none. + std::vector homeOf, courtOf, pathOf; + // farmOf: the colony a farm tile belongs to; farms: every farm laid out, two per colony. + std::vector farmOf, farmSet; // farmSet: 0 an inner farm, 1 an outer one + std::vector farms; + std::vector farmSand, farmPlot; // the farms' building plots: sand rings, grass + std::vector farmColony; + // Each tile's side for the walls between them: the plaza (0), a colony's court and lanes (1 + colony), + // its home and farms (1 + teams + colony); -1 for the court walls and the sea. homeSteps: steps + // from the homes as designed, before the gaps filled in, which is where a home's door stands. + std::vector side, homeSteps; + std::vector plaza, land, pond, wall, road, roadTile; + std::vector axis; // every colony's home axis angle + std::vector homes, courts; // their centres + std::vector heartOf; // where each colony's spoke comes into the plaza + std::vector firstPond; // each home's first pond's centre + std::string failure; +}; + +// Whether the border between tile i and its lower-sided neighbour j stays open: a lane into the plaza, +// or a home's door onto its own corridor where the corridor leaves the home. +bool borderOpen(const Layout &L, int i, int j) +{ + const int teams = L.g.teams, a = L.side[i], b = L.side[j]; + if (b == 0 && a >= 1 && a <= teams) + return true; + return a == b + teams && b >= 1 && L.pathOf[j] >= 0 && L.homeSteps[j] >= 0 && L.homeSteps[j] <= 2; +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const CarouselOptions o(request); + Layout L; + L.t = Torus(1 << request.wDec, 1 << request.hDec); + const Torus &t = L.t; + const int n = t.size(); + L.g = geometryFor(request); + const Geometry &g = L.g; + if (!g.failure.empty()) + { + L.failure = g.failure; + return L; + } + const int teams = g.teams; + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("carousel-phase", 3600) / 3600.0 * 2 * kPi; + L.homeOf.assign(n, -1); + L.courtOf.assign(n, -1); + L.pathOf.assign(n, -1); + L.plaza.assign(n, 0); + L.land.assign(n, 0); + L.pond.assign(n, 0); + L.wall.assign(n, 0); + L.road.assign(n, 0); + L.roadTile.assign(n, 0); + + // One home outline for every colony, turned to each home's axis. + const RadialShape homeShape(g.homeR, kHomeRoughness, context, "carousel-home"); + const RadialShape pondShape(g.pondR, 0.2, context, "carousel-pond"); + const RadialShape courtShape(g.courtR, 0.0001, context, "carousel-court"); + const RadialShape plazaShape(g.plazaR, 0.0001, context, "carousel-plaza"); + const RadialShape plazaPondShape(g.plazaPondR, 0.0001, context, "carousel-plaza-pond"); + + // The court's exact place: its edge `wall` tiles from the next home's outline where that + // outline faces it. The outline is the same shape turned, so this is the same for every colony. + double sweep = g.sweep; + for (int pass = 0; pass < 3; ++pass) + { + const ShapePoint home = polarPoint(0, 0, g.homeRadius, g.wedge); + const ShapePoint court = polarPoint(0, 0, g.homeRadius, sweep); + const double toward = + homeShape.radiusAt(std::atan2(court.y - home.y, court.x - home.x) - g.wedge); + const double next = courtSweep(g, toward); + if (next <= 0) + break; + sweep = next; + } + + L.sweep = sweep; + std::vector> paths; + for (int k = 0; k < teams; ++k) + { + const double a = L.phase + g.wedge * k, c = a + sweep; + L.axis.push_back(a); + L.homes.push_back(polarPoint(L.cx, L.cy, g.homeRadius, a)); + L.courts.push_back(polarPoint(L.cx, L.cy, g.homeRadius, c)); + // The corridor follows the homes' ring from the home's axis to its court, and the spoke runs + // in from the court to inside the plaza's edge at the middle of the wedge, leaning back + // towards its own home and away from the next, which the court is pressed against. + const double entry = std::min(c, a + g.wedge / 2); + L.heartOf.push_back(polarPoint(L.cx, L.cy, g.plazaR - 4, entry)); + std::vector corridor = arcPath(L.cx, L.cy, g.homeRadius, a, c, g.corridorHalf); + const ShapePoint in = polarPoint(L.cx, L.cy, g.plazaR - g.spokeHalf, entry); + std::vector spoke = {{L.courts[k].x, L.courts[k].y, g.spokeHalf}, + {in.x, in.y, g.spokeHalf}}; + std::vector mask(n, 0); + strokePath(mask, t, corridor); + strokePath(mask, t, spoke); + for (int i = 0; i < n; ++i) + if (mask[i]) + L.pathOf[i] = k; + paths.push_back(corridor); + paths.push_back(spoke); + if (g.roads) + { + // The road runs down the middle of the corridor, through the court and down the spoke, then on + // across the plaza to its pond, so the way in to the middle never overgrows; the home's interior + // is left to build on. + const double inset = (g.homeReach + 1) / g.homeRadius; + std::vector line = arcPath(L.cx, L.cy, g.homeRadius, a + inset, c, 0); + tracePath(L.road, t, line); + const ShapePoint pond = polarPoint(L.cx, L.cy, g.plazaPondR, entry); + tracePath(L.road, t, {{L.courts[k].x, L.courts[k].y, 0}, {in.x, in.y, 0}, {pond.x, pond.y, 0}}); + } + } + + // The shapes: homes with their ponds, courts, the plaza with its pond, and the wall between each + // court and the next home. + for (int k = 0; k < teams; ++k) + { + L.firstPond.push_back(stampPondHome(t, L.homes[k], L.axis[k], homeShape, pondShape, + {g.homeR, g.pondR, kLakeSeaGap, g.ponds}, L.pond, + [&](int i) + { + L.homeOf[i] = k; + L.pathOf[i] = -1; + })); + forEachTileInShape(t, L.courts[k].x, L.courts[k].y, courtShape, 0, + [&](int i, double, double) + { + L.courtOf[i] = k; + L.pathOf[i] = -1; + }); + } + // The walls, once every home and court is down: a thin line of stone between each court and the next + // home, just the tiles no more than `court-wall` steps from both - the land where they face each + // other. The band only bounds where to look, on the home's side of the court's + // middle and never over the court's own corridor or spoke. + for (int k = 0; k < teams; ++k) + { + const ShapePoint next = L.homes[(k + 1) % teams]; + std::vector band(n, 0), court(n, 0), home(n, 0); + strokePath(band, t, {{L.courts[k].x, L.courts[k].y, g.courtR * kWallBand}, {next.x, next.y, g.courtR * kWallBand}}); + for (int i = 0; i < n; ++i) + { + court[i] = L.courtOf[i] == k; + home[i] = L.homeOf[i] == (k + 1) % teams; + } + const std::vector fromCourt = stepsFrom(t, court), fromHome = stepsFrom(t, home); + const double ax = next.x - L.courts[k].x, ay = next.y - L.courts[k].y; + for (int i = 0; i < n; ++i) + if (band[i] && L.courtOf[i] < 0 && L.homeOf[i] < 0 && L.pathOf[i] != k && + fromCourt[i] <= g.wall && fromHome[i] <= g.wall) + { + const double along = + t.offsetX(int(std::lround(L.courts[k].x)), i % t.w) * ax + + t.offsetY(int(std::lround(L.courts[k].y)), i / t.w) * ay; + if (along <= 0) + continue; + L.wall[i] = 1; + L.pathOf[i] = -1; + } + } + fillShape(L.plaza, t, L.cx, L.cy, plazaShape); + fillShape(L.pond, t, L.cx, L.cy, plazaPondShape); + for (int i = 0; i < n; ++i) + { + if (L.plaza[i]) + L.pathOf[i] = -1; + L.land[i] = L.homeOf[i] >= 0 || L.courtOf[i] >= 0 || L.pathOf[i] >= 0 || L.plaza[i] || L.wall[i]; + if (L.pond[i]) + L.road[i] = 0; + // Roads only on the lanes, courts and plaza, never inside a home. + if (L.road[i] && L.pathOf[i] < 0 && L.courtOf[i] < 0 && !L.plaza[i]) + L.road[i] = 0; + } + + // The pieces that must not join keep water between them: every colony's corridor and spoke stay + // clear of every other home, and of the next colony's corridor and spoke outside the plaza. + { + std::vector nearestHome(n, -1), homeSteps(n, -1), pathSteps(n, -1); + for (int k = 0; k < teams; ++k) + { + std::vector home(n, 0), path(n, 0); + for (int i = 0; i < n; ++i) + { + home[i] = L.homeOf[i] == k; + path[i] = L.pathOf[i] == (k + 1) % teams; + } + const std::vector fromHome = stepsFrom(t, home); + const std::vector fromPath = stepsFrom(t, path); + for (int i = 0; i < n; ++i) + { + if (L.pathOf[i] < 0) + continue; + // Beside its own court a path is the court's mouth, which the wall parts from the home. + const int own = L.pathOf[i]; + const bool mouth = std::hypot(t.offsetX(int(std::lround(L.courts[own].x)), i % t.w), + t.offsetY(int(std::lround(L.courts[own].y)), i / t.w)) < + g.courtR + g.corridorHalf; + if (own != k && !mouth && fromHome[i] < kLaneHomeGap) + { + L.failure = "Too many colonies for this map: a spoke or corridor passes too close to a home."; + return L; + } + const double d = std::hypot(i % t.w - L.cx, i / t.w - L.cy); + if (teams > 1 && L.pathOf[i] == k && L.pathOf[i] != (k + 1) % teams && + fromPath[i] < kPieceGap && d > g.plazaR + kPieceGap) + { + L.failure = "Too many colonies for this map: the spokes crowd each other."; + return L; + } + } + } + } + // The wall must part the court from the home: at least one tile of it between them everywhere. + for (int k = 0; k < teams; ++k) + { + std::vector next(n, 0); + for (int i = 0; i < n; ++i) + next[i] = L.homeOf[i] == (k + 1) % teams; + const std::vector steps = stepsFrom(t, next); + for (int i = 0; i < n; ++i) + if (L.courtOf[i] == k && steps[i] < 2) + { + L.failure = "A court touches the next home; make the court wall thicker."; + return L; + } + } + std::vector farmSets; // the sets of farms laid: 0 inner, 1 outer + std::vector farmOrigins; // per laid set, every colony's row origin + // The farms (growFarmFields): every colony's home reaches one farm in towards the plaza, filling the + // sea between its corridor and the spokes either side, and one out beyond the ring, sharing the + // sea round the ring equally with the others. Rows run along the colony's axis at the widths that + // yield most (bestFarmRows); every farm's coast is sealed like the rest. + { + std::vector inner(n, 0), outer(n, 0); + for (int i = 0; i < n; ++i) + { + const double d = std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); + inner[i] = d < g.homeRadius; + outer[i] = d >= g.homeRadius; + } + std::vector innerSeeds, outerSeeds, anchors; + for (int k = 0; k < teams; ++k) + { + innerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius - g.homeReach - 2, L.axis[k])); + outerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius + g.homeReach + 2, L.axis[k])); + anchors.push_back(L.homes[k]); + } + std::vector owners; + for (int k = 0; k < teams; ++k) + owners.push_back(k); + // Rows run along every colony's axis; a colony whose axis falls on the diagonal gets more + // ground, so every colony's farms yield alike (farmYield). + const std::vector innerFields = + growFarmFields(t, L.land, L.homeOf, inner, innerSeeds, owners, anchors, kFarmGap, kNeckHalf, L.axis); + const std::vector outerFields = + growFarmFields(t, L.land, L.homeOf, outer, outerSeeds, owners, anchors, kFarmGap, kNeckHalf, L.axis); + L.farmOf.assign(n, -1); + L.farmSet.assign(n, -1); + L.farmSand.assign(n, 0); + L.farmPlot.assign(n, 0); + // Each set of farms, inner and outer, is laid only where every colony's field has room, so a + // small or crowded map loses a set for everyone rather than favouring anyone. + const double homeArea = kPi * g.homeR * g.homeR; + for (int set = 0; set < 2; ++set) + { + const std::vector &fields = set == 0 ? innerFields : outerFields; + std::vector size(teams, 0); + for (int i = 0; i < n; ++i) + if (fields[i] >= 0) + ++size[fields[i]]; + if (*std::min_element(size.begin(), size.end()) < kMinimumFarmShare * homeArea) + continue; + // The rows are laid once the gaps round the farm have filled in, over the whole walled field. + farmSets.push_back(set); + for (int i = 0; i < n; ++i) + if (fields[i] >= 0 && L.homeOf[i] < 0) + { + L.farmOf[i] = fields[i]; + L.farmSet[i] = set; + L.land[i] = 1; + } + for (int k = 0; k < teams; ++k) + farmOrigins.push_back(set == 0 ? innerSeeds[k] : outerSeeds[k]); + } + } + // No water between the pieces: the sea between a home, its farms, the lanes, the courts and the plaza + // fills in from the homes and farms (fillToNearest), so the lanes, courts and plaza keep exactly the + // width they were drawn and every gap beside them becomes its colony's ground. A single line of stone + // then stands where two sides meet (labelBorders), on the home's or farm's side, so no wall narrows a + // lane and a tower behind it stands as close to what it shoots at as the wall allows. Sea too far + // from any home or farm keeps a thin margin of the nearest piece and its coast is sealed. Two borders + // stay open: a home's door onto its own corridor and every spoke's way into the plaza. + { + // Farm labels: one per colony per set of farms (inner, outer). + const int homeLabel = 0, farmLabel = teams, courtLabel = 3 * teams, pathLabel = 4 * teams, plazaLabel = 5 * teams; + std::vector piece(n, -1), grown(n, -1); + std::vector sea(n, 0), home(n, 0); + for (int i = 0; i < n; ++i) + { + piece[i] = L.wall[i] ? -1 + : L.homeOf[i] >= 0 ? homeLabel + L.homeOf[i] + : L.farmOf[i] >= 0 ? farmLabel + L.farmSet[i] * teams + L.farmOf[i] + : L.courtOf[i] >= 0 ? courtLabel + L.courtOf[i] + : L.pathOf[i] >= 0 ? pathLabel + L.pathOf[i] + : L.plaza[i] ? plazaLabel + : -1; + grown[i] = piece[i] < courtLabel ? piece[i] : -1; + sea[i] = !L.land[i]; + home[i] = L.homeOf[i] >= 0; + } + std::vector filled = fillToNearest(t, grown, sea, kFillReach); + for (int i = 0; i < n; ++i) + { + if (filled[i]) + piece[i] = grown[i]; + sea[i] = sea[i] && !filled[i]; + } + // What sea is left keeps a margin of land beside every piece, so its beach and coast wall never + // eat into a lane. + const std::vector margin = fillToNearest(t, piece, sea, kSeaMargin); + for (int i = 0; i < n; ++i) + filled[i] = filled[i] || margin[i]; + L.homeSteps = stepsFrom(t, home); + for (int i = 0; i < n; ++i) + { + if (!filled[i]) + continue; + const int p = piece[i]; + L.land[i] = 1; + if (p >= plazaLabel) + L.plaza[i] = 1; + else if (p >= pathLabel) + L.pathOf[i] = p - pathLabel; + else if (p >= courtLabel) + L.courtOf[i] = p - courtLabel; + else if (p >= farmLabel) + { + L.farmOf[i] = (p - farmLabel) % teams; + L.farmSet[i] = (p - farmLabel) / teams; + } + else + L.homeOf[i] = p; + } + // The sides the walls go between, numbered so each wall stands on the higher: the plaza (0), a + // colony's court and lanes (1 + colony), and its home and farms (1 + teams + colony). + std::vector &side = L.side; + side.assign(n, -1); + for (int i = 0; i < n; ++i) + side[i] = piece[i] < 0 ? -1 + : piece[i] >= plazaLabel ? 0 + : piece[i] >= courtLabel ? 1 + (piece[i] - courtLabel) % teams + : 1 + teams + piece[i] % teams; + const std::vector borders = + labelBorders(t, side, [&](int i, int j) { return borderOpen(L, i, j); }); + for (int i = 0; i < n; ++i) + if (borders[i]) + L.wall[i] = 1; + } + // The farms' rows (layFarm), over each whole field inside its walls, along the colony's axis at the + // widths that yield most (bestFarmRows), with just enough land round the water for a beach and a wall. + { + const FarmPlot plot; + // Against the open sea a farm keeps a wider margin, for the beach, the sealed coast and the land + // between them and the water rows. + std::vector openSea(n, 0); + for (int i = 0; i < n; ++i) + openSea[i] = !L.land[i]; + const std::vector fromSea = stepsFrom(t, openSea); + for (size_t s = 0; s < farmSets.size(); ++s) + for (int k = 0; k < teams; ++k) + { + std::vector region(n, 0); + for (int i = 0; i < n; ++i) + region[i] = L.farmOf[i] == k && L.farmSet[i] == farmSets[s] && !L.wall[i] && + (fromSea[i] < 0 || fromSea[i] > kFarmSeaRim); + TerrainSketch rows(n, GRASS); + L.farms.push_back(layFarm(rows, t, region, L.axis[k], farmOrigins[s * teams + k], kFarmRim, + bestFarmRows(L.axis[k]), o.farmPlots ? &plot : nullptr, kFarmBridgeSpacing)); + L.farmColony.push_back(k); + const Farm &farm = L.farms.back(); + for (int i = 0; i < n; ++i) + { + if (farm.water[i]) + L.pond[i] = 1; + if (farm.sand[i]) + L.farmSand[i] = 1; + if (farm.plot[i]) + L.farmPlot[i] = 1; + } + } + } + // A road keeps well inside the land (keepRoadInland), so its sand never meets a beach, and no road + // vertex touches a wall tile, since stone only stands on grass. + { + std::vector water(n, 0); + for (int i = 0; i < n; ++i) + water[i] = !L.land[i] || L.pond[i]; + keepRoadInland(L.road, t, water, kRoadSeaGap); + for (int i = 0; i < n; ++i) + for (int dy = -1; dy <= 0 && L.road[i]; ++dy) + for (int dx = -1; dx <= 0; ++dx) + if (L.wall[t.at(i % t.w + dx, i / t.w + dy)]) + L.road[i] = 0; + } + L.roadTile = roadTiles(t, L.road); + return L; +} + +// The sea: every water vertex but the ponds'. +std::vector seaMargin(const Map &map, const Layout &L) +{ + // Neither a sand road nor a farm plot's sand ring is beach: neither may carry the sea's margin inland. + std::vector notBeach = L.roadTile; + const std::vector plotSand = roadTiles(L.t, L.farmSand); + for (int i = 0; i < L.t.size(); ++i) + notBeach[i] = notBeach[i] || plotSand[i]; + return MapGeneration::seaMargin(map, L.t, seaVertices(map, L.t, L.pond), notBeach); +} + +// The design's stone, once the terrain is laid: every coast sealed, and the wall between every court +// and the next home. +std::vector stoneTiles(const Map &map, const Layout &L) +{ + std::vector stone = sealCoasts(map, L.t, seaMargin(map, L), L.land); + const DesignedStone wall = designedStone(map, L.t, L.wall); + for (int i = 0; i < L.t.size(); ++i) + if (wall.stone[i]) + stone[i] = 1; + return stone; +} + +void furnish(Map &map, const Layout &L, GenerationContext &context, const CarouselOptions &o, + const std::vector &pads) +{ + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(); + const std::vector reserved = swarmSurroundings(t, context); + const Fertility::Field fertility = Fertility::forMap(map, false); + // The wedge frame starts half a wedge before colony 0's axis, so a wedge's middle is its home. + const WedgeFrame wedges(t, L.phase - g.wedge / 2, g.teams); + const WedgeField patch{wedges, PeriodicNoise(t.w, t.h, 12, context.stream("carousel-patch"))}; + const WedgeField split{wedges, PeriodicNoise(t.w, t.h, 6, context.stream("carousel-split"))}; + // Nothing is planted within a few steps of a corridor or spoke, so no field grows across a door. + std::vector paths(n, 0); + for (int i = 0; i < n; ++i) + paths[i] = L.pathOf[i] >= 0; + const std::vector fromPaths = stepsFrom(t, paths); + const auto free = [&](int i) + { + return !L.road[i] && !L.roadTile[i] && !reserved[i] && !pads[i] && fromPaths[i] > kDoorClearance && + clearGround(map, i % t.w, i / t.w); + }; + for (int k = 0; k < g.teams; ++k) + { + const auto eligible = [&](int i) { return L.homeOf[i] == k && free(i); }; + plantPondHomeKit(map, t, context, L.firstPond[k], L.axis[k], g.pondR, kHomeWheat, kHomeWood, + eligible); + furnishGround( + map, t, context, fertility, eligible, patch, split, + [&](int area) + { + return GroundAmounts{int(scaledCount(area * kHomeWheatShare / 100, o.wheat)), + int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, 0}; + }, + "carousel-home-stone", "carousel-home-fruit"); + } + // The courts: one grove of the same fruit each, out on the court's far side from the plaza, clear of + // where its tower stands against the wall. + const int courtFruit = CHERRY + int(context.bounded("carousel-court", 3)); + if (scaledCount(1, o.fruit) > 0) + for (int k = 0; k < g.teams; ++k) + plantRound(map, t, context, L.courts[k].x, L.courts[k].y, g.courtR * 0.45, {L.axis[k]}, courtFruit, + 1, 4, [&](int i) { return L.courtOf[i] == k && free(i); }); + // The plaza: only fruit, so nothing overgrows its groves - in every wedge the orchard's three groves + // on the pond's shore, halfway between one spoke's arrival and the next. + const auto plazaFree = [&](int i) { return L.plaza[i] && free(i); }; + std::vector between; + for (int k = 0; k < g.teams; ++k) + between.push_back(std::min(L.axis[k] + g.wedge / 2, L.axis[k] + L.sweep) + g.wedge / 2); + if (scaledCount(1, o.fruit) > 0) + plantOrchard(map, t, context, L.cx, L.cy, g.plazaPondR + 4, between, 5, 5, 1, plazaFree); + // The farms: wheat along the water on every crop row and a small woodlot on one (plantFarm). + for (size_t f = 0; f < L.farms.size(); ++f) + { + const Farm &farm = L.farms[f]; + int crops = 0; + for (int i = 0; i < n; ++i) + crops += farm.row[i] >= 0 && farm.row[i] % 2 == 0; + const int k = L.farmColony[f]; + plantFarm(map, t, farm, int(scaledCount(crops * kFarmWheatShare / 100, o.wheat)), + int(scaledCount(crops * kFarmWoodShare / 100, o.wood)), + [&](int i) { return (L.farmOf[i] == k || L.homeOf[i] == k) && free(i); }); + } +} + +// The towers every colony starts with and the open pads beside them (chooseTowerSites): the carousel's +// siege line. Every colony's sites stand in its own home, packed against the thin wall its home shares +// with the previous colony's court, and are chosen for how much of that colony's elbow - its court and +// the lanes within kElbowReach of it - they cover across the wall. So every colony shells one +// neighbour's way out while the other neighbour shells its own. With the control at 0 every site is an +// open pad. +TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &context, + const CarouselOptions &o, const std::vector &stone) +{ + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(); + const std::vector fromWall = stepsFrom(t, L.wall); + const std::vector reserved = swarmSurroundings(t, context); + std::vector owner(n, -1); + std::vector buildable(n, 0), target(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + owner[i] = L.homeOf[i] >= 0 ? L.homeOf[i] + : L.courtOf[i] >= 0 ? L.courtOf[i] + : L.farmOf[i] >= 0 ? L.farmOf[i] + : L.pathOf[i]; + buildable[i] = L.homeOf[i] >= 0 && fromWall[i] >= 0 && fromWall[i] <= kSiegeLine && map.isGrass(x, y) && + !stone[i] && !L.roadTile[i] && !reserved[i] && !map.isResource(x, y); + // The elbows: every court and the lanes near it. + const int k = L.courtOf[i] >= 0 ? L.courtOf[i] : L.pathOf[i]; + target[i] = k >= 0 && !map.isWater(x, y) && !stone[i] && + std::hypot(t.offsetX(int(std::lround(L.courts[k].x)), x), + t.offsetY(int(std::lround(L.courts[k].y)), y)) <= g.courtR + kElbowReach; + } + TowerRequest request; + request.range = kTowerRange[std::clamp(o.towers - 1, 0, 2)]; + request.towers = o.towers > 0 ? o.towerCount : 0; + request.pads = o.towers > 0 ? kTowerPads : o.towerCount + kTowerPads; + request.spacing = kTowerSpacing; + request.otherWeight = 1; + request.ownWeight = 0; + request.against = &stone; + return chooseTowerSites(t, owner, buildable, target, swarmSurroundings(t, context, 0), g.teams, request); +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "carousel layout"; + const CarouselOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.size(); + + context.stage = "carousel terrain"; + TerrainSketch terrain(n, WATER); + for (int i = 0; i < n; ++i) + { + if (L.land[i]) + terrain[i] = L.road[i] || L.farmSand[i] ? SAND : GRASS; + if (L.pond[i]) + terrain[i] = WATER; + } + layBeaches(terrain, t); + writeUndermap(map, terrain); + const std::vector stone = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + if (stone[i]) + map.setResource(i % t.w, i / t.w, STONE, 1); + + context.stage = "carousel colonies"; + const auto home = [&](int team) + { + std::vector ground(n, 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && !stone[i] && !L.roadTile[i] && + map.isGrass(i % t.w, i / t.w); + return ground; + }; + // The swarm stands in from the home's middle towards the plaza, with the ponds behind it. + const auto anchor = [&](int team) { return pondHomeAnchor(L.homes[team], L.axis[team], L.g.homeR); }; + if (!settleColonies(game, context, "carousel-starts", home, anchor)) + return false; + + context.stage = "carousel towers"; + TowerPlan towers = planTowers(map, L, context, o, stone); + // No site may close a colony's way through its court to the plaza (dropBlockingSites); a crowded map + // may not seat every tower either, so every colony keeps as many as the fewest got, as long as every + // colony has one. + { + std::vector open(n, 0), plazaHeart(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + open[i] = !map.isWater(x, y) && !stone[i] && map.getBuilding(x, y) == NOGBID; + plazaHeart[i] = L.plaza[i] && open[i] && std::hypot(i % t.w - L.cx, i / t.w - L.cy) < L.g.plazaPondR + 3; + } + std::vector> sources(teams); + const std::vector> goals(teams, plazaHeart); + for (int k = 0; k < teams; ++k) + for (int dy = -1; dy <= 4; ++dy) + for (int dx = -1; dx <= 4; ++dx) + if (dx < 0 || dy < 0 || dx > 3 || dy > 3) + sources[k].push_back(t.at(context.bootX[k] + dx, context.bootY[k] + dy)); + dropBlockingSites(t, towers, open, sources, goals); + } + if (evenTowerPlan(towers) < (o.towers > 0 && o.towerCount > 0 ? 1 : 0)) + { + context.detail = "a colony has no room for its towers"; + return false; + } + if (!raiseTowers(game, towers, std::clamp(o.towers - 1, 0, 2))) + { + context.detail = "a tower site no longer fits"; + return false; + } + const std::vector pads = towerFootprints(t, towers); + + context.stage = "carousel resources"; + furnish(map, L, context, o, pads); + const WedgeFrame algaeWedges(t, L.phase - L.g.wedge / 2, L.g.teams); + seedAlgae(map, context, t, "carousel-algae", o.algae, AlgaeBand::shallows(1, 4), &algaeWedges); + secureStartingCrops(game, context, t, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, &stone); + clearFarmPlots(map, t, L.farms); + + // Keep every colony's walk open, clearing only deposits on it: home to court, court to plaza. + context.stage = "carousel roads"; + const std::vector> workers = unitTilesByTeam(map, teams); + // The walk from a court ends at the orchard round the plaza's pond, not at the plaza's edge. + std::vector plaza(n, 0); + for (int i = 0; i < n; ++i) + plaza[i] = L.plaza[i] && !map.isWater(i % t.w, i / t.w) && !stone[i] && + std::hypot(i % t.w - L.cx, i / t.w - L.cy) < L.g.plazaPondR + 3; + for (int team = 0; team < teams; ++team) + { + if (workers[team].empty()) + continue; + std::vector court(n, 0); + std::vector courtTiles; + for (int i = 0; i < n; ++i) + // The court's own coast wall is neither where a walk starts nor where it ends, or the + // walk would clear it. + if (L.courtOf[i] == team && !stone[i] && !map.isWater(i % t.w, i / t.w)) + { + court[i] = 1; + courtTiles.push_back(i); + } + if (!openRoad(map, t, workers[team], court, &stone) || + !openRoad(map, t, courtTiles, plaza, &stone)) + { + context.detail = "colony " + std::to_string(team) + " has no walk to its court and the plaza"; + return false; + } + } + return true; +} + +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 2) + return "Carousels need at least two colonies."; + // The geometry's own reason stays in the candidate's diagnostics; the player sees one message + // that says what to change. + return geometryFor(r).failure.empty() ? "" : "The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies."; +} + +// Checked on the finished world against the rebuilt design: every designed stone stands; every +// colony walks to colony 0; nothing landing from the sea gets into any home, court, path or the +// plaza; with every corridor and spoke shut, no home, court or the plaza reaches any other (the +// court walls hold and every home has a single door); a tower in every court reaches the next home; +// and the colonies' walks to the plaza are even. +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "carousel"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + const auto where = [&](int i) + { return "(" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")"; }; + const std::vector stone = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + if (stone[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "The stone at " + where(i) + " is missing."; + const ColonyWalk walk = walkFromFirstColony(map, teams, "the carousel", ""); + if (!walk.error.empty()) + return walk.error; + + const std::vector open = walkableTiles(map); + const std::vector margin = seaMargin(map, L); + if (const int entry = seaEntry(map, t, margin, L.land); entry >= 0) + return "Land at " + where(entry) + " can be reached from the sea."; + + // Every piece on its own with the corridors and spokes shut. A piece is its ground inside the + // coast wall: the sand lane outside it is the sea's, and already checked above. + std::vector piece(n, -1); + for (int i = 0; i < n; ++i) + if (!margin[i]) + piece[i] = L.homeOf[i] >= 0 ? L.homeOf[i] + : L.farmOf[i] >= 0 ? L.farmOf[i] + : L.courtOf[i] >= 0 ? teams + L.courtOf[i] + : L.plaza[i] ? 2 * teams + : -1; + std::vector paths(n, 0); + for (int i = 0; i < n; ++i) + paths[i] = L.pathOf[i] >= 0; + // Every farm is joined to its home: nearly all of its crop land, and all of its plot, is a walk from + // its colony's workers. + for (size_t f = 0; f < L.farms.size(); ++f) + if (farmReachable(map, t, L.farms[f], walk.workers[L.farmColony[f]]) < kFarmReach) + return "Colony " + std::to_string(L.farmColony[f]) + " cannot walk into one of its farms."; + // Every border the design walls is stone in the finished world: no two sides meet on open ground but + // at a door or a lane's way into the plaza. + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (L.side[i] < 0 || stone[i] || map.isWater(x, y)) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int j = t.at(x + dx, y + dy); + if (L.side[j] >= 0 && L.side[j] < L.side[i] && !stone[j] && !map.isWater(j % t.w, j / t.w) && + !margin[i] && !margin[j] && !borderOpen(L, i, j)) + return "The wall between two parts of the carousel has a gap at " + where(i) + "."; + } + } + if (const int leak = pieceLeak(map, t, piece, paths); leak >= 0) + return "With the corridors shut, " + where(leak) + " can still be reached from another part of the carousel."; + + // The siege line: from the ground of every home next to the wall, a tower reaches the previous + // colony's court across it. + for (int k = 0; k < teams; ++k) + { + std::vector court(n, 0), next(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + court[i] = L.courtOf[i] == k && !map.isWater(x, y) && !stone[i]; + next[i] = L.homeOf[i] == (k + 1) % teams && map.isGrass(x, y) && !stone[i] && !map.isResource(x, y); + } + const int reach = towerReach(t, next, court); + if (reach > kTowerRange[kSiegeTower]) + return "No tower in home " + std::to_string((k + 1) % teams) + " reaches court " + std::to_string(k) + + " across the wall; it is " + std::to_string(reach) + " tiles away."; + } + + std::vector targets; + for (int k = 0; k < teams; ++k) + { + // The walkable tile nearest where the colony's spoke comes into the plaza. + const int hx = int(std::lround(L.heartOf[k].x)), hy = int(std::lround(L.heartOf[k].y)); + const int seed = seedNear(t, hx, hy, 3, [&](int i) { return open[i] && !margin[i]; }); + targets.push_back(seed >= 0 ? seed : t.at(hx, hy)); + } + const WalkSpread spread = walkSpread(map, t, walk.workers, targets); + if (spread.unreached >= 0) + return "Colony " + std::to_string(spread.unreached) + " cannot walk to the plaza."; + if (spread.tooUneven(kWalkSpread)) + return "The colonies' walks to the plaza differ by " + + std::to_string(spread.longest - spread.shortest) + " steps."; + return ""; +} +} // namespace + +CarouselOptions::CarouselOptions(const GenerationRequest &r) + : homeSize(r.option("home-size")), corridorWidth(r.option("corridor-width")), + spokeWidth(r.option("spoke-width")), + courtSize(r.option("court-size")), courtWall(r.option("court-wall")), + plazaSize(r.option("plaza-size")), + towers(r.option("starting-towers")), towerCount(r.option("tower-count")), + sandRoads(r.option("sand-roads") != 0), farmPlots(r.option("farm-plots") != 0), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition carouselDefinition() +{ + return {"carousel", + 22, + "Carousel", + 1, + false, + // Each home's radius and each court's as percentages of the standard; the corridors' width and + // the spokes' in tiles; the wall between a court and the next home in tiles; the plaza's + // radius as a share of the half side. + {{"home-size", "Home size", 60, 160, 10, 100, ControlGroup::Layout}, + {"corridor-width", "Corridor width", 3, 17, 2, 3, ControlGroup::Terrain}, + {"spoke-width", "Spoke width", 3, 17, 2, 3, ControlGroup::Terrain}, + {"court-size", "Court size", 20, 160, 10, 30, ControlGroup::Layout}, + {"court-wall", "Court wall", 1, 5, 1, 2, ControlGroup::Terrain}, + {"plaza-size", "Plaza size", 14, 34, 2, 24, ControlGroup::Layout}, + // The towers every colony starts with, all against its walls: their level (0 for none, just + // open pads) and how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, + // Off, the corridors and spokes are grass from coast wall to coast wall. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // On, every farm has a 10x4 clearing of grass ringed with sand in its middle, for a + // swarm or an inn. + GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), + // Every home's ambient fields, the plaza's farmland and outcrops, the courts' and the + // orchard's fruit, and the algae; every home's kit and the walls' stone are unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/CarouselGenerator.h b/src/map/generator/generators/CarouselGenerator.h new file mode 100644 index 000000000..84da551f4 --- /dev/null +++ b/src/map/generator/generators/CarouselGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct CarouselOptions +{ + int homeSize, corridorWidth, spokeWidth, courtSize, courtWall, plazaSize, towers, towerCount; + bool sandRoads, farmPlots; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit CarouselOptions(const GenerationRequest &r); +}; +GeneratorDefinition carouselDefinition(); diff --git a/src/map/generator/generators/SwitchbacksGenerator.cpp b/src/map/generator/generators/SwitchbacksGenerator.cpp new file mode 100644 index 000000000..4f3d1547f --- /dev/null +++ b/src/map/generator/generators/SwitchbacksGenerator.cpp @@ -0,0 +1,826 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "SwitchbacksGenerator.h" +#include "Drawing.h" +#include "Farmland.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Territories.h" +#include "Towers.h" +#include "Walls.h" +#include "Wedge.h" +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Switchbacks: a plateau in the middle of the sea, and round it a ring of homes, each joined to the +// plateau by a mountain of solid stone with one trail cut through it. The trail zigzags: out of the +// home it runs along one side of the mountain, across to the other, back, and across again, one leg +// after another, before it climbs out onto the plateau. Walking from a home to the plateau takes +// several times the distance as the crow flies, but the walls between the legs are only a few tiles +// thick, and a defence tower shoots over stone (Building::findBestTarget scans square rings with no +// line of sight). So a colony that holds the plateau can build towers at the trail heads that reach +// back down the last legs, and a colony at home can cover its own first legs the same way: every +// attack walks the whole trail under fire from towers that ignore the zigzag. +// +// Every coast is sealed, as City states' homes are: stone stands on every solid-grass tile that +// touches the sea's margin, so a swimmer can land on a beach but never get inside. The trails are the +// only way anywhere for the whole game. Each home's farms are its water (a map too small for farms gives it two ponds), inland and clear +// of the sea, for algae and for fields that regrow; the plateau has a pond and an orchard of all +// three fruits, the prize every trail climbs to. +// +// Everything is designed once in the wedge's frame, along the colony's axis and across it, and +// turned round the centre for every colony, so every colony's ground and trail are the same and the +// layout is fair for any colony count. The map stays round, in a circle on the shorter side, with sea +// filling a rectangle's ends. The design is a pure function of the request, so validateWorld rebuilds +// it and checks the finished world. +// +// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be cleared, +// so a mountain's walls are permanent and its trail is the only door; a tower scans round its +// footprint with no line of sight, which is what makes a thin wall between two legs a firing line; +// grass may never touch water, so a coast wall always stands a tile in from its sand lane; wheat and +// wood regrow only near water, which is why every home has its ponds and the plateau its pond; and +// fruit lets an inn pull hungry enemy units across, which makes the plateau's orchard worth the climb. +// +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a plateau 28 tiles in radius; mountains 40 tiles +// wide and 40 deep, each with a trail 7 wide making three legs 26 tiles long, 10 tiles apart with 3 +// tiles of stone between them; homes 24 tiles in radius on the rim. +namespace +{ + +// Sea kept between the homes' outer coast and the map's wrap, as a share of the half side. +constexpr double kRimShare = 0.04; +constexpr int kRimMinimum = 4; +// A home's radius at 100% of home size: a share of the half side, never below this many tiles, never +// across more than this share of its ring's arc (half share, as in Carousel), and never more than +// the room left outside the mountains; its outline wobbles by up to this share. +constexpr double kHomeShare = 0.2; +constexpr double kMinimumHome = 14; +constexpr double kHomeArcShare = 0.2; +constexpr double kHomeRoughness = 0.12; +// A home needs room for its ponds, their gap to the sea and this much more for the swarm. +constexpr double kSwarmRoom = 4; +// A pond keeps this much land between its water and the coast, so its beach never meets the sea's. +constexpr double kLakeSeaGap = 5; +constexpr double kPondShare = 0.18; +constexpr double kMinimumPond = 2.5; +constexpr double kMaximumPond = 7; +constexpr double kPlateauPondShare = 0.25; +// How far a small map narrows its trail and walls (see geometryFor), and the narrowest of each. +constexpr double kMinimumScale = 0.72; +constexpr int kMinimumTrail = 5; +constexpr int kMinimumWall = 2; +// A trail needs at least this many legs to zigzag at all. +constexpr int kMinimumLegs = 2; +// Stone kept between a trail's outermost legs and the mountain's sides. +constexpr double kSideWall = 3; +// A leg's centre line runs this far either side of the mountain's axis: a share of the half side, +// but at least two trail widths. +constexpr double kSpanShare = 0.1; +// Water kept between two mountains: enough for a beach and a coast wall on each. +constexpr double kPieceGap = 4; +// Every home starts identical: fixed wheat and wood beside its first pond, stone behind it, unscaled. +constexpr int kHomeWheat = 30; +constexpr int kHomeWood = 30; +// Ambient farmland on a home's ground, as percentages of their tiles at 100. +constexpr int kHomeWheatShare = 4; +constexpr int kHomeWoodShare = 2; +// Deposits keep this many steps from every trail, so no field grows across a trail's mouth. +constexpr int kTrailClearance = 6; +// A sand road vertex keeps this many steps from any water. +constexpr int kRoadSeaGap = 5; +// The most the colonies' walks to the plateau may differ. +constexpr int kWalkSpread = 12; +// The farms: water kept between a farm and any other land, the neck joining it to its home, the land +// a farm keeps between its water rows and its coast, the smallest farm worth having as a share of the +// home's area, and the crops planted on its crop rows at 100. +constexpr int kFarmGap = 3; +// The smallest half side that has farms. +constexpr int kFarmMinimumHalf = 100; +constexpr double kNeckHalf = 8; +// The least share of a farm's crop land its colony must be able to walk to. +constexpr double kFarmReach = 0.95; +// How far apart, along a farm's rows, the sand bridges across its water rows stand. +constexpr int kFarmBridgeSpacing = 16; +constexpr int kFarmRim = 6; +constexpr double kMinimumFarmShare = 0.5; +// A farm is a wheat farm: wheat on this share of its crop rows at 100, and a small woodlot of this +// share along one row. +constexpr int kFarmWheatShare = 45; +constexpr int kFarmWoodShare = 3; +// Open 2x2 pads beside every colony's `tower-count` towers, and the spacing between sites. +constexpr int kTowerPads = 4; +constexpr int kTowerSpacing = 4; +// A tower keeps this many steps from water and stone on every side, so it never closes a passage. +// Tiles either side of a trail's centre line kept free of towers, the walkway attackers and workers use. +constexpr int kWalkway = 1; +// A tower hugs a wall: rock within this many tiles of its footprint towards the middle of the map. +constexpr int kWallHug = 3; +// The level of tower (0, 1 or 2) that must reach a trail's first leg from its home and its last leg +// from the plateau: level 1 shoots 7 tiles. +constexpr int kCoverTower = 1; + +struct Geometry +{ + int teams = 0, half = 0, legs = 0, ponds = 1; + double wedge = 0, outer = 0; + double trailHalf = 0, narrowestHalf = 0, legWall = 0, pitch = 0, span = 0, blockHalf = 0; + double plateauR = 0, plateauPondR = 0; + double innerLeg = 0, outerLeg = 0, blockOut = 0; // along the axis from the centre + double homeR = 0, homeReach = 0, homeRadius = 0, pondR = 0; + bool roads = true; + std::string failure; +}; + +// The legs share the ground between the plateau's edge and the home's (`homeEdge` along the axis): every +// wall between two legs, and between the end legs and the plateau and the home, is exactly the control's +// thickness, as many legs fit as leave each trail at least its narrowest width, and the trails widen to +// use up the rest. Walls stay thin, so a tower on one leg reaches across to the next. +void layLegs(Geometry &g, double homeEdge) +{ + const double wall = g.legWall, narrowest = g.narrowestHalf; + const double depth = homeEdge - g.plateauR, end = wall + 1; + // With L legs of half width h: depth = 2 (h + end) + (L - 1) (2 h + wall), so h = (depth - 2 end - + // (L - 1) wall) / (2 L). + g.legs = std::max(0, int(std::floor((depth - 2 * end + wall) / (2 * narrowest + wall)))); + g.trailHalf = g.legs > 0 ? (depth - 2 * end - (g.legs - 1) * wall) / (2 * g.legs) : narrowest; + g.pitch = 2 * g.trailHalf + wall; + g.innerLeg = g.plateauR + end + g.trailHalf; + g.outerLeg = homeEdge - end - g.trailHalf; + g.blockHalf = g.span + g.trailHalf + kSideWall; +} + +Geometry geometryFor(const GenerationRequest &r) +{ + const SwitchbacksOptions o(r); + Geometry g; + g.teams = std::max(1, r.nbTeams); + g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; + g.wedge = 2 * kPi / g.teams; + g.outer = g.half - std::max(kRimMinimum, kRimShare * g.half); + // The trail and wall controls are widths on a 256-tile map; a smaller map narrows both with the + // square root of its size, down to a trail still wide enough for its road and a wall two tiles + // thick, so a 128-tile map still fits a zigzag. + const double narrow = std::clamp(std::sqrt(g.half / 128.0), kMinimumScale, 1.0); + const int trailWidth = std::max(kMinimumTrail, 2 * int(std::lround((o.trailWidth * narrow - 1) / 2)) + 1); + g.trailHalf = trailWidth / 2.0; + g.narrowestHalf = g.trailHalf; + g.legWall = std::max(kMinimumWall, int(std::lround(o.legWall * narrow))); + g.span = std::max(4.0 * g.trailHalf, kSpanShare * g.half); + g.blockHalf = g.span + g.trailHalf + kSideWall; + // The plateau is a share of the half side, but on a crowded ring it grows until the mountains + // fit round it with water between them at their innermost legs. + const double halfAngle = std::min(g.wedge / 2, 1.5); + g.plateauR = std::max(g.half * o.plateauSize / 100.0, + (g.blockHalf + kPieceGap) / std::tan(halfAngle) - (g.trailHalf + g.legWall + 1)); + g.plateauPondR = std::max(kMinimumPond, kPlateauPondShare * g.plateauR); + // Homes stand on the rim, as big as their share of the ring allows. The mountain fills the ground + // between the plateau and the homes with as many legs as fit a pitch apart, the innermost a wall's + // thickness of stone from the plateau and the outermost the same from the home, so a bigger map + // gets a longer trail rather than a sea between mountain and home. + const double ringShare = kHomeArcShare * g.wedge; + // ...and never so big that the mountain between it and the plateau has no room for two legs. + const double end = g.trailHalf + g.legWall + 1; + // However its rough outline falls, a home reaches in no further than its wobbled reach. + const double mountain = (g.outer - g.plateauR - 2 * end - (2 * g.trailHalf + g.legWall)) / 2; + const double reach = std::min({std::max(kMinimumHome, kHomeShare * g.half) * (1 + kHomeRoughness), + ringShare * g.outer / (1 + ringShare), mountain}); + g.homeReach = reach * o.homeSize / 100.0; + g.homeR = g.homeReach / (1 + kHomeRoughness); + g.homeRadius = g.outer - g.homeReach; + layLegs(g, g.homeRadius - g.homeR); + g.blockOut = g.homeRadius; + g.pondR = std::clamp(kPondShare * g.homeR, kMinimumPond, kMaximumPond); + // A home's farms are its water; a map too small for farms gives every home two ponds instead. + g.ponds = g.half >= kFarmMinimumHalf ? 0 : 2; + g.roads = o.sandRoads; + // A home must hold its swarm, its kit and any ponds: at least what a home with a pond needs. + if (g.homeR < g.pondR + kLakeSeaGap + kSwarmRoom) + g.failure = "Too many colonies for this map: the homes are too small."; + else if (g.legs < kMinimumLegs) + g.failure = "No room for a trail on this map: use a bigger map, a smaller plateau or a narrower trail."; + else if (g.plateauR - g.plateauPondR < kLakeSeaGap + 3) + g.failure = "The plateau is too small for its pond on this map."; + // Measured at the trail's narrowest width, as the plateau was sized: trails widened to use up a + // mountain's depth are trimmed to their wedges near the plateau, and the validator checks the rest. + else if (g.innerLeg * std::tan(std::min(g.wedge / 2, 1.5)) < g.span + g.narrowestHalf + kSideWall + kPieceGap) + g.failure = "Too many colonies for this map: the mountains crowd each other at the plateau."; + return g; +} + +struct Layout +{ + Torus t{1, 1}; + Geometry g; + double phase = 0, cx = 0, cy = 0; + std::vector homeOf, blockOf, trailOf, legOf; // legOf: which leg (0 first) a trail tile is on + std::vector farmOf, farmColony; + std::vector farms; + std::vector farmSand, farmPlot; // the farms' building plots: sand rings, grass + std::vector plateau, land, pond, stone, road, roadTile; + std::vector axis; + std::vector frames; + std::vector> trails; // every colony's trail as drawn // every colony's frame: along its axis from the centre, and across + std::vector homes, firstPond, summit; // summit: where each trail reaches the plateau + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const SwitchbacksOptions o(request); + Layout L; + L.t = Torus(1 << request.wDec, 1 << request.hDec); + const Torus &t = L.t; + const int n = t.size(); + L.g = geometryFor(request); + const Geometry &g = L.g; + if (!g.failure.empty()) + { + L.failure = g.failure; + return L; + } + const int teams = g.teams; + L.cx = t.w / 2; + L.cy = t.h / 2; + L.phase = context.bounded("switchbacks-phase", 3600) / 3600.0 * 2 * kPi; + L.homeOf.assign(n, -1); + L.blockOf.assign(n, -1); + L.trailOf.assign(n, -1); + L.legOf.assign(n, -1); + L.plateau.assign(n, 0); + L.land.assign(n, 0); + L.pond.assign(n, 0); + L.stone.assign(n, 0); + L.road.assign(n, 0); + L.roadTile.assign(n, 0); + for (int k = 0; k < teams; ++k) + { + L.axis.push_back(L.phase + g.wedge * k); + L.frames.push_back({L.cx, L.cy, L.axis.back()}); + } + + const RadialShape homeShape(g.homeR, kHomeRoughness, context, "switchbacks-home"); + // The home's outline is rough, so its inner edge is where the outline facing the plateau says: + // the legs are laid out from there, the same for every colony. + layLegs(L.g, g.homeRadius - homeShape.radiusAt(kPi)); + if (L.g.legs < kMinimumLegs) + { + L.failure = "No room for a trail on this map: use a bigger map, a smaller plateau or a narrower trail."; + return L; + } + const RadialShape pondShape(g.pondR, 0.2, context, "switchbacks-pond"); + const RadialShape plateauShape(g.plateauR, 0.0001, context, "switchbacks-plateau"); + const RadialShape plateauPondShape(g.plateauPondR, 0.0001, context, "switchbacks-plateau-pond"); + + // The mountains: a straight block along each axis from inside the plateau to inside the home. The + // plateau is round and the block square, so the block starts where the plateau's edge meets the + // block's sides, and no strip of sea is left at its corners. + const double blockIn = std::sqrt(std::max(0.0, g.plateauR * g.plateauR - g.blockHalf * g.blockHalf)) - 2; + const WedgeFrame wedges(t, L.phase - g.wedge / 2, teams); + const double halfAngle = std::min(g.wedge / 2, 1.5); + for (int i = 0; i < n; ++i) + { + const WedgeFrame::Cell cell = wedges.cell(i % t.w, i / t.w); + const ShapePoint p = L.frames[cell.k].project(cell.dx, cell.dy); + // Near the plateau, where the wedges narrow, a mountain keeps half the gap between mountains + // from its wedge's edge, so neighbours never touch. + const double room = p.x * std::tan(halfAngle) - (kPieceGap + 1) / (2 * std::cos(halfAngle)); + if (p.x >= blockIn && p.x <= g.homeRadius && std::abs(p.y) <= std::min(g.blockHalf, room)) + L.blockOf[i] = cell.k; + } + + for (int k = 0; k < teams; ++k) + { + // The trail (zigzagPath): out of the home down one side of the mountain, then leg after leg + // across it from the outermost in, turning down the sides, and up onto the plateau. + const double top = g.plateauR - g.trailHalf - 2; + Zigzag trail = zigzagPath(L.frames[k], g.homeRadius - 0.3 * g.homeR, g.outerLeg, g.pitch, + g.legs, g.span, top, g.trailHalf); + // The trail leaves from the home's middle, so it reaches the home however small the home is + // beside the mountain's side. + const ShapePoint middle = L.frames[k].at(g.homeRadius, 0); + trail.path.insert(trail.path.begin(), {middle.x, middle.y, g.trailHalf}); + L.summit.push_back(L.frames[k].at(top, trail.finishAcross)); + std::vector mask(n, 0); + strokePath(mask, t, trail.path); + for (int i = 0; i < n; ++i) + if (mask[i]) + L.trailOf[i] = k; + for (size_t j = 0; j < trail.legs.size(); ++j) + { + std::vector leg(n, 0); + strokePath(leg, t, trail.legs[j]); + for (int i = 0; i < n; ++i) + if (leg[i]) + L.legOf[i] = int(j); + } + if (g.roads) + tracePath(L.road, t, trail.path); + L.trails.push_back(trail.path); + L.homes.push_back(polarPoint(L.cx, L.cy, g.homeRadius, L.axis[k])); + } + for (int k = 0; k < teams; ++k) + L.firstPond.push_back(stampPondHome(t, L.homes[k], L.axis[k], homeShape, pondShape, + {g.homeR, g.pondR, kLakeSeaGap, g.ponds}, L.pond, + [&](int i) { L.homeOf[i] = k; })); + fillShape(L.plateau, t, L.cx, L.cy, plateauShape); + fillShape(L.pond, t, L.cx, L.cy, plateauPondShape); + for (int i = 0; i < n; ++i) + { + const bool open = L.homeOf[i] >= 0 || L.plateau[i]; + if (open) + { + L.trailOf[i] = -1; + L.legOf[i] = -1; + } + L.land[i] = open || L.blockOf[i] >= 0 || L.trailOf[i] >= 0; + L.stone[i] = !open && L.blockOf[i] >= 0 && L.trailOf[i] < 0; + if (L.trailOf[i] < 0) + L.road[i] = 0; + } + + // The mountains must keep water between each other and between each and every home but its own, + // beyond their innermost legs; inside those the climbs come together at the plateau, and the + // validator checks that no climb meets another's trail. + for (int k = 0; k < teams && teams > 1; ++k) + { + std::vector mine(n, 0); + for (int i = 0; i < n; ++i) + mine[i] = L.blockOf[i] == k || L.homeOf[i] == k || L.trailOf[i] == k; + const std::vector steps = stepsFrom(t, mine); + for (int i = 0; i < n; ++i) + { + const int other = L.blockOf[i] >= 0 ? L.blockOf[i] + : L.trailOf[i] >= 0 ? L.trailOf[i] + : L.homeOf[i]; + // Chebyshev steps: two tiles of water between is enough for a beach on each side. + if (other >= 0 && other != k && !L.plateau[i] && steps[i] < kPieceGap - 1 && + std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) > + g.innerLeg + g.trailHalf) + { + L.failure = "Too many colonies for this map: the mountains crowd each other."; + return L; + } + } + } + // The farms (growFarmFields): every home reaches out sideways, across its axis, to a farm on either + // flank, and the farms share the open sea between the mountains and round the rim equally. Their + // rows run across the axis at the widths that yield most (bestFarmRows), and every farm's coast is + // sealed like the rest. + L.farmOf.assign(n, -1); + L.farmSand.assign(n, 0); + L.farmPlot.assign(n, 0); + // A map under kFarmMinimumHalf across its shorter half has no room between its mountains for farms + // that stay joined to their homes; it has none. + if (g.half >= kFarmMinimumHalf) + { + std::vector seeds, anchors; + for (int k = 0; k < teams; ++k) + for (const double side : {-1.0, 1.0}) + { + seeds.push_back(L.frames[k].at(g.homeRadius, side * (g.homeReach + 2))); + anchors.push_back(L.homes[k]); + } + const std::vector everywhere(n, 1); + const FarmPlot plot; + // Rows run across every colony's axis; fields whose rows fall on the diagonal get more ground, + // so every farm yields alike (farmYield). + std::vector owners; + std::vector angles; + for (size_t f = 0; f < seeds.size(); ++f) + { + owners.push_back(int(f) / 2); + angles.push_back(L.axis[f / 2] + kPi / 2); + } + const std::vector fields = + growFarmFields(t, L.land, L.homeOf, everywhere, seeds, owners, anchors, kFarmGap, kNeckHalf, angles); + const double homeArea = kPi * g.homeR * g.homeR; + for (int f = 0; f < 2 * teams; ++f) + { + const int k = f / 2; + std::vector region(n, 0); + int size = 0; + for (int i = 0; i < n; ++i) + if (fields[i] == f) + { + region[i] = 1; + ++size; + } + if (size < kMinimumFarmShare * homeArea) + { + L.failure = "Too many colonies for this map: there is no room for every colony's farms."; + return L; + } + const double across = L.axis[k] + kPi / 2; + TerrainSketch rows(n, GRASS); + L.farms.push_back(layFarm(rows, t, region, across, seeds[f], kFarmRim, bestFarmRows(across), o.farmPlots ? &plot : nullptr, kFarmBridgeSpacing)); + L.farmColony.push_back(k); + for (int i = 0; i < n; ++i) + if (region[i] && L.homeOf[i] < 0) + { + L.farmOf[i] = k; + L.land[i] = 1; + if (L.farms.back().water[i]) + L.pond[i] = 1; + if (L.farms.back().sand[i]) + L.farmSand[i] = 1; + if (L.farms.back().plot[i]) + L.farmPlot[i] = 1; + } + } + } + // A road keeps well inside the land, so its sand never meets a beach. + std::vector water(n, 0); + for (int i = 0; i < n; ++i) + water[i] = !L.land[i] || L.pond[i]; + keepRoadInland(L.road, t, water, kRoadSeaGap); + L.roadTile = roadTiles(t, L.road); + return L; +} + +// The sea: every water vertex but the ponds'. +std::vector seaMargin(const Map &map, const Layout &L) +{ + // Neither a sand road nor a farm plot's sand ring is beach: neither may carry the sea's margin inland. + std::vector notBeach = L.roadTile; + const std::vector plotSand = roadTiles(L.t, L.farmSand); + for (int i = 0; i < L.t.size(); ++i) + notBeach[i] = notBeach[i] || plotSand[i]; + return MapGeneration::seaMargin(map, L.t, seaVertices(map, L.t, L.pond), notBeach); +} + +// The design's stone once the terrain is laid: every coast sealed, and every mountain's rock. +std::vector stoneTiles(const Map &map, const Layout &L) +{ + std::vector stone = sealCoasts(map, L.t, seaMargin(map, L), L.land); + const DesignedStone rock = designedStone(map, L.t, L.stone); + for (int i = 0; i < L.t.size(); ++i) + if (rock.stone[i]) + stone[i] = 1; + return stone; +} + +void furnish(Map &map, const Layout &L, GenerationContext &context, const SwitchbacksOptions &o, + const std::vector &pads) +{ + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(); + const std::vector reserved = swarmSurroundings(t, context); + const Fertility::Field fertility = Fertility::forMap(map, false); + const WedgeFrame wedges(t, L.phase - g.wedge / 2, g.teams); + const WedgeField patch{wedges, PeriodicNoise(t.w, t.h, 12, context.stream("switchbacks-patch"))}; + const WedgeField split{wedges, PeriodicNoise(t.w, t.h, 6, context.stream("switchbacks-split"))}; + std::vector trails(n, 0); + for (int i = 0; i < n; ++i) + trails[i] = L.trailOf[i] >= 0; + const std::vector fromTrails = stepsFrom(t, trails); + const auto free = [&](int i) + { + return !L.roadTile[i] && !reserved[i] && !pads[i] && fromTrails[i] > kTrailClearance && + clearGround(map, i % t.w, i / t.w); + }; + for (int k = 0; k < g.teams; ++k) + { + const auto eligible = [&](int i) { return L.homeOf[i] == k && free(i); }; + plantPondHomeKit(map, t, context, L.firstPond[k], L.axis[k], g.pondR, kHomeWheat, kHomeWood, + eligible); + furnishGround( + map, t, context, fertility, eligible, patch, split, + [&](int area) + { + return GroundAmounts{int(scaledCount(area * kHomeWheatShare / 100, o.wheat)), + int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, + int(scaledCount(1, o.fruit))}; + }, + "switchbacks-home-stone", "switchbacks-home-fruit"); + } + // The plateau: only fruit, so nothing overgrows its groves - in every wedge the orchard's three groves + // on the pond's shore, halfway between one trail's summit and the next. + const auto plateauFree = [&](int i) { return L.plateau[i] && free(i); }; + if (scaledCount(1, o.fruit) > 0) + { + std::vector between; + for (int k = 0; k < g.teams; ++k) + between.push_back(L.axis[k] + g.wedge / 2); + plantOrchard(map, t, context, L.cx, L.cy, g.plateauPondR + 4, between, 5, 5, 1, plateauFree); + } + // The farms: wheat along the water on every crop row and a small woodlot on one (plantFarm). + for (size_t f = 0; f < L.farms.size(); ++f) + { + const Farm &farm = L.farms[f]; + int crops = 0; + for (int i = 0; i < n; ++i) + crops += farm.row[i] >= 0 && farm.row[i] % 2 == 0; + const int k = L.farmColony[f]; + plantFarm(map, t, farm, int(scaledCount(crops * kFarmWheatShare / 100, o.wheat)), + int(scaledCount(crops * kFarmWoodShare / 100, o.wood)), + [&](int i) { return L.farmOf[i] == k && free(i); }); + } +} + +// The towers every colony starts with and the open pads beside them (chooseTowerSites), all built into +// its switchback: on the trail, against the inner side of each wall - the wall between the tower and the +// middle of the map - so each tower shoots across the stone at the next leg up, where attackers coming +// down from the plateau must pass. Sites keep clear of the walkway down the trail's middle and are chosen +// for how much of the colony's own trail they cover. With the control at 0 every site is an open pad. +TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &context, + const SwitchbacksOptions &o, const std::vector &stone) +{ + const Torus &t = L.t; + const int n = t.size(); + // The walkway: the trail's centre line and a tile either side, which no tower may take. + std::vector centre(n, 0); + for (int i = 0; i < n; ++i) + centre[i] = L.road[i]; + if (!L.g.roads) + for (int k = 0; k < L.g.teams; ++k) + tracePath(centre, t, L.trails[k]); + const std::vector fromCentre = stepsFrom(t, centre); + const std::vector reserved = swarmSurroundings(t, context); + // Against a wall on the side towards the middle of the map: rock within a couple of tiles that way. + const auto facesInwardWall = [&](int i) + { + const double dx = t.offsetX(i % t.w, int(L.cx)), dy = t.offsetY(i / t.w, int(L.cy)); + const double d = std::max(1.0, std::hypot(dx, dy)); + for (int step = 1; step <= kWallHug; ++step) + if (L.stone[t.at(int(std::lround(i % t.w + dx / d * step)), int(std::lround(i / t.w + dy / d * step)))]) + return true; + return false; + }; + std::vector owner(n, -1); + std::vector buildable(n, 0), target(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + owner[i] = L.homeOf[i] >= 0 ? L.homeOf[i] : L.farmOf[i] >= 0 ? L.farmOf[i] : L.trailOf[i]; + buildable[i] = L.trailOf[i] >= 0 && map.isGrass(x, y) && !stone[i] && !L.roadTile[i] && !reserved[i] && + (fromCentre[i] < 0 || fromCentre[i] > kWalkway) && !map.isResource(x, y) && facesInwardWall(i); + target[i] = L.trailOf[i] >= 0 && !map.isWater(x, y) && !stone[i]; + } + TowerRequest request; + request.range = kTowerRange[std::clamp(o.towers - 1, 0, 2)]; + request.towers = o.towers > 0 ? o.towerCount : 0; + request.pads = o.towers > 0 ? kTowerPads : o.towerCount + kTowerPads; + request.spacing = kTowerSpacing; + request.otherWeight = 0; + request.ownWeight = 1; + request.against = &stone; + return chooseTowerSites(t, owner, buildable, target, swarmSurroundings(t, context, 0), L.g.teams, request); +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "switchbacks layout"; + const SwitchbacksOptions o(context.request); + Map &map = game.map; + const int teams = context.request.nbTeams; + map.makeHomogenMap(WATER); + for (int i = 0; i < teams; ++i) + game.addTeam(); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + const Torus &t = L.t; + const int n = t.size(); + + context.stage = "switchbacks terrain"; + TerrainSketch terrain(n, WATER); + for (int i = 0; i < n; ++i) + { + if (L.land[i]) + terrain[i] = L.road[i] || L.farmSand[i] ? SAND : GRASS; + if (L.pond[i]) + terrain[i] = WATER; + } + layBeaches(terrain, t); + writeUndermap(map, terrain); + const std::vector stone = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + if (stone[i]) + map.setResource(i % t.w, i / t.w, STONE, 1); + + context.stage = "switchbacks colonies"; + const auto home = [&](int team) + { + std::vector ground(n, 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && !stone[i] && !L.roadTile[i] && + map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) { return pondHomeAnchor(L.homes[team], L.axis[team], L.g.homeR); }; + if (!settleColonies(game, context, "switchbacks-starts", home, anchor)) + return false; + + context.stage = "switchbacks towers"; + TowerPlan towers = planTowers(map, L, context, o, stone); + // No site may close a colony's trail to the plateau (dropBlockingSites); a trail may not seat every + // tower either, so every colony then keeps as many as the fewest got, and a trail too narrow for any + // gets none. + { + std::vector open(n, 0), heart(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + open[i] = !map.isWater(x, y) && !stone[i] && map.getBuilding(x, y) == NOGBID; + heart[i] = L.plateau[i] && open[i] && + std::hypot(t.offsetX(int(L.cx), x), t.offsetY(int(L.cy), y)) < L.g.plateauPondR + 3; + } + std::vector> sources(teams); + const std::vector> goals(teams, heart); + for (int k = 0; k < teams; ++k) + for (int dy = -1; dy <= 4; ++dy) + for (int dx = -1; dx <= 4; ++dx) + if (dx < 0 || dy < 0 || dx > 3 || dy > 3) + sources[k].push_back(t.at(context.bootX[k] + dx, context.bootY[k] + dy)); + dropBlockingSites(t, towers, open, sources, goals); + } + evenTowerPlan(towers); + if (!raiseTowers(game, towers, std::clamp(o.towers - 1, 0, 2))) + { + context.detail = "a tower site no longer fits"; + return false; + } + const std::vector pads = towerFootprints(t, towers); + + context.stage = "switchbacks resources"; + furnish(map, L, context, o, pads); + const WedgeFrame algaeWedges(t, L.phase - L.g.wedge / 2, L.g.teams); + seedAlgae(map, context, t, "switchbacks-algae", o.algae, AlgaeBand::shallows(1, 4), &algaeWedges); + secureStartingCrops(game, context, t, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, &stone); + clearFarmPlots(map, t, L.farms); + + // Keep every trail open, clearing only deposits on it, from each home to the plateau's pond shore. + context.stage = "switchbacks trails"; + const std::vector> workers = unitTilesByTeam(map, teams); + std::vector heart(n, 0); + for (int i = 0; i < n; ++i) + heart[i] = L.plateau[i] && !map.isWater(i % t.w, i / t.w) && !stone[i] && + std::hypot(i % t.w - L.cx, i / t.w - L.cy) < L.g.plateauPondR + 3; + for (int team = 0; team < teams; ++team) + if (!workers[team].empty() && !openRoad(map, t, workers[team], heart, &stone)) + { + context.detail = "colony " + std::to_string(team) + " has no trail to the plateau"; + return false; + } + return true; +} + +std::string validateRequest(const GenerationRequest &r) +{ + if (r.nbTeams < 2) + return "Switchbacks need at least two colonies."; + // The geometry's own reason stays in the candidate's diagnostics; the player sees one message + // that says what to change. + return geometryFor(r).failure.empty() ? "" : "The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies."; +} + +// Checked on the finished world against the rebuilt design: every designed stone stands; every colony +// walks to colony 0; nothing landing from the sea gets inside; with the trails shut no home reaches the +// plateau or another home; with only each trail's middle leg shut, the same (the walls between the +// legs hold, so no leg can be skipped); a tower at home reaches the first leg and one on the plateau the +// last; and the colonies' walks to the plateau are even. +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "switchbacks"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(), teams = context.request.nbTeams; + const auto where = [&](int i) + { return "(" + std::to_string(i % t.w) + ", " + std::to_string(i / t.w) + ")"; }; + const std::vector stone = stoneTiles(map, L); + for (int i = 0; i < n; ++i) + if (stone[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "The stone at " + where(i) + " is missing."; + const ColonyWalk walk = walkFromFirstColony(map, teams, "the switchbacks", ""); + if (!walk.error.empty()) + return walk.error; + + const std::vector open = walkableTiles(map); + const std::vector margin = seaMargin(map, L); + if (const int entry = seaEntry(map, t, margin, L.land); entry >= 0) + return "Land at " + where(entry) + " can be reached from the sea."; + + std::vector piece(n, -1); + std::vector trails(n, 0), middle(n, 0); + for (int i = 0; i < n; ++i) + { + if (!margin[i]) + piece[i] = L.homeOf[i] >= 0 ? L.homeOf[i] : L.farmOf[i] >= 0 ? L.farmOf[i] : L.plateau[i] ? teams : -1; + trails[i] = L.trailOf[i] >= 0; + middle[i] = L.legOf[i] == g.legs / 2; + } + // Every farm is joined to its home: nearly all of its crop land, and all of its plot, is a walk from + // its colony's workers. + for (size_t f = 0; f < L.farms.size(); ++f) + if (farmReachable(map, t, L.farms[f], walk.workers[L.farmColony[f]]) < kFarmReach) + return "Colony " + std::to_string(L.farmColony[f]) + " cannot walk into one of its farms."; + if (const int leak = pieceLeak(map, t, piece, trails); leak >= 0) + return "With the trails shut, " + where(leak) + " can still be reached from another part of the map."; + if (const int leak = pieceLeak(map, t, piece, middle); leak >= 0) + return "With the middle legs shut, " + where(leak) + " can still be reached: a wall between legs has a gap."; + + for (int k = 0; k < teams; ++k) + { + std::vector homeGround(n, 0), plateauGround(n, 0), first(n, 0), last(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + const bool buildable = map.isGrass(x, y) && !map.isResource(x, y) && !L.roadTile[i]; + homeGround[i] = buildable && L.homeOf[i] == k; + plateauGround[i] = buildable && L.plateau[i]; + first[i] = L.trailOf[i] == k && L.legOf[i] == 0; + last[i] = L.trailOf[i] == k && L.legOf[i] == g.legs - 1; + } + if (towerReach(t, homeGround, first) > kTowerRange[kCoverTower]) + return "No tower at home " + std::to_string(k) + " reaches its trail's first leg."; + if (towerReach(t, plateauGround, last) > kTowerRange[kCoverTower]) + return "No tower on the plateau reaches trail " + std::to_string(k) + "'s last leg."; + } + + std::vector targets; + for (int k = 0; k < teams; ++k) + { + const int hx = int(std::lround(L.summit[k].x)), hy = int(std::lround(L.summit[k].y)); + const int seed = seedNear(t, hx, hy, 3, [&](int i) { return open[i] && !margin[i]; }); + targets.push_back(seed >= 0 ? seed : t.at(hx, hy)); + } + const WalkSpread spread = walkSpread(map, t, walk.workers, targets); + if (spread.unreached >= 0) + return "Colony " + std::to_string(spread.unreached) + " cannot climb to the plateau."; + if (spread.tooUneven(kWalkSpread)) + return "The colonies' walks to the plateau differ by " + + std::to_string(spread.longest - spread.shortest) + " steps."; + return ""; +} +} // namespace + +SwitchbacksOptions::SwitchbacksOptions(const GenerationRequest &r) + : trailWidth(r.option("trail-width")), legWall(r.option("leg-wall")), + plateauSize(r.option("plateau-size")), homeSize(r.option("home-size")), + towers(r.option("starting-towers")), towerCount(r.option("tower-count")), + sandRoads(r.option("sand-roads") != 0), farmPlots(r.option("farm-plots") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition switchbacksDefinition() +{ + return {"switchbacks", + 24, + "Switchbacks", + 1, + false, + // The trail's width and the stone between its legs in tiles; the plateau's radius as a + // share of the half side (the mountains fill the rest with as many legs as fit); each home's + // radius as a percentage of the standard. + {{"trail-width", "Trail width", 5, 9, 2, 7, ControlGroup::Terrain}, + {"leg-wall", "Wall between legs", 2, 6, 1, 2, ControlGroup::Terrain}, + {"plateau-size", "Plateau size", 14, 34, 2, 22, ControlGroup::Layout}, + {"home-size", "Home size", 60, 160, 10, 100, ControlGroup::Layout}, + // The towers every colony starts with, all against its walls: their level (0 for none, just + // open pads) and how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, + // Off, the trails are grass from wall to wall. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // On, every farm has a 10x4 clearing of grass ringed with sand in its middle, for a + // swarm or an inn. + GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), + // Every home's ambient fields and grove, the plateau's farmland and orchard, and the + // algae; every home's kit and the mountains' stone are unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; +} diff --git a/src/map/generator/generators/SwitchbacksGenerator.h b/src/map/generator/generators/SwitchbacksGenerator.h new file mode 100644 index 000000000..becdd5657 --- /dev/null +++ b/src/map/generator/generators/SwitchbacksGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct SwitchbacksOptions +{ + int trailWidth, legWall, plateauSize, homeSize, towers, towerCount; + bool sandRoads, farmPlots; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit SwitchbacksOptions(const GenerationRequest &r); +}; +GeneratorDefinition switchbacksDefinition(); diff --git a/src/map/generator/shared/Farmland.cpp b/src/map/generator/shared/Farmland.cpp new file mode 100644 index 000000000..9bdff2f98 --- /dev/null +++ b/src/map/generator/shared/Farmland.cpp @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Farmland.h" +#include "Map.h" +#include "Resources.h" +#include "Drawing.h" +#include "Territories.h" +#include "Topology.h" +#include +#include +namespace MapGeneration +{ +namespace +{ +// How far from its point a field's seed may move to find open ground. +constexpr int kSeedSearch = 16; +// A field loses any strip narrower than twice this: the beach and wall each side would fill it. +constexpr int kOpening = 3; +} // namespace + +FarmRows bestFarmRows(double angle) +{ + // Rows repeat every quarter turn and mirror about the diagonal, so only the angle from the nearest + // axis matters, 0 to 45 degrees. + double a = std::fmod(std::abs(angle), kPi / 2); + a = std::min(a, kPi / 2 - a); + const double diagonal = std::min(1.0, 2 * std::sin(2 * a)); + return {10 + 2 * diagonal, 8 + diagonal}; +} + +double farmYield(double angle) +{ + // tools/farm_row_fit.py's best yields at 0, 11.25, 22.5, 33.75 and 45 degrees from an axis. + constexpr double yields[] = {0.1489, 0.1322, 0.1225, 0.1141, 0.1126}; + double a = std::fmod(std::abs(angle), kPi / 2); + a = std::min(a, kPi / 2 - a); + const double step = a / (kPi / 16); + const int below = std::min(3, int(step)); + const double along = std::min(1.0, step - below); + return yields[below] + (yields[below + 1] - yields[below]) * along; +} + +Farm layFarm(TerrainSketch &sketch, const Torus &t, const std::vector ®ion, + double angle, ShapePoint origin, int rim, const FarmRows &rows, const FarmPlot *plot, + int bridgeSpacing, bool caps) +{ + const int n = t.size(); + Farm farm; + farm.water.assign(n, 0); + farm.row.assign(n, -1); + std::vector outside(n, 0); + for (int i = 0; i < n; ++i) + outside[i] = !region[i]; + const std::vector fromEdge = stepsFrom(t, outside); + // Across the rows: the rows run along `angle`, so a vertex's place across them is its offset along + // the normal, shifted so the crop row at the origin is centred on it. + const double nx = -std::sin(angle), ny = std::cos(angle), period = rows.period(); + const int ox = int(std::lround(origin.x)), oy = int(std::lround(origin.y)); + std::vector cycle(n, 0); + std::vector wet(n, 0); + int lowest = 0; + bool any = false; + for (int i = 0; i < n; ++i) + { + if (!region[i]) + continue; + const double across = + t.offsetX(ox, i % t.w) * nx + t.offsetY(oy, i / t.w) * ny + rows.crops / 2; + cycle[i] = int(std::floor(across / period)); + wet[i] = across - cycle[i] * period >= rows.crops; + lowest = any ? std::min(lowest, cycle[i]) : cycle[i]; + any = true; + } + // Rows are numbered from zero at the lowest, so a row's parity says crops (even) or water (odd). + std::vector seen; + for (int i = 0; i < n; ++i) + { + if (!region[i]) + continue; + farm.row[i] = 2 * (cycle[i] - lowest) + wet[i]; + if (wet[i] && fromEdge[i] >= rim) + { + farm.water[i] = 1; + sketch[i] = WATER; + if (farm.row[i] >= int(seen.size())) + seen.resize(farm.row[i] + 1, 0); + seen[farm.row[i]] = 1; + } + } + farm.sand.assign(n, 0); + farm.plot.assign(n, 0); + // Caps: a ring of sand vertices round the field just inside its rim, where the water rows' beaches + // begin, closing every crop row's ends and outer sides, so wheat and wood cannot grow out of the rows + // into the ground round the farm (a home it opens into). Sand is walkable, so workers still cross. + if (caps && rim >= 2) + for (int i = 0; i < n; ++i) + if (region[i] && !farm.water[i] && fromEdge[i] == rim - 1) + { + farm.sand[i] = 1; + sketch[i] = SAND; + } + // Bridges: every `bridgeSpacing` tiles along the rows, a line of sand vertices straight across every + // water row. The tiles either side of such a line are no longer pure water, so workers walk across, + // two tiles wide, instead of round the end of a long row. + if (bridgeSpacing > 0) + { + const double ax = std::cos(angle), ay = std::sin(angle); + for (int i = 0; i < n; ++i) + { + if (!farm.water[i]) + continue; + const double along = t.offsetX(ox, i % t.w) * ax + t.offsetY(oy, i / t.w) * ay; + const double off = along - bridgeSpacing * std::round(along / bridgeSpacing); + if (std::abs(off) < kBridgeHalfWidth) + { + farm.water[i] = 0; + farm.sand[i] = 1; + sketch[i] = SAND; + } + } + } + if (plot) + { + // The clearing's middle: the region's most inland vertex, the first on a tie. + int middle = -1; + for (int i = 0; i < n; ++i) + if (region[i] && (middle < 0 || fromEdge[i] > fromEdge[middle])) + middle = i; + const int margin = plot->ring + 1; + const int x0 = middle % t.w - plot->width / 2, y0 = middle / t.w - plot->height / 2; + // The grass tiles span vertices x0..x0+width by y0..y0+height; the ring and a vertex more + // round them must lie as far inside the region as its water rows, so the ring's sand never + // meets the region's own beach. + bool fits = middle >= 0; + for (int dy = -margin; dy <= plot->height + margin && fits; ++dy) + for (int dx = -margin; dx <= plot->width + margin && fits; ++dx) + fits = fromEdge[t.at(x0 + dx, y0 + dy)] >= rim; + if (fits) + { + farm.plotX = t.x(x0); + farm.plotY = t.y(y0); + for (int dy = -margin; dy <= plot->height + margin; ++dy) + for (int dx = -margin; dx <= plot->width + margin; ++dx) + { + const int i = t.at(x0 + dx, y0 + dy); + const bool grass = dx >= 0 && dx <= plot->width && dy >= 0 && dy <= plot->height; + const bool ring = !grass && dx >= -plot->ring && dx <= plot->width + plot->ring && + dy >= -plot->ring && dy <= plot->height + plot->ring; + // The clearing trumps the rows and bridges alike: its grass is grass. + if (farm.water[i] || (grass && farm.sand[i])) + { + farm.water[i] = 0; + farm.sand[i] = 0; + sketch[i] = GRASS; + } + if (ring) + { + farm.sand[i] = 1; + sketch[i] = SAND; + } + } + for (int dy = 0; dy < plot->height; ++dy) + for (int dx = 0; dx < plot->width; ++dx) + farm.plot[t.at(x0 + dx, y0 + dy)] = 1; + seen.assign(seen.size(), 0); + for (int i = 0; i < n; ++i) + if (farm.water[i]) + seen[farm.row[i]] = 1; + } + } + farm.rows = int(std::count(seen.begin(), seen.end(), 1)); + return farm; +} + +std::vector growFarmFields(const Torus &t, const std::vector &occupied, + const std::vector &homeOf, const std::vector &area, + const std::vector &seeds, const std::vector &owners, + const std::vector &anchors, int gap, double neckHalfWidth, + const std::vector &rowAngles) +{ + const int n = t.size(); + // Open ground: not designed, allowed, and clear of all land that is no home by the gap. Homes' + // surroundings stay open here; each field is held off other colonies' homes once grown. + std::vector foreign(n, 0); + for (int i = 0; i < n; ++i) + foreign[i] = occupied[i] && homeOf[i] < 0; + const std::vector fromForeign = stepsFrom(t, foreign); + std::vector free(n, 0); + for (int i = 0; i < n; ++i) + free[i] = area[i] && !occupied[i] && (fromForeign[i] < 0 || fromForeign[i] >= gap); + // Each seed starts from the free tile near its point that lies in the biggest stretch of free ground + // (nearest the point on a tie), so a field never starts boxed in a pocket beside its home. + const std::vector stretch = connectedRegions(free, t.w, t.h, true); + std::vector stretchSize; + for (int label : stretch) + if (label >= 0) + { + if (label >= int(stretchSize.size())) + stretchSize.resize(label + 1, 0); + ++stretchSize[label]; + } + std::vector> starts(seeds.size()); + std::vector snapped(seeds); + for (size_t s = 0; s < seeds.size(); ++s) + { + const int sx = int(std::lround(seeds[s].x)), sy = int(std::lround(seeds[s].y)); + int best = -1, bestSize = 0, bestDistance = 0; + for (int dy = -kSeedSearch; dy <= kSeedSearch; ++dy) + for (int dx = -kSeedSearch; dx <= kSeedSearch; ++dx) + { + const int i = t.at(sx + dx, sy + dy); + if (!free[i]) + continue; + const int size = stretchSize[stretch[i]], distance = dx * dx + dy * dy; + if (best < 0 || size > bestSize || (size == bestSize && distance < bestDistance)) + { + best = i; + bestSize = size; + bestDistance = distance; + } + } + if (best < 0) + continue; + snapped[s] = {double(sx + t.offsetX(sx, best % t.w)), double(sy + t.offsetY(sy, best / t.w))}; + starts[s].push_back(best); + } + std::vector worth; + for (double angle : rowAngles) + worth.push_back(farmYield(angle)); + std::vector labels = + growTerritories(t, free, starts, [](int) { return 0; }, worth.size() == seeds.size() ? &worth : nullptr) + .labels; + // Every field keeps the gap from every home but its own. + int colonies = 0; + for (int owner : owners) + colonies = std::max(colonies, owner + 1); + for (int k = 0; k < colonies; ++k) + { + std::vector others(n, 0); + for (int i = 0; i < n; ++i) + others[i] = homeOf[i] >= 0 && homeOf[i] != k; + const std::vector fromOthers = stepsFrom(t, others); + for (int i = 0; i < n; ++i) + if (labels[i] >= 0 && owners[labels[i]] == k && fromOthers[i] >= 0 && fromOthers[i] < gap) + labels[i] = -1; + } + separateTerritories(t, labels, gap); + // A strip of field narrower than a beach and a wall on each side would be all sand and stone, and + // cut the ground beyond it off: every field is opened (shrunk by kOpening, then grown back as far + // within itself), which removes such strips and keeps the rest. + for (size_t s = 0; s < seeds.size(); ++s) + { + std::vector outside(n, 0); + for (int i = 0; i < n; ++i) + outside[i] = labels[i] != int(s) && homeOf[i] != owners[s]; + const std::vector fromOutside = stepsFrom(t, outside); + std::vector core(n, 0); + for (int i = 0; i < n; ++i) + core[i] = !outside[i] && fromOutside[i] > kOpening; + const std::vector fromCore = stepsFrom(t, core); + for (int i = 0; i < n; ++i) + if (labels[i] == int(s) && (fromCore[i] < 0 || fromCore[i] > kOpening)) + labels[i] = -1; + } + // Trimming a field to its gaps can leave slivers cut off from the rest; a field keeps only the + // ground joined to its own home. + for (size_t s = 0; s < seeds.size(); ++s) + { + std::vector mine(n, 0), home(n, 0); + for (int i = 0; i < n; ++i) + { + mine[i] = labels[i] == int(s) || homeOf[i] == owners[s]; + home[i] = homeOf[i] == owners[s]; + } + const std::vector joined = stepsFrom(t, home, mine); + for (int i = 0; i < n; ++i) + if (labels[i] == int(s) && joined[i] < 0) + labels[i] = -1; + } + // However the trimming left it, every field is joined to its home by a neck the full width from the + // home's middle to the field's nearest ground, wide enough that its beaches and walls leave a broad + // opening between them. + for (size_t s = 0; s < seeds.size() && s < anchors.size(); ++s) + { + const int ax = int(std::lround(anchors[s].x)), ay = int(std::lround(anchors[s].y)); + int nearest = -1, best = 0; + for (int i = 0; i < n; ++i) + if (labels[i] == int(s)) + { + const int d = t.dist2(ax, ay, i % t.w, i / t.w); + if (nearest < 0 || d < best) + { + nearest = i; + best = d; + } + } + if (nearest < 0) + continue; + std::vector neck(n, 0), otherFields(n, 0); + for (int i = 0; i < n; ++i) + otherFields[i] = labels[i] >= 0 && labels[i] != int(s); + const std::vector fromOtherFields = stepsFrom(t, otherFields); + const double nx = ax + t.offsetX(ax, nearest % t.w), ny = ay + t.offsetY(ay, nearest / t.w); + strokePath(neck, t, {{anchors[s].x, anchors[s].y, neckHalfWidth}, {nx, ny, neckHalfWidth}}); + for (int i = 0; i < n; ++i) + // The neck keeps the same gap from land that is no home, and from every other field, as the + // field does, so it never runs round the end of a wall or into someone else's farm. + if (neck[i] && !occupied[i] && labels[i] < 0 && (fromForeign[i] < 0 || fromForeign[i] >= gap) && + (fromOtherFields[i] < 0 || fromOtherFields[i] >= gap)) + labels[i] = int(s); + } + return labels; +} + +double farmReachable(const Map &map, const Torus &t, const Farm &farm, const std::vector &sources) +{ + const int n = t.size(); + std::vector open(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + const bool deposit = map.isResource(x, y); + open[i] = !map.isWater(x, y) && map.getBuilding(x, y) == NOGBID && + (!deposit || map.getResource(x, y).type == CORN || map.getResource(x, y).type == WOOD); + } + const std::vector steps = stepsFrom(t, tileMask(t, sources), open); + int land = 0, reached = 0; + for (int i = 0; i < n; ++i) + { + if (farm.plot[i] && steps[i] < 0) + return 0; + // Crop land is the rows' grass: the sand lane outside a sealed coast is not the farm's to work. + if (farm.row[i] < 0 || farm.row[i] % 2 || !open[i] || !map.isGrass(i % t.w, i / t.w)) + continue; + ++land; + reached += steps[i] >= 0; + } + if (getenv("TMP_FARM") && reached < land) fprintf(stderr, "farm land %d reached %d\n", land, reached); // TMP + return land ? double(reached) / land : 0; +} + +void clearFarmPlots(Map &map, const Torus &t, const std::vector &farms) +{ + for (const Farm &farm : farms) + for (int i = 0; i < t.size(); ++i) + if (farm.plot[i] && map.isResource(i % t.w, i / t.w)) + map.setNoResource(i % t.w, i / t.w, 1); +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Farmland.h b/src/map/generator/shared/Farmland.h new file mode 100644 index 000000000..07f779129 --- /dev/null +++ b/src/map/generator/shared/Farmland.h @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Geometry.h" +#include "Grid.h" +#include "Sketch.h" +#include +class Map; +struct GenerationContext; +namespace MapGeneration +{ +// Farmland: ground laid out like a real farm, in long rows of crops with a strip of water between +// every two rows and only the beach the engine insists on in between. Wheat and wood regrow on a tile +// only when a random probe up to 15 tiles away lands on water and the opposite probe does not land +// on sand (Map::growResources), so a crop row next to water yields steadily, and a farm's yield is +// set by how wide its crop and water rows are. + +/// The widths of a farm's crop rows and water rows, measured across the rows in tiles. +struct FarmRows +{ + double crops, water; + double period() const { return crops + water; } +}; + +/// The row widths that make a farm yield most for its area, for rows running at `angle` (radians). +/// Wider rows hold more crops but more of them sit far from water; narrower ones lose a larger share +/// of their ground to beaches. tools/farm_row_fit.py sums every crop tile's exact chance of regrowing +/// for every width pair: rows along an axis do best at 10 tiles of crops and 8 of water, and diagonal +/// rows, whose stepped edges lose more tiles to the beach, at 12 and 9. The optimum is flat (the next +/// best pairs are within 2%), so this is a rough line between the two through the sine of twice the +/// angle, which the fit follows closely by 22 degrees. +FarmRows bestFarmRows(double angle); + +/// How much a farm yields per tile of its area, laid at bestFarmRows for rows running at `angle`: every +/// crop tile's chance of regrowing each time the engine visits it, summed and divided by the area, as +/// tools/farm_row_fit.py measures it at 0, 11.25, 22.5, 33.75 and 45 degrees from an axis (0.149 down +/// to 0.113) and interpolated between. Rows along an axis yield about a third more per tile than +/// diagonal rows, whose stepped edges lose more tiles to the beach, so two farms given the same yield +/// need areas in the inverse ratio. +double farmYield(double angle); + +/// A farm laid over a region: which undermap vertices are water, and for every vertex of the region +/// the row it lies in (even rows are crops, odd rows water; -1 outside the region). +struct Farm +{ + std::vector water; + std::vector row; + int rows = 0; // how many water rows the region holds + // The building plot, when one was asked for and fits: its sand ring (undermap vertices), its + // pure-grass tiles, and the top-left of those tiles (-1 without a plot). + std::vector sand, plot; + int plotX = -1, plotY = -1; +}; + +/// A clearing in the middle of a farm for buildings: `width` by `height` tiles of pure grass with a +/// ring of sand `ring` undermap vertices wide round it (two vertices make a full tile of sand), so +/// no crop grows onto it and nothing but the clearing is buildable. 10 by 4 seats a swarm or an inn +/// with room to walk round it. +struct FarmPlot +{ + int width = 10, height = 4, ring = 2; +}; + +/// Lays rows over `region` running at `angle` (radians), with the row at `origin` a crop row centred +/// on it, and no water within `rim` steps of the region's edge, so the region's own coast (or wall) +/// keeps a margin of land and every crop row joins the rim at both ends. With a `plot`, a building +/// clearing is stamped at the region's most inland point (farthest from its edge), clear of every +/// water row by its sand ring and a vertex more and at least `rim` from the edge; a region too small +/// for it gets none. With a `bridgeSpacing`, a sand bridge crosses every water row each that many tiles +/// along the rows, so workers cross a long farm instead of walking round its rows. With `caps` (on), a +/// ring of sand just inside the rim closes the crop rows, so their wheat and wood never spread beyond +/// the rows into the ground round the farm. Keep the plot's and +/// bridges' sand (`Farm::sand`) out of any beach flood, as a sand road is. Stamps the water and the sand +/// into `sketch`; lay beaches afterwards as usual. +/// Half the width, in vertices along the rows, of a bridge's sand line: wide enough that a diagonal line +/// stays unbroken. +constexpr double kBridgeHalfWidth = 0.75; +Farm layFarm(TerrainSketch &sketch, const Torus &, const std::vector ®ion, + double angle, ShapePoint origin, int rim, const FarmRows &rows, + const FarmPlot *plot = nullptr, int bridgeSpacing = 0, bool caps = true); + +/// Farm fields grown into open ground straight out of the homes they belong to. `homeOf` gives the +/// designed land's owner where it is a home (0 or more) and -1 elsewhere, and `occupied` marks all the +/// designed land. Field `s` belongs to home `owners[s]` and starts from the open tile near `seeds[s]` +/// in the biggest stretch of open ground (searched 16 tiles round the point). Every open tile that +/// `area` allows is shared out between the fields by growth (growTerritories): by equal area, or, given +/// every field's `rowAngles`, by equal yield (farmYield), so a field whose rows run on the diagonal gets +/// more ground than one whose rows run along an axis; then every +/// field keeps `gap` tiles of water from all designed land but its own home, and from every other +/// field (separateTerritories). A field may run right up to its own home, so the two are one piece of +/// land with no coast between. Strips too narrow to survive their beaches and walls and slivers cut off +/// from the home are dropped, and then every field is joined to its home by a neck `neckHalfWidth` wide +/// from the home's middle to the field's nearest ground, so the opening is always broad. Returns every tile's field, the seed's index, or -1. A seed with no open ground near +/// it gets no field, so a caller checks every field's size before using them. +std::vector growFarmFields(const Torus &, const std::vector &occupied, + const std::vector &homeOf, const std::vector &area, + const std::vector &seeds, const std::vector &owners, + const std::vector &anchors, int gap, double neckHalfWidth, + const std::vector &rowAngles = {}); + +/// How much of a farm a colony can work: the share of the farm's crop-row grass (and the whole of its +/// plot, if any, or 0 is returned) that a unit from `sources` can walk to once crops are cleared - +/// water, stone and buildings block, wheat and wood do not. A validator's proof that a farm is joined +/// to its home. +double farmReachable(const Map &, const Torus &, const Farm &, const std::vector &sources); + +/// Clears any deposit a later layer left on a farm's building plot, so every plot stays buildable. Run +/// it after the last deposits go down. +void clearFarmPlots(Map &, const Torus &, const std::vector &); + +/// Plants a laid farm, once the terrain is written. A farm is a wheat farm: `wheat` tiles go on the +/// crop rows' pure grass that `eligible` allows, nearest the water first (where it regrows best), and +/// `wood` tiles make one small woodlot along a single crop row, the one with the most room, nearest its +/// water. Returns how many tiles were planted. +template +int plantFarm(Map &map, const Torus &t, const Farm &farm, int wheat, int wood, Eligible eligible); +} // namespace MapGeneration + +#include "Map.h" +#include "Resources.h" +#include +#include +template +int MapGeneration::plantFarm(Map &map, const Torus &t, const Farm &farm, int wheat, int wood, + Eligible eligible) +{ + const std::vector fromWater = stepsFrom(t, farm.water); + std::vector> crops; + std::vector rowRoom; + for (int i = 0; i < t.size(); ++i) + if (farm.row[i] >= 0 && farm.row[i] % 2 == 0 && fromWater[i] >= 0 && !farm.plot[i] && eligible(i) && + map.isGrass(i % t.w, i / t.w)) + { + crops.push_back({fromWater[i], i}); + if (farm.row[i] >= int(rowRoom.size())) + rowRoom.resize(farm.row[i] + 1, 0); + ++rowRoom[farm.row[i]]; + } + std::stable_sort(crops.begin(), crops.end()); + // The woodlot's row: the crop row with the most room, the first on a tie. + const int woodRow = rowRoom.empty() ? -1 : int(std::max_element(rowRoom.begin(), rowRoom.end()) - rowRoom.begin()); + int planted = 0, woods = 0, wheats = 0; + for (const auto &entry : crops) + { + const int i = entry.second; + if (woods < wood && farm.row[i] == woodRow) + { + map.setResource(i % t.w, i / t.w, WOOD, 1); + ++woods; + ++planted; + } + } + for (const auto &entry : crops) + { + const int i = entry.second; + if (wheats >= wheat) + break; + if (map.isResource(i % t.w, i / t.w)) + continue; + map.setResource(i % t.w, i / t.w, CORN, 1); + ++wheats; + ++planted; + } + return planted; +} diff --git a/src/map/generator/shared/Homes.h b/src/map/generator/shared/Homes.h index 34df5edd7..84a3ea51e 100644 --- a/src/map/generator/shared/Homes.h +++ b/src/map/generator/shared/Homes.h @@ -67,7 +67,7 @@ inline MapGeneratorPoint pondHomeAnchor(ShapePoint centre, double axis, double r } /// A pond home's starter kit: wheat and wood either side of its first pond on the swarm's side, where -/// they regrow, and a stone clump out behind the pond. +/// they regrow. No stone: a pond home is walled in stone already, so a clump would only take room. template void plantPondHomeKit(Map &map, const Torus &t, GenerationContext &context, ShapePoint firstPond, double axis, double pondRadius, int wheat, int wood, Eligible eligible) @@ -75,7 +75,7 @@ void plantPondHomeKit(Map &map, const Torus &t, GenerationContext &context, Shap const KitFrame frame{int(std::lround(firstPond.x)), int(std::lround(firstPond.y)), axis}; const double reach = pondRadius * 1.2 + 3; const Kit kit{frame.at(-0.4 * reach, -reach, 12), frame.at(-0.4 * reach, reach, 12), - frame.at(reach + 3, 0, 10), wheat, wood, 2}; + frame.at(reach + 3, 0, 10), wheat, wood, -1}; plantKit(map, t, context, kit, eligible); } diff --git a/src/map/generator/shared/Planting.h b/src/map/generator/shared/Planting.h index 08774d04e..30e77a42a 100644 --- a/src/map/generator/shared/Planting.h +++ b/src/map/generator/shared/Planting.h @@ -83,9 +83,9 @@ struct Kit { KitSeed wheat, wood, stone; int wheatTiles, woodTiles; // patch sizes - int stoneRadius; // a clump + int stoneRadius; // a clump; below 0 for none }; -/// A home's starter kit: a wheat patch, a wood patch and a stone clump, each from the nearest +/// A home's starter kit: a wheat patch, a wood patch and a stone clump (none with a negative radius), each from the nearest /// eligible tile to its seed, in that order. A deposit whose seed finds no eligible tile is left /// out, since the guarantee that follows tops a colony up. template @@ -97,6 +97,8 @@ void plantKit(Map &map, const Torus &t, GenerationContext &context, const Kit &k growPatch(map, t, seed, CORN, kit.wheatTiles, eligible); if (const int seed = seedNear(t, kit.wood.x, kit.wood.y, kit.wood.within, eligible); seed >= 0) growPatch(map, t, seed, WOOD, kit.woodTiles, eligible); + if (kit.stoneRadius < 0) + return; if (const int seed = seedNear(t, kit.stone.x, kit.stone.y, kit.stone.within, eligible); seed >= 0) placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), STONE, diff --git a/src/map/generator/shared/Settlements.cpp b/src/map/generator/shared/Settlements.cpp index c4afa4736..ad883b273 100644 --- a/src/map/generator/shared/Settlements.cpp +++ b/src/map/generator/shared/Settlements.cpp @@ -126,7 +126,7 @@ bool placeSettlement(Game &game, GenerationContext &context, int team, } int placeTower(Game &game, int team, int level, double x, double y, int within, - const std::vector &allowed) + const std::vector &allowed, bool stocked) { const int type = globalContainer->buildingsTypes.getTypeNum("defencetower", level, false); const BuildingType *tower = globalContainer->buildingsTypes.get(type); @@ -161,7 +161,7 @@ int placeTower(Game &game, int team, int level, double x, double y, int within, Building *building = game.addBuilding(best % w, best / w, type, team, 1, 0); if (!building) return -1; - building->bullets = tower->maxBullets; + building->bullets = stocked ? tower->maxBullets : 0; // The colony's lists were built when its swarm went down; the tower joins its turrets the way // Team::createLists would have taken it in. game.teams[team]->turrets.push_back(building); diff --git a/src/map/generator/shared/Settlements.h b/src/map/generator/shared/Settlements.h index f3fe401ea..eb2dfe00f 100644 --- a/src/map/generator/shared/Settlements.h +++ b/src/map/generator/shared/Settlements.h @@ -12,10 +12,10 @@ bool placeSettlement(Game &, GenerationContext &, int team, const std::vector &allowed); + const std::vector &allowed, bool stocked = true); } // namespace MapGeneration diff --git a/src/map/generator/shared/Territories.h b/src/map/generator/shared/Territories.h index c4151cb75..2028d915a 100644 --- a/src/map/generator/shared/Territories.h +++ b/src/map/generator/shared/Territories.h @@ -3,6 +3,7 @@ #include "Grid.h" #include "Sketch.h" #include "Topology.h" +#include #include #include #include @@ -52,9 +53,14 @@ struct Territories /// Unlike wedges round the centre, which are equal only on a round design, this shares out a /// square map's corners and the ground across its wrap as fairly as the middle, for any number of /// claimants. +/// +/// With `worth` (one value per claimant), territories are shared out by value instead of area: the +/// claimant whose area times worth is least takes the next tile, so a claimant whose ground is worth +/// less per tile ends with proportionally more of it. template Territories growTerritories(const Torus &t, const std::vector &eligible, - const std::vector> &seeds, Cost cost) + const std::vector> &seeds, Cost cost, + const std::vector *worth = nullptr) { const int n = t.w * t.h, claimants = int(seeds.size()); Territories result; @@ -97,8 +103,11 @@ Territories growTerritories(const Torus &t, const std::vector &el { int k = -1; for (int c = 0; c < claimants; ++c) - if (growing[c] && (k < 0 || result.areas[c] < result.areas[k])) + { + const double value = result.areas[c] * (worth ? (*worth)[c] : 1.0); + if (growing[c] && (k < 0 || value < result.areas[k] * (worth ? (*worth)[k] : 1.0))) k = c; + } if (k < 0) break; int taken = -1; @@ -120,44 +129,116 @@ Territories growTerritories(const Torus &t, const std::vector &el } return result; } -/// Trims the spurs a race for the cheapest tile leaves along territories' borders: for `passes` -/// passes, every labelled tile six or more of whose eight neighbours share another label takes that -/// label, all at once per pass. Tiles of `keep` (a territory's frontage) never change. Areas move by a -/// few tiles each pass, so a caller that needs them equal measures them again afterwards. +/// Smooths territories' borders, which a race for the cheapest tile leaves ragged: for `passes` +/// passes, every labelled tile whose neighbourhood - the square `radius` tiles round it - is more than +/// `share` another territory's takes that label, all at once per pass. A radius of 1 trims single-tile +/// spurs; a few passes at 3 or 4 straighten a wandering border into a smooth curve. Tiles of `keep` (a +/// territory's frontage) never change. Areas move a little, so a caller that needs them equal measures +/// them again afterwards. inline void smoothLabels(const Torus &t, std::vector &labels, int passes, - const std::vector &keep) + const std::vector &keep, int radius = 1, double share = 0.7) { + std::vector counts; for (int pass = 0; pass < passes; ++pass) { std::vector smoothed = labels; + int top = 0; + for (int label : labels) + top = std::max(top, label + 1); + counts.assign(top, 0); + const int window = (2 * radius + 1) * (2 * radius + 1) - 1; for (int i = 0; i < t.size(); ++i) { if (labels[i] < 0 || keep[i]) continue; - int best = labels[i], bestCount = 0; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) + std::vector touched; + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) { const int label = labels[t.at(i % t.w + dx, i / t.w + dy)]; - if ((!dx && !dy) || label < 0 || label == labels[i]) - continue; - int count = 0; - for (int ey = -1; ey <= 1; ++ey) - for (int ex = -1; ex <= 1; ++ex) - count += (ex || ey) && labels[t.at(i % t.w + ex, i / t.w + ey)] == label; - if (count > bestCount) - { - bestCount = count; - best = label; - } + if ((dx || dy) && label >= 0 && label != labels[i] && counts[label]++ == 0) + touched.push_back(label); } - if (bestCount >= 6) + int best = -1; + for (int label : touched) + { + if (counts[label] > share * window && (best < 0 || counts[label] > counts[best])) + best = label; + } + if (best >= 0) smoothed[i] = best; + for (int label : touched) + counts[label] = 0; } labels.swap(smoothed); } } +/// Opens a gap between neighbouring territories: every tile within `gap` / 2 steps (rounded up) of a +/// tile of another territory loses its label, so any two territories end at least `gap` tiles apart +/// and can be separate islands, or separate fields behind their own walls. +inline void separateTerritories(const Torus &t, std::vector &labels, int gap) +{ + std::vector border(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + { + if (labels[i] < 0) + continue; + for (int dy = -1; dy <= 1 && !border[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int other = labels[t.at(i % t.w + dx, i / t.w + dy)]; + if (other >= 0 && other != labels[i]) + { + border[i] = 1; + break; + } + } + } + const std::vector fromBorder = stepsFrom(t, border); + const int half = (gap + 1) / 2; + for (int i = 0; i < t.size(); ++i) + if (labels[i] >= 0 && fromBorder[i] >= 0 && fromBorder[i] < half) + labels[i] = -1; +} + +/// Closes the gaps between labelled regions: every `eligible` tile with no label, within `reach` steps +/// of a labelled tile over eligible ground, takes the label of the region nearest it (4-connected +/// steps, the lowest tile index first on a tie). A design that kept its pieces apart with water can +/// fill that water in and part them with a wall on the border instead (labelBorders). Returns the +/// tiles filled. +inline std::vector fillToNearest(const Torus &t, std::vector &labels, + const std::vector &eligible, int reach) +{ + std::vector filled(t.size(), 0); + std::vector steps(t.size(), -1), frontier, next; + for (int i = 0; i < t.size(); ++i) + if (labels[i] >= 0) + { + steps[i] = 0; + frontier.push_back(i); + } + for (int step = 1; step <= reach && !frontier.empty(); ++step) + { + next.clear(); + for (int i : frontier) + { + const int x = i % t.w, y = i / t.w; + for (int j : {t.at(x, y - 1), t.at(x - 1, y), t.at(x + 1, y), t.at(x, y + 1)}) + if (steps[j] < 0 && eligible[j] && labels[j] < 0) + { + steps[j] = step; + labels[j] = labels[i]; + filled[j] = 1; + next.push_back(j); + } + } + std::sort(next.begin(), next.end()); + frontier.swap(next); + } + return filled; +} + /// The ground `sources` cannot walk to: every tile of `ground` a flood from them over `ground` does not /// reach. Land a lake or a wall has closed off, which a design usually fills rather than leaves as a /// pocket nobody can use. @@ -217,6 +298,73 @@ int growFarLake(const Torus &t, std::vector &water, const std::ve queued, stamp); } +/// A lake of exactly `target` tiles beside a site, on one side of it: `depth` gives the region as in +/// growFarLake (-1 outside), `room` every tile's steps from what the lake must keep clear of, and only +/// tiles with at least `gap` of room, and at least `siteGap` from the site, may be water. The seed lies +/// on the `side` (+1 left, -1 right) of the line through the site along `heading` (radians, from the +/// region's way in towards the site), and the whole lake keeps to that side of the line, so it never +/// comes between the site and the way in. The seed lies between `siteGap` and `reach` tiles from it: the roomiest such +/// tile, room counted only up to what a round lake of the target needs, then the nearest to half way +/// out, in a stretch of roomy tiles big enough for the whole lake. Two calls, one each side, give a +/// home a lake on either flank, the same size for everyone. Adds the lake to `water`; returns its size, +/// 0 when that side has no room. +template +int growLakeBeside(const Torus &t, std::vector &water, const std::vector &depth, + const std::vector &room, int gap, int target, int site, double heading, int side, + int siteGap, int reach, NoiseAt noiseAt, std::vector &queued, int stamp) +{ + const int n = t.size(), sx = site % t.w, sy = site / t.w; + const auto away = [&](int i) + { return std::sqrt(double(t.dist2(sx, sy, i % t.w, i / t.w))); }; + const double hx = std::cos(heading), hy = std::sin(heading); + // How far a tile lies to the left of the line through the site along the heading (right is + // negative). The whole lake keeps to its side, clear of the line by half the site gap, so it never + // comes between the site and the way in. + const auto lateral = [&](int i) + { + const double dx = t.offsetX(sx, i % t.w), dy = t.offsetY(sy, i / t.w); + return hx * dy - hy * dx; + }; + std::vector roomy(n, 0); + for (int i = 0; i < n; ++i) + roomy[i] = depth[i] >= 0 && room[i] >= gap && !water[i] && away(i) >= siteGap && + lateral(i) * side >= siteGap; + const std::vector stretch = connectedRegions(roomy, t.w, t.h, true); + std::vector stretchSize; + for (int label : stretch) + if (label >= 0) + { + if (label >= int(stretchSize.size())) + stretchSize.resize(label + 1, 0); + ++stretchSize[label]; + } + const int enough = gap + int(std::ceil(std::sqrt(target / 3.14159265358979))); + const double ideal = (siteGap + reach) / 2.0; + int seed = -1; + double seedScore = 0; + for (int i = 0; i < n; ++i) + { + if (!roomy[i] || stretchSize[stretch[i]] < target || away(i) > reach) + continue; + const double score = 4 * std::min(room[i], enough) - std::abs(away(i) - ideal); + if (seed < 0 || score > seedScore) + { + seed = i; + seedScore = score; + } + } + if (seed < 0) + return 0; + return growWater( + t, water, seed, target, [&](int i) { return roomy[i] != 0; }, + [&](int i) + { + const double d = std::sqrt(double(t.dist2(seed % t.w, seed / t.w, i % t.w, i / t.w))); + return std::int64_t(d * 1000) + std::int64_t(noiseAt(i) * 2500); + }, + queued, stamp); +} + /// The site in a region the same number of steps from its way in as every other region's: among the /// tiles whose `depth` is within `spread` of `target` (trying 0, then 1, up to `spread`), the one with /// the most `room` (steps from anything a site must keep clear of), at least `minimumRoom`; ties go to diff --git a/src/map/generator/shared/Towers.cpp b/src/map/generator/shared/Towers.cpp new file mode 100644 index 000000000..19b5d6e05 --- /dev/null +++ b/src/map/generator/shared/Towers.cpp @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Towers.h" +#include "Game.h" +#include "Settlements.h" +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +// Whether a tower on footprint `a` reaches any tile of footprint `b` (both top-left tiles). +bool footprintsInRange(const Torus &t, int a, int b, int range) +{ + const int dx = t.offsetX(a % t.w, b % t.w), dy = t.offsetY(a / t.w, b / t.w); + // The footprints are two wide: a tile of b at dx..dx+1 is within range of a's 0..1 when the gap + // between the spans is at most the range. + const auto gap = [](int d) { return d > 0 ? std::max(0, d - 1) : std::max(0, -d - 1); }; + return gap(dx) <= range && gap(dy) <= range; +} +} // namespace + +TowerPlan chooseTowerSites(const Torus &t, const std::vector &owner, + const std::vector &buildable, + const std::vector &target, + const std::vector &keepOutOfRange, int colonies, + const TowerRequest &request) +{ + const int n = t.size(), r = request.range; + TowerPlan plan; + plan.towers.assign(colonies, {}); + plan.pads.assign(colonies, {}); + // Every colony's candidate footprints, best first: the other colonies' target tiles in range, + // then the fewest of other colonies' protected tiles, then row order. + std::vector>> ranked(colonies); + std::vector protectedInRange(n, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x, k = owner[i]; + if (k < 0 || k >= colonies) + continue; + bool fits = true; + for (int fy = 0; fy < 2 && fits; ++fy) + for (int fx = 0; fx < 2 && fits; ++fx) + { + const int j = t.at(x + fx, y + fy); + fits = buildable[j] && owner[j] == k; + } + if (fits && request.against) + { + bool touches = false; + for (int fy = -1; fy <= 2 && !touches; ++fy) + for (int fx = -1; fx <= 2 && !touches; ++fx) + touches = (fx < 0 || fx > 1 || fy < 0 || fy > 1) && (*request.against)[t.at(x + fx, y + fy)]; + fits = touches; + } + if (!fits) + continue; + int score = 0; + for (int dy = -r; dy <= r + 1; ++dy) + for (int dx = -r; dx <= r + 1; ++dx) + { + const int j = t.at(x + dx, y + dy); + if (owner[j] >= 0 && owner[j] != k) + { + score += (target[j] != 0) * request.otherWeight; + if (keepOutOfRange[j]) + protectedInRange[i] = 1; + } + else if (owner[j] == k) + score += (target[j] != 0) * request.ownWeight; + } + ranked[k].push_back({-score, i}); + } + for (auto &list : ranked) + std::stable_sort(list.begin(), list.end()); + const auto spaced = [&](const std::vector &sites, int i) + { + for (int s : sites) + if (t.chebyshev(s % t.w, s / t.w, i % t.w, i / t.w) < request.spacing) + return false; + return true; + }; + // Towers, a colony at a time in turn. Two passes: first only sites that reach someone, then any. + std::vector next(colonies, 0); + for (int pass = 0; pass < 2; ++pass) + { + std::fill(next.begin(), next.end(), 0); + for (bool progress = true; progress;) + { + progress = false; + for (int k = 0; k < colonies; ++k) + { + if (int(plan.towers[k].size()) >= request.towers) + continue; + while (next[k] < ranked[k].size()) + { + const auto [negative, i] = ranked[k][next[k]++]; + if ((pass == 0 && negative >= 0) || protectedInRange[i] || !spaced(plan.towers[k], i)) + continue; + plan.towers[k].push_back(i); + progress = true; + break; + } + } + } + } + // A tower starts stocked only where no other colony's tower is within its range: towers that face + // each other start empty, so the game does not open with them shooting each other down. + plan.stocked.assign(colonies, {}); + for (int k = 0; k < colonies; ++k) + for (int i : plan.towers[k]) + { + bool alone = true; + for (int other = 0; other < colonies && alone; ++other) + if (other != k) + for (int s : plan.towers[other]) + if (footprintsInRange(t, i, s, r)) + { + alone = false; + break; + } + plan.stocked[k].push_back(alone); + } + // Pads: the best remaining sites, spaced from the colony's towers and pads. + for (int k = 0; k < colonies; ++k) + { + std::vector taken = plan.towers[k]; + for (const auto &[negative, i] : ranked[k]) + { + if (int(plan.pads[k].size()) >= request.pads) + break; + if (!spaced(taken, i)) + continue; + plan.pads[k].push_back(i); + taken.push_back(i); + } + } + return plan; +} + +int dropBlockingSites(const Torus &t, TowerPlan &plan, const std::vector &open, + const std::vector> &sources, + const std::vector> &goals) +{ + int dropped = 0; + const auto blocked = [&](size_t k) + { + std::vector walk = open; + const std::vector footprints = towerFootprints(t, plan); + for (int i = 0; i < t.size(); ++i) + if (footprints[i]) + walk[i] = 0; + const std::vector steps = stepsFrom(t, tileMask(t, sources[k]), walk); + for (int i = 0; i < t.size(); ++i) + if (goals[k][i] && steps[i] >= 0) + return false; + return true; + }; + for (size_t k = 0; k < plan.towers.size() && k < sources.size(); ++k) + while (blocked(k) && (!plan.pads[k].empty() || !plan.towers[k].empty())) + { + if (!plan.pads[k].empty()) + plan.pads[k].pop_back(); + else + { + plan.towers[k].pop_back(); + plan.stocked[k].pop_back(); + } + ++dropped; + } + return dropped; +} + +int evenTowerPlan(TowerPlan &plan) +{ + if (plan.towers.empty()) + return 0; + size_t towers = plan.towers[0].size(), pads = plan.pads[0].size(); + for (size_t k = 0; k < plan.towers.size(); ++k) + { + towers = std::min(towers, plan.towers[k].size()); + pads = std::min(pads, plan.pads[k].size()); + } + for (size_t k = 0; k < plan.towers.size(); ++k) + { + plan.towers[k].resize(towers); + plan.stocked[k].resize(towers); + plan.pads[k].resize(pads); + } + return int(towers); +} + +std::vector roomyGround(const Torus &t, const std::vector &open, int room) +{ + std::vector closed(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + closed[i] = !open[i]; + const std::vector fromClosed = stepsFrom(t, closed); + std::vector roomy(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + roomy[i] = open[i] && (fromClosed[i] < 0 || fromClosed[i] >= room); + return roomy; +} + +bool raiseTowers(Game &game, const TowerPlan &plan, int level) +{ + const Torus t(game.map); + for (size_t k = 0; k < plan.towers.size(); ++k) + for (size_t j = 0; j < plan.towers[k].size(); ++j) + { + const int site = plan.towers[k][j]; + std::vector footprint(t.size(), 0); + for (int dy = 0; dy < 2; ++dy) + for (int dx = 0; dx < 2; ++dx) + footprint[t.at(site % t.w + dx, site / t.w + dy)] = 1; + // placeTower measures from the footprint's middle, so the site's middle finds it exactly. + if (placeTower(game, int(k), level, site % t.w + 1, site / t.w + 1, 1, footprint, + plan.stocked[k][j] != 0) != site) + return false; + } + return true; +} + +std::vector towerFootprints(const Torus &t, const TowerPlan &plan) +{ + std::vector tiles(t.size(), 0); + for (const auto *lists : {&plan.towers, &plan.pads}) + for (const auto &sites : *lists) + for (int s : sites) + for (int dy = 0; dy < 2; ++dy) + for (int dx = 0; dx < 2; ++dx) + tiles[t.at(s % t.w + dx, s / t.w + dy)] = 1; + return tiles; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Towers.h b/src/map/generator/shared/Towers.h new file mode 100644 index 000000000..27373b65a --- /dev/null +++ b/src/map/generator/shared/Towers.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include +class Game; +namespace MapGeneration +{ +// Where colonies' towers go. A defence tower is 2x2 and scans square rings out to its range beyond +// every side of its footprint, with no line of sight, so a tower beside a wall covers the ground on +// the other side of it. These choose the sites that cover the most of other colonies' ground over +// the walls between them, the same number for every colony, and keep open pads for more. + +/// Every colony's chosen tower sites, and its open pads: 2x2 footprints left clear for players to +/// build towers on later. Tiles are the footprints' top-left corners. +struct TowerPlan +{ + std::vector> towers, pads; + // Per tower, whether it starts with its bullets: only where no other colony's tower is in range. + std::vector> stocked; +}; + +/// What a plan asks for. +struct TowerRequest +{ + int range = 7; // the range the sites are scored at + int towers = 0; // towers per colony + int pads = 0; // open pads per colony, beyond its towers + int spacing = 6; // the least Chebyshev distance between two of a colony's sites + // What a target tile in range is worth: one of another colony's (offence, over the walls) and one + // of the site's own colony's (defence, covering the ground attackers must cross). + int otherWeight = 1, ownWeight = 0; + // With a wall, every site (tower or pad) stands directly against it: some tile of `against` touches the + // footprint, diagonals included, so the tower shoots over the wall rather than from inland. + const std::vector *against = nullptr; +}; + +/// Chooses tower sites for every colony. `owner` gives each tile's colony (-1 for none); a site is a +/// 2x2 footprint wholly on one colony's `buildable` ground, scored by the `target` tiles within its +/// range: each of another colony's worth `otherWeight` (the ground it can shoot at over the walls) and +/// each of its own colony's worth `ownWeight` (the ground it defends, such as its own lanes). Sites are taken +/// best first, a colony at a time in turn, so every colony gets its first choice before any gets its +/// second, and every colony ends with the same number. No site may reach a tile of another colony's +/// `keepOutOfRange` (its swarm). Towers facing each other across a wall are the point, but a tower +/// within range of another colony's tower starts empty (`stocked`), so the game does not open with +/// towers shooting each other down. Pads need only keep their spacing. A colony whose ground offers +/// no site that reaches anyone takes its best remaining ones anyway. Returns fewer than asked only +/// where a colony has no room; the caller decides whether that fails the map. +TowerPlan chooseTowerSites(const Torus &, const std::vector &owner, + const std::vector &buildable, + const std::vector &target, + const std::vector &keepOutOfRange, int colonies, + const TowerRequest &); + +/// Keeps a plan from closing the ways a map promises: for every colony, while `goals` cannot be walked to +/// from `sources[k]` over `open` ground with the plan's footprints (towers and pads alike, since a pad is +/// where a tower will go) blocked, its last-chosen site is dropped. Returns how many sites were dropped. +/// Run evenTowerPlan afterwards so every colony still has the same count. +int dropBlockingSites(const Torus &, TowerPlan &, const std::vector &open, + const std::vector> &sources, + const std::vector> &goals); + +/// Evens a plan out where some colony got fewer sites than asked: every colony keeps only as many +/// towers, and as many pads, as the colony with fewest, its best ones, so no colony starts with more +/// battlements than another. Returns the towers per colony left. +int evenTowerPlan(TowerPlan &); + +/// Ground roomy enough for a tower that blocks nothing: the tiles of `open` (land a unit walks on) at +/// least `room` steps from any tile that is not. A 2x2 tower on a strip narrower than that could close +/// the strip; a generator ands this into the buildable ground it hands chooseTowerSites. +std::vector roomyGround(const Torus &, const std::vector &open, int room); + +/// Builds a plan's towers: every colony's tower sites get a completed tower of `level` (placeTower), +/// stocked where the plan says. Pads stay open. False, and nothing more built, at the first site that no longer +/// fits. +bool raiseTowers(Game &, const TowerPlan &, int level); + +/// The tiles of every footprint in a plan (towers and pads): the ground a generator keeps clear of +/// deposits and roads so the sites stay buildable. +std::vector towerFootprints(const Torus &, const TowerPlan &); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Walls.cpp b/src/map/generator/shared/Walls.cpp index bb89ba9ab..f411af829 100644 --- a/src/map/generator/shared/Walls.cpp +++ b/src/map/generator/shared/Walls.cpp @@ -163,10 +163,11 @@ int pieceLeak(const Map &map, const Torus &t, const std::vector &piece, const std::vector &shut) { const int n = t.w * t.h; - std::vector open = walkableTiles(map); + // Crops, fruit and buildings go in time, so only water and stone part two pieces for good. + std::vector open(n, 0); for (int i = 0; i < n; ++i) - if (shut[i]) - open[i] = 0; + open[i] = !shut[i] && !map.isWater(i % t.w, i / t.w) && + !(map.isResource(i % t.w, i / t.w) && map.getResource(i % t.w, i / t.w).type == STONE); int pieces = 0; for (int p : piece) pieces = std::max(pieces, p + 1); @@ -186,21 +187,25 @@ int pieceLeak(const Map &map, const Torus &t, const std::vector &piece, int towerReach(const Torus &t, const std::vector &buildable, const std::vector &target) { - // With every tile open, the eight-connected flood counts Chebyshev steps. + // With every tile open, the eight-connected flood counts Chebyshev steps. A tower scans rings round + // its whole 2x2 footprint (BuildingUtils::turretScanTile), so its reach is the nearest of the four. const std::vector distance = stepsFrom(t, target); int best = INT_MAX; for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) { - const int i = y * t.w + x; - if (distance[i] < 0 || distance[i] >= best) - continue; bool fits = true; + int nearest = INT_MAX; for (int dy = 0; dy < kTowerFootprint && fits; ++dy) for (int dx = 0; dx < kTowerFootprint && fits; ++dx) - fits = buildable[t.at(x + dx, y + dy)]; + { + const int i = t.at(x + dx, y + dy); + fits = buildable[i]; + if (distance[i] >= 0) + nearest = std::min(nearest, distance[i]); + } if (fits) - best = distance[i]; + best = std::min(best, nearest); } return best; } diff --git a/src/map/generator/shared/Walls.h b/src/map/generator/shared/Walls.h index 899a42956..8fd027f7d 100644 --- a/src/map/generator/shared/Walls.h +++ b/src/map/generator/shared/Walls.h @@ -48,6 +48,33 @@ std::vector labelBorders(const Torus &, const std::vector &l /// border, the third back on the first's. std::vector labelBorders(const Torus &, const std::vector &labels, int thickness); +/// labelBorders with doors: the border between a tile and its lower-labelled neighbour is walled +/// unless `open(tile, neighbour)` leaves it open, as where a home meets its own corridor. A tile is +/// wall when any such closed border touches it, so the wall still stands on the higher label's side. +template +std::vector labelBorders(const Torus &t, const std::vector &labels, Open open) +{ + std::vector wall(t.size(), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (labels[i] < 0) + continue; + for (int dy = -1; dy <= 1 && !wall[i]; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int j = t.at(x + dx, y + dy); + if (labels[j] >= 0 && labels[j] < labels[i] && !open(i, j)) + { + wall[i] = 1; + break; + } + } + } + return wall; +} + /// A designed band of stone, once the terrain is laid: the tiles of `wall` that are pure grass and /// so can take stone, and how many were not (the beach pass reached them, or the design put water or /// sand there) - any such tile is a gap in the wall, which a design should refuse rather than ship. @@ -66,20 +93,23 @@ std::vector reachesWithShut(const Map &, const Torus &, const std::vector &shut); /// The check that a map's parts stay apart with their doors shut: floods each part's walkable tiles -/// (a `piece` label of 0 or more) over walkable land with the `shut` tiles closed, and returns the +/// (a `piece` label of 0 or more) over land that is not stone, crops and buildings counted as the ground they +/// will leave, with the `shut` tiles closed, and returns the /// first tile of a different part any flood reaches, or -1 when every part keeps to itself. int pieceLeak(const Map &, const Torus &, const std::vector &piece, const std::vector &shut); /// The size of a defence tower's footprint, and the range of each of its three levels /// (BuildingTypesDefence.cpp). A tower scans square rings round its footprint's top-left tile with -/// no line of sight (Building::findBestTarget), so a wall stops walking but not shooting. +/// no line of sight (Building::findBestTarget, BuildingUtils::turretScanTile), reaching its range in +/// tiles beyond every side of its footprint, so a wall stops walking but not shooting. constexpr int kTowerFootprint = 2; constexpr int kTowerRange[3] = {5, 7, 9}; /// How close a tower built on `buildable` can come to shooting at `target`: the least Chebyshev -/// distance, across the wrap, from the top-left tile of any 2x2 footprint wholly on `buildable` to -/// any tile of `target`. INT_MAX when no footprint fits or there is no target. +/// distance, across the wrap, from any tile of a 2x2 footprint wholly on `buildable` to any tile of +/// `target`. A tower of range r hits a target this far away or nearer. INT_MAX when no footprint fits +/// or there is no target. int towerReach(const Torus &, const std::vector &buildable, const std::vector &target); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 12b99004e..5527865cf 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -301,7 +301,7 @@ class MapGeneratorDefaultsTest screen.descriptor.option("test-shape") == 1 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 21); + assert(registry.selectionIndex(101) == 24); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); diff --git a/test/MapGeneratorToolkitChecks.h b/test/MapGeneratorToolkitChecks.h index 84955b2f1..4f1138e92 100644 --- a/test/MapGeneratorToolkitChecks.h +++ b/test/MapGeneratorToolkitChecks.h @@ -23,6 +23,8 @@ #include "Territories.h" #include "Walls.h" #include "Homes.h" +#include "Farmland.h" +#include "Towers.h" #include "Wedge.h" #include #include @@ -928,7 +930,7 @@ inline void wallChecks() buildable[strip.at(x, y)] = 1; target[strip.at(9, y)] = 1; } - assert(towerReach(strip, buildable, target) == 7); + assert(towerReach(strip, buildable, target) == 6); std::fill(buildable.begin(), buildable.end(), 0); for (int y = 0; y < strip.h; ++y) buildable[strip.at(2, y)] = 1; @@ -1371,6 +1373,196 @@ inline void graphMazeChecks() assert(firstRegionLeak(t, reached, labels, [](int, int) { return false; }).tile < 0); } +// Farms and towers: bestFarmRows follows the fit; layFarm lays alternating rows clear of the rim and +// plantFarm fills crop rows with wheat nearest the water and keeps its wood to one row; growFarmFields shares open water equally and parts the +// fields; separateTerritories opens a gap, fillToNearest closes it and labelBorders walls it but for a door; +// growLakeBeside keeps to its side; chooseTowerSites covers +// the other colony, starts towers facing each other empty and gives every colony its count. +inline void farmAndTowerChecks() +{ + const FarmRows straight = bestFarmRows(0), diagonal = bestFarmRows(kPi / 4), turned = bestFarmRows(kPi / 2); + assert(straight.crops == 10 && straight.water == 8 && diagonal.crops == 12 && diagonal.water == 9); + assert(turned.crops == straight.crops && bestFarmRows(-kPi / 4).crops == diagonal.crops); + assert(std::abs(farmYield(0) - 0.1489) < 1e-9 && std::abs(farmYield(kPi / 4) - 0.1126) < 1e-9); + assert(std::abs(farmYield(kPi / 8) - 0.1225) < 1e-9 && farmYield(kPi / 2) == farmYield(0)); + assert(farmYield(0.1) < farmYield(0) && farmYield(0.1) > farmYield(kPi / 16)); + { + // Shared out by worth: a claimant worth half per tile ends with about twice the area. + const Torus open(40, 40); + const std::vector all(open.size(), 1); + const std::vector worth = {1.0, 0.5}; + const Territories byWorth = + growTerritories(open, all, {{open.at(5, 20)}, {open.at(25, 20)}}, [](int) { return 0; }, &worth); + assert(std::abs(byWorth.areas[1] - 2 * byWorth.areas[0]) <= 2); + } + + Game game(nullptr); + grassMap(game, 6, 6); + Map &map = game.map; + const Torus t(map); + std::vector region(t.size(), 0); + for (int y = 10; y < 54; ++y) + for (int x = 10; x < 54; ++x) + region[t.at(x, y)] = 1; + TerrainSketch sketch(t.size(), GRASS); + const Farm farm = layFarm(sketch, t, region, 0, {32, 32}, 4, {6, 4}); + assert(farm.rows >= 3 && !farm.water[t.at(32, 32)] && farm.row[t.at(32, 32)] % 2 == 0); + for (int i = 0; i < t.size(); ++i) + { + if (!region[i]) + assert(farm.row[i] == -1 && !farm.water[i]); + if (farm.water[i]) + { + assert(farm.row[i] % 2 == 1 && sketch[i] == WATER); + const int x = i % t.w, y = i / t.w; + assert(x >= 13 && x < 51 && y >= 13 && y < 51); + } + } + assert(farm.water[t.at(32, 36)] && !farm.water[t.at(32, 34)]); + { + // Bridges: every 8 tiles along the rows from the origin, a line of sand across each water row. + TerrainSketch bridged(t.size(), GRASS); + const Farm crossed = layFarm(bridged, t, region, 0, {32, 32}, 4, {6, 4}, nullptr, 8); + assert(!crossed.water[t.at(32, 36)] && crossed.sand[t.at(32, 36)] && bridged[t.at(32, 36)] == SAND); + assert(crossed.sand[t.at(24, 36)] && crossed.water[t.at(33, 36)] && crossed.water[t.at(28, 36)]); + assert(!crossed.sand[t.at(32, 32)] && crossed.rows == farm.rows); + // A building plot's grass trumps a bridge through it. + TerrainSketch plotted(t.size(), GRASS); + const FarmPlot clearing; + const Farm both = layFarm(plotted, t, region, 0, {32, 32}, 4, {6, 4}, &clearing, 4); + assert(both.plotX >= 0); + for (int dy = 0; dy <= clearing.height; ++dy) + for (int dx = 0; dx <= clearing.width; ++dx) + { + const int i = t.at(both.plotX + dx, both.plotY + dy); + assert(!both.sand[i] && !both.water[i] && plotted[i] == GRASS); + } + } + layBeaches(sketch, t); + writeUndermap(map, sketch); + const int planted = plantFarm(map, t, farm, 10, 10, [](int) { return true; }); + assert(planted == 20 && countResource(map, CORN) == 10 && countResource(map, WOOD) == 10); + int woodRow = -1; + for (int i = 0; i < t.size(); ++i) + if (map.getResource(i % t.w, i / t.w).type == WOOD) + { + assert(woodRow < 0 || farm.row[i] == woodRow); + woodRow = farm.row[i]; + } + + const Torus sea(64, 64); + std::vector land(sea.size(), 0), everywhere(sea.size(), 1); + for (int y = 28; y < 36; ++y) + for (int x = 0; x < 64; ++x) + land[sea.at(x, y)] = 1; + // The strip is two homes, colony 0's to the west and colony 1's to the east. + std::vector homeOf(sea.size(), -1); + for (int i = 0; i < sea.size(); ++i) + if (land[i]) + homeOf[i] = i % sea.w < 32 ? 0 : 1; + const std::vector fields = growFarmFields(sea, land, homeOf, everywhere, {{16, 26}, {48, 26}}, {0, 1}, + {{16, 32}, {48, 32}}, 4, 2); + int sizes[2] = {0, 0}; + bool touches[2] = {false, false}; + for (int i = 0; i < sea.size(); ++i) + if (fields[i] >= 0 && !land[i]) + { + ++sizes[fields[i]]; + for (const auto &step : kCardinalSteps) + touches[fields[i]] = touches[fields[i]] || homeOf[sea.at(i % 64 + step[0], i / 64 + step[1])] == fields[i]; + } + assert(sizes[0] > 400 && std::abs(sizes[0] - sizes[1]) < sizes[0] / 5 && touches[0] && touches[1]); + std::vector first(sea.size(), 0), eastHome(sea.size(), 0); + for (int i = 0; i < sea.size(); ++i) + { + first[i] = fields[i] == 0 && !land[i]; + eastHome[i] = homeOf[i] == 1; + } + const std::vector fromFirst = stepsFrom(sea, first), fromEast = stepsFrom(sea, eastHome); + for (int i = 0; i < sea.size(); ++i) + { + if (fields[i] == 1 && !land[i]) + assert(fromFirst[i] >= 4); + if (first[i]) + assert(fromEast[i] >= 4); + } + + std::vector halves(sea.size(), 0); + for (int i = 0; i < sea.size(); ++i) + halves[i] = i % sea.w < 32 ? 0 : 1; + separateTerritories(sea, halves, 4); + assert(halves[sea.at(31, 5)] == -1 && halves[sea.at(32, 5)] == -1 && halves[sea.at(29, 5)] == 0); + + // fillToNearest closes the gap again, each tile to its nearer side, and labelBorders with a door + // walls the border everywhere but where the door leaves it open. + { + std::vector gap(sea.size(), 0); + for (int i = 0; i < sea.size(); ++i) + gap[i] = halves[i] < 0; + std::vector near = halves; + assert(fillToNearest(sea, near, gap, 1)[sea.at(30, 5)] && near[sea.at(30, 5)] == 0 && near[sea.at(31, 5)] == -1); + const std::vector filled = fillToNearest(sea, halves, gap, 4); + assert(filled[sea.at(31, 5)] && halves[sea.at(31, 5)] == 0 && halves[sea.at(32, 5)] == 1); + for (int i = 0; i < sea.size(); ++i) + assert(halves[i] >= 0); + const auto door = [&](int i, int) { return i / sea.w >= 10 && i / sea.w < 14; }; + const std::vector walls = labelBorders(sea, halves, door); + assert(walls[sea.at(32, 5)] && !walls[sea.at(31, 5)] && !walls[sea.at(32, 11)]); + std::vector reached(sea.size(), 0); + for (int i = 0; i < sea.size(); ++i) + reached[i] = !walls[i]; + assert(firstRegionLeak(sea, reached, halves, [](int, int) { return false; }).tile >= 0); + // Through the door the sides meet; with the door's rows shut, nothing leaks, across the wrap too. + for (int i = 0; i < sea.size(); ++i) + if (i / sea.w >= 9 && i / sea.w < 15) + reached[i] = 0; + assert(firstRegionLeak(sea, reached, halves, [](int, int) { return false; }).tile < 0); + } + + std::vector lake(sea.size(), 0); + std::vector queued(sea.size(), 0), depth(sea.size(), 0), room(sea.size(), 20); + const int site = sea.at(32, 32); + assert(growLakeBeside(sea, lake, depth, room, 2, 40, site, 0, 1, 6, 20, [](int) { return 0.0; }, queued, 1) == 40); + assert(growLakeBeside(sea, lake, depth, room, 2, 40, site, 0, -1, 6, 20, [](int) { return 0.0; }, queued, 2) == 40); + for (int i = 0; i < sea.size(); ++i) + if (lake[i]) + assert(std::abs(sea.offsetY(32, i / sea.w)) >= 6); + + std::vector owner(sea.size(), -1); + std::vector buildable(sea.size(), 0), target(sea.size(), 0), keep(sea.size(), 0); + for (int y = 0; y < 64; ++y) + for (int x = 0; x < 64; ++x) + { + const int i = sea.at(x, y); + if (x >= 4 && x < 28) + owner[i] = 0; + if (x >= 32 && x < 60) + owner[i] = 1; + buildable[i] = owner[i] >= 0; + target[i] = owner[i] >= 0; + } + TowerRequest request; + request.range = 7; + request.towers = 3; + request.pads = 2; + request.spacing = 4; + const TowerPlan plan = chooseTowerSites(sea, owner, buildable, target, keep, 2, request); + assert(plan.towers[0].size() == 3 && plan.towers[1].size() == 3 && plan.pads[0].size() == 2); + assert(plan.stocked[0].size() == 3 && plan.stocked[1].size() == 3); + for (size_t a = 0; a < 3; ++a) + { + // A tower with the other colony's tower in range starts empty; one without starts stocked. + bool facing = false; + const int s = plan.towers[0][a]; + for (int o : plan.towers[1]) + facing = facing || std::max(std::abs(sea.offsetX(s % 64, o % 64)), std::abs(sea.offsetY(s / 64, o / 64))) - 1 <= request.range; + assert(bool(plan.stocked[0][a]) == !facing); + assert(s % 64 >= 19); // beside the four-tile gap, the only border within range + } + const std::vector footprints = towerFootprints(sea, plan); + assert(std::count(footprints.begin(), footprints.end(), 1) == 4 * 10); +} + inline void toolkitChecks() { floodChecks(); @@ -1391,6 +1583,7 @@ inline void toolkitChecks() wallChecks(); territoryChecks(); arenaChecks(); + farmAndTowerChecks(); subtileRasterChecks(); tessellationChecks(); graphMazeChecks(); diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index bcf5e6d34..ad9637181 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -168,3 +168,27 @@ macos-arm64 21 1 8 8 4 3 ok 14159020535153467794 macos-arm64 21 1 8 8 8 1 ok 3788158320130144245 macos-arm64 21 1 9 8 4 1 ok 14597284168628427777 macos-arm64 21 1 9 9 4 1 ok 7348842460505219783 +macos-arm64 22 1 7 7 4 1 ok 8819869978616031071 +macos-arm64 22 1 8 8 2 1 ok 730574824765298860 +macos-arm64 22 1 8 8 4 1 ok 9691963763756535666 +macos-arm64 22 1 8 8 4 2 ok 12280328850293820652 +macos-arm64 22 1 8 8 4 3 ok 16156515469674000849 +macos-arm64 22 1 8 8 8 1 ok 12707724408115629147 +macos-arm64 22 1 9 8 4 1 ok 16181991639738054527 +macos-arm64 22 1 9 9 4 1 ok 18170981972027515569 +macos-arm64 23 1 7 7 4 1 failed 0 +macos-arm64 23 1 8 8 2 1 ok 11310442957433887598 +macos-arm64 23 1 8 8 4 1 ok 7375768657791499996 +macos-arm64 23 1 8 8 4 2 ok 1481389391399968114 +macos-arm64 23 1 8 8 4 3 ok 13401851896821371429 +macos-arm64 23 1 8 8 8 1 ok 5249400730010945541 +macos-arm64 23 1 9 8 4 1 ok 10936074571144773422 +macos-arm64 23 1 9 9 4 1 ok 16289602417876527911 +macos-arm64 24 1 7 7 4 1 ok 6532907721709105135 +macos-arm64 24 1 8 8 2 1 ok 9467112104662893497 +macos-arm64 24 1 8 8 4 1 ok 10198941300671204125 +macos-arm64 24 1 8 8 4 2 ok 384900143150519072 +macos-arm64 24 1 8 8 4 3 ok 285345205556791712 +macos-arm64 24 1 8 8 8 1 ok 12174400163941988640 +macos-arm64 24 1 9 8 4 1 ok 10026254367617799143 +macos-arm64 24 1 9 9 4 1 ok 7691831584358967898 diff --git a/tools/farm_row_fit.py b/tools/farm_row_fit.py new file mode 100644 index 000000000..a7d107f57 --- /dev/null +++ b/tools/farm_row_fit.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-or-later +"""The fit behind MapGeneration::bestFarmRows (src/map/generator/shared/Farmland.h). + +A farm is rows of grass and water at some angle. Wheat and wood regrow on a tile when a probe +offset by the difference of two draws of 0..15 on each axis lands on pure water and the opposite +probe does not land on pure sand (Map::growResources). This script lays rows of every width pair +over a torus, applies the beach rule (every land vertex beside water turns to sand) and the +corner rule (a tile is pure grass, sand or water only when all four corners are), and sums each +pure-grass tile's exact chance of passing that test. The best pair per angle, and how flat the +optimum is, are printed; bestFarmRows is a rough line through them. + +Needs numpy. Takes about half a minute. +""" +import math + +import numpy as np + +N = 180 +WEIGHT = np.array([16 - abs(d) for d in range(-15, 16)], float) / 256.0 + + +def row_yield(grass, water, theta): + ys, xs = np.mgrid[0:N, 0:N] + phase = np.mod(xs * math.cos(theta) + ys * math.sin(theta), grass + water) + wet = phase < water + near = np.zeros_like(wet) + for dy in (-1, 0, 1): + for dx in (-1, 0, 1): + near |= np.roll(np.roll(wet, dy, 0), dx, 1) + vertex = np.where(wet, 2, np.where(near, 1, 0)) + corners = [vertex, np.roll(vertex, -1, 1), np.roll(vertex, -1, 0), + np.roll(np.roll(vertex, -1, 0), -1, 1)] + grass_tiles = np.all([c == 0 for c in corners], axis=0) + sand_tiles = np.all([c == 1 for c in corners], axis=0).astype(float) + water_tiles = np.all([c == 2 for c in corners], axis=0).astype(float) + chance = np.zeros((N, N)) + for i, dy in enumerate(range(-15, 16)): + w_rows = np.roll(water_tiles, -dy, 0) + s_rows = np.roll(sand_tiles, dy, 0) + for j, dx in enumerate(range(-15, 16)): + chance += WEIGHT[i] * WEIGHT[j] * np.roll(w_rows, -dx, 1) * (1 - np.roll(s_rows, dx, 1)) + return (chance * grass_tiles).sum() / (N * N) + + +def main(): + for degrees in (0, 11.25, 22.5, 33.75, 45): + theta = math.radians(degrees) + scores = {(g, w): row_yield(g, w, theta) for g in range(4, 25, 2) for w in range(2, 15)} + best = max(scores, key=scores.get) + for g in (best[0] - 1, best[0] + 1): + scores[(g, best[1])] = row_yield(g, best[1], theta) + best = max(scores, key=scores.get) + top = sorted(scores.items(), key=lambda kv: -kv[1])[:5] + print(f"{degrees:6}: grass {best[0]}, water {best[1]}, yield {scores[best]:.4f}; " + f"next best {[(k, round(v, 4)) for k, v in top[1:]]}") + + +if __name__ == '__main__': + main() From de35b7b6f6faeb7c68592ccd352d470a07a6d79f Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 14:41:06 -0400 Subject: [PATCH 113/136] Tidy the walled-arena generators and document their design A cleanup pass over Carousel, Switchbacks and Amphitheatre and their shared pieces. Every map comes out byte-identical: all 192 golden rows match and the lobby sweep has 0 failing combinations. - Carousel: split the 380-line design() into named steps (drawLanes, stampPieces, crowding, claimFarmFields, fillAndWall, layFarmRows, finishRoads). Remove leftover pond geometry, unused vectors and water-channel wording. Rewrite the header to explain the siege concept, why the lanes are tight, and how the fill-and-wall step works. Group and explain every tuning constant, and correct the validator summary, which described the old siege direction. - Homes: stampPondHome, pondHomeAnchor and plantPondHomeKit become stampRoundHome, homeSwarmSite and plantHomeKit, with ponds optional. The pond-size constants both maps duplicated now live in Homes.h, with a homeHasRoom size floor. - Towers: add startingTowerRequest and settleStartingTowers, replacing the plan, route check, even-out and raise sequence each map repeated. - Farmland: remove a leftover debug print from farmReachable, and rewrite the layFarm and growFarmFields docs as step lists covering caps, bridges and plots. - Switchbacks and Amphitheatre: correct stale comments (ponds, quarry kits, bays, tower counts) and use the shared helpers. - Framework doc: rewrite the Carousel and Switchbacks sections and the Homes and Towers toolkit rows to match the code. - clang-format the files these generators own. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 158 +-- .../generators/AmphitheatreGenerator.cpp | 206 +-- .../generators/CarouselGenerator.cpp | 1148 ++++++++--------- .../generators/SwitchbacksGenerator.cpp | 325 ++--- src/map/generator/shared/Farmland.cpp | 39 +- src/map/generator/shared/Farmland.h | 81 +- src/map/generator/shared/Homes.h | 120 +- src/map/generator/shared/Towers.cpp | 65 +- src/map/generator/shared/Towers.h | 18 +- 9 files changed, 1154 insertions(+), 1006 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index b16d1ca3c..2f3f4b610 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -36,8 +36,8 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `GraphMaze` | A maze carved through a tessellation's cell graph: `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | | `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic, or shared by value when each claimant's ground has a worth per tile; `smoothLabels`, a majority filter over any radius that trims spurs or, at a radius of 3 or 4, straightens borders into curves; `separateTerritories`, a gap opened between neighbouring territories; `fillToNearest`, the reverse: unclaimed ground near labelled regions given to the nearest, so water between pieces becomes land; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `growLakeBeside`, one on either flank of a site, kept wholly to its side of the line from the way in; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | | `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges across the water rows every so many tiles along them, so workers need not walk round a long row (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives, and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | -| `Towers` | Tower sites chosen for the ground they cover over the walls, other colonies' (offence) or their own (defence), each weighted: `chooseTowerSites` (best first, a colony at a time in turn, no stocked tower in range of another colony's tower or swarm, plus open 2x2 pads for players to build more), `raiseTowers`, `towerFootprints`, `evenTowerPlan` (every colony trimmed to the fewest sites any got) and `roomyGround` (no tower on a strip it could close); a tower in range of another colony's tower starts empty; a request can require every site to stand directly against a wall, as all three arena maps do | -| `Homes` | Homes built alike: `stampPondHome`, `pondHomeAnchor` and `plantPondHomeKit` (a round home island facing out from the middle, its swarm towards its door, any ponds (none, where farms are the water) kept clear of the coast, and a wheat and wood kit round the first pond, with no stone since the home is walled in stone; Carousel, Switchbacks), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | +| `Towers` | Tower sites chosen for the ground they cover over the walls, other colonies' (offence) or their own (defence), each weighted: `startingTowerRequest` (a request from a map's level and count controls), `chooseTowerSites` (best first, a colony at a time in turn, optionally every site directly against a wall, no stocked tower in range of another colony's tower or swarm, plus open 2x2 pads for players to build more), `settleStartingTowers` (the sequence every arena map runs: `dropBlockingSites` so no site closes a colony's walk to a goal, `evenTowerPlan` so every colony has as many sites as the fewest got, then `raiseTowers`), `towerFootprints` and `roomyGround` (no tower on a strip it could close); a tower in range of another colony's tower starts empty | +| `Homes` | Homes built alike: `stampRoundHome`, `homeSwarmSite` and `plantHomeKit` (a round home facing out from the middle, its swarm towards its door, optional ponds kept clear of its edge, and a wheat and wood kit near the swarm with no stone, since these homes are walled in stone; `homeHasRoom` is the size floor; Carousel, Switchbacks), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | | `Pipeline` | The stages round the others: `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | | `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | @@ -769,60 +769,60 @@ further it is from home. ## Carousel -A ring of walled homes in the sea, each with one door that leads clockwise. A colony's corridor -sweeps round the ring to a court pressed against the next colony's home, and a spoke runs from every -court in to the plaza. Court and home are parted by a thin wall of stone that no unit can cross but a -tower shoots over, so every colony besieges one neighbour from its court while the other besieges it. -Every home also reaches two walled wheat farms, one in towards the plaza and one out beyond the ring. -The pieces are parted by single lines of stone with no water beside them, so a siege tower stands -right against the wall it shoots across. - -- **Geometry.** Designed once in the wedge's frame and turned for every colony; round on a - rectangular map. The ring's outer coast is `kRingShare` of the half side (a little more on maps - under 512), leaving a band of sea round it for the outer farms. A home is `kHomeShare` of the half - side but never wider than a fifth of the ring's arc per colony. The court sits on the homes' ring - where its edge is `court-wall` tiles from the next home's rough outline, found by solving for the - chord; the corridor is an `arcPath` from the home's axis to the court, and the spoke, `spoke-width` - tiles wide (default 3, as the corridors' `corridor-width` is, so every lane is a tight laneway and the court at the elbow a small circle, `court-size` 30, about 3 tiles in radius), leans back from the court to the middle - of the wedge so it clears the next home. -- **Farms.** `growFarmFields` shares the sea inside the ring (each cell between a home, its corridor - and two spokes) and the sea outside it between the colonies' homes by equal yield for each colony's - row angle, so a colony whose rows fall on the diagonal gets more ground. Each farm grows straight - out of its home with no coast between, keeps three tiles from all other land (filled in later and walled), and is joined - by a broad neck. With `farm-plots` (on), every farm has a 10x4 grass plot ringed with sand in its - middle for a swarm or an inn. Once the gaps round it have filled in and - its walls stand, `layFarm` lays rows along the colony's axis at `bestFarmRows` widths over the whole - field, right up to its walls with three tiles of land kept round the water (six against open sea), with a sand bridge across the water rows every 16 tiles, and `plantFarm` plants wheat along the - water with one small woodlot. A set of farms (inner or outer) is laid only where every colony's - field has room. -- **Walls.** No water lies between the pieces. Once the farms are grown, the sea within - `kFillReach` (24) tiles of a home or farm fills in from the nearest one (`fillToNearest`), so the - lanes, courts and plaza keep exactly the width they were drawn; any sea left keeps a - `kSeaMargin` (3) strip of the nearest piece before its sealed coast. A one-tile line of stone then stands wherever two colonies' sides meet - (`labelBorders` with doors): a home with its farms is one side, a court with its lanes another, - and the plaza a third. The walls stand on the home's or farm's side, so no wall narrows a lane. Two borders stay open: a home's door onto its own corridor, and every spoke's way into the - plaza. The wall between a court and the next home is the land no more than `court-wall` tiles - (default 2) from both (`designedStone`). Whatever sea is left beyond the outermost pieces has its - coast sealed (`sealCoasts`), so nothing landing from the sea gets in; the farms' water rows are - inland water, not sea. Sand roads run down the corridors, through the courts, down the spokes and across the plaza to its pond, so the way in never overgrows; they stay - `kRoadSeaGap` from water (`keepRoadInland`) and never touch a wall tile. -- **Homes.** `stampPondHome` with no ponds, since its farms are its water; the swarm stands towards the corridor; wheat and - wood kits beside the swarm, with no stone clump since the walls are stone already; ambient fields by `furnishGround` with `WedgeField` noise. Nothing is planted - within six steps of a corridor or spoke, or on a tower pad. -- **Prizes.** One grove of one fruit per court; on the plaza only fruit, so nothing overgrows it: an - orchard of the three fruits between every two spokes' arrivals (`plantOrchard`). -- **Starting towers.** `starting-towers` (0 none, otherwise the level plus one; default 2, level 1) - and `tower-count` (0 to 12, default 3): the siege line. The towers and three open pads per colony stand in its own home, packed within six - tiles of the thin wall it shares with the previous colony's court, chosen for how much of that - colony's elbow - its court and its lanes within twelve tiles - they cover across the wall - (`chooseTowerSites` counting other colonies' elbows only). Every colony shells one neighbour's way - out while the other neighbour shells its own. Every site stands directly against stone (`TowerRequest::against`). At level 0, every site is an open pad. +A ring of walled homes round a plaza, where every colony besieges one neighbour and is besieged by +the other. A colony's single door opens onto a narrow corridor that follows the ring to a small court +(the elbow) pressed against the next colony's home, and a narrow spoke runs from the court to the +plaza. The court and that home are parted by a thin wall of stone that no unit crosses but a tower +shoots over, so every colony's towers, in its own home against that wall, cover its neighbour's only +way out. The concept depends on the lanes being tight, so they default narrow and every wall is a +single line of stone with no water beside it. + +- **Geometry** (`geometryFor`, `drawLanes`). Designed once in the wedge's frame and turned for every + colony; round on a rectangular map. The homes' outer edge is `kRingShare` of the half side (a little + more on smaller maps, and more on a crowded ring), and a home is `kHomeShare` of the half side but + never wider than a fifth of the ring's arc per colony. The court sits on the homes' ring where its + edge is `court-wall` tiles from the next home's rough outline, found by solving for the chord. The + corridor (`corridor-width`, default 3) is an `arcPath` from the home's axis to the court; the spoke + (`spoke-width`, default 3) leans back from the court to the middle of the wedge so it clears the next + home. The court is a circle of `court-size` (default 30, about 3 tiles in radius). +- **Pieces** (`stampPieces`, `crowding`). Homes (`stampRoundHome`, no ponds) and courts over the lanes; + the court wall is just the land within `court-wall` tiles (default 2) of both the court and the next + home; the plaza with its pond. A ring too crowded for a wall between a lane and another home, or for + two colonies' lanes to stay apart, is refused. +- **Farms** (`claimFarmFields`, `layFarmRows`). `growFarmFields` grows every colony one field in towards + the plaza and one out beyond the ring, each set shared out by equal yield for the colony's row angle, + so a colony whose rows fall on the diagonal gets more ground. A set is kept only where every colony's + field has room. Once the walls stand (below), `layFarm` lays rows along the colony's axis at + `bestFarmRows` widths over the whole walled field, keeping three tiles of land round the water (six + against open sea), with a sand cap closing the crop rows, a sand bridge across the water every 16 + tiles and, with `farm-plots` (on), a 10x4 building plot. `plantFarm` plants wheat along the water + with one small woodlot. The farms are the homes' only water. +- **Filling in and walls** (`fillAndWall`). The sea within `kFillReach` (24) tiles of a home or farm + becomes that colony's ground (`fillToNearest`); lanes, courts and the plaza never grow, so they keep + exactly their drawn width. Sea left beyond keeps a `kSeaMargin` (3) strip of the nearest piece before + its coast, which is sealed (`sealCoasts`). Every tile then has a side (the plaza; a colony's court and + lanes; a colony's home and farms), and a single line of stone stands wherever two sides meet + (`labelBorders` with doors), always on the home's or farm's side, so no wall narrows a lane. Two + borders stay open: each home's door onto its own corridor, and each spoke's way into the plaza. +- **Roads** (`finishRoads`). With `sand-roads` (on), a sand road runs down the corridor, through the + court, down the spoke and across the plaza to its pond, so the way in can never be fully overgrown. + Roads keep `kRoadSeaGap` from water and never touch a wall tile. +- **Homes and prizes** (`furnish`). A wheat and wood kit beside every swarm (`plantHomeKit`, no stone: + the walls are stone) and scattered farmland (`furnishGround`); nothing is planted within six steps of + a lane. One grove of one fruit per court, and on the plaza only fruit: an orchard of the three fruits + between every two spokes' arrivals, with no wheat or wood to smother it. +- **Starting towers** (`planTowers`). `starting-towers` sets their level (0 none, when every site is + an open pad; default 2, level 1) and `tower-count` how many (default 3), with three open pads. Every + site stands in the colony's own home, directly against stone and within six tiles of a wall, chosen + for how much of the previous colony's elbow (its court and lanes within twelve tiles) it covers + (`chooseTowerSites` counting other colonies' elbows only). `settleStartingTowers` drops any site + that would close a colony's walk to the plaza and evens the counts. - **Checked, not assumed.** `validateWorld` rebuilds the design and requires every designed stone, - every colony reachable, 95% of every farm's crop land and all of its plot a walk from its colony - (`farmReachable`), no land reachable from the sea (`seaEntry`), every home with its farms, - every court and the plaza apart with the corridors and spokes shut (`pieceLeak`), a level-1 tower - in every court within range of the next home (`towerReach`), and even walks to the plaza - (`walkSpread`). + every colony reachable, no land reachable from the sea (`seaEntry`), 95% of every farm's crop land + and all of its plot a walk from its colony (`farmReachable`), every designed border standing as stone + except at the doors, every home, farm, court and the plaza apart with the lanes shut (`pieceLeak`), a + level-1 tower in every home within range of the previous colony's court (`towerReach`), and even walks + to the plaza (`walkSpread`). ## Amphitheatre @@ -841,9 +841,8 @@ is a meeting at a known place. cannot. Borders become stone (`labelBorders`, `border-wall` thick), and any unclaimed tile becomes stone. The areas come out within a few percent of each other. - **Homes.** Every swarm stands the same number of steps from its ramp (`siteAtDepth`), four tenths - of the way to the shallowest territory's far end; the wheat and wood kit (no stone clump: the territory is walled in stone) faces the nearest sea; - ambient fields by - `furnishGround`. + of the way to the shallowest territory's far end; the wheat and wood kit (no stone clump: the + territory is walled in stone) faces the nearest sea; scattered fields by `furnishGround`. - **Inland seas.** `bay-size` percent of the smallest territory, split into two seas, one either side of the home (`growLakeBeside`), each wholly on its side of the line from the ramp through the home and ten tiles clear of it, seven from any wall. Where any territory lacks the room, every territory @@ -853,7 +852,9 @@ is a meeting at a known place. - **Prizes.** Orchards of the three fruits in the pit and on the innermost terrace, and an outcrop on every terrace, the same at every colony's angle. - **Starting towers.** `tower-count` towers (default 3, at `starting-towers` level) and three open - pads per colony in its territory, each directly against stone, away from its ramp, covering the most of its neighbours' territories over the border walls (`chooseTowerSites`). + pads per colony in its territory, each directly against stone and away from its ramp, covering the + most of its neighbours' territories over the border walls (`chooseTowerSites`, + `settleStartingTowers`). - **Checked, not assumed.** Every designed stone present, every ramp walkable, territories within `kAreaTolerance` percent, every colony's seas the same size, no territory reaching another or the arena with the outer ramps shut, and even walks to the ramps and to the pit. @@ -862,8 +863,9 @@ is a meeting at a known place. A plateau in the middle of the sea and a ring of homes on the rim, each joined to the plateau by a mountain of stone with one zigzag trail through it. Walking the trail takes several times the -straight-line distance, but the walls between legs are a few tiles thick, so towers at either end -cover the nearest legs. Every home reaches a walled wheat farm on either flank. +straight-line distance, but the walls between legs are thin, so the trail is a fortification: every +colony's towers stand on its own trail against the inner side of each wall and shoot across it at the +next leg up. Every home reaches a walled wheat farm on either flank. - **Geometry.** Designed in every colony's `AxisFrame` and turned round the centre; round on a rectangular map. Homes stand on the rim; the mountain fills the ground between the plateau and the @@ -874,25 +876,27 @@ cover the nearest legs. Every home reaches a walled wheat farm on either flank. - **Trail.** `zigzagPath` gives the trail and each leg's straight run; everything else in the mountain is stone. A sand road runs down the trail's middle. - **Farms.** On maps at least 200 tiles across their shorter side (a 128-tile side has no room - between the mountains), two fields per colony, growing out of either flank of the home, share all the open sea - between the colonies by equal yield for their row angles (`growFarmFields`), joined to the home with - no coast between, with a 10x4 building plot each under `farm-plots` (on); rows run across the axis at - `bestFarmRows` widths (`layFarm`) with a sand bridge across the water every 16 tiles, wheat with one woodlot (`plantFarm`). Neighbouring colonies' - farms meet across three tiles of water and two sealed coasts, within a range-7 tower's reach. + between the mountains), two fields per colony, growing out of either flank of the home, share all the + open sea between the colonies by equal yield for their row angles (`growFarmFields`), joined to the + home with no coast between. Rows run across the axis at `bestFarmRows` widths (`layFarm`), with a sand + cap, a sand bridge across the water every 16 tiles and a 10x4 building plot under `farm-plots` (on); + wheat with one woodlot (`plantFarm`). Neighbouring colonies' farms meet across three tiles of water and + two sealed coasts, within a range-7 tower's reach. - **Walls.** Every coast sealed, farms included; mountains keep water between each other beyond their innermost legs. -- **Homes and plateau.** Pond homes as in Carousel, with two ponds only on maps too small for farms; the plateau has a pond and only fruit, an - orchard of the three fruits between every two summits, so nothing overgrows it. -- **Starting towers.** `tower-count` towers (default 3) and four open pads per colony, all on its trail beside the - walkway down its middle, each against the inner side of a wall - the side towards the middle of the - map - so it shoots across the stone at the next leg up, where attackers coming down from the plateau - pass (`chooseTowerSites` counting the colony's own trail). A trail too narrow for a tower beside its - walkway gets fewer, the same for every colony (`evenTowerPlan`), and no site may close a trail - (`dropBlockingSites`). +- **Homes and plateau.** Round homes (`stampRoundHome`) whose farms are their water, with two ponds + only on maps too small for farms; a wheat and wood kit with no stone. The plateau has a pond and only + fruit, an orchard of the three fruits between every two summits, so nothing overgrows it. +- **Starting towers.** `tower-count` towers (default 3) and four open pads per colony, all on its trail + beside the walkway down its middle, each directly against stone on the inner side of a wall - the side + towards the middle of the map - so it shoots across the stone at the next leg up, where attackers + coming down from the plateau pass (`chooseTowerSites` counting the colony's own trail). A trail too + narrow for a tower beside its walkway gets fewer, the same for every colony, and no site may close a + trail (`settleStartingTowers`). - **Checked, not assumed.** Every designed stone present, every farm walkable from its colony - (`farmReachable`), no land reachable from the sea, homes and their farms and the plateau apart with the trails shut and with only the middle legs shut (so no - leg can be skipped), a level-1 tower at home reaching the first leg and one on the plateau reaching - the last, and even walks to the plateau. + (`farmReachable`), no land reachable from the sea, homes and their farms and the plateau apart with the + trails shut and with only the middle legs shut (so no leg can be skipped), a level-1 tower at home + reaching the first leg and one on the plateau reaching the last, and even walks to the plateau. ## Compatibility notes diff --git a/src/map/generator/generators/AmphitheatreGenerator.cpp b/src/map/generator/generators/AmphitheatreGenerator.cpp index 2f46b5688..767082039 100644 --- a/src/map/generator/generators/AmphitheatreGenerator.cpp +++ b/src/map/generator/generators/AmphitheatreGenerator.cpp @@ -38,31 +38,34 @@ using namespace MapGeneration; // // A territory is walled off from its neighbours along its whole border and has no other way out // than its ramp, so a colony that wants anything beyond its own ground has to come down into the -// amphitheatre. There is no sea: each territory's water is a private bay at its far end, the ground -// deepest from its ramp, enclosed by its own land, so its algae and its regrowing fields are its -// own and no swimmer can use it to cross into another territory. +// amphitheatre. There is no sea: each territory's water is two inland seas of its own, one on either +// side of its home (or, where a territory has no room beside its home, one at its far end), enclosed by +// its own land, so its algae and its regrowing fields are its own and no swimmer can use them to cross +// into another territory. Every colony's towers stand against its border walls, covering the +// neighbouring territories over them. // // The arena is designed round the centre and turned for every colony, like a wedge design. The // territories cannot be: a square map's corners and the ground across its wrap belong to no wedge, // and with an odd number of colonies the corners fall unevenly. So the territories are grown instead // (growTerritories): every colony's frontage on the outer wall is its own arc of equal length, and // from there the smallest territory always takes the next tile, until the whole of the ground outside -// the walls is shared out to the tile. Every bay is then grown to the same number of tiles, and every -// swarm stands the same number of steps from its ramp, so what the growth cannot make identical is +// the walls is shared out to the tile. Every colony's seas are then grown to the same number of tiles, +// and every swarm stands the same number of steps from its ramp, so what the growth cannot make identical is // measured and held equal instead. The design is a pure function of the request, so validateWorld // rebuilds it and checks the finished world. // // GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be // cleared, so the walls and the ramps are permanent; a unit may step diagonally, which is why every // border is walled where any neighbour has another label (labelBorders); water blocks walking until a -// colony swims, which is why a bay must lie wholly inside its territory; wheat and wood regrow only -// near water, which is why every territory's fields grow round its bay and the terraces are dry; and +// colony swims, which is why a sea must lie wholly inside its territory; a defence tower shoots over +// stone with no line of sight, which is why towers stand against the border walls; wheat and wood regrow +// only near water, which is why every territory's fields grow round its seas and the terraces are dry; and // fruit lets an inn pull hungry enemy units across, which makes the pit's orchard the prize at the // bottom of every ramp. // // THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a pit 15 tiles in radius, three walls 13 tiles // apart with ramps 7 wide, so the outer wall stands 41 tiles out; four territories of about 11,000 -// tiles each, each with a bay of 1,500 tiles. +// tiles each, each with 1,500 tiles of inland sea; three towers per colony. namespace { @@ -78,8 +81,8 @@ constexpr int kSmoothingPasses = 4; constexpr int kSmoothingRadius = 4; // The territory control's noise: a border wanders by up to this many steps at 100. constexpr double kRoughSteps = 24; -// A bay keeps this many steps from any wall, so its beach never reaches stone and its neighbours' -// bays are always parted by land. +// An inland sea keeps this many steps from any wall, so its beach never reaches stone and its +// neighbours' seas are always parted by land. constexpr int kBayWallGap = 7; // A swarm needs this much room round its site, and its site may miss the common depth by this much. constexpr int kSwarmRoom = 5; @@ -92,7 +95,8 @@ constexpr int kLakeReach = 60; // A swarm stands at least this many steps from its ramp's mouth, and this many short of the nearest // bay, measured from the ramp. constexpr int kHomeRampDepth = 14; -// Every home starts identical: fixed wheat and wood facing its bay, a quarry behind, unscaled. +// Every home's starter kit: this much wheat and wood facing its nearest sea, unscaled; no stone, since +// the territory is walled in it. constexpr int kHomeWheat = 30; constexpr int kHomeWood = 30; // Ambient farmland on a territory, as percentages of its tiles at 100. @@ -103,7 +107,7 @@ constexpr int kRampClearance = 5; // Open 2x2 pads beside every colony's `tower-count` towers, and the spacing between sites. constexpr int kTowerPads = 3; constexpr int kTowerSpacing = 5; -// A tower keeps this many steps from water and stone on every side, so it never closes a passage. +// A tower keeps this many steps from water on every side, so it never closes a strip of shore. constexpr int kTowerRoom = 4; // The most the territories' areas may differ, in percent of the smallest: growth shares the ground // out to the tile, and trimming the spurs off the borders moves a little of it. @@ -137,7 +141,8 @@ Geometry geometryFor(const GenerationRequest &r) g.outer = g.ringR.back(); const double inside = kPi * (g.outer + kRingHalf + 1) * (g.outer + kRingHalf + 1); if (g.outer + kRingHalf + 8 > g.half) - g.failure = "The amphitheatre does not fit on this map: use fewer rings or narrower terraces."; + g.failure = + "The amphitheatre does not fit on this map: use fewer rings or narrower terraces."; else if ((double(g.width) * g.height - inside) / g.teams < kMinimumTerritory) g.failure = "Too many colonies for this map: their territories would be too small."; else if (g.ringR[0] * g.wedge < 2 * g.rampHalf + 6) @@ -154,6 +159,9 @@ struct Layout std::vector zone, territory, depth; // rampOf: the colony whose outer ramp a tile is (or -1); innerRamp marks every other ramp tile. std::vector rampOf; + // ring: the ring walls' stone; border: the stone between territories; unclaimed: ground no territory + // took, also stone; bay: every territory's inland seas (the water the `bay-size` control sizes); + // pocket: land a sea closed off from its ramp, also stone. std::vector ring, innerRamp, border, unclaimed, bay, pocket; std::vector> mouths; // each colony's tiles just outside its outer ramp std::vector anchor, rampMiddle; // each colony's swarm centre and middle of its outer ramp @@ -244,7 +252,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) frontage[i] = 1; } } - const PeriodicNoise wander(t.w, t.h, std::max(16, g.half / 4), context.stream("amphitheatre-borders")); + const PeriodicNoise wander(t.w, t.h, std::max(16, g.half / 4), + context.stream("amphitheatre-borders")); const double swing = kRoughSteps * 1000 * o.roughness / 100.0; L.territory = growTerritories(t, outside, seeds, [&](int i) { return long(wander.at(i % t.w, i / t.w) * swing); }) @@ -311,7 +320,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // each half the bay size; otherwise one at the far end of every territory (growFarLake). Every // colony's seas are the same size, clear of every wall and of the home, and any land a sea closes off // becomes stone (strandedGround). - const int total = int(std::lround(*std::min_element(L.areas.begin(), L.areas.end()) * o.baySize / 100.0)); + const int total = + int(std::lround(*std::min_element(L.areas.begin(), L.areas.end()) * o.baySize / 100.0)); const PeriodicNoise shoreline(t.w, t.h, 8, context.stream("amphitheatre-bays")); const auto noise = [&](int i) { return shoreline.at(i % t.w, i / t.w); }; std::vector queued(n, 0); @@ -330,18 +340,21 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { const int site = L.anchor[k]; const int mouth = L.rampMiddle[k]; - const double heading = std::atan2(t.offsetY(mouth / t.w, site / t.w), t.offsetX(mouth % t.w, site % t.w)); + const double heading = std::atan2(t.offsetY(mouth / t.w, site / t.w), + t.offsetX(mouth % t.w, site % t.w)); for (const int side : {1, -1}) { - const int grown = growLakeBeside(t, L.bay, depth, fromWalls, kBayWallGap, total / 2, site, heading, - side, kLakeSiteGap, kLakeReach, noise, queued, 2 * k + (side > 0) + 1); + const int grown = growLakeBeside( + t, L.bay, depth, fromWalls, kBayWallGap, total / 2, site, heading, side, + kLakeSiteGap, kLakeReach, noise, queued, 2 * k + (side > 0) + 1); fits = fits && grown == total / 2; L.bayTiles[k] += grown; } } else { - L.bayTiles[k] = growFarLake(t, L.bay, depth, fromWalls, kBayWallGap, total, noise, queued, 100 + k); + L.bayTiles[k] = growFarLake(t, L.bay, depth, fromWalls, kBayWallGap, total, noise, + queued, 100 + k); fits = L.bayTiles[k] == total; } } @@ -382,9 +395,8 @@ std::vector stoneTiles(const Map &map, const Layout &L) } // The towers every colony starts with and the open pads beside them (chooseTowerSites): sites in a -// colony's territory, away from its ramp, that cover the most of neighbouring territories over the -// border walls, none in range of another colony's tower or swarm. With the control at 0 every site is -// an open pad. +// colony's territory, directly against stone and away from its ramp, that cover the most of +// neighbouring territories over the border walls, none in range of another colony's swarm. TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &context, const AmphitheatreOptions &o, const std::vector &stone) { @@ -402,22 +414,18 @@ TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &c { const int x = i % t.w, y = i / t.w; owner[i] = L.territory[i]; - buildable[i] = map.isGrass(x, y) && !stone[i] && !reserved[i] && fromRamps[i] > kRampClearance && - !map.isResource(x, y); + buildable[i] = map.isGrass(x, y) && !stone[i] && !reserved[i] && + fromRamps[i] > kRampClearance && !map.isResource(x, y); target[i] = !map.isWater(x, y) && !stone[i]; } - // No tower on a shore strip narrow enough for it to close; against the walls is where they belong. + // No tower on a shore strip narrow enough for it to close. std::vector land(n, 0); for (int i = 0; i < n; ++i) land[i] = !map.isWater(i % t.w, i / t.w); const std::vector roomy = roomyGround(t, land, kTowerRoom); for (int i = 0; i < n; ++i) buildable[i] = buildable[i] && roomy[i]; - TowerRequest request; - request.range = kTowerRange[std::clamp(o.towers - 1, 0, 2)]; - request.towers = o.towers > 0 ? o.towerCount : 0; - request.pads = o.towers > 0 ? kTowerPads : o.towerCount + kTowerPads; - request.spacing = kTowerSpacing; + TowerRequest request = startingTowerRequest(o.towers, o.towerCount, kTowerPads, kTowerSpacing); request.against = &stone; return chooseTowerSites(t, owner, buildable, target, swarms, L.g.teams, request); } @@ -470,16 +478,9 @@ bool generate(Game &game, GenerationContext &context) TowerPlan towers = planTowers(map, L, context, o, stone); // A crowded map may not seat every tower: every colony then keeps as many as the fewest got, as // long as every colony has one. - if (evenTowerPlan(towers) < (o.towers > 0 && o.towerCount > 0 ? 1 : 0)) - { - context.detail = "a colony has no room for its towers"; - return false; - } - if (!raiseTowers(game, towers, std::clamp(o.towers - 1, 0, 2))) - { - context.detail = "a tower site no longer fits"; + if (!settleStartingTowers(game, context, towers, o.towers, o.towers > 0 && o.towerCount > 0, + nullptr)) return false; - } const std::vector pads = towerFootprints(t, towers); context.stage = "amphitheatre resources"; @@ -489,42 +490,48 @@ bool generate(Game &game, GenerationContext &context) ramps[i] = L.rampOf[i] >= 0 || L.innerRamp[i]; const std::vector fromRamps = stepsFrom(t, ramps); const auto free = [&](int i) - { return !reserved[i] && !pads[i] && fromRamps[i] > kRampClearance && clearGround(map, i % t.w, i / t.w); }; + { + return !reserved[i] && !pads[i] && fromRamps[i] > kRampClearance && + clearGround(map, i % t.w, i / t.w); + }; const Fertility::Field fertility = Fertility::forMap(map, false); const PeriodicNoise patch(t.w, t.h, 12, context.stream("amphitheatre-patch")); const PeriodicNoise split(t.w, t.h, 6, context.stream("amphitheatre-split")); for (int k = 0; k < teams; ++k) { const auto eligible = [&](int i) { return L.territory[i] == k && free(i); }; - // The kit faces the bay: wheat and wood between the swarm and its shore, the quarry behind. + // The kit faces the nearest sea: wheat and wood between the swarm and its shore. const int ax = L.anchor[k] % t.w, ay = L.anchor[k] / t.w; int bayTile = -1; for (int i = 0; i < n; ++i) if (L.bay[i] && L.territory[i] == k && - (bayTile < 0 || t.dist2(ax, ay, i % t.w, i / t.w) < t.dist2(ax, ay, bayTile % t.w, bayTile / t.w))) + (bayTile < 0 || + t.dist2(ax, ay, i % t.w, i / t.w) < t.dist2(ax, ay, bayTile % t.w, bayTile / t.w))) bayTile = i; const double facing = - bayTile < 0 ? 0 : std::atan2(t.offsetY(ay, bayTile / t.w), t.offsetX(ax, bayTile % t.w)); + bayTile < 0 ? 0 + : std::atan2(t.offsetY(ay, bayTile / t.w), t.offsetX(ax, bayTile % t.w)); const KitFrame frame{ax, ay, facing}; plantKit(map, t, context, - Kit{frame.at(9, -6, 12), frame.at(9, 6, 12), frame.at(-8, 0, 10), kHomeWheat, kHomeWood, -1}, + Kit{frame.at(9, -6, 12), frame.at(9, 6, 12), frame.at(-8, 0, 10), kHomeWheat, + kHomeWood, -1}, eligible); furnishGround( - map, t, context, fertility, eligible, - [&](int i) { return patch.at(i % t.w, i / t.w); }, + map, t, context, fertility, eligible, [&](int i) { return patch.at(i % t.w, i / t.w); }, [&](int i) { return split.at(i % t.w, i / t.w); }, [&](int area) { return GroundAmounts{int(scaledCount(area * kHomeWheatShare / 100, o.wheat)), - int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, - int(scaledCount(1, o.fruit))}; + int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, + int(scaledCount(1, o.fruit))}; }, "amphitheatre-home-stone", "amphitheatre-home-fruit"); } // The arena's prizes, the same in every wedge (plantOrchard, plantRound): groves of the three fruits // on the innermost terrace and in the pit, a quarter-wedge round from the ramps, and a stone outcrop // on every terrace. - const auto arena = [&](int zone) { return [&, zone](int i) { return L.zone[i] == zone && free(i); }; }; + const auto arena = [&](int zone) + { return [&, zone](int i) { return L.zone[i] == zone && free(i); }; }; std::vector quarter, back; for (int k = 0; k < teams; ++k) { @@ -535,25 +542,30 @@ bool generate(Game &game, GenerationContext &context) { plantOrchard(map, t, context, L.cx, L.cy, 0.55 * g.pitR, quarter, 5, 4, 1, arena(0)); if (g.rings > 1) - plantOrchard(map, t, context, L.cx, L.cy, (g.ringR[0] + g.ringR[1]) / 2, quarter, 5, 4, 1, arena(1)); + plantOrchard(map, t, context, L.cx, L.cy, (g.ringR[0] + g.ringR[1]) / 2, quarter, 5, 4, + 1, arena(1)); } for (int j = 1; j < g.rings && scaledCount(1, o.stone) > 0; ++j) - plantRound(map, t, context, L.cx, L.cy, (g.ringR[j - 1] + g.ringR[j]) / 2, back, STONE, 1, 4, arena(j)); + plantRound(map, t, context, L.cx, L.cy, (g.ringR[j - 1] + g.ringR[j]) / 2, back, STONE, 1, + 4, arena(j)); std::vector bayOf(n, -1); for (int i = 0; i < n; ++i) if (L.bay[i]) bayOf[i] = L.territory[i]; - seedAlgae(map, context, t, "amphitheatre-algae", o.algae, AlgaeBand::shallows(1, 4), bayOf, teams); + seedAlgae(map, context, t, "amphitheatre-algae", o.algae, AlgaeBand::shallows(1, 4), bayOf, + teams); secureStartingCrops(game, context, t, 24, 32, 0, &stone); - reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, + &stone); // Keep every colony's walk open, clearing only deposits on it: home to its ramp, ramp to the pit. context.stage = "amphitheatre roads"; const std::vector> workers = unitTilesByTeam(map, teams); std::vector pit(n, 0); for (int i = 0; i < n; ++i) - pit[i] = L.zone[i] == 0 && !stone[i] && - std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) < 0.4 * g.pitR; + pit[i] = + L.zone[i] == 0 && !stone[i] && + std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) < 0.4 * g.pitR; for (int team = 0; team < teams; ++team) { if (workers[team].empty()) @@ -566,9 +578,11 @@ bool generate(Game &game, GenerationContext &context) ramp[i] = 1; rampTiles.push_back(i); } - if (!openRoad(map, t, workers[team], ramp, &stone) || !openRoad(map, t, rampTiles, pit, &stone)) + if (!openRoad(map, t, workers[team], ramp, &stone) || + !openRoad(map, t, rampTiles, pit, &stone)) { - context.detail = "colony " + std::to_string(team) + " has no walk to its ramp and the pit"; + context.detail = + "colony " + std::to_string(team) + " has no walk to its ramp and the pit"; return false; } } @@ -581,12 +595,15 @@ std::string validateRequest(const GenerationRequest &r) return "An amphitheatre needs at least two colonies."; // The geometry's own reason stays in the candidate's diagnostics; the player sees one message // that says what to change. - return geometryFor(r).failure.empty() ? "" : "The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies."; + return geometryFor(r).failure.empty() + ? "" + : "The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower " + "terraces or fewer colonies."; } // Checked on the finished world against the rebuilt design: every designed stone stands and every // ramp is open; every colony walks to colony 0; the territories are equal to within a few percent and -// every bay has the same number of tiles; with the outer ramps shut, no territory reaches another or +// every colony's seas have the same number of tiles; with the outer ramps shut, no territory reaches another or // the arena; and the colonies' walks to their ramps and to the pit are even. std::string validateWorld(const Game &game, const GenerationContext &context) { @@ -618,7 +635,8 @@ std::string validateWorld(const Game &game, const GenerationContext &context) const int smallest = *std::min_element(area.begin(), area.end()); const int largest = *std::max_element(area.begin(), area.end()); if (largest - smallest > std::max(4, smallest * kAreaTolerance / 100)) - return "The territories differ in size by " + std::to_string(largest - smallest) + " tiles."; + return "The territories differ in size by " + std::to_string(largest - smallest) + + " tiles."; // Each bay was grown to the same number of undermap corners; a tile is water only when all four of // its corners are, so the tiles that read as water can differ a little with the outline, but every // bay must be there. @@ -633,11 +651,14 @@ std::string validateWorld(const Game &game, const GenerationContext &context) std::vector outerRamps(n, 0); for (int i = 0; i < n; ++i) { - piece[i] = L.territory[i] >= 0 ? L.territory[i] : L.zone[i] >= 0 && L.zone[i] < g.rings ? teams : -1; + piece[i] = L.territory[i] >= 0 ? L.territory[i] + : L.zone[i] >= 0 && L.zone[i] < g.rings ? teams + : -1; outerRamps[i] = L.rampOf[i] >= 0; } if (const int leak = pieceLeak(map, t, piece, outerRamps); leak >= 0) - return "With the outer ramps shut, " + where(leak) + " can still be reached from elsewhere."; + return "With the outer ramps shut, " + where(leak) + + " can still be reached from elsewhere."; const auto nearestOpen = [&](int tile) { @@ -676,34 +697,35 @@ AmphitheatreOptions::AmphitheatreOptions(const GenerationRequest &r) GeneratorDefinition amphitheatreDefinition() { - return {"amphitheatre", - 23, - "Amphitheatre", - 1, - false, - // Rings of wall; each ramp's width in tiles; the pit's radius and each terrace's width as - // shares of the half side; how far the territories' borders wander; each bay's size as a - // percentage of the smallest territory; the borders' thickness in tiles. - {{"rings", "Rings", 2, 4, 1, 3, ControlGroup::Layout}, - {"ramp-width", "Ramp width", 5, 11, 2, 7, ControlGroup::Terrain}, - {"pit-size", "Pit size", 8, 30, 2, 16, ControlGroup::Layout}, - {"terrace-width", "Terrace width", 6, 20, 1, 14, ControlGroup::Layout}, - {"territory-roughness", "Border roughness", 0, 100, 10, 50, ControlGroup::Terrain}, - {"bay-size", "Bay size", 6, 24, 2, 14, ControlGroup::Terrain}, - {"border-wall", "Border wall", 1, 3, 1, 2, ControlGroup::Terrain}, - // The towers every colony starts with, all against its walls: their level (0 for none, just - // open pads) and how many. - {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, - {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, - // Every territory's ambient fields and grove, the arena's fruit and outcrops, and the - // bays' algae; every home's kit and the walls' stone are unscaled. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - validateRequest, - validateWorld}; + return { + "amphitheatre", + 23, + "Amphitheatre", + 1, + false, + // Rings of wall; each ramp's width in tiles; the pit's radius and each terrace's width as + // shares of the half side; how far the territories' borders wander; every colony's inland seas + // together as a percentage of the smallest territory; the borders' thickness in tiles. + {{"rings", "Rings", 2, 4, 1, 3, ControlGroup::Layout}, + {"ramp-width", "Ramp width", 5, 11, 2, 7, ControlGroup::Terrain}, + {"pit-size", "Pit size", 8, 30, 2, 16, ControlGroup::Layout}, + {"terrace-width", "Terrace width", 6, 20, 1, 14, ControlGroup::Layout}, + {"territory-roughness", "Border roughness", 0, 100, 10, 50, ControlGroup::Terrain}, + {"bay-size", "Bay size", 6, 24, 2, 14, ControlGroup::Terrain}, + {"border-wall", "Border wall", 1, 3, 1, 2, ControlGroup::Terrain}, + // The towers every colony starts with, all against its walls: their level (0 for none, just + // open pads) and how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, + // Every territory's ambient fields and grove, the arena's fruit and outcrops, and the + // bays' algae; every home's kit and the walls' stone are unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; } diff --git a/src/map/generator/generators/CarouselGenerator.cpp b/src/map/generator/generators/CarouselGenerator.cpp index 158fb685b..0440b7004 100644 --- a/src/map/generator/generators/CarouselGenerator.cpp +++ b/src/map/generator/generators/CarouselGenerator.cpp @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "CarouselGenerator.h" +#include "Drawing.h" +#include "Farmland.h" #include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" -#include "Drawing.h" -#include "Farmland.h" #include "Geometry.h" #include "Grid.h" #include "Homes.h" @@ -20,143 +20,148 @@ #include "Walls.h" #include "Wedge.h" #include -#include #include #include #include using namespace MapGeneration; -// Carousel: a ring of walled homes in the sea, each with a single door that leads clockwise. A -// colony's door opens onto a corridor that sweeps round to a small court pressed against the next -// colony's home, and from every court a spoke runs in to the plaza in the middle of the map. The -// court and the home beside it are parted by nothing but a thin wall of stone: no unit can walk -// through it, but a tower shoots over it. Nothing but stone parts any two pieces: the sea between them -// fills in, and a single line of stone stands where they meet. So every -// colony besieges one neighbour from its court while the other neighbour besieges it, and the -// only way to walk into anyone's home is the long way round, through the plaza, the enemy's court -// and the corridor behind it. +// Carousel: a ring of walled homes round a plaza, where every colony besieges one neighbour and is +// besieged by the other. +// +// THE SHAPE. Each home has a single door, on its clockwise side. The door opens onto a narrow corridor +// that follows the ring round to a small court - the elbow - pressed against the next colony's home, +// and from the court a narrow spoke runs in to the plaza in the middle of the map. The court and the +// home beside it are parted by a thin wall of stone. No unit can walk through it, but a defence tower +// shoots straight over it. So every colony's towers, standing in its own home against that wall, cover +// its neighbour's elbow: the one way that neighbour has out of its home. Every colony shells one +// neighbour's exit while the other neighbour shells its own, and the only way to walk into anyone's +// home is the long way round: through the plaza, down the enemy's spoke, across its court under fire, +// and back along its corridor. // -// Every coast is sealed, as City states' homes are: stone stands on every solid-grass tile that -// touches the sea's margin, so a swimmer can land on a beach and walk its sand lane but never get -// inside. Swimming therefore opens nothing; the corridors, spokes and courts are the only routes -// for the whole game. Each home has no pond: its two farms, inland and clear of the sea, are its water -// algae and for fields that regrow. +// WHY THE LANES ARE TIGHT. The concept only works if the laneways are narrow: a wide corridor or court +// lets an attacker walk out of range of the siege towers, and lets a defender build its own towers +// in the court. So the corridors and spokes default to 3 tiles and the court to a circle about 3 tiles +// in radius, and every wall is a single line of stone with no water beside it, so a tower behind a +// wall stands as close as possible to the lane it covers. // -// Everything is designed once in the wedge's frame (a home, its corridor, its court, the wall and -// the spoke, measured by angle round the centre and radius out from it) and turned round the -// centre for every colony, so every colony's ground is the same and the layout is fair for any -// colony count. Like City states it stays round on a rectangular map, in a circle on the shorter -// side, with sea filling the rest. The whole design is a pure function of the request, so -// validateWorld rebuilds it and checks the finished world. +// WHY THE FARMS. Each home reaches two walled wheat farms, one in towards the plaza and one out beyond +// the ring, laid in rows at the widths that regrow best (Farmland). The farms are the homes' water: +// there are no ponds, which would only take building room. Every set of farms is shared out by equal +// yield for its rows' angle, so no colony's farms grow less for facing the wrong way. The plaza has +// only fruit - an orchard for inns to pull hungry enemies across - with no wheat or wood to smother it. // -// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be -// cleared, so the walls are permanent and a sealed coast stays sealed; a defence tower scans square -// rings round its footprint with no line of sight (Building::findBestTarget), so a wall two tiles -// thick stops walking but not shooting; grass may never touch water, so every coast wall stands one -// tile in from a sand lane, and the ponds keep well away from the sea so their beaches never join -// its margin; wheat and wood regrow only near water, which is why every home has its ponds; and -// fruit lets an inn pull hungry enemy units across, which makes the plaza's orchard worth the walk. +// HOW IT IS BUILT. Everything is designed once, in the wedge's frame (angle round the centre and +// distance out from it), and turned round the centre for every colony, so every colony's ground is the +// same and the layout is fair for any colony count. The pieces - homes, lanes, courts and the plaza - +// are drawn first, then the farms grown into the sea between them. Then all the sea near a home or farm +// fills in with that colony's ground, and a line of stone is drawn wherever two colonies' ground meets +// (see fillAndWall). What sea is left beyond the outermost pieces is sealed off: stone stands on every +// grass tile touching its beaches, so a swimmer can land but never walk in. The design is a pure +// function of the request, so validateWorld rebuilds it and checks the finished world against it. +// +// GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be cleared, so +// the walls are permanent; a defence tower scans square rings round its footprint with no line of sight +// (BuildingUtils::turretScanTile), so a wall stops walking but not shooting; stone stands only on pure +// grass and grass may never touch water, so a wall never runs along water; wheat and wood regrow only +// near water and spread onto the grass round them, which is why the farms are rows of crops between +// water, capped with sand; and fruit lets an inn pull hungry enemy units across, which makes the +// plaza's orchard worth the walk. // // THE SIZES AT THE DEFAULTS (256x256, 4 colonies): homes 26 tiles in radius 94 tiles out from the -// centre, corridors 3 wide sweeping about 50 degrees, courts 3 in radius, a wall 2 thick, spokes 3 -// wide and a plaza 31 in radius with a pond and an orchard. +// centre, corridors 3 wide sweeping about 50 degrees, courts 3 in radius, a court wall 2 thick, spokes 3 +// wide and a plaza 31 in radius with a pond and an orchard; three towers per colony. namespace { -// The homes' outer coast, as a share of the half side on a 512-tile map (a little more on smaller -// ones): the sea beyond it is where the outer farms grow. +// THE RING. The homes' outer edge, as a share of the half side on a 512-tile map (smaller maps give the +// ring more of their room, since homes cannot shrink with them); a crowded ring grows until every colony +// has at least this many tiles of arc, up to this share of the half side. constexpr double kRingShare = 0.7; -// ...but never less than this many tiles of the ring's outer coast per colony, nor more than this -// share of the half side. constexpr double kArcPerColony = 110; constexpr double kMaximumRingShare = 0.9; -// The farms: water kept between a farm and any other land, the neck joining it to its home, the land -// a farm keeps between its water rows and its coast, and the smallest farm worth having, as a share -// of the home's area. -constexpr int kFarmGap = 3; -// How far the ground between the pieces fills in from the homes and farms, and the margin of land any -// sea left beyond that keeps beside every piece. -constexpr int kFillReach = 24; -constexpr int kSeaMargin = 3; -constexpr double kNeckHalf = 8; -// The least share of a farm's crop land its colony must be able to walk to. -constexpr double kFarmReach = 0.95; -// How far apart, along a farm's rows, the sand bridges across its water rows stand. -constexpr int kFarmBridgeSpacing = 16; -// Land kept round a farm's water rows inside its walls, and the further margin kept from open sea. -constexpr int kFarmRim = 3; -constexpr int kFarmSeaRim = 3; -constexpr double kMinimumFarmShare = 0.5; -// A farm is a wheat farm: wheat on this share of its crop rows at 100, and a small woodlot of this -// share along one row. -constexpr int kFarmWheatShare = 45; -constexpr int kFarmWoodShare = 3; -// Towers: every colony starts with `tower-count` of them, and this many open 2x2 pads beside them for -// more, all sited against the walls (chooseTowerSites). -constexpr int kTowerPads = 3; -constexpr int kTowerSpacing = 2; -// A colony's elbow, which its neighbour's towers aim at: its court and its lanes this many tiles beyond -// the court's edge. The siege line: towers stand in a home no more than this many steps from the wall. -constexpr double kElbowReach = 12; -constexpr int kSiegeLine = 6; -// A home's radius at 100% of home size, as a share of the half side, but never across more than -// this share of the ring's arc per colony (as a half share: the diameter takes twice it), so crowded -// rings get smaller homes; its outline wobbles by up to this share. A home needs room for its ponds, -// their gap to the sea and this much more for the swarm. + +// THE HOMES. A home's radius at 100% of home size, as a share of the half side and never below this many +// tiles, but never across more than this share of the ring's arc per colony, so a crowded ring gets +// smaller homes; its outline wobbles by up to this share. constexpr double kHomeShare = 0.2; constexpr double kHomeArcShare = 0.2; constexpr double kMinimumHome = 14; -constexpr double kSwarmRoom = 4; constexpr double kHomeRoughness = 0.12; -// A court's radius at 100% of court size, in tiles at 256 and never below the smallest court that -// seats a tower with room round it. +// Every home's starter kit: this much wheat and wood beside the swarm, unscaled, 1:1. +constexpr int kHomeWheat = 30; +constexpr int kHomeWood = 30; +// A home's scattered farmland, as percentages of its tiles at 100% wheat and wood. +constexpr int kHomeWheatShare = 4; +constexpr int kHomeWoodShare = 2; + +// THE LANES AND COURTS. A court's radius at 100% of court size, in tiles on a 256-tile map (it scales +// with the square root of the map), and never below this, nor narrower than its corridor. constexpr double kCourtRadius = 9; constexpr double kMinimumCourt = 3; -// The wall between a court and the next home is a band this many court radii wide, so it reaches -// past the court's sides and the court's own coast wall never cuts in between its grass and the wall. +// The corridor must run at least this far between its home and its court, or the court is simply part +// of the home. +constexpr double kMinimumCorridor = 10; +// The court wall is drawn inside a band this many court radii wide from the court to the next home, so +// it covers where they face each other and nowhere else. constexpr double kWallBand = 1.3; -// Ground kept between pieces that must not join: two colonies' lanes (a wall on one of them must not -// close it), and the plaza's clearance from the homes' ring. -constexpr double kPieceGap = 4; -// A lane passing another colony's home needs only a tile between them for the wall. +// Two colonies' lanes keep at least this many steps apart outside the plaza, so a wall between them +// never closes either; the plaza keeps twice this from the homes' ring. +constexpr double kLaneGap = 4; +// A lane passing another colony's home needs only a tile between them, for the wall. constexpr int kLaneHomeGap = 2; -// The corridor must run at least this far between its home and its court, or the court is simply -// part of the home. -constexpr double kMinimumCorridor = 10; -// A pond keeps this much land between its water and the home's coast, so its beach never meets the -// sea's margin (where it did, a swimmer could step from one beach to the other, round the wall). -constexpr double kLakeSeaGap = 5; -// A pond's radius as a share of its home's, clamped. -constexpr double kPondShare = 0.18; -constexpr double kMinimumPond = 2.5; -constexpr double kMaximumPond = 7; -// The plaza's pond as a share of the plaza. +// The plaza's pond as a share of the plaza's radius, and the land kept round it. constexpr double kPlazaPondShare = 0.28; -// Every home starts identical: fixed wheat and wood beside its first pond and a stone clump behind -// it, all unscaled, wheat and wood 1:1. -constexpr int kHomeWheat = 30; -constexpr int kHomeWood = 30; -// Ambient farmland on a home's ground, as percentages of their tiles at 100. -constexpr int kHomeWheatShare = 4; -constexpr int kHomeWoodShare = 2; -// Deposits keep this many steps from every corridor and spoke, so a door never grows shut. -constexpr int kDoorClearance = 6; -// A sand road vertex keeps this many steps from any water. +constexpr double kPlazaPondShore = 5; + +// THE FARMS. While they grow, farms keep this many steps from other colonies' ground (the gap fills in +// afterwards), join their home through a neck this wide, and are laid in a set (every inner farm, or +// every outer one) only if every colony's field in it is at least this share of a home's area. +constexpr int kFarmGap = 3; +constexpr double kNeckHalf = 8; +constexpr double kMinimumFarmShare = 0.5; +// Land kept round a farm's water rows inside its walls (enough for a beach and the sand cap), and the +// further margin kept from any open sea, whose beach and sealed coast need more. +constexpr int kFarmRim = 3; +constexpr int kFarmSeaRim = 3; +// A sand bridge crosses a farm's water rows every this many tiles along them. +constexpr int kFarmBridgeSpacing = 16; +// A farm is a wheat farm: wheat on this share of its crop rows at 100%, and a woodlot of this share. +constexpr int kFarmWheatShare = 45; +constexpr int kFarmWoodShare = 3; +// The least share of a farm's crop land its colony must be able to walk to. +constexpr double kFarmReach = 0.95; + +// FILLING IN. The sea within this many steps of a home or farm becomes that colony's ground; sea left +// beyond it keeps this many tiles of the nearest piece's ground before its coast. +constexpr int kFillReach = 24; +constexpr int kSeaMargin = 3; +// A road's corridor sand keeps this many steps from any water, so it never joins a beach. constexpr int kRoadSeaGap = 5; -// The most the colonies' walks to the plaza may differ. -constexpr int kWalkSpread = 12; -// A tower in a court must reach the next home at this level (0, 1 or 2): level 1 shoots 7 tiles. +// Deposits keep this many steps from every corridor and spoke, so no field ever grows a door shut. +constexpr int kDoorClearance = 6; + +// THE TOWERS. Open 2x2 pads per colony beside its `tower-count` towers, and the least distance between +// sites, kept small so the siege line packs against its wall. +constexpr int kTowerPads = 3; +constexpr int kTowerSpacing = 2; +// A colony's elbow, which the next colony's towers aim at: its court and its lanes up to this many tiles +// beyond the court's edge. Towers stand in a home no more than this many steps from a wall. +constexpr double kElbowReach = 12; +constexpr int kSiegeLine = 6; + +// CHECKS. The level (0, 1 or 2) of tower that must reach the neighbour's court from a home: level 1 +// shoots 7 tiles. The most the colonies' walks to the plaza may differ. constexpr int kSiegeTower = 1; +constexpr int kWalkSpread = 12; struct Geometry { int teams = 0, half = 0; double wedge = 0, outer = 0; double homeR = 0, homeReach = 0, homeRadius = 0; // radius, wobbled reach, centre's distance out - double corridorHalf = 0, spokeHalf = 0, courtR = 0, wall = 0, plazaR = 0, plazaPondR = 0, pondR = 0; - double sweep = 0; // from a home's axis to its court's - double courtRadius = 0; // the court centre's distance out: the homes' ring - int ponds = 1; + double corridorHalf = 0, spokeHalf = 0, courtR = 0, wall = 0, plazaR = 0, plazaPondR = 0; + double sweep = + 0; // from a home's axis to its court's, before the court is fitted to the outline bool roads = true; std::string failure; }; @@ -179,17 +184,17 @@ Geometry geometryFor(const GenerationRequest &r) g.half = std::min(1 << r.wDec, 1 << r.hDec) / 2; const double scale = g.half / 128.0; g.wedge = 2 * kPi / g.teams; - // The ring of homes keeps well inside the map, leaving a band of sea round it for the outer farms; - // smaller maps give the ring more of their room, since homes cannot shrink with them. - const double ringShare = g.half >= 256 ? kRingShare : g.half >= 128 ? kRingShare + 0.06 : kRingShare + 0.18; - // A crowded ring grows until every colony has its arc, up to most of the map. + const double ringShare = g.half >= 256 ? kRingShare + : g.half >= 128 ? kRingShare + 0.06 + : kRingShare + 0.18; g.outer = std::min(kMaximumRingShare * g.half, std::max(ringShare * g.half, g.teams * kArcPerColony / (2 * kPi))); - // A home is a share of the half side, but never wider than its share of the ring: with its - // centre at outer - reach, a reach of s * wedge * (outer - reach) solves to the bound below. + // A home is a share of the half side, but never wider than its share of the ring: with its centre at + // outer - reach, a reach of s * wedge * (outer - reach) solves to the bound below. const double arcShare = kHomeArcShare * g.wedge; - const double reach = std::min(std::max(kMinimumHome, kHomeShare * g.half) * (1 + kHomeRoughness), - arcShare * g.outer / (1 + arcShare)); + const double reach = + std::min(std::max(kMinimumHome, kHomeShare * g.half) * (1 + kHomeRoughness), + arcShare * g.outer / (1 + arcShare)); g.homeReach = reach * o.homeSize / 100.0; g.homeR = g.homeReach / (1 + kHomeRoughness); g.homeRadius = g.outer - g.homeReach; @@ -198,23 +203,19 @@ Geometry geometryFor(const GenerationRequest &r) g.courtR = std::max(kMinimumCourt, kCourtRadius * std::sqrt(scale) * o.courtSize / 100.0); g.courtR = std::max(g.courtR, g.corridorHalf + 0.5); g.wall = o.courtWall; - // The plaza is a share of the half side, held clear of the homes' ring. g.plazaR = std::min(g.half * o.plazaSize / 100.0, - g.homeRadius - g.homeReach - g.corridorHalf - 2 * kPieceGap); - g.plazaPondR = std::max(kMinimumPond, kPlazaPondShare * g.plazaR); - g.pondR = std::clamp(kPondShare * g.homeR, kMinimumPond, kMaximumPond); - // No ponds: every home's farms are its water. - g.ponds = 0; + g.homeRadius - g.homeReach - g.corridorHalf - 2 * kLaneGap); + g.plazaPondR = std::max(kHomeSmallestPond, kPlazaPondShare * g.plazaR); g.roads = o.sandRoads; g.sweep = courtSweep(g, g.homeReach); - // A home must still hold its swarm, its kit and its towers: at least what a home with a pond once needed. - if (g.homeR < g.pondR + kLakeSeaGap + kSwarmRoom) + if (!homeHasRoom(g.homeR)) g.failure = "Too many colonies for this map: the homes are too small."; else if (g.sweep <= 0) g.failure = "Too many colonies for this map: the homes do not fit round the ring."; else if (g.homeRadius * g.sweep - g.homeReach - g.courtR < kMinimumCorridor) - g.failure = "Too many colonies for this map: no room for a corridor between a home and its court."; - else if (g.plazaR - g.plazaPondR < kLakeSeaGap + 3) + g.failure = + "Too many colonies for this map: no room for a corridor between a home and its court."; + else if (g.plazaR - g.plazaPondR < kPlazaPondShore + 3) g.failure = "The plaza is too small for its pond on this map."; return g; } @@ -224,72 +225,47 @@ struct Layout Torus t{1, 1}; Geometry g; double phase = 0, cx = 0, cy = 0, sweep = 0; - // Per tile: which colony's home, court or path (corridor and spoke) it belongs to, -1 for none. - std::vector homeOf, courtOf, pathOf; - // farmOf: the colony a farm tile belongs to; farms: every farm laid out, two per colony. - std::vector farmOf, farmSet; // farmSet: 0 an inner farm, 1 an outer one + std::vector axis; // every colony's home axis angle + std::vector homes, courts; // their centres + std::vector heartOf; // where each colony's spoke comes into the plaza + // Per tile, the colony whose home, court, lane (corridor or spoke) or farm it is, -1 for none; a farm + // tile's set is 0 for an inner farm and 1 for an outer one. + std::vector homeOf, courtOf, pathOf, farmOf, farmSet; + // Per tile, the side it lies on for the walls between sides: the plaza (0), a colony's court and + // lanes (1 + colony), its home and farms (1 + teams + colony); -1 for the court walls and open sea. + std::vector side; + // Steps from the homes as first drawn, before the sea filled in: where a home's door stands. + std::vector homeSteps; + std::vector plaza, land, pond, wall, road, roadTile; + // Every farm laid, the colony it belongs to, and all the farms' sand (plot rings, bridges and caps). std::vector farms; - std::vector farmSand, farmPlot; // the farms' building plots: sand rings, grass std::vector farmColony; - // Each tile's side for the walls between them: the plaza (0), a colony's court and lanes (1 + colony), - // its home and farms (1 + teams + colony); -1 for the court walls and the sea. homeSteps: steps - // from the homes as designed, before the gaps filled in, which is where a home's door stands. - std::vector side, homeSteps; - std::vector plaza, land, pond, wall, road, roadTile; - std::vector axis; // every colony's home axis angle - std::vector homes, courts; // their centres - std::vector heartOf; // where each colony's spoke comes into the plaza - std::vector firstPond; // each home's first pond's centre + std::vector farmSand; std::string failure; }; -// Whether the border between tile i and its lower-sided neighbour j stays open: a lane into the plaza, -// or a home's door onto its own corridor where the corridor leaves the home. +// Whether the border between tile i and its lower-sided neighbour j stays open rather than walled: a lane +// into the plaza, or a home's door onto its own corridor, where the corridor leaves the home as drawn. bool borderOpen(const Layout &L, int i, int j) { const int teams = L.g.teams, a = L.side[i], b = L.side[j]; if (b == 0 && a >= 1 && a <= teams) return true; - return a == b + teams && b >= 1 && L.pathOf[j] >= 0 && L.homeSteps[j] >= 0 && L.homeSteps[j] <= 2; + return a == b + teams && b >= 1 && L.pathOf[j] >= 0 && L.homeSteps[j] >= 0 && + L.homeSteps[j] <= 2; } -Layout design(const GenerationRequest &request, GenerationContext &context) +// THE LANES: every colony's home axis, home, court, corridor and spoke, and the sand road down them. The +// corridor follows the homes' ring from the home's axis to its court; the spoke runs in from the court to +// just inside the plaza, at the middle of the wedge, leaning back towards its own home and away from the +// next, which its court is pressed against. +void drawLanes(Layout &L, const RadialShape &homeShape) { - const CarouselOptions o(request); - Layout L; - L.t = Torus(1 << request.wDec, 1 << request.hDec); const Torus &t = L.t; - const int n = t.size(); - L.g = geometryFor(request); const Geometry &g = L.g; - if (!g.failure.empty()) - { - L.failure = g.failure; - return L; - } - const int teams = g.teams; - L.cx = t.w / 2; - L.cy = t.h / 2; - L.phase = context.bounded("carousel-phase", 3600) / 3600.0 * 2 * kPi; - L.homeOf.assign(n, -1); - L.courtOf.assign(n, -1); - L.pathOf.assign(n, -1); - L.plaza.assign(n, 0); - L.land.assign(n, 0); - L.pond.assign(n, 0); - L.wall.assign(n, 0); - L.road.assign(n, 0); - L.roadTile.assign(n, 0); - - // One home outline for every colony, turned to each home's axis. - const RadialShape homeShape(g.homeR, kHomeRoughness, context, "carousel-home"); - const RadialShape pondShape(g.pondR, 0.2, context, "carousel-pond"); - const RadialShape courtShape(g.courtR, 0.0001, context, "carousel-court"); - const RadialShape plazaShape(g.plazaR, 0.0001, context, "carousel-plaza"); - const RadialShape plazaPondShape(g.plazaPondR, 0.0001, context, "carousel-plaza-pond"); - - // The court's exact place: its edge `wall` tiles from the next home's outline where that - // outline faces it. The outline is the same shape turned, so this is the same for every colony. + const int n = t.size(); + // The court's exact place: its edge `wall` tiles from the next home's outline where that outline faces + // it. The outline is the same shape turned for every home, so this holds for every colony. double sweep = g.sweep; for (int pass = 0; pass < 3; ++pass) { @@ -302,56 +278,54 @@ Layout design(const GenerationRequest &request, GenerationContext &context) break; sweep = next; } - L.sweep = sweep; - std::vector> paths; - for (int k = 0; k < teams; ++k) + for (int k = 0; k < g.teams; ++k) { const double a = L.phase + g.wedge * k, c = a + sweep; L.axis.push_back(a); L.homes.push_back(polarPoint(L.cx, L.cy, g.homeRadius, a)); L.courts.push_back(polarPoint(L.cx, L.cy, g.homeRadius, c)); - // The corridor follows the homes' ring from the home's axis to its court, and the spoke runs - // in from the court to inside the plaza's edge at the middle of the wedge, leaning back - // towards its own home and away from the next, which the court is pressed against. const double entry = std::min(c, a + g.wedge / 2); L.heartOf.push_back(polarPoint(L.cx, L.cy, g.plazaR - 4, entry)); - std::vector corridor = arcPath(L.cx, L.cy, g.homeRadius, a, c, g.corridorHalf); const ShapePoint in = polarPoint(L.cx, L.cy, g.plazaR - g.spokeHalf, entry); - std::vector spoke = {{L.courts[k].x, L.courts[k].y, g.spokeHalf}, - {in.x, in.y, g.spokeHalf}}; std::vector mask(n, 0); - strokePath(mask, t, corridor); - strokePath(mask, t, spoke); + strokePath(mask, t, arcPath(L.cx, L.cy, g.homeRadius, a, c, g.corridorHalf)); + strokePath(mask, t, + {{L.courts[k].x, L.courts[k].y, g.spokeHalf}, {in.x, in.y, g.spokeHalf}}); for (int i = 0; i < n; ++i) if (mask[i]) L.pathOf[i] = k; - paths.push_back(corridor); - paths.push_back(spoke); if (g.roads) { - // The road runs down the middle of the corridor, through the court and down the spoke, then on - // across the plaza to its pond, so the way in to the middle never overgrows; the home's interior - // is left to build on. + // The road runs down the corridor from just outside the home, through the court, down the spoke + // and across the plaza to its pond, so the way to the middle can never be fully overgrown. const double inset = (g.homeReach + 1) / g.homeRadius; - std::vector line = arcPath(L.cx, L.cy, g.homeRadius, a + inset, c, 0); - tracePath(L.road, t, line); + tracePath(L.road, t, arcPath(L.cx, L.cy, g.homeRadius, a + inset, c, 0)); const ShapePoint pond = polarPoint(L.cx, L.cy, g.plazaPondR, entry); - tracePath(L.road, t, {{L.courts[k].x, L.courts[k].y, 0}, {in.x, in.y, 0}, {pond.x, pond.y, 0}}); + tracePath(L.road, t, + {{L.courts[k].x, L.courts[k].y, 0}, {in.x, in.y, 0}, {pond.x, pond.y, 0}}); } } +} - // The shapes: homes with their ponds, courts, the plaza with its pond, and the wall between each - // court and the next home. +// THE PIECES: homes and courts over the lanes, the wall between every court and the next home, and the +// plaza with its pond. The court wall is just the land no more than `court-wall` steps from both the +// court and the next home, on the home's side of the court's middle and never over the court's own lanes: +// the thinnest wall that parts them. +void stampPieces(Layout &L, const RadialShape &homeShape, const RadialShape &courtShape, + const RadialShape &plazaShape, const RadialShape &plazaPondShape) +{ + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(), teams = g.teams; for (int k = 0; k < teams; ++k) { - L.firstPond.push_back(stampPondHome(t, L.homes[k], L.axis[k], homeShape, pondShape, - {g.homeR, g.pondR, kLakeSeaGap, g.ponds}, L.pond, - [&](int i) - { - L.homeOf[i] = k; - L.pathOf[i] = -1; - })); + stampRoundHome(t, L.homes[k], L.axis[k], homeShape, g.homeR, 0, nullptr, L.pond, + [&](int i) + { + L.homeOf[i] = k; + L.pathOf[i] = -1; + }); forEachTileInShape(t, L.courts[k].x, L.courts[k].y, courtShape, 0, [&](int i, double, double) { @@ -359,15 +333,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.pathOf[i] = -1; }); } - // The walls, once every home and court is down: a thin line of stone between each court and the next - // home, just the tiles no more than `court-wall` steps from both - the land where they face each - // other. The band only bounds where to look, on the home's side of the court's - // middle and never over the court's own corridor or spoke. for (int k = 0; k < teams; ++k) { const ShapePoint next = L.homes[(k + 1) % teams]; std::vector band(n, 0), court(n, 0), home(n, 0); - strokePath(band, t, {{L.courts[k].x, L.courts[k].y, g.courtR * kWallBand}, {next.x, next.y, g.courtR * kWallBand}}); + strokePath(band, t, + {{L.courts[k].x, L.courts[k].y, g.courtR * kWallBand}, + {next.x, next.y, g.courtR * kWallBand}}); for (int i = 0; i < n; ++i) { court[i] = L.courtOf[i] == k; @@ -379,9 +351,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (band[i] && L.courtOf[i] < 0 && L.homeOf[i] < 0 && L.pathOf[i] != k && fromCourt[i] <= g.wall && fromHome[i] <= g.wall) { - const double along = - t.offsetX(int(std::lround(L.courts[k].x)), i % t.w) * ax + - t.offsetY(int(std::lround(L.courts[k].y)), i / t.w) * ay; + const double along = t.offsetX(int(std::lround(L.courts[k].x)), i % t.w) * ax + + t.offsetY(int(std::lround(L.courts[k].y)), i / t.w) * ay; if (along <= 0) continue; L.wall[i] = 1; @@ -394,53 +365,47 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { if (L.plaza[i]) L.pathOf[i] = -1; - L.land[i] = L.homeOf[i] >= 0 || L.courtOf[i] >= 0 || L.pathOf[i] >= 0 || L.plaza[i] || L.wall[i]; - if (L.pond[i]) - L.road[i] = 0; - // Roads only on the lanes, courts and plaza, never inside a home. - if (L.road[i] && L.pathOf[i] < 0 && L.courtOf[i] < 0 && !L.plaza[i]) + L.land[i] = + L.homeOf[i] >= 0 || L.courtOf[i] >= 0 || L.pathOf[i] >= 0 || L.plaza[i] || L.wall[i]; + // Roads only on the lanes, courts and plaza: never in a home, and never in water. + if (L.pond[i] || (L.road[i] && L.pathOf[i] < 0 && L.courtOf[i] < 0 && !L.plaza[i])) L.road[i] = 0; } +} - // The pieces that must not join keep water between them: every colony's corridor and spoke stay - // clear of every other home, and of the next colony's corridor and spoke outside the plaza. +// A crowded ring the design refuses: a lane running past another colony's home with no room for a wall, +// two colonies' lanes too close outside the plaza, or a court touching the next home. +std::string crowding(const Layout &L) +{ + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(), teams = g.teams; + for (int k = 0; k < teams; ++k) { - std::vector nearestHome(n, -1), homeSteps(n, -1), pathSteps(n, -1); - for (int k = 0; k < teams; ++k) + std::vector home(n, 0), nextPath(n, 0); + for (int i = 0; i < n; ++i) { - std::vector home(n, 0), path(n, 0); - for (int i = 0; i < n; ++i) - { - home[i] = L.homeOf[i] == k; - path[i] = L.pathOf[i] == (k + 1) % teams; - } - const std::vector fromHome = stepsFrom(t, home); - const std::vector fromPath = stepsFrom(t, path); - for (int i = 0; i < n; ++i) - { - if (L.pathOf[i] < 0) - continue; - // Beside its own court a path is the court's mouth, which the wall parts from the home. - const int own = L.pathOf[i]; - const bool mouth = std::hypot(t.offsetX(int(std::lround(L.courts[own].x)), i % t.w), - t.offsetY(int(std::lround(L.courts[own].y)), i / t.w)) < - g.courtR + g.corridorHalf; - if (own != k && !mouth && fromHome[i] < kLaneHomeGap) - { - L.failure = "Too many colonies for this map: a spoke or corridor passes too close to a home."; - return L; - } - const double d = std::hypot(i % t.w - L.cx, i / t.w - L.cy); - if (teams > 1 && L.pathOf[i] == k && L.pathOf[i] != (k + 1) % teams && - fromPath[i] < kPieceGap && d > g.plazaR + kPieceGap) - { - L.failure = "Too many colonies for this map: the spokes crowd each other."; - return L; - } - } + home[i] = L.homeOf[i] == k; + nextPath[i] = L.pathOf[i] == (k + 1) % teams; + } + const std::vector fromHome = stepsFrom(t, home), fromNextPath = stepsFrom(t, nextPath); + for (int i = 0; i < n; ++i) + { + const int own = L.pathOf[i]; + if (own < 0) + continue; + // Beside its own court a lane is the court's mouth, which the court wall parts from the home. + const bool mouth = std::hypot(t.offsetX(int(std::lround(L.courts[own].x)), i % t.w), + t.offsetY(int(std::lround(L.courts[own].y)), i / t.w)) < + g.courtR + g.corridorHalf; + if (own != k && !mouth && fromHome[i] < kLaneHomeGap) + return "Too many colonies for this map: a spoke or corridor passes too close to a " + "home."; + const double d = std::hypot(i % t.w - L.cx, i / t.w - L.cy); + if (teams > 1 && own == k && fromNextPath[i] < kLaneGap && d > g.plazaR + kLaneGap) + return "Too many colonies for this map: the spokes crowd each other."; } } - // The wall must part the court from the home: at least one tile of it between them everywhere. for (int k = 0; k < teams; ++k) { std::vector next(n, 0); @@ -449,206 +414,256 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const std::vector steps = stepsFrom(t, next); for (int i = 0; i < n; ++i) if (L.courtOf[i] == k && steps[i] < 2) + return "A court touches the next home; make the court wall thicker."; + } + return ""; +} + +// A set of farms laid: which set (0 inner, 1 outer), and every colony's row origin. +struct FarmSet +{ + int set; + std::vector origins; +}; + +// THE FARM FIELDS (growFarmFields): every colony's home reaches one field in towards the plaza, in the +// sea between its corridor and the spokes, and one out beyond the ring. Each set is shared out by equal +// yield for every colony's row angle (its axis), so a colony whose rows fall on the diagonal gets more +// ground. A set is kept only if every colony's field in it has room, so a small or crowded map loses a +// set for everyone rather than favouring anyone. The rows are laid later, once the fields' walls stand. +std::vector claimFarmFields(Layout &L) +{ + const Torus &t = L.t; + const Geometry &g = L.g; + const int n = t.size(), teams = g.teams; + std::vector inner(n, 0), outer(n, 0); + for (int i = 0; i < n; ++i) + { + const double d = std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); + inner[i] = d < g.homeRadius; + outer[i] = d >= g.homeRadius; + } + std::vector innerSeeds, outerSeeds; + std::vector owners; + for (int k = 0; k < teams; ++k) + { + innerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius - g.homeReach - 2, L.axis[k])); + outerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius + g.homeReach + 2, L.axis[k])); + owners.push_back(k); + } + const std::vector fields[2] = { + growFarmFields(t, L.land, L.homeOf, inner, innerSeeds, owners, L.homes, kFarmGap, kNeckHalf, + L.axis), + growFarmFields(t, L.land, L.homeOf, outer, outerSeeds, owners, L.homes, kFarmGap, kNeckHalf, + L.axis)}; + const double homeArea = kPi * g.homeR * g.homeR; + std::vector sets; + for (int set = 0; set < 2; ++set) + { + std::vector size(teams, 0); + for (int i = 0; i < n; ++i) + if (fields[set][i] >= 0) + ++size[fields[set][i]]; + if (*std::min_element(size.begin(), size.end()) < kMinimumFarmShare * homeArea) + continue; + sets.push_back({set, set == 0 ? innerSeeds : outerSeeds}); + for (int i = 0; i < n; ++i) + if (fields[set][i] >= 0 && L.homeOf[i] < 0) { - L.failure = "A court touches the next home; make the court wall thicker."; - return L; + L.farmOf[i] = fields[set][i]; + L.farmSet[i] = set; + L.land[i] = 1; } } - std::vector farmSets; // the sets of farms laid: 0 inner, 1 outer - std::vector farmOrigins; // per laid set, every colony's row origin - // The farms (growFarmFields): every colony's home reaches one farm in towards the plaza, filling the - // sea between its corridor and the spokes either side, and one out beyond the ring, sharing the - // sea round the ring equally with the others. Rows run along the colony's axis at the widths that - // yield most (bestFarmRows); every farm's coast is sealed like the rest. + return sets; +} + +// FILLING IN AND WALLING. Nothing but stone parts two colonies' ground, so every wall stands right beside +// what it guards: +// - The sea within kFillReach of a home or farm becomes that home's or farm's ground, whichever is +// nearest (fillToNearest). Lanes, courts and the plaza never grow, so they keep exactly the width they +// were drawn. Sea left beyond that keeps a kSeaMargin strip of the nearest piece before its beach, so +// a beach and its sealed coast never eat into a lane. +// - Every tile gets a side: the plaza, a colony's lanes and court, or a colony's home and farms. A +// single line of stone stands wherever two sides meet (labelBorders), on the higher side - always the +// home's or farm's, so no wall narrows a lane. Two borders stay open (borderOpen): each home's door +// onto its own corridor, and each spoke's way into the plaza. +void fillAndWall(Layout &L) +{ + const Torus &t = L.t; + const int n = t.size(), teams = L.g.teams; + // Fill labels: a home per colony, a farm per colony per set, then courts, lanes and the plaza. + const int farmLabel = teams, courtLabel = 3 * teams, pathLabel = 4 * teams, + plazaLabel = 5 * teams; + std::vector piece(n, -1), grown(n, -1); + std::vector sea(n, 0), home(n, 0); + for (int i = 0; i < n; ++i) { - std::vector inner(n, 0), outer(n, 0); - for (int i = 0; i < n; ++i) - { - const double d = std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); - inner[i] = d < g.homeRadius; - outer[i] = d >= g.homeRadius; - } - std::vector innerSeeds, outerSeeds, anchors; - for (int k = 0; k < teams; ++k) + piece[i] = L.wall[i] ? -1 + : L.homeOf[i] >= 0 ? L.homeOf[i] + : L.farmOf[i] >= 0 ? farmLabel + L.farmSet[i] * teams + L.farmOf[i] + : L.courtOf[i] >= 0 ? courtLabel + L.courtOf[i] + : L.pathOf[i] >= 0 ? pathLabel + L.pathOf[i] + : L.plaza[i] ? plazaLabel + : -1; + grown[i] = piece[i] < courtLabel ? piece[i] : -1; + sea[i] = !L.land[i]; + home[i] = L.homeOf[i] >= 0; + } + std::vector filled = fillToNearest(t, grown, sea, kFillReach); + for (int i = 0; i < n; ++i) + { + if (filled[i]) + piece[i] = grown[i]; + sea[i] = sea[i] && !filled[i]; + } + const std::vector margin = fillToNearest(t, piece, sea, kSeaMargin); + L.homeSteps = stepsFrom(t, home); + for (int i = 0; i < n; ++i) + { + if (!filled[i] && !margin[i]) + continue; + const int p = piece[i]; + L.land[i] = 1; + if (p >= plazaLabel) + L.plaza[i] = 1; + else if (p >= pathLabel) + L.pathOf[i] = p - pathLabel; + else if (p >= courtLabel) + L.courtOf[i] = p - courtLabel; + else if (p >= farmLabel) { - innerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius - g.homeReach - 2, L.axis[k])); - outerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius + g.homeReach + 2, L.axis[k])); - anchors.push_back(L.homes[k]); + L.farmOf[i] = (p - farmLabel) % teams; + L.farmSet[i] = (p - farmLabel) / teams; } - std::vector owners; + else + L.homeOf[i] = p; + } + L.side.assign(n, -1); + for (int i = 0; i < n; ++i) + L.side[i] = piece[i] < 0 ? -1 + : piece[i] >= plazaLabel ? 0 + : piece[i] >= courtLabel ? 1 + (piece[i] - courtLabel) % teams + : 1 + teams + piece[i] % teams; + const std::vector borders = + labelBorders(t, L.side, [&](int i, int j) { return borderOpen(L, i, j); }); + for (int i = 0; i < n; ++i) + if (borders[i]) + L.wall[i] = 1; +} + +// THE FARM ROWS (layFarm): over each whole field inside its walls, along the colony's axis at the widths +// that yield most (bestFarmRows), with a sand cap closing the crop rows, a sand bridge across the water +// every kFarmBridgeSpacing tiles and, with `farm-plots`, a building plot in the middle. +void layFarmRows(Layout &L, const std::vector &sets, bool plots) +{ + const Torus &t = L.t; + const int n = t.size(), teams = L.g.teams; + const FarmPlot plot; + std::vector openSea(n, 0); + for (int i = 0; i < n; ++i) + openSea[i] = !L.land[i]; + const std::vector fromSea = stepsFrom(t, openSea); + for (const FarmSet &set : sets) for (int k = 0; k < teams; ++k) - owners.push_back(k); - // Rows run along every colony's axis; a colony whose axis falls on the diagonal gets more - // ground, so every colony's farms yield alike (farmYield). - const std::vector innerFields = - growFarmFields(t, L.land, L.homeOf, inner, innerSeeds, owners, anchors, kFarmGap, kNeckHalf, L.axis); - const std::vector outerFields = - growFarmFields(t, L.land, L.homeOf, outer, outerSeeds, owners, anchors, kFarmGap, kNeckHalf, L.axis); - L.farmOf.assign(n, -1); - L.farmSet.assign(n, -1); - L.farmSand.assign(n, 0); - L.farmPlot.assign(n, 0); - // Each set of farms, inner and outer, is laid only where every colony's field has room, so a - // small or crowded map loses a set for everyone rather than favouring anyone. - const double homeArea = kPi * g.homeR * g.homeR; - for (int set = 0; set < 2; ++set) { - const std::vector &fields = set == 0 ? innerFields : outerFields; - std::vector size(teams, 0); + std::vector region(n, 0); for (int i = 0; i < n; ++i) - if (fields[i] >= 0) - ++size[fields[i]]; - if (*std::min_element(size.begin(), size.end()) < kMinimumFarmShare * homeArea) - continue; - // The rows are laid once the gaps round the farm have filled in, over the whole walled field. - farmSets.push_back(set); + region[i] = L.farmOf[i] == k && L.farmSet[i] == set.set && !L.wall[i] && + (fromSea[i] < 0 || fromSea[i] > kFarmSeaRim); + TerrainSketch rows(n, GRASS); + L.farms.push_back(layFarm(rows, t, region, L.axis[k], set.origins[k], kFarmRim, + bestFarmRows(L.axis[k]), plots ? &plot : nullptr, + kFarmBridgeSpacing)); + L.farmColony.push_back(k); + const Farm &farm = L.farms.back(); for (int i = 0; i < n; ++i) - if (fields[i] >= 0 && L.homeOf[i] < 0) - { - L.farmOf[i] = fields[i]; - L.farmSet[i] = set; - L.land[i] = 1; - } - for (int k = 0; k < teams; ++k) - farmOrigins.push_back(set == 0 ? innerSeeds[k] : outerSeeds[k]); - } - } - // No water between the pieces: the sea between a home, its farms, the lanes, the courts and the plaza - // fills in from the homes and farms (fillToNearest), so the lanes, courts and plaza keep exactly the - // width they were drawn and every gap beside them becomes its colony's ground. A single line of stone - // then stands where two sides meet (labelBorders), on the home's or farm's side, so no wall narrows a - // lane and a tower behind it stands as close to what it shoots at as the wall allows. Sea too far - // from any home or farm keeps a thin margin of the nearest piece and its coast is sealed. Two borders - // stay open: a home's door onto its own corridor and every spoke's way into the plaza. - { - // Farm labels: one per colony per set of farms (inner, outer). - const int homeLabel = 0, farmLabel = teams, courtLabel = 3 * teams, pathLabel = 4 * teams, plazaLabel = 5 * teams; - std::vector piece(n, -1), grown(n, -1); - std::vector sea(n, 0), home(n, 0); - for (int i = 0; i < n; ++i) - { - piece[i] = L.wall[i] ? -1 - : L.homeOf[i] >= 0 ? homeLabel + L.homeOf[i] - : L.farmOf[i] >= 0 ? farmLabel + L.farmSet[i] * teams + L.farmOf[i] - : L.courtOf[i] >= 0 ? courtLabel + L.courtOf[i] - : L.pathOf[i] >= 0 ? pathLabel + L.pathOf[i] - : L.plaza[i] ? plazaLabel - : -1; - grown[i] = piece[i] < courtLabel ? piece[i] : -1; - sea[i] = !L.land[i]; - home[i] = L.homeOf[i] >= 0; - } - std::vector filled = fillToNearest(t, grown, sea, kFillReach); - for (int i = 0; i < n; ++i) - { - if (filled[i]) - piece[i] = grown[i]; - sea[i] = sea[i] && !filled[i]; - } - // What sea is left keeps a margin of land beside every piece, so its beach and coast wall never - // eat into a lane. - const std::vector margin = fillToNearest(t, piece, sea, kSeaMargin); - for (int i = 0; i < n; ++i) - filled[i] = filled[i] || margin[i]; - L.homeSteps = stepsFrom(t, home); - for (int i = 0; i < n; ++i) - { - if (!filled[i]) - continue; - const int p = piece[i]; - L.land[i] = 1; - if (p >= plazaLabel) - L.plaza[i] = 1; - else if (p >= pathLabel) - L.pathOf[i] = p - pathLabel; - else if (p >= courtLabel) - L.courtOf[i] = p - courtLabel; - else if (p >= farmLabel) { - L.farmOf[i] = (p - farmLabel) % teams; - L.farmSet[i] = (p - farmLabel) / teams; + if (farm.water[i]) + L.pond[i] = 1; + if (farm.sand[i]) + L.farmSand[i] = 1; } - else - L.homeOf[i] = p; } - // The sides the walls go between, numbered so each wall stands on the higher: the plaza (0), a - // colony's court and lanes (1 + colony), and its home and farms (1 + teams + colony). - std::vector &side = L.side; - side.assign(n, -1); - for (int i = 0; i < n; ++i) - side[i] = piece[i] < 0 ? -1 - : piece[i] >= plazaLabel ? 0 - : piece[i] >= courtLabel ? 1 + (piece[i] - courtLabel) % teams - : 1 + teams + piece[i] % teams; - const std::vector borders = - labelBorders(t, side, [&](int i, int j) { return borderOpen(L, i, j); }); - for (int i = 0; i < n; ++i) - if (borders[i]) - L.wall[i] = 1; - } - // The farms' rows (layFarm), over each whole field inside its walls, along the colony's axis at the - // widths that yield most (bestFarmRows), with just enough land round the water for a beach and a wall. +} + +// THE ROADS, finished: no road vertex within kRoadSeaGap of water (keepRoadInland), so its sand never +// carries a beach inland, and none touching a wall tile, since stone stands only on grass. +void finishRoads(Layout &L) +{ + const Torus &t = L.t; + const int n = t.size(); + std::vector water(n, 0); + for (int i = 0; i < n; ++i) + water[i] = !L.land[i] || L.pond[i]; + keepRoadInland(L.road, t, water, kRoadSeaGap); + for (int i = 0; i < n; ++i) + for (int dy = -1; dy <= 0 && L.road[i]; ++dy) + for (int dx = -1; dx <= 0; ++dx) + if (L.wall[t.at(i % t.w + dx, i / t.w + dy)]) + L.road[i] = 0; + L.roadTile = roadTiles(t, L.road); +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const CarouselOptions o(request); + Layout L; + L.t = Torus(1 << request.wDec, 1 << request.hDec); + const int n = L.t.size(); + L.g = geometryFor(request); + const Geometry &g = L.g; + if (!g.failure.empty()) { - const FarmPlot plot; - // Against the open sea a farm keeps a wider margin, for the beach, the sealed coast and the land - // between them and the water rows. - std::vector openSea(n, 0); - for (int i = 0; i < n; ++i) - openSea[i] = !L.land[i]; - const std::vector fromSea = stepsFrom(t, openSea); - for (size_t s = 0; s < farmSets.size(); ++s) - for (int k = 0; k < teams; ++k) - { - std::vector region(n, 0); - for (int i = 0; i < n; ++i) - region[i] = L.farmOf[i] == k && L.farmSet[i] == farmSets[s] && !L.wall[i] && - (fromSea[i] < 0 || fromSea[i] > kFarmSeaRim); - TerrainSketch rows(n, GRASS); - L.farms.push_back(layFarm(rows, t, region, L.axis[k], farmOrigins[s * teams + k], kFarmRim, - bestFarmRows(L.axis[k]), o.farmPlots ? &plot : nullptr, kFarmBridgeSpacing)); - L.farmColony.push_back(k); - const Farm &farm = L.farms.back(); - for (int i = 0; i < n; ++i) - { - if (farm.water[i]) - L.pond[i] = 1; - if (farm.sand[i]) - L.farmSand[i] = 1; - if (farm.plot[i]) - L.farmPlot[i] = 1; - } - } + L.failure = g.failure; + return L; } - // A road keeps well inside the land (keepRoadInland), so its sand never meets a beach, and no road - // vertex touches a wall tile, since stone only stands on grass. + L.cx = L.t.w / 2; + L.cy = L.t.h / 2; + L.phase = context.bounded("carousel-phase", 3600) / 3600.0 * 2 * kPi; + L.homeOf.assign(n, -1); + L.courtOf.assign(n, -1); + L.pathOf.assign(n, -1); + L.farmOf.assign(n, -1); + L.farmSet.assign(n, -1); + L.plaza.assign(n, 0); + L.land.assign(n, 0); + L.pond.assign(n, 0); + L.wall.assign(n, 0); + L.road.assign(n, 0); + L.farmSand.assign(n, 0); + // One outline for every home, turned to each home's axis; the court and plaza are true circles. + const RadialShape homeShape(g.homeR, kHomeRoughness, context, "carousel-home"); + const RadialShape courtShape(g.courtR, 0.0001, context, "carousel-court"); + const RadialShape plazaShape(g.plazaR, 0.0001, context, "carousel-plaza"); + const RadialShape plazaPondShape(g.plazaPondR, 0.0001, context, "carousel-plaza-pond"); + + drawLanes(L, homeShape); + stampPieces(L, homeShape, courtShape, plazaShape, plazaPondShape); + if (const std::string crowded = crowding(L); !crowded.empty()) { - std::vector water(n, 0); - for (int i = 0; i < n; ++i) - water[i] = !L.land[i] || L.pond[i]; - keepRoadInland(L.road, t, water, kRoadSeaGap); - for (int i = 0; i < n; ++i) - for (int dy = -1; dy <= 0 && L.road[i]; ++dy) - for (int dx = -1; dx <= 0; ++dx) - if (L.wall[t.at(i % t.w + dx, i / t.w + dy)]) - L.road[i] = 0; + L.failure = crowded; + return L; } - L.roadTile = roadTiles(t, L.road); + const std::vector farmSets = claimFarmFields(L); + fillAndWall(L); + layFarmRows(L, farmSets, o.farmPlots); + finishRoads(L); return L; } -// The sea: every water vertex but the ponds'. +// The sea's margin: the land a swimmer can stand on. Neither a road's sand nor a farm's is beach, so +// neither carries the margin inland. std::vector seaMargin(const Map &map, const Layout &L) { - // Neither a sand road nor a farm plot's sand ring is beach: neither may carry the sea's margin inland. std::vector notBeach = L.roadTile; - const std::vector plotSand = roadTiles(L.t, L.farmSand); + const std::vector farmSand = roadTiles(L.t, L.farmSand); for (int i = 0; i < L.t.size(); ++i) - notBeach[i] = notBeach[i] || plotSand[i]; + notBeach[i] = notBeach[i] || farmSand[i]; return MapGeneration::seaMargin(map, L.t, seaVertices(map, L.t, L.pond), notBeach); } -// The design's stone, once the terrain is laid: every coast sealed, and the wall between every court -// and the next home. +// The design's stone, once the terrain is laid: every coast of the remaining sea sealed, and every wall. std::vector stoneTiles(const Map &map, const Layout &L) { std::vector stone = sealCoasts(map, L.t, seaMargin(map, L), L.land); @@ -659,6 +674,8 @@ std::vector stoneTiles(const Map &map, const Layout &L) return stone; } +// The resources: every home's kit and scattered farmland, a fruit grove in every court, the plaza's +// orchard (fruit only, so nothing smothers it), and the farms' wheat and woodlots. void furnish(Map &map, const Layout &L, GenerationContext &context, const CarouselOptions &o, const std::vector &pads) { @@ -667,50 +684,46 @@ void furnish(Map &map, const Layout &L, GenerationContext &context, const Carous const int n = t.size(); const std::vector reserved = swarmSurroundings(t, context); const Fertility::Field fertility = Fertility::forMap(map, false); - // The wedge frame starts half a wedge before colony 0's axis, so a wedge's middle is its home. + // The wedge frame starts half a wedge before colony 0's axis, so a wedge's middle is its home and + // every home samples the same patches. const WedgeFrame wedges(t, L.phase - g.wedge / 2, g.teams); const WedgeField patch{wedges, PeriodicNoise(t.w, t.h, 12, context.stream("carousel-patch"))}; const WedgeField split{wedges, PeriodicNoise(t.w, t.h, 6, context.stream("carousel-split"))}; - // Nothing is planted within a few steps of a corridor or spoke, so no field grows across a door. std::vector paths(n, 0); for (int i = 0; i < n; ++i) paths[i] = L.pathOf[i] >= 0; const std::vector fromPaths = stepsFrom(t, paths); const auto free = [&](int i) { - return !L.road[i] && !L.roadTile[i] && !reserved[i] && !pads[i] && fromPaths[i] > kDoorClearance && - clearGround(map, i % t.w, i / t.w); + return !L.road[i] && !L.roadTile[i] && !reserved[i] && !pads[i] && + fromPaths[i] > kDoorClearance && clearGround(map, i % t.w, i / t.w); }; for (int k = 0; k < g.teams; ++k) { const auto eligible = [&](int i) { return L.homeOf[i] == k && free(i); }; - plantPondHomeKit(map, t, context, L.firstPond[k], L.axis[k], g.pondR, kHomeWheat, kHomeWood, - eligible); + plantHomeKit(map, t, context, L.homes[k], L.axis[k], g.homeR, kHomeWheat, kHomeWood, + eligible); furnishGround( map, t, context, fertility, eligible, patch, split, [&](int area) { return GroundAmounts{int(scaledCount(area * kHomeWheatShare / 100, o.wheat)), - int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, 0}; + int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, 0}; }, "carousel-home-stone", "carousel-home-fruit"); } - // The courts: one grove of the same fruit each, out on the court's far side from the plaza, clear of - // where its tower stands against the wall. const int courtFruit = CHERRY + int(context.bounded("carousel-court", 3)); if (scaledCount(1, o.fruit) > 0) for (int k = 0; k < g.teams; ++k) - plantRound(map, t, context, L.courts[k].x, L.courts[k].y, g.courtR * 0.45, {L.axis[k]}, courtFruit, - 1, 4, [&](int i) { return L.courtOf[i] == k && free(i); }); - // The plaza: only fruit, so nothing overgrows its groves - in every wedge the orchard's three groves - // on the pond's shore, halfway between one spoke's arrival and the next. + plantRound(map, t, context, L.courts[k].x, L.courts[k].y, g.courtR * 0.45, {L.axis[k]}, + courtFruit, 1, 4, [&](int i) { return L.courtOf[i] == k && free(i); }); + // The orchard's three groves in every wedge, on the pond's shore halfway between two spokes' arrivals. const auto plazaFree = [&](int i) { return L.plaza[i] && free(i); }; std::vector between; for (int k = 0; k < g.teams; ++k) between.push_back(std::min(L.axis[k] + g.wedge / 2, L.axis[k] + L.sweep) + g.wedge / 2); if (scaledCount(1, o.fruit) > 0) plantOrchard(map, t, context, L.cx, L.cy, g.plazaPondR + 4, between, 5, 5, 1, plazaFree); - // The farms: wheat along the water on every crop row and a small woodlot on one (plantFarm). for (size_t f = 0; f < L.farms.size(); ++f) { const Farm &farm = L.farms[f]; @@ -724,12 +737,10 @@ void furnish(Map &map, const Layout &L, GenerationContext &context, const Carous } } -// The towers every colony starts with and the open pads beside them (chooseTowerSites): the carousel's -// siege line. Every colony's sites stand in its own home, packed against the thin wall its home shares -// with the previous colony's court, and are chosen for how much of that colony's elbow - its court and -// the lanes within kElbowReach of it - they cover across the wall. So every colony shells one -// neighbour's way out while the other neighbour shells its own. With the control at 0 every site is an -// open pad. +// THE SIEGE LINE (chooseTowerSites): every colony's towers and pads stand in its own home, directly +// against stone and within kSiegeLine steps of a wall, chosen for how much of the previous colony's elbow +// - its court and lanes within kElbowReach - they cover across the wall. Nothing scores for covering the +// colony's own lanes: the design is attack, not defence. TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &context, const CarouselOptions &o, const std::vector &stone) { @@ -743,27 +754,25 @@ TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &c for (int i = 0; i < n; ++i) { const int x = i % t.w, y = i / t.w; - owner[i] = L.homeOf[i] >= 0 ? L.homeOf[i] + owner[i] = L.homeOf[i] >= 0 ? L.homeOf[i] : L.courtOf[i] >= 0 ? L.courtOf[i] : L.farmOf[i] >= 0 ? L.farmOf[i] : L.pathOf[i]; - buildable[i] = L.homeOf[i] >= 0 && fromWall[i] >= 0 && fromWall[i] <= kSiegeLine && map.isGrass(x, y) && - !stone[i] && !L.roadTile[i] && !reserved[i] && !map.isResource(x, y); - // The elbows: every court and the lanes near it. + buildable[i] = L.homeOf[i] >= 0 && fromWall[i] >= 0 && fromWall[i] <= kSiegeLine && + map.isGrass(x, y) && !stone[i] && !L.roadTile[i] && !reserved[i] && + !map.isResource(x, y); const int k = L.courtOf[i] >= 0 ? L.courtOf[i] : L.pathOf[i]; - target[i] = k >= 0 && !map.isWater(x, y) && !stone[i] && - std::hypot(t.offsetX(int(std::lround(L.courts[k].x)), x), - t.offsetY(int(std::lround(L.courts[k].y)), y)) <= g.courtR + kElbowReach; + target[i] = + k >= 0 && !map.isWater(x, y) && !stone[i] && + std::hypot(t.offsetX(int(std::lround(L.courts[k].x)), x), + t.offsetY(int(std::lround(L.courts[k].y)), y)) <= g.courtR + kElbowReach; } - TowerRequest request; - request.range = kTowerRange[std::clamp(o.towers - 1, 0, 2)]; - request.towers = o.towers > 0 ? o.towerCount : 0; - request.pads = o.towers > 0 ? kTowerPads : o.towerCount + kTowerPads; - request.spacing = kTowerSpacing; + TowerRequest request = startingTowerRequest(o.towers, o.towerCount, kTowerPads, kTowerSpacing); request.otherWeight = 1; request.ownWeight = 0; request.against = &stone; - return chooseTowerSites(t, owner, buildable, target, swarmSurroundings(t, context, 0), g.teams, request); + return chooseTowerSites(t, owner, buildable, target, swarmSurroundings(t, context, 0), g.teams, + request); } bool generate(Game &game, GenerationContext &context) @@ -805,47 +814,26 @@ bool generate(Game &game, GenerationContext &context) { std::vector ground(n, 0); for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && !stone[i] && !L.roadTile[i] && - map.isGrass(i % t.w, i / t.w); + ground[i] = + L.homeOf[i] == team && !stone[i] && !L.roadTile[i] && map.isGrass(i % t.w, i / t.w); return ground; }; - // The swarm stands in from the home's middle towards the plaza, with the ponds behind it. - const auto anchor = [&](int team) { return pondHomeAnchor(L.homes[team], L.axis[team], L.g.homeR); }; + const auto anchor = [&](int team) + { return homeSwarmSite(L.homes[team], L.axis[team], L.g.homeR); }; if (!settleColonies(game, context, "carousel-starts", home, anchor)) return false; + // No tower or pad may close a colony's way to the plaza's heart; every colony keeps as many as the + // fewest got, and must have at least one when towers were asked for. context.stage = "carousel towers"; TowerPlan towers = planTowers(map, L, context, o, stone); - // No site may close a colony's way through its court to the plaza (dropBlockingSites); a crowded map - // may not seat every tower either, so every colony keeps as many as the fewest got, as long as every - // colony has one. - { - std::vector open(n, 0), plazaHeart(n, 0); - for (int i = 0; i < n; ++i) - { - const int x = i % t.w, y = i / t.w; - open[i] = !map.isWater(x, y) && !stone[i] && map.getBuilding(x, y) == NOGBID; - plazaHeart[i] = L.plaza[i] && open[i] && std::hypot(i % t.w - L.cx, i / t.w - L.cy) < L.g.plazaPondR + 3; - } - std::vector> sources(teams); - const std::vector> goals(teams, plazaHeart); - for (int k = 0; k < teams; ++k) - for (int dy = -1; dy <= 4; ++dy) - for (int dx = -1; dx <= 4; ++dx) - if (dx < 0 || dy < 0 || dx > 3 || dy > 3) - sources[k].push_back(t.at(context.bootX[k] + dx, context.bootY[k] + dy)); - dropBlockingSites(t, towers, open, sources, goals); - } - if (evenTowerPlan(towers) < (o.towers > 0 && o.towerCount > 0 ? 1 : 0)) - { - context.detail = "a colony has no room for its towers"; - return false; - } - if (!raiseTowers(game, towers, std::clamp(o.towers - 1, 0, 2))) - { - context.detail = "a tower site no longer fits"; + std::vector plazaHeart(n, 0); + for (int i = 0; i < n; ++i) + plazaHeart[i] = + L.plaza[i] && std::hypot(i % t.w - L.cx, i / t.w - L.cy) < L.g.plazaPondR + 3; + if (!settleStartingTowers(game, context, towers, o.towers, o.towers > 0 && o.towerCount > 0, + &plazaHeart)) return false; - } const std::vector pads = towerFootprints(t, towers); context.stage = "carousel resources"; @@ -853,16 +841,17 @@ bool generate(Game &game, GenerationContext &context) const WedgeFrame algaeWedges(t, L.phase - L.g.wedge / 2, L.g.teams); seedAlgae(map, context, t, "carousel-algae", o.algae, AlgaeBand::shallows(1, 4), &algaeWedges); secureStartingCrops(game, context, t, 24, 32, 0, &stone); - reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, + &stone); clearFarmPlots(map, t, L.farms); - // Keep every colony's walk open, clearing only deposits on it: home to court, court to plaza. + // Keep every colony's walk open, clearing only deposits on it: from home to its court, and from the + // court to the orchard round the plaza's pond. context.stage = "carousel roads"; const std::vector> workers = unitTilesByTeam(map, teams); - // The walk from a court ends at the orchard round the plaza's pond, not at the plaza's edge. - std::vector plaza(n, 0); + std::vector heart(n, 0); for (int i = 0; i < n; ++i) - plaza[i] = L.plaza[i] && !map.isWater(i % t.w, i / t.w) && !stone[i] && + heart[i] = L.plaza[i] && !map.isWater(i % t.w, i / t.w) && !stone[i] && std::hypot(i % t.w - L.cx, i / t.w - L.cy) < L.g.plazaPondR + 3; for (int team = 0; team < teams; ++team) { @@ -871,20 +860,19 @@ bool generate(Game &game, GenerationContext &context) std::vector court(n, 0); std::vector courtTiles; for (int i = 0; i < n; ++i) - // The court's own coast wall is neither where a walk starts nor where it ends, or the - // walk would clear it. if (L.courtOf[i] == team && !stone[i] && !map.isWater(i % t.w, i / t.w)) { court[i] = 1; courtTiles.push_back(i); } if (!openRoad(map, t, workers[team], court, &stone) || - !openRoad(map, t, courtTiles, plaza, &stone)) + !openRoad(map, t, courtTiles, heart, &stone)) { - context.detail = "colony " + std::to_string(team) + " has no walk to its court and the plaza"; + context.detail = + "colony " + std::to_string(team) + " has no walk to its court and the plaza"; return false; } - } + } return true; } @@ -892,16 +880,23 @@ std::string validateRequest(const GenerationRequest &r) { if (r.nbTeams < 2) return "Carousels need at least two colonies."; - // The geometry's own reason stays in the candidate's diagnostics; the player sees one message - // that says what to change. - return geometryFor(r).failure.empty() ? "" : "The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies."; + // The geometry's own reason stays in the candidate's diagnostics; the player sees one message that + // says what to change. + return geometryFor(r).failure.empty() ? "" + : "The carousel does not fit this map; use a bigger map, " + "smaller homes or fewer colonies."; } -// Checked on the finished world against the rebuilt design: every designed stone stands; every -// colony walks to colony 0; nothing landing from the sea gets into any home, court, path or the -// plaza; with every corridor and spoke shut, no home, court or the plaza reaches any other (the -// court walls hold and every home has a single door); a tower in every court reaches the next home; -// and the colonies' walks to the plaza are even. +// Checked on the finished world against the rebuilt design: +// - every designed stone stands, and every colony walks to colony 0; +// - nothing landing from the sea gets onto any land (seaEntry); +// - 95% of every farm's crop land, and all of its plot, is a walk from its colony (farmReachable); +// - every border the design walls is stone: no two sides meet on open ground except at a door or a +// spoke's way into the plaza; +// - with every corridor and spoke shut, no home, farm, court or the plaza reaches another (pieceLeak, +// counting crops and buildings as passable, since they go in time); +// - from every home, a level-1 tower reaches the previous colony's court across the wall (the siege); +// - and the colonies' walks to the plaza are even. std::string validateWorld(const Game &game, const GenerationContext &context) { GenerationContext replay(context.request); @@ -926,26 +921,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (const int entry = seaEntry(map, t, margin, L.land); entry >= 0) return "Land at " + where(entry) + " can be reached from the sea."; - // Every piece on its own with the corridors and spokes shut. A piece is its ground inside the - // coast wall: the sand lane outside it is the sea's, and already checked above. - std::vector piece(n, -1); - for (int i = 0; i < n; ++i) - if (!margin[i]) - piece[i] = L.homeOf[i] >= 0 ? L.homeOf[i] - : L.farmOf[i] >= 0 ? L.farmOf[i] - : L.courtOf[i] >= 0 ? teams + L.courtOf[i] - : L.plaza[i] ? 2 * teams - : -1; - std::vector paths(n, 0); - for (int i = 0; i < n; ++i) - paths[i] = L.pathOf[i] >= 0; - // Every farm is joined to its home: nearly all of its crop land, and all of its plot, is a walk from - // its colony's workers. for (size_t f = 0; f < L.farms.size(); ++f) if (farmReachable(map, t, L.farms[f], walk.workers[L.farmColony[f]]) < kFarmReach) - return "Colony " + std::to_string(L.farmColony[f]) + " cannot walk into one of its farms."; - // Every border the design walls is stone in the finished world: no two sides meet on open ground but - // at a door or a lane's way into the plaza. + return "Colony " + std::to_string(L.farmColony[f]) + + " cannot walk into one of its farms."; + for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) { @@ -956,16 +936,31 @@ std::string validateWorld(const Game &game, const GenerationContext &context) for (int dx = -1; dx <= 1; ++dx) { const int j = t.at(x + dx, y + dy); - if (L.side[j] >= 0 && L.side[j] < L.side[i] && !stone[j] && !map.isWater(j % t.w, j / t.w) && - !margin[i] && !margin[j] && !borderOpen(L, i, j)) - return "The wall between two parts of the carousel has a gap at " + where(i) + "."; + if (L.side[j] >= 0 && L.side[j] < L.side[i] && !stone[j] && + !map.isWater(j % t.w, j / t.w) && !margin[i] && !margin[j] && + !borderOpen(L, i, j)) + return "The wall between two parts of the carousel has a gap at " + + where(i) + "."; } } + + // A piece is its ground off the sea's margin (the sand lane outside a sealed coast, checked above). + std::vector piece(n, -1); + std::vector paths(n, 0); + for (int i = 0; i < n; ++i) + { + if (!margin[i]) + piece[i] = L.homeOf[i] >= 0 ? L.homeOf[i] + : L.farmOf[i] >= 0 ? L.farmOf[i] + : L.courtOf[i] >= 0 ? teams + L.courtOf[i] + : L.plaza[i] ? 2 * teams + : -1; + paths[i] = L.pathOf[i] >= 0; + } if (const int leak = pieceLeak(map, t, piece, paths); leak >= 0) - return "With the corridors shut, " + where(leak) + " can still be reached from another part of the carousel."; + return "With the corridors shut, " + where(leak) + + " can still be reached from another part of the carousel."; - // The siege line: from the ground of every home next to the wall, a tower reaches the previous - // colony's court across it. for (int k = 0; k < teams; ++k) { std::vector court(n, 0), next(n, 0); @@ -973,12 +968,14 @@ std::string validateWorld(const Game &game, const GenerationContext &context) { const int x = i % t.w, y = i / t.w; court[i] = L.courtOf[i] == k && !map.isWater(x, y) && !stone[i]; - next[i] = L.homeOf[i] == (k + 1) % teams && map.isGrass(x, y) && !stone[i] && !map.isResource(x, y); + next[i] = L.homeOf[i] == (k + 1) % teams && map.isGrass(x, y) && !stone[i] && + !map.isResource(x, y); } const int reach = towerReach(t, next, court); if (reach > kTowerRange[kSiegeTower]) - return "No tower in home " + std::to_string((k + 1) % teams) + " reaches court " + std::to_string(k) + - " across the wall; it is " + std::to_string(reach) + " tiles away."; + return "No tower in home " + std::to_string((k + 1) % teams) + " reaches court " + + std::to_string(k) + " across the wall; it is " + std::to_string(reach) + + " tiles away."; } std::vector targets; @@ -1001,50 +998,51 @@ std::string validateWorld(const Game &game, const GenerationContext &context) CarouselOptions::CarouselOptions(const GenerationRequest &r) : homeSize(r.option("home-size")), corridorWidth(r.option("corridor-width")), - spokeWidth(r.option("spoke-width")), - courtSize(r.option("court-size")), courtWall(r.option("court-wall")), - plazaSize(r.option("plaza-size")), + spokeWidth(r.option("spoke-width")), courtSize(r.option("court-size")), + courtWall(r.option("court-wall")), plazaSize(r.option("plaza-size")), towers(r.option("starting-towers")), towerCount(r.option("tower-count")), - sandRoads(r.option("sand-roads") != 0), farmPlots(r.option("farm-plots") != 0), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) + sandRoads(r.option("sand-roads") != 0), farmPlots(r.option("farm-plots") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) { } GeneratorDefinition carouselDefinition() { - return {"carousel", - 22, - "Carousel", - 1, - false, - // Each home's radius and each court's as percentages of the standard; the corridors' width and - // the spokes' in tiles; the wall between a court and the next home in tiles; the plaza's - // radius as a share of the half side. - {{"home-size", "Home size", 60, 160, 10, 100, ControlGroup::Layout}, - {"corridor-width", "Corridor width", 3, 17, 2, 3, ControlGroup::Terrain}, - {"spoke-width", "Spoke width", 3, 17, 2, 3, ControlGroup::Terrain}, - {"court-size", "Court size", 20, 160, 10, 30, ControlGroup::Layout}, - {"court-wall", "Court wall", 1, 5, 1, 2, ControlGroup::Terrain}, - {"plaza-size", "Plaza size", 14, 34, 2, 24, ControlGroup::Layout}, - // The towers every colony starts with, all against its walls: their level (0 for none, just - // open pads) and how many. - {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, - {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, - // Off, the corridors and spokes are grass from coast wall to coast wall. - GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), - // On, every farm has a 10x4 clearing of grass ringed with sand in its middle, for a - // swarm or an inn. - GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), - // Every home's ambient fields, the plaza's farmland and outcrops, the courts' and the - // orchard's fruit, and the algae; every home's kit and the walls' stone are unscaled. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - validateRequest, - validateWorld}; + return { + "carousel", + 22, + "Carousel", + 1, + false, + // Each home's radius and each court's as percentages of the standard; the corridors' and the + // spokes' widths and the wall between a court and the next home in tiles; the plaza's radius as + // a share of the half side. The lanes and courts default tight: the siege only works in them. + {{"home-size", "Home size", 60, 160, 10, 100, ControlGroup::Layout}, + {"corridor-width", "Corridor width", 3, 17, 2, 3, ControlGroup::Terrain}, + {"spoke-width", "Spoke width", 3, 17, 2, 3, ControlGroup::Terrain}, + {"court-size", "Court size", 20, 160, 10, 30, ControlGroup::Layout}, + {"court-wall", "Court wall", 1, 5, 1, 2, ControlGroup::Terrain}, + {"plaza-size", "Plaza size", 14, 34, 2, 24, ControlGroup::Layout}, + // The towers every colony starts with, all against its walls: their level (0 for none, just + // open pads) and how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, + // Off, the lanes and the plaza are grass from wall to wall. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // On, every farm has a 10x4 clearing of grass ringed with sand in its middle, for a swarm or + // an inn. + GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), + // Every home's scattered fields, the farms' wheat and woodlots, the courts' and the orchard's + // fruit, and the algae; every home's kit, the walls' stone and the towers are unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; } diff --git a/src/map/generator/generators/SwitchbacksGenerator.cpp b/src/map/generator/generators/SwitchbacksGenerator.cpp index 4f3d1547f..2f9cc6b5d 100644 --- a/src/map/generator/generators/SwitchbacksGenerator.cpp +++ b/src/map/generator/generators/SwitchbacksGenerator.cpp @@ -28,37 +28,40 @@ using namespace MapGeneration; // Switchbacks: a plateau in the middle of the sea, and round it a ring of homes, each joined to the -// plateau by a mountain of solid stone with one trail cut through it. The trail zigzags: out of the -// home it runs along one side of the mountain, across to the other, back, and across again, one leg -// after another, before it climbs out onto the plateau. Walking from a home to the plateau takes -// several times the distance as the crow flies, but the walls between the legs are only a few tiles -// thick, and a defence tower shoots over stone (Building::findBestTarget scans square rings with no -// line of sight). So a colony that holds the plateau can build towers at the trail heads that reach -// back down the last legs, and a colony at home can cover its own first legs the same way: every -// attack walks the whole trail under fire from towers that ignore the zigzag. +// plateau by a mountain of solid stone with one trail cut through it. // -// Every coast is sealed, as City states' homes are: stone stands on every solid-grass tile that -// touches the sea's margin, so a swimmer can land on a beach but never get inside. The trails are the -// only way anywhere for the whole game. Each home's farms are its water (a map too small for farms gives it two ponds), inland and clear -// of the sea, for algae and for fields that regrow; the plateau has a pond and an orchard of all -// three fruits, the prize every trail climbs to. +// THE SHAPE. The trail zigzags: out of the home it runs along one side of the mountain, across to the +// other, back, and across again, one leg after another, before it climbs out onto the plateau. Walking +// from a home to the plateau takes several times the distance as the crow flies, but the walls between +// the legs are only `leg-wall` tiles thick (2 by default), and a defence tower shoots over stone. So the +// trail is a fortification: every colony's towers stand on its own trail, against the inner side of each +// wall (the side towards the middle of the map), and shoot across it at the next leg up, where +// attackers coming down from the plateau must pass. Every attack walks the whole zigzag under fire from +// towers that ignore it. // -// Everything is designed once in the wedge's frame, along the colony's axis and across it, and -// turned round the centre for every colony, so every colony's ground and trail are the same and the -// layout is fair for any colony count. The map stays round, in a circle on the shorter side, with sea -// filling a rectangle's ends. The design is a pure function of the request, so validateWorld rebuilds -// it and checks the finished world. +// THE WATER. Every coast is sealed: stone stands on every grass tile touching the sea's margin, so a +// swimmer can land on a beach but never get inside, and the trails are the only way anywhere for the +// whole game. On maps big enough, each home reaches two walled wheat farms, one on either flank, laid in +// rows that regrow best and shared out by equal yield (Farmland); they are the homes' water. A map too +// small for farms gives every home two ponds instead. The plateau has a pond and only fruit, the prize +// every trail climbs to, with no wheat or wood to smother it. +// +// HOW IT IS BUILT. Everything is designed once in the wedge's frame, along the colony's axis and across +// it, and turned round the centre for every colony, so every colony's ground and trail are the same and +// the layout is fair for any colony count. The map stays round, in a circle on the shorter side, with sea +// filling a rectangle's ends. The design is a pure function of the request, so validateWorld rebuilds it +// and checks the finished world. // // GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be cleared, -// so a mountain's walls are permanent and its trail is the only door; a tower scans round its -// footprint with no line of sight, which is what makes a thin wall between two legs a firing line; -// grass may never touch water, so a coast wall always stands a tile in from its sand lane; wheat and -// wood regrow only near water, which is why every home has its ponds and the plateau its pond; and -// fruit lets an inn pull hungry enemy units across, which makes the plateau's orchard worth the climb. +// so a mountain's walls are permanent and its trail is the only door; a tower scans round its footprint +// with no line of sight (BuildingUtils::turretScanTile), which is what makes a thin wall between two legs +// a firing line; grass may never touch water, so a coast wall always stands a tile in from its sand lane; +// wheat and wood regrow only near water, which is why every home has farms or ponds; and fruit lets an +// inn pull hungry enemy units across, which makes the plateau's orchard worth the climb. // -// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a plateau 28 tiles in radius; mountains 40 tiles -// wide and 40 deep, each with a trail 7 wide making three legs 26 tiles long, 10 tiles apart with 3 -// tiles of stone between them; homes 24 tiles in radius on the rim. +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a plateau 28 tiles in radius; mountains about 40 +// tiles wide, each with a trail of several legs about 26 tiles long and 2 tiles of stone between them; +// homes 24 tiles in radius on the rim, each with a farm on either flank; three towers per colony. namespace { @@ -72,14 +75,9 @@ constexpr double kHomeShare = 0.2; constexpr double kMinimumHome = 14; constexpr double kHomeArcShare = 0.2; constexpr double kHomeRoughness = 0.12; -// A home needs room for its ponds, their gap to the sea and this much more for the swarm. -constexpr double kSwarmRoom = 4; -// A pond keeps this much land between its water and the coast, so its beach never meets the sea's. -constexpr double kLakeSeaGap = 5; -constexpr double kPondShare = 0.18; -constexpr double kMinimumPond = 2.5; -constexpr double kMaximumPond = 7; +// The plateau's pond as a share of the plateau's radius, and the land kept round it. constexpr double kPlateauPondShare = 0.25; +constexpr double kPlateauPondShore = 5; // How far a small map narrows its trail and walls (see geometryFor), and the narrowest of each. constexpr double kMinimumScale = 0.72; constexpr int kMinimumTrail = 5; @@ -93,10 +91,11 @@ constexpr double kSideWall = 3; constexpr double kSpanShare = 0.1; // Water kept between two mountains: enough for a beach and a coast wall on each. constexpr double kPieceGap = 4; -// Every home starts identical: fixed wheat and wood beside its first pond, stone behind it, unscaled. +// Every home's starter kit: this much wheat and wood beside the swarm, unscaled; no stone, since the +// mountains are stone. constexpr int kHomeWheat = 30; constexpr int kHomeWood = 30; -// Ambient farmland on a home's ground, as percentages of their tiles at 100. +// A home's scattered farmland, as percentages of its tiles at 100% wheat and wood. constexpr int kHomeWheatShare = 4; constexpr int kHomeWoodShare = 2; // Deposits keep this many steps from every trail, so no field grows across a trail's mouth. @@ -105,16 +104,17 @@ constexpr int kTrailClearance = 6; constexpr int kRoadSeaGap = 5; // The most the colonies' walks to the plateau may differ. constexpr int kWalkSpread = 12; -// The farms: water kept between a farm and any other land, the neck joining it to its home, the land -// a farm keeps between its water rows and its coast, the smallest farm worth having as a share of the -// home's area, and the crops planted on its crop rows at 100. +// The farms: water kept between a farm and any other land, and the neck joining it to its home. constexpr int kFarmGap = 3; -// The smallest half side that has farms. -constexpr int kFarmMinimumHalf = 100; constexpr double kNeckHalf = 8; +// The smallest half side that has farms: below it the sea between the mountains is too narrow for farms +// that stay joined to their homes, and every home gets ponds instead. +constexpr int kFarmMinimumHalf = 100; // The least share of a farm's crop land its colony must be able to walk to. constexpr double kFarmReach = 0.95; -// How far apart, along a farm's rows, the sand bridges across its water rows stand. +// How far apart, along a farm's rows, the sand bridges across its water rows stand; the land a farm keeps +// between its water rows and its coast (a beach, a coast wall and the sand cap); and the smallest farm +// worth having, as a share of the home's area. constexpr int kFarmBridgeSpacing = 16; constexpr int kFarmRim = 6; constexpr double kMinimumFarmShare = 0.5; @@ -125,7 +125,6 @@ constexpr int kFarmWoodShare = 3; // Open 2x2 pads beside every colony's `tower-count` towers, and the spacing between sites. constexpr int kTowerPads = 4; constexpr int kTowerSpacing = 4; -// A tower keeps this many steps from water and stone on every side, so it never closes a passage. // Tiles either side of a trail's centre line kept free of towers, the walkway attackers and workers use. constexpr int kWalkway = 1; // A tower hugs a wall: rock within this many tiles of its footprint towards the middle of the map. @@ -136,12 +135,12 @@ constexpr int kCoverTower = 1; struct Geometry { - int teams = 0, half = 0, legs = 0, ponds = 1; + int teams = 0, half = 0, legs = 0, ponds = 0; double wedge = 0, outer = 0; double trailHalf = 0, narrowestHalf = 0, legWall = 0, pitch = 0, span = 0, blockHalf = 0; double plateauR = 0, plateauPondR = 0; double innerLeg = 0, outerLeg = 0, blockOut = 0; // along the axis from the centre - double homeR = 0, homeReach = 0, homeRadius = 0, pondR = 0; + double homeR = 0, homeReach = 0, homeRadius = 0; bool roads = true; std::string failure; }; @@ -176,7 +175,8 @@ Geometry geometryFor(const GenerationRequest &r) // square root of its size, down to a trail still wide enough for its road and a wall two tiles // thick, so a 128-tile map still fits a zigzag. const double narrow = std::clamp(std::sqrt(g.half / 128.0), kMinimumScale, 1.0); - const int trailWidth = std::max(kMinimumTrail, 2 * int(std::lround((o.trailWidth * narrow - 1) / 2)) + 1); + const int trailWidth = + std::max(kMinimumTrail, 2 * int(std::lround((o.trailWidth * narrow - 1) / 2)) + 1); g.trailHalf = trailWidth / 2.0; g.narrowestHalf = g.trailHalf; g.legWall = std::max(kMinimumWall, int(std::lround(o.legWall * narrow))); @@ -185,9 +185,10 @@ Geometry geometryFor(const GenerationRequest &r) // The plateau is a share of the half side, but on a crowded ring it grows until the mountains // fit round it with water between them at their innermost legs. const double halfAngle = std::min(g.wedge / 2, 1.5); - g.plateauR = std::max(g.half * o.plateauSize / 100.0, - (g.blockHalf + kPieceGap) / std::tan(halfAngle) - (g.trailHalf + g.legWall + 1)); - g.plateauPondR = std::max(kMinimumPond, kPlateauPondShare * g.plateauR); + g.plateauR = + std::max(g.half * o.plateauSize / 100.0, + (g.blockHalf + kPieceGap) / std::tan(halfAngle) - (g.trailHalf + g.legWall + 1)); + g.plateauPondR = std::max(kHomeSmallestPond, kPlateauPondShare * g.plateauR); // Homes stand on the rim, as big as their share of the ring allows. The mountain fills the ground // between the plateau and the homes with as many legs as fit a pitch apart, the innermost a wall's // thickness of stone from the plateau and the outermost the same from the home, so a bigger map @@ -197,28 +198,30 @@ Geometry geometryFor(const GenerationRequest &r) const double end = g.trailHalf + g.legWall + 1; // However its rough outline falls, a home reaches in no further than its wobbled reach. const double mountain = (g.outer - g.plateauR - 2 * end - (2 * g.trailHalf + g.legWall)) / 2; - const double reach = std::min({std::max(kMinimumHome, kHomeShare * g.half) * (1 + kHomeRoughness), - ringShare * g.outer / (1 + ringShare), mountain}); + const double reach = + std::min({std::max(kMinimumHome, kHomeShare * g.half) * (1 + kHomeRoughness), + ringShare * g.outer / (1 + ringShare), mountain}); g.homeReach = reach * o.homeSize / 100.0; g.homeR = g.homeReach / (1 + kHomeRoughness); g.homeRadius = g.outer - g.homeReach; layLegs(g, g.homeRadius - g.homeR); g.blockOut = g.homeRadius; - g.pondR = std::clamp(kPondShare * g.homeR, kMinimumPond, kMaximumPond); // A home's farms are its water; a map too small for farms gives every home two ponds instead. g.ponds = g.half >= kFarmMinimumHalf ? 0 : 2; g.roads = o.sandRoads; - // A home must hold its swarm, its kit and any ponds: at least what a home with a pond needs. - if (g.homeR < g.pondR + kLakeSeaGap + kSwarmRoom) + if (!homeHasRoom(g.homeR)) g.failure = "Too many colonies for this map: the homes are too small."; else if (g.legs < kMinimumLegs) - g.failure = "No room for a trail on this map: use a bigger map, a smaller plateau or a narrower trail."; - else if (g.plateauR - g.plateauPondR < kLakeSeaGap + 3) + g.failure = "No room for a trail on this map: use a bigger map, a smaller plateau or a " + "narrower trail."; + else if (g.plateauR - g.plateauPondR < kPlateauPondShore + 3) g.failure = "The plateau is too small for its pond on this map."; // Measured at the trail's narrowest width, as the plateau was sized: trails widened to use up a // mountain's depth are trimmed to their wedges near the plateau, and the validator checks the rest. - else if (g.innerLeg * std::tan(std::min(g.wedge / 2, 1.5)) < g.span + g.narrowestHalf + kSideWall + kPieceGap) - g.failure = "Too many colonies for this map: the mountains crowd each other at the plateau."; + else if (g.innerLeg * std::tan(std::min(g.wedge / 2, 1.5)) < + g.span + g.narrowestHalf + kSideWall + kPieceGap) + g.failure = + "Too many colonies for this map: the mountains crowd each other at the plateau."; return g; } @@ -227,15 +230,18 @@ struct Layout Torus t{1, 1}; Geometry g; double phase = 0, cx = 0, cy = 0; - std::vector homeOf, blockOf, trailOf, legOf; // legOf: which leg (0 first) a trail tile is on + std::vector homeOf, blockOf, trailOf, + legOf; // legOf: which leg (0 first) a trail tile is on std::vector farmOf, farmColony; std::vector farms; - std::vector farmSand, farmPlot; // the farms' building plots: sand rings, grass + std::vector farmSand; // all the farms' sand: plot rings, bridges and caps std::vector plateau, land, pond, stone, road, roadTile; std::vector axis; - std::vector frames; - std::vector> trails; // every colony's trail as drawn // every colony's frame: along its axis from the centre, and across - std::vector homes, firstPond, summit; // summit: where each trail reaches the plateau + std::vector + frames; // every colony's frame: along its axis from the centre, and across + std::vector> trails; // every colony's trail as drawn + std::vector homes, kitCentre, + summit; // kitCentre: stampRoundHome's; summit: where each trail reaches the plateau std::string failure; }; @@ -279,17 +285,19 @@ Layout design(const GenerationRequest &request, GenerationContext &context) layLegs(L.g, g.homeRadius - homeShape.radiusAt(kPi)); if (L.g.legs < kMinimumLegs) { - L.failure = "No room for a trail on this map: use a bigger map, a smaller plateau or a narrower trail."; + L.failure = "No room for a trail on this map: use a bigger map, a smaller plateau or a " + "narrower trail."; return L; } - const RadialShape pondShape(g.pondR, 0.2, context, "switchbacks-pond"); + const RadialShape pondShape(homePondRadius(g.homeR), 0.2, context, "switchbacks-pond"); const RadialShape plateauShape(g.plateauR, 0.0001, context, "switchbacks-plateau"); const RadialShape plateauPondShape(g.plateauPondR, 0.0001, context, "switchbacks-plateau-pond"); // The mountains: a straight block along each axis from inside the plateau to inside the home. The // plateau is round and the block square, so the block starts where the plateau's edge meets the // block's sides, and no strip of sea is left at its corners. - const double blockIn = std::sqrt(std::max(0.0, g.plateauR * g.plateauR - g.blockHalf * g.blockHalf)) - 2; + const double blockIn = + std::sqrt(std::max(0.0, g.plateauR * g.plateauR - g.blockHalf * g.blockHalf)) - 2; const WedgeFrame wedges(t, L.phase - g.wedge / 2, teams); const double halfAngle = std::min(g.wedge / 2, 1.5); for (int i = 0; i < n; ++i) @@ -334,9 +342,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.homes.push_back(polarPoint(L.cx, L.cy, g.homeRadius, L.axis[k])); } for (int k = 0; k < teams; ++k) - L.firstPond.push_back(stampPondHome(t, L.homes[k], L.axis[k], homeShape, pondShape, - {g.homeR, g.pondR, kLakeSeaGap, g.ponds}, L.pond, - [&](int i) { L.homeOf[i] = k; })); + L.kitCentre.push_back(stampRoundHome(t, L.homes[k], L.axis[k], homeShape, g.homeR, g.ponds, + &pondShape, L.pond, [&](int i) { L.homeOf[i] = k; })); fillShape(L.plateau, t, L.cx, L.cy, plateauShape); fillShape(L.pond, t, L.cx, L.cy, plateauPondShape); for (int i = 0; i < n; ++i) @@ -378,12 +385,11 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } } // The farms (growFarmFields): every home reaches out sideways, across its axis, to a farm on either - // flank, and the farms share the open sea between the mountains and round the rim equally. Their - // rows run across the axis at the widths that yield most (bestFarmRows), and every farm's coast is - // sealed like the rest. + // flank, and the farms share the open sea between the mountains and round the rim by equal yield. Their + // rows run across the axis at the widths that yield most (bestFarmRows), with sand caps, bridges and a + // building plot (layFarm), and every farm's coast is sealed like the rest. L.farmOf.assign(n, -1); L.farmSand.assign(n, 0); - L.farmPlot.assign(n, 0); // A map under kFarmMinimumHalf across its shorter half has no room between its mountains for farms // that stay joined to their homes; it has none. if (g.half >= kFarmMinimumHalf) @@ -406,8 +412,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) owners.push_back(int(f) / 2); angles.push_back(L.axis[f / 2] + kPi / 2); } - const std::vector fields = - growFarmFields(t, L.land, L.homeOf, everywhere, seeds, owners, anchors, kFarmGap, kNeckHalf, angles); + const std::vector fields = growFarmFields( + t, L.land, L.homeOf, everywhere, seeds, owners, anchors, kFarmGap, kNeckHalf, angles); const double homeArea = kPi * g.homeR * g.homeR; for (int f = 0; f < 2 * teams; ++f) { @@ -422,12 +428,15 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } if (size < kMinimumFarmShare * homeArea) { - L.failure = "Too many colonies for this map: there is no room for every colony's farms."; + L.failure = + "Too many colonies for this map: there is no room for every colony's farms."; return L; } const double across = L.axis[k] + kPi / 2; TerrainSketch rows(n, GRASS); - L.farms.push_back(layFarm(rows, t, region, across, seeds[f], kFarmRim, bestFarmRows(across), o.farmPlots ? &plot : nullptr, kFarmBridgeSpacing)); + L.farms.push_back(layFarm(rows, t, region, across, seeds[f], kFarmRim, + bestFarmRows(across), o.farmPlots ? &plot : nullptr, + kFarmBridgeSpacing)); L.farmColony.push_back(k); for (int i = 0; i < n; ++i) if (region[i] && L.homeOf[i] < 0) @@ -438,8 +447,6 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.pond[i] = 1; if (L.farms.back().sand[i]) L.farmSand[i] = 1; - if (L.farms.back().plot[i]) - L.farmPlot[i] = 1; } } } @@ -483,7 +490,8 @@ void furnish(Map &map, const Layout &L, GenerationContext &context, const Switch const std::vector reserved = swarmSurroundings(t, context); const Fertility::Field fertility = Fertility::forMap(map, false); const WedgeFrame wedges(t, L.phase - g.wedge / 2, g.teams); - const WedgeField patch{wedges, PeriodicNoise(t.w, t.h, 12, context.stream("switchbacks-patch"))}; + const WedgeField patch{wedges, + PeriodicNoise(t.w, t.h, 12, context.stream("switchbacks-patch"))}; const WedgeField split{wedges, PeriodicNoise(t.w, t.h, 6, context.stream("switchbacks-split"))}; std::vector trails(n, 0); for (int i = 0; i < n; ++i) @@ -497,15 +505,15 @@ void furnish(Map &map, const Layout &L, GenerationContext &context, const Switch for (int k = 0; k < g.teams; ++k) { const auto eligible = [&](int i) { return L.homeOf[i] == k && free(i); }; - plantPondHomeKit(map, t, context, L.firstPond[k], L.axis[k], g.pondR, kHomeWheat, kHomeWood, - eligible); + plantHomeKit(map, t, context, L.kitCentre[k], L.axis[k], g.homeR, kHomeWheat, kHomeWood, + eligible); furnishGround( map, t, context, fertility, eligible, patch, split, [&](int area) { return GroundAmounts{int(scaledCount(area * kHomeWheatShare / 100, o.wheat)), - int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, - int(scaledCount(1, o.fruit))}; + int(scaledCount(area * kHomeWoodShare / 100, o.wood)), 0, + int(scaledCount(1, o.fruit))}; }, "switchbacks-home-stone", "switchbacks-home-fruit"); } @@ -517,7 +525,8 @@ void furnish(Map &map, const Layout &L, GenerationContext &context, const Switch std::vector between; for (int k = 0; k < g.teams; ++k) between.push_back(L.axis[k] + g.wedge / 2); - plantOrchard(map, t, context, L.cx, L.cy, g.plateauPondR + 4, between, 5, 5, 1, plateauFree); + plantOrchard(map, t, context, L.cx, L.cy, g.plateauPondR + 4, between, 5, 5, 1, + plateauFree); } // The farms: wheat along the water on every crop row and a small woodlot on one (plantFarm). for (size_t f = 0; f < L.farms.size(); ++f) @@ -558,7 +567,8 @@ TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &c const double dx = t.offsetX(i % t.w, int(L.cx)), dy = t.offsetY(i / t.w, int(L.cy)); const double d = std::max(1.0, std::hypot(dx, dy)); for (int step = 1; step <= kWallHug; ++step) - if (L.stone[t.at(int(std::lround(i % t.w + dx / d * step)), int(std::lround(i / t.w + dy / d * step)))]) + if (L.stone[t.at(int(std::lround(i % t.w + dx / d * step)), + int(std::lround(i / t.w + dy / d * step)))]) return true; return false; }; @@ -568,19 +578,17 @@ TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &c { const int x = i % t.w, y = i / t.w; owner[i] = L.homeOf[i] >= 0 ? L.homeOf[i] : L.farmOf[i] >= 0 ? L.farmOf[i] : L.trailOf[i]; - buildable[i] = L.trailOf[i] >= 0 && map.isGrass(x, y) && !stone[i] && !L.roadTile[i] && !reserved[i] && - (fromCentre[i] < 0 || fromCentre[i] > kWalkway) && !map.isResource(x, y) && facesInwardWall(i); + buildable[i] = L.trailOf[i] >= 0 && map.isGrass(x, y) && !stone[i] && !L.roadTile[i] && + !reserved[i] && (fromCentre[i] < 0 || fromCentre[i] > kWalkway) && + !map.isResource(x, y) && facesInwardWall(i); target[i] = L.trailOf[i] >= 0 && !map.isWater(x, y) && !stone[i]; } - TowerRequest request; - request.range = kTowerRange[std::clamp(o.towers - 1, 0, 2)]; - request.towers = o.towers > 0 ? o.towerCount : 0; - request.pads = o.towers > 0 ? kTowerPads : o.towerCount + kTowerPads; - request.spacing = kTowerSpacing; + TowerRequest request = startingTowerRequest(o.towers, o.towerCount, kTowerPads, kTowerSpacing); request.otherWeight = 0; request.ownWeight = 1; request.against = &stone; - return chooseTowerSites(t, owner, buildable, target, swarmSurroundings(t, context, 0), L.g.teams, request); + return chooseTowerSites(t, owner, buildable, target, swarmSurroundings(t, context, 0), + L.g.teams, request); } bool generate(Game &game, GenerationContext &context) @@ -622,51 +630,36 @@ bool generate(Game &game, GenerationContext &context) { std::vector ground(n, 0); for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && !stone[i] && !L.roadTile[i] && - map.isGrass(i % t.w, i / t.w); + ground[i] = + L.homeOf[i] == team && !stone[i] && !L.roadTile[i] && map.isGrass(i % t.w, i / t.w); return ground; }; - const auto anchor = [&](int team) { return pondHomeAnchor(L.homes[team], L.axis[team], L.g.homeR); }; + const auto anchor = [&](int team) + { return homeSwarmSite(L.homes[team], L.axis[team], L.g.homeR); }; if (!settleColonies(game, context, "switchbacks-starts", home, anchor)) return false; context.stage = "switchbacks towers"; TowerPlan towers = planTowers(map, L, context, o, stone); - // No site may close a colony's trail to the plateau (dropBlockingSites); a trail may not seat every - // tower either, so every colony then keeps as many as the fewest got, and a trail too narrow for any - // gets none. - { - std::vector open(n, 0), heart(n, 0); - for (int i = 0; i < n; ++i) - { - const int x = i % t.w, y = i / t.w; - open[i] = !map.isWater(x, y) && !stone[i] && map.getBuilding(x, y) == NOGBID; - heart[i] = L.plateau[i] && open[i] && - std::hypot(t.offsetX(int(L.cx), x), t.offsetY(int(L.cy), y)) < L.g.plateauPondR + 3; - } - std::vector> sources(teams); - const std::vector> goals(teams, heart); - for (int k = 0; k < teams; ++k) - for (int dy = -1; dy <= 4; ++dy) - for (int dx = -1; dx <= 4; ++dx) - if (dx < 0 || dy < 0 || dx > 3 || dy > 3) - sources[k].push_back(t.at(context.bootX[k] + dx, context.bootY[k] + dy)); - dropBlockingSites(t, towers, open, sources, goals); - } - evenTowerPlan(towers); - if (!raiseTowers(game, towers, std::clamp(o.towers - 1, 0, 2))) - { - context.detail = "a tower site no longer fits"; + // No tower or pad may close a colony's trail to the plateau's heart; every colony keeps as many as the + // fewest got, and a trail too narrow for any tower simply has none. + std::vector plateauHeart(n, 0); + for (int i = 0; i < n; ++i) + plateauHeart[i] = L.plateau[i] && + std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) < + L.g.plateauPondR + 3; + if (!settleStartingTowers(game, context, towers, o.towers, false, &plateauHeart)) return false; - } const std::vector pads = towerFootprints(t, towers); context.stage = "switchbacks resources"; furnish(map, L, context, o, pads); const WedgeFrame algaeWedges(t, L.phase - L.g.wedge / 2, L.g.teams); - seedAlgae(map, context, t, "switchbacks-algae", o.algae, AlgaeBand::shallows(1, 4), &algaeWedges); + seedAlgae(map, context, t, "switchbacks-algae", o.algae, AlgaeBand::shallows(1, 4), + &algaeWedges); secureStartingCrops(game, context, t, 24, 32, 0, &stone); - reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, + &stone); clearFarmPlots(map, t, L.farms); // Keep every trail open, clearing only deposits on it, from each home to the plateau's pond shore. @@ -691,7 +684,10 @@ std::string validateRequest(const GenerationRequest &r) return "Switchbacks need at least two colonies."; // The geometry's own reason stays in the candidate's diagnostics; the player sees one message // that says what to change. - return geometryFor(r).failure.empty() ? "" : "The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies."; + return geometryFor(r).failure.empty() + ? "" + : "The switchbacks do not fit this map; use a bigger map, a smaller plateau, " + "narrower trails or fewer colonies."; } // Checked on the finished world against the rebuilt design: every designed stone stands; every colony @@ -729,7 +725,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) for (int i = 0; i < n; ++i) { if (!margin[i]) - piece[i] = L.homeOf[i] >= 0 ? L.homeOf[i] : L.farmOf[i] >= 0 ? L.farmOf[i] : L.plateau[i] ? teams : -1; + piece[i] = L.homeOf[i] >= 0 ? L.homeOf[i] + : L.farmOf[i] >= 0 ? L.farmOf[i] + : L.plateau[i] ? teams + : -1; trails[i] = L.trailOf[i] >= 0; middle[i] = L.legOf[i] == g.legs / 2; } @@ -737,11 +736,14 @@ std::string validateWorld(const Game &game, const GenerationContext &context) // its colony's workers. for (size_t f = 0; f < L.farms.size(); ++f) if (farmReachable(map, t, L.farms[f], walk.workers[L.farmColony[f]]) < kFarmReach) - return "Colony " + std::to_string(L.farmColony[f]) + " cannot walk into one of its farms."; + return "Colony " + std::to_string(L.farmColony[f]) + + " cannot walk into one of its farms."; if (const int leak = pieceLeak(map, t, piece, trails); leak >= 0) - return "With the trails shut, " + where(leak) + " can still be reached from another part of the map."; + return "With the trails shut, " + where(leak) + + " can still be reached from another part of the map."; if (const int leak = pieceLeak(map, t, piece, middle); leak >= 0) - return "With the middle legs shut, " + where(leak) + " can still be reached: a wall between legs has a gap."; + return "With the middle legs shut, " + where(leak) + + " can still be reached: a wall between legs has a gap."; for (int k = 0; k < teams; ++k) { @@ -791,36 +793,37 @@ SwitchbacksOptions::SwitchbacksOptions(const GenerationRequest &r) GeneratorDefinition switchbacksDefinition() { - return {"switchbacks", - 24, - "Switchbacks", - 1, - false, - // The trail's width and the stone between its legs in tiles; the plateau's radius as a - // share of the half side (the mountains fill the rest with as many legs as fit); each home's - // radius as a percentage of the standard. - {{"trail-width", "Trail width", 5, 9, 2, 7, ControlGroup::Terrain}, - {"leg-wall", "Wall between legs", 2, 6, 1, 2, ControlGroup::Terrain}, - {"plateau-size", "Plateau size", 14, 34, 2, 22, ControlGroup::Layout}, - {"home-size", "Home size", 60, 160, 10, 100, ControlGroup::Layout}, - // The towers every colony starts with, all against its walls: their level (0 for none, just - // open pads) and how many. - {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, - {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, - // Off, the trails are grass from wall to wall. - GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), - // On, every farm has a 10x4 clearing of grass ringed with sand in its middle, for a - // swarm or an inn. - GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), - // Every home's ambient fields and grove, the plateau's farmland and orchard, and the - // algae; every home's kit and the mountains' stone are unscaled. - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - validateRequest, - validateWorld}; + return { + "switchbacks", + 24, + "Switchbacks", + 1, + false, + // The trail's width and the stone between its legs in tiles; the plateau's radius as a + // share of the half side (the mountains fill the rest with as many legs as fit); each home's + // radius as a percentage of the standard. + {{"trail-width", "Trail width", 5, 9, 2, 7, ControlGroup::Terrain}, + {"leg-wall", "Wall between legs", 2, 6, 1, 2, ControlGroup::Terrain}, + {"plateau-size", "Plateau size", 14, 34, 2, 22, ControlGroup::Layout}, + {"home-size", "Home size", 60, 160, 10, 100, ControlGroup::Layout}, + // The towers every colony starts with, all against its walls: their level (0 for none, just + // open pads) and how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, + // Off, the trails are grass from wall to wall. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // On, every farm has a 10x4 clearing of grass ringed with sand in its middle, for a + // swarm or an inn. + GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), + // Every home's scattered fields and grove, the farms' wheat and woodlots, the plateau's + // orchard, and the algae; every home's kit, the mountains' stone and the towers are unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + validateRequest, + validateWorld}; } diff --git a/src/map/generator/shared/Farmland.cpp b/src/map/generator/shared/Farmland.cpp index 9bdff2f98..873dcf487 100644 --- a/src/map/generator/shared/Farmland.cpp +++ b/src/map/generator/shared/Farmland.cpp @@ -142,9 +142,11 @@ Farm layFarm(TerrainSketch &sketch, const Torus &t, const std::vectorwidth + margin; ++dx) { const int i = t.at(x0 + dx, y0 + dy); - const bool grass = dx >= 0 && dx <= plot->width && dy >= 0 && dy <= plot->height; - const bool ring = !grass && dx >= -plot->ring && dx <= plot->width + plot->ring && - dy >= -plot->ring && dy <= plot->height + plot->ring; + const bool grass = + dx >= 0 && dx <= plot->width && dy >= 0 && dy <= plot->height; + const bool ring = !grass && dx >= -plot->ring && + dx <= plot->width + plot->ring && dy >= -plot->ring && + dy <= plot->height + plot->ring; // The clearing trumps the rows and bridges alike: its grass is grass. if (farm.water[i] || (grass && farm.sand[i])) { @@ -172,10 +174,12 @@ Farm layFarm(TerrainSketch &sketch, const Torus &t, const std::vector growFarmFields(const Torus &t, const std::vector &occupied, - const std::vector &homeOf, const std::vector &area, - const std::vector &seeds, const std::vector &owners, - const std::vector &anchors, int gap, double neckHalfWidth, - const std::vector &rowAngles) + const std::vector &homeOf, + const std::vector &area, + const std::vector &seeds, + const std::vector &owners, + const std::vector &anchors, int gap, + double neckHalfWidth, const std::vector &rowAngles) { const int n = t.size(); // Open ground: not designed, allowed, and clear of all land that is no home by the gap. Homes' @@ -220,14 +224,16 @@ std::vector growFarmFields(const Torus &t, const std::vector } if (best < 0) continue; - snapped[s] = {double(sx + t.offsetX(sx, best % t.w)), double(sy + t.offsetY(sy, best / t.w))}; + snapped[s] = {double(sx + t.offsetX(sx, best % t.w)), + double(sy + t.offsetY(sy, best / t.w))}; starts[s].push_back(best); } std::vector worth; for (double angle : rowAngles) worth.push_back(farmYield(angle)); std::vector labels = - growTerritories(t, free, starts, [](int) { return 0; }, worth.size() == seeds.size() ? &worth : nullptr) + growTerritories( + t, free, starts, [](int) { return 0; }, worth.size() == seeds.size() ? &worth : nullptr) .labels; // Every field keeps the gap from every home but its own. int colonies = 0; @@ -240,7 +246,8 @@ std::vector growFarmFields(const Torus &t, const std::vector others[i] = homeOf[i] >= 0 && homeOf[i] != k; const std::vector fromOthers = stepsFrom(t, others); for (int i = 0; i < n; ++i) - if (labels[i] >= 0 && owners[labels[i]] == k && fromOthers[i] >= 0 && fromOthers[i] < gap) + if (labels[i] >= 0 && owners[labels[i]] == k && fromOthers[i] >= 0 && + fromOthers[i] < gap) labels[i] = -1; } separateTerritories(t, labels, gap); @@ -304,14 +311,16 @@ std::vector growFarmFields(const Torus &t, const std::vector for (int i = 0; i < n; ++i) // The neck keeps the same gap from land that is no home, and from every other field, as the // field does, so it never runs round the end of a wall or into someone else's farm. - if (neck[i] && !occupied[i] && labels[i] < 0 && (fromForeign[i] < 0 || fromForeign[i] >= gap) && + if (neck[i] && !occupied[i] && labels[i] < 0 && + (fromForeign[i] < 0 || fromForeign[i] >= gap) && (fromOtherFields[i] < 0 || fromOtherFields[i] >= gap)) labels[i] = int(s); } return labels; } -double farmReachable(const Map &map, const Torus &t, const Farm &farm, const std::vector &sources) +double farmReachable(const Map &map, const Torus &t, const Farm &farm, + const std::vector &sources) { const int n = t.size(); std::vector open(n, 0); @@ -319,8 +328,9 @@ double farmReachable(const Map &map, const Torus &t, const Farm &farm, const std { const int x = i % t.w, y = i / t.w; const bool deposit = map.isResource(x, y); - open[i] = !map.isWater(x, y) && map.getBuilding(x, y) == NOGBID && - (!deposit || map.getResource(x, y).type == CORN || map.getResource(x, y).type == WOOD); + open[i] = + !map.isWater(x, y) && map.getBuilding(x, y) == NOGBID && + (!deposit || map.getResource(x, y).type == CORN || map.getResource(x, y).type == WOOD); } const std::vector steps = stepsFrom(t, tileMask(t, sources), open); int land = 0, reached = 0; @@ -334,7 +344,6 @@ double farmReachable(const Map &map, const Torus &t, const Farm &farm, const std ++land; reached += steps[i] >= 0; } - if (getenv("TMP_FARM") && reached < land) fprintf(stderr, "farm land %d reached %d\n", land, reached); // TMP return land ? double(reached) / land : 0; } diff --git a/src/map/generator/shared/Farmland.h b/src/map/generator/shared/Farmland.h index 07f779129..f86a5954b 100644 --- a/src/map/generator/shared/Farmland.h +++ b/src/map/generator/shared/Farmland.h @@ -13,6 +13,13 @@ namespace MapGeneration // only when a random probe up to 15 tiles away lands on water and the opposite probe does not land // on sand (Map::growResources), so a crop row next to water yields steadily, and a farm's yield is // set by how wide its crop and water rows are. +// +// A farm is built in three steps a generator composes with its own walls: growFarmFields shares open +// ground out into fields joined to their homes (by equal yield for their rows' angles), layFarm lays +// the rows over a field, and plantFarm plants them once the terrain is written. Around a laid farm, +// sand does three jobs: a cap ring closes the crop rows so wheat and wood never spread into the ground +// round the farm; bridges cross the water rows so workers need not walk round a long row; and a ring +// round the building plot keeps crops off it. Walls round a farm are the map's business, not the farm's. /// The widths of a farm's crop rows and water rows, measured across the rows in tiles. struct FarmRows @@ -45,8 +52,8 @@ struct Farm std::vector water; std::vector row; int rows = 0; // how many water rows the region holds - // The building plot, when one was asked for and fits: its sand ring (undermap vertices), its - // pure-grass tiles, and the top-left of those tiles (-1 without a plot). + // Every sand vertex the farm laid (its cap ring, its bridges and its plot's ring); the plot's + // pure-grass tiles, when one was asked for and fits, and the top-left of those tiles (-1 without). std::vector sand, plot; int plotX = -1, plotY = -1; }; @@ -60,42 +67,48 @@ struct FarmPlot int width = 10, height = 4, ring = 2; }; -/// Lays rows over `region` running at `angle` (radians), with the row at `origin` a crop row centred -/// on it, and no water within `rim` steps of the region's edge, so the region's own coast (or wall) -/// keeps a margin of land and every crop row joins the rim at both ends. With a `plot`, a building -/// clearing is stamped at the region's most inland point (farthest from its edge), clear of every -/// water row by its sand ring and a vertex more and at least `rim` from the edge; a region too small -/// for it gets none. With a `bridgeSpacing`, a sand bridge crosses every water row each that many tiles -/// along the rows, so workers cross a long farm instead of walking round its rows. With `caps` (on), a -/// ring of sand just inside the rim closes the crop rows, so their wheat and wood never spread beyond -/// the rows into the ground round the farm. Keep the plot's and -/// bridges' sand (`Farm::sand`) out of any beach flood, as a sand road is. Stamps the water and the sand -/// into `sketch`; lay beaches afterwards as usual. /// Half the width, in vertices along the rows, of a bridge's sand line: wide enough that a diagonal line /// stays unbroken. constexpr double kBridgeHalfWidth = 0.75; + +/// Lays rows over `region` running at `angle` (radians), with the row at `origin` a crop row centred on +/// it, and no water within `rim` steps of the region's edge, so the region's own coast (or wall) keeps a +/// margin of land and every crop row joins the rim at both ends. Then, in order: +/// - with `caps` (on), a ring of sand vertices just inside the rim, where the water rows' beaches begin, +/// closes every crop row, so its wheat and wood never spread out of the farm; +/// - with a `bridgeSpacing`, a line of sand vertices crosses every water row each that many tiles along +/// the rows, starting at the origin: the tiles either side are no longer pure water, so workers walk +/// across, two tiles wide; +/// - with a `plot`, a building clearing is stamped at the region's most inland point, clear of every +/// water row by its sand ring and a vertex more and at least `rim` from the edge (a region too small +/// for it gets none); its grass always wins over a cap or bridge running through it. +/// Stamps the water and the sand into `sketch`; lay beaches afterwards as usual, keeping the farm's sand +/// (`Farm::sand`) out of any beach flood, as a sand road is. Farm layFarm(TerrainSketch &sketch, const Torus &, const std::vector ®ion, double angle, ShapePoint origin, int rim, const FarmRows &rows, const FarmPlot *plot = nullptr, int bridgeSpacing = 0, bool caps = true); -/// Farm fields grown into open ground straight out of the homes they belong to. `homeOf` gives the -/// designed land's owner where it is a home (0 or more) and -1 elsewhere, and `occupied` marks all the -/// designed land. Field `s` belongs to home `owners[s]` and starts from the open tile near `seeds[s]` -/// in the biggest stretch of open ground (searched 16 tiles round the point). Every open tile that -/// `area` allows is shared out between the fields by growth (growTerritories): by equal area, or, given -/// every field's `rowAngles`, by equal yield (farmYield), so a field whose rows run on the diagonal gets -/// more ground than one whose rows run along an axis; then every -/// field keeps `gap` tiles of water from all designed land but its own home, and from every other -/// field (separateTerritories). A field may run right up to its own home, so the two are one piece of -/// land with no coast between. Strips too narrow to survive their beaches and walls and slivers cut off -/// from the home are dropped, and then every field is joined to its home by a neck `neckHalfWidth` wide -/// from the home's middle to the field's nearest ground, so the opening is always broad. Returns every tile's field, the seed's index, or -1. A seed with no open ground near -/// it gets no field, so a caller checks every field's size before using them. +/// Farm fields grown into open ground straight out of the homes they belong to. `occupied` marks all the +/// designed land and `homeOf` its owner where it is a home (0 or more; -1 elsewhere). Field `s` belongs +/// to home `owners[s]` and starts from the open tile near `seeds[s]` (searched 16 tiles round it) in the +/// biggest stretch of open ground. In order: +/// - every open tile that `area` allows is shared out between the fields by growth (growTerritories): +/// by equal area, or, given every field's `rowAngles`, by equal yield (farmYield), so a field whose +/// rows run on the diagonal gets more ground than one whose rows run along an axis; +/// - every field keeps `gap` tiles from all designed land but its own home, and from every other field +/// (separateTerritories), while it may run right up to its own home, so the two are one piece of land; +/// - strips too narrow to survive their beaches and walls, and slivers cut off from the home, go; +/// - every field is joined to its home by a neck `neckHalfWidth` wide from the home's middle (`anchors`) +/// to the field's nearest ground, keeping the same gaps, so the opening is always broad. +/// Returns every tile's field (the seed's index) or -1. A seed with no open ground near it gets no field, +/// so a caller checks every field's size before using them. std::vector growFarmFields(const Torus &, const std::vector &occupied, - const std::vector &homeOf, const std::vector &area, - const std::vector &seeds, const std::vector &owners, - const std::vector &anchors, int gap, double neckHalfWidth, - const std::vector &rowAngles = {}); + const std::vector &homeOf, + const std::vector &area, + const std::vector &seeds, + const std::vector &owners, + const std::vector &anchors, int gap, + double neckHalfWidth, const std::vector &rowAngles = {}); /// How much of a farm a colony can work: the share of the farm's crop-row grass (and the whole of its /// plot, if any, or 0 is returned) that a unit from `sources` can walk to once crops are cleared - @@ -127,8 +140,8 @@ int MapGeneration::plantFarm(Map &map, const Torus &t, const Farm &farm, int whe std::vector> crops; std::vector rowRoom; for (int i = 0; i < t.size(); ++i) - if (farm.row[i] >= 0 && farm.row[i] % 2 == 0 && fromWater[i] >= 0 && !farm.plot[i] && eligible(i) && - map.isGrass(i % t.w, i / t.w)) + if (farm.row[i] >= 0 && farm.row[i] % 2 == 0 && fromWater[i] >= 0 && !farm.plot[i] && + eligible(i) && map.isGrass(i % t.w, i / t.w)) { crops.push_back({fromWater[i], i}); if (farm.row[i] >= int(rowRoom.size())) @@ -137,7 +150,9 @@ int MapGeneration::plantFarm(Map &map, const Torus &t, const Farm &farm, int whe } std::stable_sort(crops.begin(), crops.end()); // The woodlot's row: the crop row with the most room, the first on a tie. - const int woodRow = rowRoom.empty() ? -1 : int(std::max_element(rowRoom.begin(), rowRoom.end()) - rowRoom.begin()); + const int woodRow = + rowRoom.empty() ? -1 + : int(std::max_element(rowRoom.begin(), rowRoom.end()) - rowRoom.begin()); int planted = 0, woods = 0, wheats = 0; for (const auto &entry : crops) { diff --git a/src/map/generator/shared/Homes.h b/src/map/generator/shared/Homes.h index 84a3ea51e..4d47ffe4b 100644 --- a/src/map/generator/shared/Homes.h +++ b/src/map/generator/shared/Homes.h @@ -13,69 +13,94 @@ #include namespace MapGeneration { -// Homes that stand alone in the sea, as a round island turned to face out from the map's middle -// with its door on the inner side: the swarm stands in from the middle towards the door, one pond -// lies out behind it and a second, if any, on one flank. Every pond keeps a gap of land to the home's -// coast, so its beach never meets the sea's. Carousel and Switchbacks both build their homes this way. +// Round homes: a home that stands alone as a round patch of ground turned to face out from the map's +// middle, its door on the inner side. The swarm stands in from the middle towards the door, and the +// home's starter kit lies within a short walk of it. A round home may carry ponds of its own - one out +// behind the swarm and a second on a flank - for a map that gives its homes no other water; every pond +// keeps a gap of land to the home's edge, so where the home is a coast its beach never meets the sea's. +// Carousel's homes have no ponds (their farms are their water); Switchbacks' have two only on a map too +// small for farms. -/// The farthest a pond home's first pond lies from its middle beyond its own radius, and the swarm from -/// its middle: a short walk apart however big the home, so the kit round the pond is near the swarm. -constexpr double kPondHomeReach = 8; +/// How far a round home's first pond lies from its middle beyond its own radius, and its swarm from its +/// middle: a short walk apart however big the home, so the kit planted round the pond is near the swarm. +constexpr double kHomeSwarmReach = 8; +/// A pond's radius as a share of its home's, clamped; the land a pond keeps from the home's edge; and +/// the room a home needs beyond those for its swarm. +constexpr double kHomePondShare = 0.18; +constexpr double kHomeSmallestPond = 2.5; +constexpr double kHomeLargestPond = 7; +constexpr double kHomePondGap = 5; +constexpr double kHomeSwarmRoom = 4; -/// How a pond home is proportioned: its radius, its ponds' radius, the land kept between a pond and -/// the coast, and how many ponds (0 to 2). -struct PondHomeStyle +/// The radius of a round home's ponds, and of the circle its kit is planted round. +inline double homePondRadius(double homeRadius) { - double radius, pondRadius, lakeSeaGap; - int ponds; -}; + return std::clamp(kHomePondShare * homeRadius, kHomeSmallestPond, kHomeLargestPond); +} + +/// Whether a round home is big enough for its swarm, its kit and a pond with its gap to the edge. The +/// same floor holds whether or not the home has ponds, so a design refuses the same crowded maps. +inline bool homeHasRoom(double homeRadius) +{ + return homeRadius >= homePondRadius(homeRadius) + kHomePondGap + kHomeSwarmRoom; +} -/// Stamps a pond home: `visit(tile)` for every tile of the `home` outline centred at `centre` and -/// turned to `axis` (the heading out from the map's middle), and its ponds' outlines into `pondMask`, -/// turned the same way. Returns the first pond's centre, where the kit is planted. +/// Stamps a round home: `visit(tile)` for every tile of the `home` outline centred at `centre` and +/// turned to `axis` (the heading out from the map's middle), and `ponds` (0 to 2) of the `pond` outline +/// into `pondMask`, turned the same way; `pond` may be null when there are none. Returns where the kit +/// goes: the first pond's centre, or the home's middle without ponds. template -ShapePoint stampPondHome(const Torus &t, ShapePoint centre, double axis, const RadialShape &home, - const RadialShape &pond, const PondHomeStyle &style, - std::vector &pondMask, Visit visit) +ShapePoint stampRoundHome(const Torus &t, ShapePoint centre, double axis, const RadialShape &home, + double radius, int ponds, const RadialShape *pond, + std::vector &pondMask, Visit visit) { forEachTileInShape(t, centre.x, centre.y, home, axis, [&](int i, double, double) { visit(i); }); - // The first pond lies out from the middle, as far out as the coast gap allows but never more than - // a short walk from the swarm, so a big home's kit still grows beside water; a second on the flank, + // The first pond lies out from the middle, as far out as the edge gap allows but never more than a + // short walk from the swarm, so a big home's kit still grows beside water; a second on the flank, // as close. - const double along = std::min({0.4 * style.radius, style.radius - style.pondRadius - style.lakeSeaGap - 1, - style.pondRadius + kPondHomeReach}); - const double flank = std::min(0.55 * style.radius, style.pondRadius + kPondHomeReach + 4); - const double offsets[2][2] = {{along, 0}, {-0.05 * style.radius, -flank}}; + const double pondRadius = homePondRadius(radius); + const double along = std::min( + {0.4 * radius, radius - pondRadius - kHomePondGap - 1, pondRadius + kHomeSwarmReach}); + const double flank = std::min(0.55 * radius, pondRadius + kHomeSwarmReach + 4); + const double offsets[2][2] = {{along, 0}, {-0.05 * radius, -flank}}; ShapePoint first = centre; - for (int p = 0; p < std::clamp(style.ponds, 0, 2); ++p) + for (int p = 0; p < std::clamp(ponds, 0, 2) && pond; ++p) { - const double px = centre.x + offsets[p][0] * std::cos(axis) - offsets[p][1] * std::sin(axis); - const double py = centre.y + offsets[p][0] * std::sin(axis) + offsets[p][1] * std::cos(axis); + const double px = + centre.x + offsets[p][0] * std::cos(axis) - offsets[p][1] * std::sin(axis); + const double py = + centre.y + offsets[p][0] * std::sin(axis) + offsets[p][1] * std::cos(axis); if (p == 0) first = {px, py}; - fillShape(pondMask, t, px, py, pond, axis); + fillShape(pondMask, t, px, py, *pond, axis); } return first; } -/// Where a pond home's swarm goes: in from the middle towards the door, as the top-left tile of its +/// Where a round home's swarm goes: in from the middle towards the door, as the top-left tile of its /// 4x4 footprint, which is what placeSettlement measures from. -inline MapGeneratorPoint pondHomeAnchor(ShapePoint centre, double axis, double radius) +inline MapGeneratorPoint homeSwarmSite(ShapePoint centre, double axis, double radius) { - const ShapePoint p = polarPoint(centre.x, centre.y, std::min(0.3 * radius, kPondHomeReach), axis + kPi); + const ShapePoint p = + polarPoint(centre.x, centre.y, std::min(0.3 * radius, kHomeSwarmReach), axis + kPi); return MapGeneratorPoint(int(std::lround(p.x)) - 2, int(std::lround(p.y)) - 2); } -/// A pond home's starter kit: wheat and wood either side of its first pond on the swarm's side, where -/// they regrow. No stone: a pond home is walled in stone already, so a clump would only take room. +/// A round home's starter kit, round `kitCentre` (stampRoundHome's result): a wheat patch and a wood +/// patch on either side, towards the swarm, where a pond's water would regrow them. No stone: these +/// homes are walled in stone already, so a clump would only take room. template -void plantPondHomeKit(Map &map, const Torus &t, GenerationContext &context, ShapePoint firstPond, - double axis, double pondRadius, int wheat, int wood, Eligible eligible) +void plantHomeKit(Map &map, const Torus &t, GenerationContext &context, ShapePoint kitCentre, + double axis, double homeRadius, int wheat, int wood, Eligible eligible) { - const KitFrame frame{int(std::lround(firstPond.x)), int(std::lround(firstPond.y)), axis}; - const double reach = pondRadius * 1.2 + 3; - const Kit kit{frame.at(-0.4 * reach, -reach, 12), frame.at(-0.4 * reach, reach, 12), - frame.at(reach + 3, 0, 10), wheat, wood, -1}; + const KitFrame frame{int(std::lround(kitCentre.x)), int(std::lround(kitCentre.y)), axis}; + const double reach = homePondRadius(homeRadius) * 1.2 + 3; + const Kit kit{frame.at(-0.4 * reach, -reach, 12), + frame.at(-0.4 * reach, reach, 12), + frame.at(reach + 3, 0, 10), + wheat, + wood, + -1}; plantKit(map, t, context, kit, eligible); } @@ -97,9 +122,9 @@ struct GroundAmounts /// samples `patchAt` and `splitAt` in the wedge frame, so every home gets the same patches. template void furnishGround(Map &map, const Torus &t, GenerationContext &context, - const Fertility::Field &fertility, Eligible eligible, PatchAt patchAt, - SplitAt splitAt, AmountsFor amountsFor, const char *stoneStream, - const char *fruitStream) + const Fertility::Field &fertility, Eligible eligible, PatchAt patchAt, + SplitAt splitAt, AmountsFor amountsFor, const char *stoneStream, + const char *fruitStream) { const int n = t.w * t.h; std::vector ground; @@ -132,11 +157,8 @@ void furnishGround(Map &map, const Torus &t, GenerationContext &context, plantFields(map, t, chosen, amounts.wheat, amounts.wood, splitAt); scatterClumps(context, t, ground, amounts.outcrops, stoneStream, eligible, [&](MapGeneratorPoint p) { placeResourceClump(map, context, p, STONE, 1); }); - scatterClumps(context, t, ground, amounts.groves, fruitStream, eligible, - [&](MapGeneratorPoint p) - { - placeResourceClump(map, context, p, - CHERRY + int(context.bounded(fruitStream, 3)), 1); - }); + scatterClumps( + context, t, ground, amounts.groves, fruitStream, eligible, [&](MapGeneratorPoint p) + { placeResourceClump(map, context, p, CHERRY + int(context.bounded(fruitStream, 3)), 1); }); } } // namespace MapGeneration diff --git a/src/map/generator/shared/Towers.cpp b/src/map/generator/shared/Towers.cpp index 19b5d6e05..7307e3970 100644 --- a/src/map/generator/shared/Towers.cpp +++ b/src/map/generator/shared/Towers.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "Towers.h" #include "Game.h" +#include "GenerationContext.h" #include "Settlements.h" +#include "Walls.h" #include #include #include @@ -20,6 +22,16 @@ bool footprintsInRange(const Torus &t, int a, int b, int range) } } // namespace +TowerRequest startingTowerRequest(int level, int count, int pads, int spacing) +{ + TowerRequest request; + request.range = kTowerRange[std::clamp(level - 1, 0, 2)]; + request.towers = level > 0 ? count : 0; + request.pads = level > 0 ? pads : count + pads; + request.spacing = spacing; + return request; +} + TowerPlan chooseTowerSites(const Torus &t, const std::vector &owner, const std::vector &buildable, const std::vector &target, @@ -52,7 +64,8 @@ TowerPlan chooseTowerSites(const Torus &t, const std::vector &owner, bool touches = false; for (int fy = -1; fy <= 2 && !touches; ++fy) for (int fx = -1; fx <= 2 && !touches; ++fx) - touches = (fx < 0 || fx > 1 || fy < 0 || fy > 1) && (*request.against)[t.at(x + fx, y + fy)]; + touches = (fx < 0 || fx > 1 || fy < 0 || fy > 1) && + (*request.against)[t.at(x + fx, y + fy)]; fits = touches; } if (!fits) @@ -97,7 +110,8 @@ TowerPlan chooseTowerSites(const Torus &t, const std::vector &owner, while (next[k] < ranked[k].size()) { const auto [negative, i] = ranked[k][next[k]++]; - if ((pass == 0 && negative >= 0) || protectedInRange[i] || !spaced(plan.towers[k], i)) + if ((pass == 0 && negative >= 0) || protectedInRange[i] || + !spaced(plan.towers[k], i)) continue; plan.towers[k].push_back(i); progress = true; @@ -192,7 +206,8 @@ int evenTowerPlan(TowerPlan &plan) return int(towers); } -std::vector roomyGround(const Torus &t, const std::vector &open, int room) +std::vector roomyGround(const Torus &t, const std::vector &open, + int room) { std::vector closed(t.size(), 0); for (int i = 0; i < t.size(); ++i) @@ -204,6 +219,50 @@ std::vector roomyGround(const Torus &t, const std::vector *goal) +{ + const Map &map = game.map; + const Torus t(map); + if (goal) + { + // Every colony walks out from the ring of tiles round its swarm's 4x4 footprint. + const int teams = int(plan.towers.size()); + std::vector open(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + { + const int x = i % t.w, y = i / t.w; + open[i] = + !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + } + std::vector> sources(teams); + for (int k = 0; k < teams; ++k) + for (int dy = -1; dy <= 4; ++dy) + for (int dx = -1; dx <= 4; ++dx) + if (dx < 0 || dy < 0 || dx > 3 || dy > 3) + sources[k].push_back(t.at(context.bootX[k] + dx, context.bootY[k] + dy)); + std::vector> goals(teams); + for (auto &g : goals) + { + g = *goal; + for (int i = 0; i < t.size(); ++i) + g[i] = g[i] && open[i]; + } + dropBlockingSites(t, plan, open, sources, goals); + } + if (evenTowerPlan(plan) < (everyColonyNeedsOne ? 1 : 0)) + { + context.detail = "a colony has no room for its towers"; + return false; + } + if (!raiseTowers(game, plan, std::clamp(level - 1, 0, 2))) + { + context.detail = "a tower site no longer fits"; + return false; + } + return true; +} + bool raiseTowers(Game &game, const TowerPlan &plan, int level) { const Torus t(game.map); diff --git a/src/map/generator/shared/Towers.h b/src/map/generator/shared/Towers.h index 27373b65a..c0c4d8cef 100644 --- a/src/map/generator/shared/Towers.h +++ b/src/map/generator/shared/Towers.h @@ -3,6 +3,7 @@ #include "Grid.h" #include class Game; +struct GenerationContext; namespace MapGeneration { // Where colonies' towers go. A defence tower is 2x2 and scans square rings out to its range beyond @@ -34,6 +35,12 @@ struct TowerRequest const std::vector *against = nullptr; }; +/// The request for the towers a colony starts with, from a map's two controls: `level` (0 for none, when +/// every site is an open pad, otherwise 1 to 3, which sets the range the sites are scored at) and `count` +/// towers per colony, with `pads` open pads beside them `spacing` apart. The caller adds the weights and +/// any wall the sites must stand against. +TowerRequest startingTowerRequest(int level, int count, int pads, int spacing); + /// Chooses tower sites for every colony. `owner` gives each tile's colony (-1 for none); a site is a /// 2x2 footprint wholly on one colony's `buildable` ground, scored by the `target` tiles within its /// range: each of another colony's worth `otherWeight` (the ground it can shoot at over the walls) and @@ -67,7 +74,16 @@ int evenTowerPlan(TowerPlan &); /// Ground roomy enough for a tower that blocks nothing: the tiles of `open` (land a unit walks on) at /// least `room` steps from any tile that is not. A 2x2 tower on a strip narrower than that could close /// the strip; a generator ands this into the buildable ground it hands chooseTowerSites. -std::vector roomyGround(const Torus &, const std::vector &open, int room); +std::vector roomyGround(const Torus &, const std::vector &open, + int room); + +/// Settles a plan into the world, the way every arena map does it: when `goal` is given, drops any site +/// that would close a colony's walk from its swarm to that ground (dropBlockingSites, walking round +/// water, stone and buildings); evens the plan so every colony has as many sites as the fewest got +/// (evenTowerPlan); and raises the towers (a plan at level 0 has none). False, with `context.detail` set, +/// when a site no longer fits or, with `everyColonyNeedsOne`, when some colony was left without a tower. +bool settleStartingTowers(Game &, GenerationContext &, TowerPlan &, int level, + bool everyColonyNeedsOne, const std::vector *goal); /// Builds a plan's towers: every colony's tower sites get a completed tower of `level` (placeTower), /// stocked where the plan says. Pads stay open. False, and nothing more built, at the first site that no longer From 4c21ff6374ddb9b2b63b1fb57346314b6cf86a73 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 16:50:27 -0400 Subject: [PATCH 114/136] Add landscape primitives to the map generator toolkit New shared modules for maps that the wedge-and-centre toolkit could not build easily (lattices with no centre, forests that open as they are cut, rock with scarce building room, canals, patterns with a grain): - Orbits: symmetry groups as data. Point symmetries (lifted from Symmetric arena), translation lattices of 2/4/8... colonies on the torus, latticeSites for other counts, orbit stamping/sums/noise, deposit equalising and an exact symmetry validator. - Morphology: dilate, erode, open, close, exact distance transform, clearance, small-region removal, widest-walk and narrowest-passage. - Growth: crop growth chance on a sketch, watered share, dry zones. - Contact: StepCosts cost models (walking, chopping, swimming), cost fields, colony contact matrix, cost spreads and equal-cost sites. - Points: dart-thrown sites, warped Voronoi labels (lifted from Stone highlands), Lloyd relaxation and site neighbours. - Patterns: integer Turing patterns, exactly wrapping stripes at any whole-number slant, upwind shadow and streamlines. - Channels: the beach and tower-reach arithmetic for canals, beach masks, sand bridges and crossings per colony. - Room: build anchors, sites in a region, grow-until-sites. - Biomes: land kinds as data with an estimated worth, sketch and furnish stages. Upgrades: GraphMaze runs on any CellGraph (tessellation or scattered sites); Drawing gains wanderingPath and carveCorridor; Sketch gains pureTiles and tileCorners; Homes gains regionHome and no longer assumes a map centre; Planting gains plantCover; Roads gains openColonyRoutes (lifted from Everglades); Walls gains the island margin and stone shared by Carousel and Switchbacks; generator definitions can set their start-quality weights. Existing generators moved onto the new primitives: Symmetric arena (Orbits), Stone highlands (Points, dropSmallRegions), Everglades (openColonyRoutes), Carousel and Switchbacks (shared island stone), Watershed (pureTiles), Maze (tileCorners), and Farmland, Towers and Territories (Morphology). All are byte-identical: golden test 192 rows, 0 failures; --sweep 0 failing combinations. Tools: MapGeneratorStudy overlay= (growth, sites, chop, owner), tools/render_map.py (single maps and comparison sheets, no third-party modules) and tools/new_map_generator.py (a scaffold that builds, registers and passes its own validation; exercised end to end and removed). The defaults test no longer hard-codes the catalog size. Verified: MapGeneratorDefaultsTest (new landscape checks for every module), MapGeneratorGoldenTest and --sweep, CustomGameSetupHarness, strict translation check, client build. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/map-generators/ADDING_A_GENERATOR.md | 4 +- .../GAME_RULES_FOR_MAP_DESIGN.md | 7 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 51 +- docs/map-generators/README.md | 2 + src/SConscript | 9 + src/map/generator/core/GenerationService.cpp | 3 +- src/map/generator/core/GeneratorDefinition.h | 7 + .../generators/CarouselGenerator.cpp | 13 +- .../generators/EvergladesGenerator.cpp | 92 +-- .../generator/generators/MazeGenerator.cpp | 20 +- .../generators/StoneHighlandsGenerator.cpp | 165 +----- .../generators/SwitchbacksGenerator.cpp | 14 +- .../generators/SymmetricArenaGenerator.cpp | 263 +-------- .../generators/WatershedGenerator.cpp | 25 +- src/map/generator/shared/Biomes.cpp | 207 +++++++ src/map/generator/shared/Biomes.h | 68 +++ src/map/generator/shared/Channels.cpp | 83 +++ src/map/generator/shared/Channels.h | 56 ++ src/map/generator/shared/Contact.cpp | 173 ++++++ src/map/generator/shared/Contact.h | 74 +++ src/map/generator/shared/Drawing.cpp | 52 ++ src/map/generator/shared/Drawing.h | 16 + src/map/generator/shared/Farmland.cpp | 13 +- src/map/generator/shared/GraphMaze.cpp | 103 +++- src/map/generator/shared/GraphMaze.h | 61 +- src/map/generator/shared/Growth.cpp | 56 ++ src/map/generator/shared/Growth.h | 41 ++ src/map/generator/shared/Homes.h | 56 +- src/map/generator/shared/Morphology.cpp | 257 +++++++++ src/map/generator/shared/Morphology.h | 67 +++ src/map/generator/shared/Orbits.cpp | 343 +++++++++++ src/map/generator/shared/Orbits.h | 116 ++++ src/map/generator/shared/Patterns.cpp | 181 ++++++ src/map/generator/shared/Patterns.h | 92 +++ src/map/generator/shared/Planting.h | 21 + src/map/generator/shared/Points.cpp | 186 ++++++ src/map/generator/shared/Points.h | 46 ++ src/map/generator/shared/Roads.cpp | 81 +++ src/map/generator/shared/Roads.h | 9 + src/map/generator/shared/Room.cpp | 55 ++ src/map/generator/shared/Room.h | 93 +++ src/map/generator/shared/Sketch.cpp | 25 + src/map/generator/shared/Sketch.h | 10 + src/map/generator/shared/Territories.h | 7 +- src/map/generator/shared/Towers.cpp | 11 +- src/map/generator/shared/Walls.cpp | 25 + src/map/generator/shared/Walls.h | 16 + test/MapGeneratorDefaultsTest.cpp | 4 +- test/MapGeneratorLandscapeChecks.h | 542 ++++++++++++++++++ test/MapGeneratorStudy.cpp | 70 ++- tools/new_map_generator.py | 291 ++++++++++ tools/render_map.py | 221 +++++++ 52 files changed, 3897 insertions(+), 606 deletions(-) create mode 100644 src/map/generator/shared/Biomes.cpp create mode 100644 src/map/generator/shared/Biomes.h create mode 100644 src/map/generator/shared/Channels.cpp create mode 100644 src/map/generator/shared/Channels.h create mode 100644 src/map/generator/shared/Contact.cpp create mode 100644 src/map/generator/shared/Contact.h create mode 100644 src/map/generator/shared/Growth.cpp create mode 100644 src/map/generator/shared/Growth.h create mode 100644 src/map/generator/shared/Morphology.cpp create mode 100644 src/map/generator/shared/Morphology.h create mode 100644 src/map/generator/shared/Orbits.cpp create mode 100644 src/map/generator/shared/Orbits.h create mode 100644 src/map/generator/shared/Patterns.cpp create mode 100644 src/map/generator/shared/Patterns.h create mode 100644 src/map/generator/shared/Points.cpp create mode 100644 src/map/generator/shared/Points.h create mode 100644 src/map/generator/shared/Room.cpp create mode 100644 src/map/generator/shared/Room.h create mode 100644 test/MapGeneratorLandscapeChecks.h create mode 100644 tools/new_map_generator.py create mode 100644 tools/render_map.py diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index c8a5f640c..d215daeb5 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -6,7 +6,7 @@ A generator is a registered module, not a new branch in a central dispatch switc - **`core/`** owns requests, control metadata, registration, seed streams, validation and results. It does not implement terrain recipes. - **`generators/`** contains one header/source pair per generator. The header defines its named options; the source owns its controls, its design (the layout as a pure function of the request) and the order it calls the shared stages in. A generator source holds only what is particular to its map. -- **`shared/`** is the toolkit every generator is built from: the torus grid and its floods (`Grid`), shapes (`Geometry`, `Wedge`), drawing strokes, shapes and grown branches (`Drawing`), the terrain sketch (`Sketch`), noise (`LatticeNoise`, `HeightMap`), deposits (`Planting`, `Resources`), routes (`Roads`), colonies (`Settlements`, `BalancedStarts`), the pipeline stages (`Pipeline`), the height-field stages (`Terrain`) and the start scoring (`StartQuality`). These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) lists what each gives you. +- **`shared/`** is the toolkit every generator is built from: the torus grid and its floods (`Grid`), shapes (`Geometry`, `Wedge`), fairness by symmetry groups (`Orbits`), mask arithmetic (`Morphology`), drawing strokes, shapes, grown branches and wandering paths (`Drawing`), the terrain sketch (`Sketch`), crop growth on a sketch (`Growth`), noise and patterns (`LatticeNoise`, `HeightMap`, `Patterns`), scattered sites and cells (`Points`, `Tessellation`, `GraphMaze`), water sized for its purpose (`Channels`), walls and towers (`Walls`, `Towers`), deposits and kinds of land (`Planting`, `Resources`, `Farmland`, `Biomes`), homes and building room (`Homes`, `Room`, `Territories`), routes and costs (`Roads`, `Contact`), colonies (`Settlements`, `BalancedStarts`), the pipeline stages (`Pipeline`), the height-field stages (`Terrain`) and the start scoring (`StartQuality`). These are ordinary functions with explicit map/context dependencies, not methods of a generator superclass. [Map generator framework](MAP_GENERATOR_FRAMEWORK.md) lists what each gives you. - **`shared/legacy/`** is the older area-grid toolkit (`Regions`, `Distances`, `StartingPositions`) the point-dispersion and height-field generators still build on. It does not compose with the tile masks the rest of `shared/` uses; do not start a new generator on it. - **`compatibility/`** owns the historical descriptor codec and overloaded-field conversion. New generators do not add fields to that descriptor. `MapGenerator.h` is only a small compatibility facade for older callers. @@ -16,6 +16,8 @@ Extract on the second use. When a routine you are about to write already exists ## Register a module +`tools/new_map_generator.py ""` does the steps below for you and leaves a generator that builds and passes its own validation, ready to be given its own design; read on for what it did and why. + 1. Add `ExampleGenerator.h/.cpp` under `src/map/generator/generators/`. Use a named options struct that reads its scalar values from the request by stable option ID. 2. Return a `GeneratorDefinition` from `exampleDefinition()`. Provide a stable string ID, unique nonnegative numeric ID, translation key, revision, editor-only availability, control list and callback. Numeric IDs are identifiers, never list positions; gaps are supported, and the id lives only in the definition (`GeneratorRegistry::idOf` looks one up by string; the `Method` enum names only the ids the legacy descriptor knows). `hasStartingColonies` defaults to true. Uniform alone disables normal starting-colony validation in the shipped catalog. 3. Add that definition to the single list in `GeneratorRegistry::builtins()` and its source to `src/SConscript`. Add the display name and control labels to the translation tables using the existing translation workflow. diff --git a/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md index d0e7b7e2a..5230332e9 100644 --- a/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md +++ b/docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md @@ -35,6 +35,10 @@ comments can say "because grass may not touch water" and a reader can check it h to its range, 5, 7 or 9 tiles beyond every side by level, with no line of sight (`Building::findBestTarget`, `BuildingUtils::turretScanTile`, `BuildingTypesDefence.cpp`). A wall stops walking but not shooting, so a wall's thickness decides whether towers on either side can reach each other (`towerReach`). +- **Towers shoot over water too.** A straight channel w undermap corners wide spoils w + 3 tiles of + grass and puts the banks' nearest grass w + 4 tiles apart, so a level-1 tower on one bank covers the + other across a single water corner and a level-3 tower across five (`shared/Channels`). A canal can + start a tower duel long before either side can swim. - **Players can plug gaps.** Players build stone walls, so a narrow gate, ramp or trail can be sealed by whoever holds it. How wide a map's doors are decides whether a colony can shut itself in. @@ -88,7 +92,8 @@ The shared tools encode a few measurable promises every generator is expected to depth, room and isolation from rivals; fairness is the weakest start divided by the strongest, and the lobby keeps the best-scoring of several seeds. Designed generators get fairness by construction instead: they design one colony's share and turn or mirror it onto every other - colony (`WedgeFrame`), so every colony's ground is the same. + colony (`WedgeFrame`), or slide it across a lattice of colonies with no centre at all + (`translationSymmetry`, `shared/Orbits`), so every colony's ground is the same. ## What makes a good game diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 2f3f4b610..3541218fc 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -22,22 +22,31 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Grid` | `Torus` wrap and offset arithmetic; `floodFrom`/`stepsFrom`, the one eight-connected breadth-first flood (with a step limit and the visit order for callers that need it); `walkableTiles` and `groundUnitTiles` passability masks; `unitTilesByTeam` and `firstColonyCutOff` | | `Topology` | `connectedRegions` component labelling with explicit wrap and neighbour policy; `regionAdjacency` and `graphDistances` over sparse labels | | `Geometry` | `kPi`; `ShapeTransform` (invertible stretch and rotation); `Stretch`, which places a layout designed in a circle on a map's shorter side onto a rectangular map as an ellipse touching all four sides (`toFill`, `apply`, `undo`, `heading`), exactly the identity on a square map; `RadialShape`, a seeded rough outline with a per-angle `radiusAt()`; `stampShape` and `stampRoughDisc` into a label grid; `AxisFrame`, a frame along a heading from an origin (`at(along, across)` and `project`), in which a colony's trail, gates or towers are designed once | -| `Drawing` | Drawing on the torus: `strokePath`, a thick path of points each with its own half width, rasterized through the wrap; `tracePath`, a path traced one tile thick with no gaps (a sand road); `bezierPath`, a quadratic curve as such a path; `polarPoint`; `forEachTileInShape` and `fillShape`, a `RadialShape` filled at any centre and turned to any heading within its bounding box, optionally stretched into an oval; `stretchPath`, a designed path placed on the map by a `Stretch` with its half widths kept in tiles; `bentPath`, a tapered path leaving a point along a heading and bowing sideways; `pathClearance` and `pathBounds`, the water between two stroked paths (optionally ignoring a branch's root) and a cheap bounding circle; `growBranches`, a tree grown level by level by forking in two at every tip to a `ForkStyle`, every branch offered to a caller's `accept` before it is kept and a refused one retried once at half length; `arcPath`, a circular arc as a stroke; `zigzagPath`, a switchback trail in an `AxisFrame` with each leg's straight run returned apart from its turns; `ringWithGates`, every tile of a ring visited with the gate it lies in (a wall with ramps, a moat with bridges); exact fixed-point geometry for designs that must rasterize the same everywhere: `SubtilePoint` (16 units a tile), `sealedSegmentTiles` and `traceSealedPath` (a line no unit can cross even diagonally, at any slant), `forEachTileInPolygon` and `fillPolygon` (tile centres inside a polygon, shared edges split exactly, through the wrap) | +| `Drawing` | Drawing on the torus: `strokePath`, a thick path of points each with its own half width, rasterized through the wrap; `tracePath`, a path traced one tile thick with no gaps (a sand road); `bezierPath`, a quadratic curve as such a path; `polarPoint`; `forEachTileInShape` and `fillShape`, a `RadialShape` filled at any centre and turned to any heading within its bounding box, optionally stretched into an oval; `stretchPath`, a designed path placed on the map by a `Stretch` with its half widths kept in tiles; `bentPath`, a tapered path leaving a point along a heading and bowing sideways; `wanderingPath` and `carveCorridor`, a path (and its stroke) that wanders from one point to another the short way round the torus, leaving and arriving exactly where asked, its width swelling and narrowing (a tunnel, a lane, a trail that doesn't look ruled); `pathClearance` and `pathBounds`, the water between two stroked paths (optionally ignoring a branch's root) and a cheap bounding circle; `growBranches`, a tree grown level by level by forking in two at every tip to a `ForkStyle`, every branch offered to a caller's `accept` before it is kept and a refused one retried once at half length; `arcPath`, a circular arc as a stroke; `zigzagPath`, a switchback trail in an `AxisFrame` with each leg's straight run returned apart from its turns; `ringWithGates`, every tile of a ring visited with the gate it lies in (a wall with ramps, a moat with bridges); exact fixed-point geometry for designs that must rasterize the same everywhere: `SubtilePoint` (16 units a tile), `sealedSegmentTiles` and `traceSealedPath` (a line no unit can cross even diagonally, at any slant), `forEachTileInPolygon` and `fillPolygon` (tile centres inside a polygon, shared edges split exactly, through the wrap) | | `Wedge` | `WedgeFrame`: the map as one equal wedge per colony round the centre, so a feature designed once in a wedge's frame is stamped into every wedge alike; given a `Stretch` it measures every cell in the round design frame, so the same design fills a rectangular map; `Blob`, a stretched, turned rough disc in that frame; `WedgeField`, periodic noise sampled in the wedge frame so a layer planted by it comes out the same in every wedge | -| `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `sprinkleSand`, decorative sand patches on inland grass following a caller's noise, kept a strip of grass away from every beach; `writeUndermap`; `growWater`, one body of water grown to an exact tile count by a caller's key (Stone highlands' ponds, Amphitheatre's bays); `keepRoadInland` and `roadTiles`, a sand road kept off every beach and the tiles its vertices spoil | +| `Sketch` | `TerrainSketch`, the undermap designed in memory; `layBeaches`, the order-independent beach pass; `raiseIslands`; `sprinkleSand`, decorative sand patches on inland grass following a caller's noise, kept a strip of grass away from every beach; `writeUndermap`; `pureTiles`, the tiles whose four corners all hold one terrain (the sketch as the game will draw it), and `tileCorners`, the reverse; `growWater`, one body of water grown to an exact tile count by a caller's key (Stone highlands' ponds, Amphitheatre's bays); `keepRoadInland` and `roadTiles`, a sand road kept off every beach and the tiles its vertices spoil | | `LatticeNoise` | `PeriodicNoise`, value noise that tiles the torus exactly and samples anywhere, with `periodicNoise`/`fractalNoise` (integer fields per tile, octaves) and `torusNoise` (four octaves in [-1, 1]) sampled from it, plus `percentile` | | `HeightMap`, `Noise` | Perlin noise faded across the wrap, and the stamped height fields the height-field generators shape | -| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds, or no stone for a home already walled in it), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band; optionally only its best-growing share, shared out equally between the colonies' wedges), `algaeGrowthChance` (each water tile's chance of passing the engine's algae growth test, which needs water within 15 tiles and solid sand within 30 at a reflected offset), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region), `plantRound` and `plantOrchard` (a clump, or groves of the three fruits, at the same point round a circle at every colony's angle); `seedAlgae` can also share its clumps between labelled groups of water such as private bays | +| `Planting` | The deposits a generator places by hand: `clearGround`, `growPatch`, `seedNear`, `plantKit` (a home's wheat, wood and stone from three seeds, or no stone for a home already walled in it), `KitFrame` (a home's origin and facing, so one kit design lands the same way round every home), `swarmSurroundings`, `clearAroundSwarms`, `seedAlgae` (any water or a shallows band; optionally only its best-growing share, shared out equally between the colonies' wedges), `algaeGrowthChance` (each water tile's chance of passing the engine's algae growth test, which needs water within 15 tiles and solid sand within 30 at a reflected offset), `stockIslands`, `plantFields` (the preferred tiles dealt into wheat and wood patches by an unrelated split key), `scatterClumps` (clumps dropped on random eligible tiles of a region), `plantRound` and `plantOrchard` (a clump, or groves of the three fruits, at the same point round a circle at every colony's angle), `plantCover` (one deposit over every allowed tile of a region: a forest, a wheat plain, undergrowth); `seedAlgae` can also share its clumps between labelled groups of water such as private bays | | `Resources` | The ambient layer and its fairness guards: `scaledCount`/`scaledShare`, `placeResourceClump`, `setScaledResource`, `scatterResources`, `guaranteeStartingResources`, `openCrampedStarts` | -| `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared; `cheapestWalk`, the same search by any step cost (Everglades' fords, Symmetric arena's causeway routes) | +| `Roads` | `cheapestRoute` and `openRoad`: the walk that crosses the fewest deposits, with only those cleared; `cheapestWalk`, the same search by any step cost (Symmetric arena's causeway routes); `openColonyRoutes`, the backstop that opens the cheapest way from colony 0's doorstep to any colony it cannot walk to under a `StepCosts` model, clearing deposits and fording water with sand (Everglades) | | `Settlements` | `placeSettlement`: whole-footprint home mask, nearest legal anchor, exact worker count, per-colony diagnostics; `placeTower`, a completed, stocked defence tower of a chosen level at the allowed footprint nearest a designed point | -| `Walls` | Walls a design builds and the checks that prove they hold: `seaVertices` and `seaMargin` (the land a swimmer can stand on), `sealCoasts` (stone on every grass tile touching that margin, so a coast is sealed; City states, Carousel, Switchbacks), `labelBorders` (a wall between labelled regions no unit can cross even diagonally, one to three tiles thick, or with doors a design leaves open; Stone highlands' ridges, Amphitheatre's borders, Carousel's walls), `designedStone` (a designed band's stone and any gaps the beach pass left), `reachesWithShut`, `pieceLeak` and `seaEntry` (with the doors shut, does any part reach another, or the sea reach inside), `towerReach` (how close a tower on some ground comes to shooting at a target: towers scan square rings with no line of sight), `walkSpread` (every colony's walk to its own target, and how uneven they are) and `firstRegionLeak` (reachable ground where two labelled regions meet that a design says must not; Maze's walls) | +| `Walls` | Walls a design builds and the checks that prove they hold: `seaVertices` and `seaMargin` (the land a swimmer can stand on), `sealCoasts` (stone on every grass tile touching that margin, so a coast is sealed; City states, Carousel, Switchbacks), `islandSeaMargin` and `sealedIslandStone` (the margin and the stone of walled land in the sea with ponds and sand roads, once for Carousel and Switchbacks), `labelBorders` (a wall between labelled regions no unit can cross even diagonally, one to three tiles thick, or with doors a design leaves open; Stone highlands' ridges, Amphitheatre's borders, Carousel's walls), `designedStone` (a designed band's stone and any gaps the beach pass left), `reachesWithShut`, `pieceLeak` and `seaEntry` (with the doors shut, does any part reach another, or the sea reach inside), `towerReach` (how close a tower on some ground comes to shooting at a target: towers scan square rings with no line of sight), `walkSpread` (every colony's walk to its own target, and how uneven they are) and `firstRegionLeak` (reachable ground where two labelled regions meet that a design says must not; Maze's walls) | | `Tessellation` | Polygon tilings of the torus with no terrain attached: `squareTessellation` and `hexTessellation` (cells, the corners they share and edges as identities, exact across the wrap even two cells wide), `edgeEnds`, `centreAcross`, `outline`, `transform` (the lattice's translations and mirrors), `labelTiles` (every tile's cell), `shortestEdgeSteps` and `centreClearance`; `warpLimit` and `warpCorners`, corners moved at random into irregular polygons that still tile, never closing the gap between walls that share no corner below a minimum (Maze) | -| `GraphMaze` | A maze carved through a tessellation's cell graph: `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | +| `GraphMaze` | A maze carved through any `CellGraph` (a tessellation's with `cellGraph(tiling)`, or scattered sites' with `cellGraph(torus, sites, siteNeighbours)`): `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | | `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic, or shared by value when each claimant's ground has a worth per tile; `smoothLabels`, a majority filter over any radius that trims spurs or, at a radius of 3 or 4, straightens borders into curves; `separateTerritories`, a gap opened between neighbouring territories; `fillToNearest`, the reverse: unclaimed ground near labelled regions given to the nearest, so water between pieces becomes land; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `growLakeBeside`, one on either flank of a site, kept wholly to its side of the line from the way in; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | | `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges across the water rows every so many tiles along them, so workers need not walk round a long row (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives, and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | | `Towers` | Tower sites chosen for the ground they cover over the walls, other colonies' (offence) or their own (defence), each weighted: `startingTowerRequest` (a request from a map's level and count controls), `chooseTowerSites` (best first, a colony at a time in turn, optionally every site directly against a wall, no stocked tower in range of another colony's tower or swarm, plus open 2x2 pads for players to build more), `settleStartingTowers` (the sequence every arena map runs: `dropBlockingSites` so no site closes a colony's walk to a goal, `evenTowerPlan` so every colony has as many sites as the fewest got, then `raiseTowers`), `towerFootprints` and `roomyGround` (no tower on a strip it could close); a tower in range of another colony's tower starts empty | -| `Homes` | Homes built alike: `stampRoundHome`, `homeSwarmSite` and `plantHomeKit` (a round home facing out from the middle, its swarm towards its door, optional ponds kept clear of its edge, and a wheat and wood kit near the swarm with no stone, since these homes are walled in stone; `homeHasRoom` is the size floor; Carousel, Switchbacks), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | +| `Homes` | Homes built alike: `stampRoundHome`, `homeSwarmSite` and `plantHomeKit` (a round home facing out from the middle, its swarm towards its door, optional ponds kept clear of its edge, and a wheat and wood kit near the swarm with no stone, since these homes are walled in stone; `homeHasRoom` is the size floor; Carousel, Switchbacks; the axis is any heading, out from the middle on a ring), `regionHome` (a home in ground of any shape: the swarm the same walk in from the door as every other colony's, on the roomiest such tile, facing away from the door), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | +| `Orbits` | Fairness by symmetry groups rather than wedges: `Symmetry` (signed permutation matrices on doubled centred coordinates plus a whole-tile translation, mapping tiles and corners exactly), `pointSymmetry` (the half turn, quarter turns, mirrors and all eight square symmetries; Symmetric arena), `translationSymmetry` (the roomiest lattice of 2, 4, 8... colonies on the torus, square, staggered or sheared, with no centre and served as well on a rectangular map), `latticeSites` (colonies on that lattice, or evenly staggered rows when the count has no exact group), `stampOrbits`, `orbitSum`, `orbitNoise` and `topShare` (features and fields every symmetry leaves unchanged), `equaliseDeposits` (the gameplay RNG's amounts made equal across each orbit) and `orbitMismatch` (the finished world checked exactly symmetric, colonies permuted one to one) | +| `Morphology` | Mask arithmetic on the torus in integers: `dilate`, `erode`, `openMask`, `closeMask` (squares), `distanceSquaredTo` (exact Euclidean) and `dilateRound`, `clearance` (steps to the nearest tile outside a mask), `dropSmallRegions` (specks, or pockets on the inverted mask), `widestWalkClearance` and `narrowestPassage` (the width of a walk's narrowest point, a validator's check that a tunnel or street still takes a column of units) and `slivers` (Stone highlands' pockets, Farmland's field opening, `roomyGround`, `separateTerritories`) | +| `Growth` | Where wheat and wood grow back, known on the sketch: `cropGrowthField` (the exact `Fertility::Field` of a `TerrainSketch`, not gated on deposits), `wateredShare`, `wetTiles` (a region promised dry checks 0), `dryZone` (where water would water a region: `kCropProbeReach` on each axis) and `drainWithin` | +| `Contact` | Distances by what a step costs: `StepCosts` (open, clearable, eternal, water, building; `walking`, `chopping`, `swimming`), `stepCost`, `costsFrom`, `contactMatrix` (every colony's cost to every other), `costsToTarget`, `costSpread`, `unevenCosts` (a validator's message) and `equalCostSites` (one site per colony at the same cost from home, on its own side: groves an equal chop into a forest) | +| `Points` | Irregular sites and cells: `spreadPoints` (dart throwing with a minimum spacing), `nearestSiteLabels` (warped Voronoi cells in sixteenths of a tile), `relaxPoints` (Lloyd relaxation on the torus) and `siteNeighbours` (which cells touch); Stone highlands' basins | +| `Patterns` | Structure with a grain: `turingPattern` (labyrinths and spots grown from noise by activation and inhibition, about a wavelength across, optionally stretched), `stripePhase`, `stripeSpacing`, `stripeHeading`, `stripeNormal` and `stripeDistance` (parallel stripes that wrap both seams exactly at any whole-number slant, optionally warped), `upwindSteps` (the shadow a mask casts downwind) and `traceStreamline` (a curve following a field of headings) | +| `Channels` | Water sized for its purpose: the beach arithmetic once (`kChannelSpoiledTiles`, `bankToBank`: a channel w corners wide spoils w + 3 tiles and puts banks w + 4 apart), `widestChannelTowersCross` and `narrowestChannelTowersMiss` by tower level, `beachTiles` (the rim towers stand against to cover a canal, on a sketch or a map), `bridgeAcross` (a sand bridge across water) and `crossingsPerLabel` (every colony's number of ways across) | +| `Room` | Building room as a design decision: `buildableTiles`, `buildAnchors` (every 4x4 footprint's top-left, across the wrap), `buildSites` (footprints in a region) and `growUntilSites` (a chamber grown until it holds exactly what it promised) | +| `Biomes` | Kinds of land as data: `BiomeKit` (ponds, stone ring, farmland, wood share, outcrops, groves, cover, orchard island) with `fertilePlain`, `stoneFortress`, `orchardIsland` and `forest`; `biomeWorth` (an estimate from the start scorer's weights to share ground by, to be tuned with the fairness tournament); `sketchBiome` (its ponds, island and ring) and `furnishBiome` (its deposits) | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | | `Pipeline` | The stages round the others: `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | | `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | @@ -48,7 +57,9 @@ and Terrain take a `Game` because placing buildings and units needs its mutation Controls and validation live in `core/`: `GeneratorControls` declares discrete, stepped, power-of-two and named-choice option domains once for the UI and the catalog; `GeneratorDefinition::validateRequest` is a pure check run before generation, and `validateWorld` runs after the structural checks -against the finished terrain. +against the finished terrain. `GeneratorDefinition::qualityWeights` and `qualityScale` say how the +lobby ranks a generator's candidate seeds (`scoreStarts`); a map cramped or crowded on purpose sets +them so the ranking rewards what it is meant to be. ## The designed generator @@ -460,7 +471,8 @@ An orchard island behind a moat at the exact centre, crossed by sand causeways, colonies identical starting ground. - **Symmetry.** A half turn for 2 colonies; a quarter turn for 4 on square maps and both mirrors - for 4 on rectangular ones; all eight square symmetries for 8, on square maps only. + for 4 on rectangular ones; all eight square symmetries for 8, on square maps only + (`pointSymmetry`, `shared/Orbits`). `validateRequest` rejects other colony counts and layouts that leave too little room. - **Built, not copied.** Every decision is either a function of a tile's whole orbit (integer noise summed over the orbit, the integer squared radius) or is made once for colony 0 (home, @@ -918,9 +930,11 @@ next leg up. Every home reaches a walled wheat farm on either flank. - `test/MapGeneratorStudy.cpp` builds to `MapGeneratorStudy`, a CLI harness that invokes the real production generators directly: `MapGeneratorStudy [key=value...] - [tuning] [headroom] [quality] [dump=path]`. `quality` reports `StartQualityReport`/ + [tuning] [headroom] [quality] [dump=path] [overlay=kind]`. `quality` reports `StartQualityReport`/ `ColonyQuality` per colony; `dump=` writes a plain-text terrain/resource grid for direct - inspection or scripted flood-fill checks; `--catalog` dumps every registered generator's + inspection or scripted flood-fill checks; `overlay=` writes one measure per tile beside it + (`growth`, the crop growth chance; `sites`, where a 4x4 building fits; `chop`, the cost from the + nearest colony clearing crops on the way; `owner`, which colony that is); `--catalog` dumps every registered generator's controls as JSON, with each control's `kind` (`range` or `toggle`). - `test/MapGeneratorDefaultsTest.cpp` builds to `MapGeneratorDefaultsTest`, asserting the registry's and every control's contract: discrete domains, shape bounds, topology, home @@ -933,7 +947,11 @@ next leg up. Every home reaches a walled wheat farm on either flank. cheapest route, strokes and shape fills, fields and clumps, settlements and the colony walk, the crop guarantee through and around a wall, a buried colony's room, balanced starts, per-landmass scatter, lattice noise, the wedge frame and the context's shuffle — so a change to a module fails there before it shows - up as a changed golden fingerprint downstream. + up as a changed golden fingerprint downstream. `test/MapGeneratorLandscapeChecks.h` does the same + for the landscape modules: symmetry groups against their orbits, morphology and the distance + transform against brute force, crop growth and dry zones, cost models and route opening, sites + and cell graphs, pattern wavelengths and seamless stripes, wandering paths, the channel + arithmetic against `towerReach`, building room, region homes and every biome kit. - `test/MapGeneratorGoldenTest.cpp` builds to `MapGeneratorGoldenTest` and keeps the revision rule honest. `test/map-generator-golden.txt` records, per platform, the fingerprint (terrain, resources and colony starts) every registered generator produces for three seeds at 256, one @@ -947,6 +965,15 @@ next leg up. Every home reaches a walled wheat farm on either flank. every playable landscape at the colony counts and sizes the lobby offers, five seeds at 128 and 256 and three at 512, prints the success rate per cell and fails any valid cell where no seed generated: that is what a player would see as a failed generation. CI runs all three. +- `tools/render_map.py` renders what the study dumps as PNG, one map (`one `) or a + sheet of generators beside each other at several sizes with an HTML page of captions + (`sheet --sizes 128 256 512`), optionally tinted by an overlay; no third-party + modules. Compare a new generator with its nearest neighbours this way before showing it. +- `tools/new_map_generator.py ""` scaffolds a generator that builds, registers + and passes its own validation: header and source in the designed shape (colonies on a lattice, + a home and pond each, the kit and crop guarantee), the registry entry, the SConscript line, the + next unused legacy id and its translation keys with English placeholders. Its golden rows come + from `MapGeneratorGoldenTest --update`. - The cppunit suite under `test/` (`scons && ./TestsRunner`) covers the rest of the engine and must stay green alongside all of the above. diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index 8dacabfd0..92a9eea64 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -9,6 +9,8 @@ Normal lobby generation rolls a fresh random map; the preview snapshot is the ma Build the study executable with `scons release=1 map-generator-study`. `build/src/MapGeneratorStudy --catalog` exports the current control definitions. Use `python3 tools/map_generator_study.py --help` for seeded generation studies. +Use `python3 tools/render_map.py sheet ` to render maps side by side at several sizes, +and `python3 tools/new_map_generator.py ""` to scaffold a new generator. Use `python3 tools/map_fairness_tournament.py run smoke` to measure whether generated maps favour some start positions in real games; see [Map fairness tournament](FAIRNESS_TOURNAMENT.md). Study results and screenshots belong in ignored `artifacts/`, not in this directory. diff --git a/src/SConscript b/src/SConscript index 9cb8b2a6d..3a074cb3b 100644 --- a/src/SConscript +++ b/src/SConscript @@ -215,6 +215,15 @@ map/generator/shared/Farmland.cpp map/generator/shared/Towers.cpp map/generator/shared/Tessellation.cpp map/generator/shared/GraphMaze.cpp +map/generator/shared/Orbits.cpp +map/generator/shared/Morphology.cpp +map/generator/shared/Growth.cpp +map/generator/shared/Contact.cpp +map/generator/shared/Points.cpp +map/generator/shared/Patterns.cpp +map/generator/shared/Channels.cpp +map/generator/shared/Room.cpp +map/generator/shared/Biomes.cpp map/generator/shared/LatticeNoise.cpp map/generator/shared/Pipeline.cpp map/generator/shared/Planting.cpp diff --git a/src/map/generator/core/GenerationService.cpp b/src/map/generator/core/GenerationService.cpp index 52fef85f5..4e4306002 100644 --- a/src/map/generator/core/GenerationService.cpp +++ b/src/map/generator/core/GenerationService.cpp @@ -84,7 +84,8 @@ GenerationResult GenerationService::generate(Game &game, const GenerationRequest return result; } } - result.quality = MapGeneration::scoreStarts(game, request.nbTeams); + result.quality = MapGeneration::scoreStarts(game, request.nbTeams, definition->qualityWeights, + definition->qualityScale); result.stage = "script"; const auto script = game.sgslScript.compileScript(&game); if (script.type != ErrorReport::ET_OK) diff --git a/src/map/generator/core/GeneratorDefinition.h b/src/map/generator/core/GeneratorDefinition.h index 9a9894ff7..6a76678be 100644 --- a/src/map/generator/core/GeneratorDefinition.h +++ b/src/map/generator/core/GeneratorDefinition.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "GeneratorControls.h" +#include "StartQuality.h" #include class Game; struct GenerationContext; @@ -17,4 +18,10 @@ struct GeneratorDefinition // Optional pure checks for relationships between controls and generator-specific topology. std::string (*validateRequest)(const GenerationRequest &) = nullptr; std::string (*validateWorld)(const Game &, const GenerationContext &) = nullptr; + // How the lobby ranks this generator's candidate seeds (StartQuality.h). The defaults suit most + // maps; a map that is cramped, crowded or far-flung on purpose (building room scarce by design, + // colonies meant to touch) says so here, so the ranking rewards what the map is meant to be rather + // than marking every candidate down for it. + MapGeneration::StartQualityWeights qualityWeights{}; + MapGeneration::StartQualityScale qualityScale{}; }; diff --git a/src/map/generator/generators/CarouselGenerator.cpp b/src/map/generator/generators/CarouselGenerator.cpp index 0440b7004..24e84e732 100644 --- a/src/map/generator/generators/CarouselGenerator.cpp +++ b/src/map/generator/generators/CarouselGenerator.cpp @@ -656,22 +656,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // neither carries the margin inland. std::vector seaMargin(const Map &map, const Layout &L) { - std::vector notBeach = L.roadTile; - const std::vector farmSand = roadTiles(L.t, L.farmSand); - for (int i = 0; i < L.t.size(); ++i) - notBeach[i] = notBeach[i] || farmSand[i]; - return MapGeneration::seaMargin(map, L.t, seaVertices(map, L.t, L.pond), notBeach); + return islandSeaMargin(map, L.t, L.pond, L.roadTile, L.farmSand); } // The design's stone, once the terrain is laid: every coast of the remaining sea sealed, and every wall. std::vector stoneTiles(const Map &map, const Layout &L) { - std::vector stone = sealCoasts(map, L.t, seaMargin(map, L), L.land); - const DesignedStone wall = designedStone(map, L.t, L.wall); - for (int i = 0; i < L.t.size(); ++i) - if (wall.stone[i]) - stone[i] = 1; - return stone; + return sealedIslandStone(map, L.t, seaMargin(map, L), L.land, L.wall); } // The resources: every home's kit and scattered farmland, a fruit grove in every court, the plaza's diff --git a/src/map/generator/generators/EvergladesGenerator.cpp b/src/map/generator/generators/EvergladesGenerator.cpp index c87676161..82bad0607 100644 --- a/src/map/generator/generators/EvergladesGenerator.cpp +++ b/src/map/generator/generators/EvergladesGenerator.cpp @@ -399,96 +399,12 @@ void stockSwamp(Map &map, const Layout &L, GenerationContext &context, const Eve } // Every colony must be able to walk to colony 0 at the start. Where the swamp or the sloughs box -// one in, the cheapest way through is opened: deposits on it are cleared and water on it becomes a -// sand ford. The map may look odd there; it does not fail. +// one in, the cheapest way through is opened (openColonyRoutes): deposits on it are cleared and water +// on it becomes a sand ford. The map may look odd there; it does not fail. void openRoutes(Game &game, const Layout &L, GenerationContext &context) { - Map &map = game.map; - const Torus &t = L.t; - const int n = t.w * t.h, teams = context.request.nbTeams; - if (teams < 2) - return; - const auto openAt = [&](int i) - { - const int x = i % t.w, y = i / t.w; - return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; - }; - const auto costAt = [&](int i) - { - const int x = i % t.w, y = i / t.w; - if (map.getBuilding(x, y) != NOGBID) - return -1; - if (map.isWater(x, y)) - return kStepFord; - if (map.isResource(x, y)) - { - const int type = map.getResource(x, y).type; - return type == STONE || (type >= CHERRY && type <= CHERRY + 2) ? kStepEternal - : kStepDeposit; - } - return kStepOpen; - }; - // The open tiles round a colony's swarm. - const auto doorstep = [&](int team) - { - std::vector tiles; - for (int dy = -1; dy <= 4; ++dy) - for (int dx = -1; dx <= 4; ++dx) - if (dy == -1 || dy == 4 || dx == -1 || dx == 4) - { - const int i = t.at(context.bootX[team] + dx, context.bootY[team] + dy); - if (openAt(i)) - tiles.push_back(i); - } - return tiles; - }; - bool changed = false; - for (int team = 1; team < teams; ++team) - { - std::vector open(n), source(n, 0); - for (int i = 0; i < n; ++i) - open[i] = openAt(i); - for (int i : doorstep(0)) - source[i] = 1; - const std::vector walk = stepsFrom(t, source, open); - std::vector target(n, 0); - bool arrived = false; - for (int i : doorstep(team)) - { - target[i] = 1; - arrived = arrived || walk[i] >= 0; - } - if (arrived) - continue; - // The cheapest way from anything colony 0 can reach to this colony's doorstep. - std::vector reachable; - for (int i = 0; i < n; ++i) - if (walk[i] >= 0) - reachable.push_back(i); - const std::vector route = - cheapestWalk(t, GridNeighbors::Cardinal, reachable, target, - [&](int, int to, int, int) { return costAt(to); }); - for (int i : route) - { - const int x = i % t.w, y = i / t.w; - if (map.isWater(x, y)) - { - // One sand corner turns this tile and its three other tiles into walkable shore. - map.setUMTerrain(x, y, SAND); - for (int dy = -1; dy <= 0; ++dy) - for (int dx = -1; dx <= 0; ++dx) - if (map.isResource(t.x(x + dx), t.y(y + dy))) - map.setNoResource(t.x(x + dx), t.y(y + dy), 1); - changed = true; - } - else if (map.isResource(x, y)) - { - map.setNoResource(x, y, 1); - } - } - if (changed) - map.rebuildTerrain(); - } + openColonyRoutes(game.map, context, L.t, + StepCosts{kStepOpen, kStepDeposit, kStepEternal, kStepFord, -1}); } bool generate(Game &game, GenerationContext &context) diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 8f0d3d91a..9b39e4f13 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -467,20 +467,6 @@ std::vector openCorners(const Torus &t, const MazeDesign &d) return ponds; } -// The undermap corners of every tile in `tiles`. -std::vector cornersOf(const Torus &t, const std::vector &tiles) -{ - std::vector corners(tiles.size(), 0); - for (int i = 0; i < t.size(); ++i) - if (tiles[i]) - { - const int x = i % t.w, y = i / t.w; - corners[i] = corners[t.at(x + 1, y)] = corners[t.at(x, y + 1)] = - corners[t.at(x + 1, y + 1)] = 1; - } - return corners; -} - // A sand road down every open edge, from each cell's centre through the edge's middle to the // next centre: undermap sand on the corners of every tile a sealed line passes, so each road tile is // pure sand and consecutive ones share a side. At a home it stops against the swarm's 4x4 @@ -500,7 +486,7 @@ std::vector layRoads(TerrainSketch &sketch, const Torus &t, const (ends.first.y + ends.second.y) / 2}; traceSealedPath(path, t, {g.cells[a].centre, middle, g.centreAcross(edge, a)}); } - std::vector road = cornersOf(t, path); + std::vector road = tileCorners(t, path); for (int cell = 0; cell < g.cellCount(); ++cell) if (d.isHome[cell]) for (int dy = -2; dy <= 2; ++dy) @@ -539,8 +525,8 @@ bool generate(Game &game, GenerationContext &context) context.stage = "maze terrain"; const Torus t(map); const std::vector spine = wallSpines(t, d); - const std::vector fromWall = stepsFrom(t, cornersOf(t, spine)); - const std::vector fromPond = stepsFrom(t, cornersOf(t, openCorners(t, d))); + const std::vector fromWall = stepsFrom(t, tileCorners(t, spine)); + const std::vector fromPond = stepsFrom(t, tileCorners(t, openCorners(t, d))); const int channelEdge = kFlankSteps + 1 + o.channelWidth; TerrainSketch sketch(size_t(t.size()), GRASS); for (int i = 0; i < t.size(); ++i) diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 962e8cb65..987539347 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -5,7 +5,9 @@ #include "GenerationContext.h" #include "Grid.h" #include "LatticeNoise.h" +#include "Morphology.h" #include "Pipeline.h" +#include "Points.h" #include "Resources.h" #include "Settlements.h" #include "Sketch.h" @@ -100,132 +102,6 @@ std::vector chebyshevDistance(const Torus &t, const std::vector bucketWindow(int c, int count, int radius) -{ - std::vector result; - if (count <= 2 * radius + 1) - { - for (int i = 0; i < count; ++i) - result.push_back(i); - } - else - { - for (int d = -radius; d <= radius; ++d) - result.push_back(((c + d) % count + count) % count); - } - return result; -} - -// Basin sites by dart throwing on the torus. Sites keep 0.83 of the requested spacing apart, -// which, once the map is saturated, leaves one site per spacing-squared tiles on average. -std::vector scatterSites(const Torus &t, int spacing, GenerationContext &context) -{ - const int minimum = std::max(4, spacing * 83 / 100); - const std::int64_t expected = - std::max(2, std::int64_t(t.w) * t.h / (std::int64_t(spacing) * spacing)); - const int gx = std::max(1, t.w / minimum), gy = std::max(1, t.h / minimum); - std::vector> columns(gx), rows(gy); - for (int c = 0; c < gx; ++c) - columns[c] = bucketWindow(c, gx, 1); - for (int r = 0; r < gy; ++r) - rows[r] = bucketWindow(r, gy, 1); - std::vector> buckets(size_t(gx) * gy); - std::vector sites; - // 60 darts per expected site: enough to saturate the map, after which nearly every dart lands - // too close to an existing site. - for (std::int64_t attempt = 0; attempt < expected * 60; ++attempt) - { - const int x = int(context.bounded("highlands-sites", t.w)); - const int y = int(context.bounded("highlands-sites", t.h)); - const int bx = x * gx / t.w, by = y * gy / t.h; - bool clear = true; - for (int row : rows[by]) - { - for (int column : columns[bx]) - { - for (int s : buckets[size_t(row) * gx + column]) - if (t.dist2(x, y, sites[s].x, sites[s].y) < minimum * minimum) - { - clear = false; - break; - } - if (!clear) - break; - } - if (!clear) - break; - } - if (!clear) - continue; - buckets[size_t(by) * gx + bx].push_back(int(sites.size())); - sites.push_back({x, y}); - } - return sites; -} - -// Every tile's nearest site, measured from a warped copy of the tile so cell borders wander -// instead of running straight. Positions are in sixteenths of a tile. -std::vector labelCells(const Torus &t, const std::vector &sites, int spacing, - GenerationContext &context) -{ - // The warp's noise has features one and a half valleys across and moves positions by up to 30% - // of the spacing: enough to bend the straight edges of nearest-site cells into wandering - // ridgelines, not enough to tear a cell into scraps (which the pocket rule would then have to - // fill with stone). - const int period = spacing * 3 / 2; - const std::vector warpX = - fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); - const std::vector warpY = - fractalNoise(t.w, t.h, period, 3, context.stream("highlands-warp")); - const std::int64_t amplitude = std::int64_t(spacing) * 16 * 30 / 100; - const int W = t.w * 16, H = t.h * 16; - const int gx = std::max(1, t.w / spacing), gy = std::max(1, t.h / spacing); - std::vector> buckets(size_t(gx) * gy), columns(gx), rows(gy); - for (size_t s = 0; s < sites.size(); ++s) - buckets[size_t(sites[s].y * gy / t.h) * gx + sites[s].x * gx / t.w].push_back(int(s)); - for (int c = 0; c < gx; ++c) - columns[c] = bucketWindow(c, gx, 2); - for (int r = 0; r < gy; ++r) - rows[r] = bucketWindow(r, gy, 2); - std::vector label(size_t(t.w) * t.h, 0); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - const size_t i = size_t(y) * t.w + x; - int px = x * 16 + 8 + int((warpX[i] - 32768) * amplitude / 32768); - int py = y * 16 + 8 + int((warpY[i] - 32768) * amplitude / 32768); - px = ((px % W) + W) % W; - py = ((py % H) + H) % H; - const int bx = int(std::int64_t(px) * gx / W), by = int(std::int64_t(py) * gy / H); - std::int64_t best = INT64_MAX; - int nearest = 0; - for (int row : rows[by]) - for (int column : columns[bx]) - for (int s : buckets[size_t(row) * gx + column]) - { - int dx = std::abs(px - (sites[s].x * 16 + 8)), - dy = std::abs(py - (sites[s].y * 16 + 8)); - dx = std::min(dx, W - dx); - dy = std::min(dy, H - dy); - const std::int64_t d = std::int64_t(dx) * dx + std::int64_t(dy) * dy; - if (d < best || (d == best && s < nearest)) - { - best = d; - nearest = s; - } - } - label[i] = nearest; - } - return label; -} - struct Pass { int x, y; // centre tile @@ -675,8 +551,15 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const Torus &t = L.t; const int n = t.w * t.h; - const std::vector sites = scatterSites(t, o.valleySize, context); - const std::vector label = labelCells(t, sites, o.valleySize, context); + // Basin sites by dart throwing, kept 0.83 of the requested spacing apart, which once the map is + // saturated leaves one site per spacing-squared tiles on average. The warp's noise has features one + // and a half valleys across and moves positions by up to 30% of the spacing: enough to bend the + // straight edges of nearest-site cells into wandering ridgelines, not enough to tear a cell into + // scraps (which the pocket rule would then have to fill with stone). + const std::vector sites = + spreadPoints(t, o.valleySize, context, "highlands-sites", 83, 60); + const std::vector label = + nearestSiteLabels(t, sites, o.valleySize, context, "highlands-warp", 150, 30); // A tile is ridge when any of its eight neighbours has a lower label (labelBorders), so no unit // can step, even diagonally, from one cell into another. @@ -707,26 +590,14 @@ Layout design(const GenerationRequest &request, GenerationContext &context) std::vector open(n); for (int i = 0; i < n; ++i) open[i] = !L.ridge[i]; - const std::vector region = connectedRegions(open, t.w, t.h, true, GridNeighbors::Eight); - int regions = 0; - for (int r : region) - regions = std::max(regions, r + 1); - std::vector size(regions, 0), id(regions, -1); - for (int r : region) - if (r >= 0) - ++size[r]; - for (int r = 0; r < regions; ++r) - if (size[r] >= kPocketTiles) - id[r] = L.valleys++; - L.valley.assign(n, -1); + const std::vector roomy = + dropSmallRegions(t, open, kPocketTiles, GridNeighbors::Eight); + L.valley = connectedRegions(roomy, t.w, t.h, true, GridNeighbors::Eight); for (int i = 0; i < n; ++i) - if (region[i] >= 0) - { - if (id[region[i]] < 0) - L.ridge[i] = 1; - else - L.valley[i] = id[region[i]]; - } + { + L.ridge[i] = L.ridge[i] || !roomy[i]; + L.valleys = std::max(L.valleys, L.valley[i] + 1); + } if (L.valleys < 1) { L.failure = "the ridges left no valley to live in"; diff --git a/src/map/generator/generators/SwitchbacksGenerator.cpp b/src/map/generator/generators/SwitchbacksGenerator.cpp index 2f9cc6b5d..326fff40f 100644 --- a/src/map/generator/generators/SwitchbacksGenerator.cpp +++ b/src/map/generator/generators/SwitchbacksGenerator.cpp @@ -462,23 +462,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // The sea: every water vertex but the ponds'. std::vector seaMargin(const Map &map, const Layout &L) { - // Neither a sand road nor a farm plot's sand ring is beach: neither may carry the sea's margin inland. - std::vector notBeach = L.roadTile; - const std::vector plotSand = roadTiles(L.t, L.farmSand); - for (int i = 0; i < L.t.size(); ++i) - notBeach[i] = notBeach[i] || plotSand[i]; - return MapGeneration::seaMargin(map, L.t, seaVertices(map, L.t, L.pond), notBeach); + return islandSeaMargin(map, L.t, L.pond, L.roadTile, L.farmSand); } // The design's stone once the terrain is laid: every coast sealed, and every mountain's rock. std::vector stoneTiles(const Map &map, const Layout &L) { - std::vector stone = sealCoasts(map, L.t, seaMargin(map, L), L.land); - const DesignedStone rock = designedStone(map, L.t, L.stone); - for (int i = 0; i < L.t.size(); ++i) - if (rock.stone[i]) - stone[i] = 1; - return stone; + return sealedIslandStone(map, L.t, seaMargin(map, L), L.land, L.stone); } void furnish(Map &map, const Layout &L, GenerationContext &context, const SwitchbacksOptions &o, diff --git a/src/map/generator/generators/SymmetricArenaGenerator.cpp b/src/map/generator/generators/SymmetricArenaGenerator.cpp index 79c79f579..10da2d662 100644 --- a/src/map/generator/generators/SymmetricArenaGenerator.cpp +++ b/src/map/generator/generators/SymmetricArenaGenerator.cpp @@ -8,6 +8,7 @@ #include "Grid.h" #include "GlobalContainer.h" #include "HeightMap.h" +#include "Orbits.h" #include "Resources.h" #include "Roads.h" #include "Topology.h" @@ -23,9 +24,14 @@ #include #include #include +using MapGeneration::Isometry; using MapGeneration::kPi; +using MapGeneration::orbitNoise; using MapGeneration::scaledCount; using MapGeneration::scaledShare; +using MapGeneration::stampOrbits; +using MapGeneration::Symmetry; +using MapGeneration::topShare; // A symmetric arena: an orchard of all three fruits and some stone on an island behind a moat, // crossed by causeways, with the colonies spaced round it so that every colony's ground, route @@ -69,73 +75,6 @@ int wrap(int v, int n) return v < 0 ? v + n : v; } -// A symmetry of the map about its centre, as a signed permutation matrix acting on doubled -// centred coordinates: tile (x, y) is (2x + 1 - W, 2y + 1 - H) and undermap corner (u, v) is -// (2u - W, 2v - H). A tile's terrain comes from its four corners (Map::regenerateMap), and the -// same matrix maps a tile's corners onto its image's corners, so the tile rotation -// (x, y) -> (W-1-y, x) is the corner rotation (u, v) -> (W-v, u). -struct Isometry -{ - int a, b, c, d; -}; - -struct Symmetry -{ - int width = 0, height = 0; - // elements[0] is the identity; colony k starts on elements[k]'s image of colony 0's home. - std::vector elements; - - int order() const { return int(elements.size()); } - int tile(int e, int x, int y) const - { - const Isometry &g = elements[e]; - const int X = 2 * x + 1 - width, Y = 2 * y + 1 - height; - return wrap((g.c * X + g.d * Y + height - 1) / 2, height) * width + - wrap((g.a * X + g.b * Y + width - 1) / 2, width); - } - int corner(int e, int u, int v) const - { - const Isometry &g = elements[e]; - const int U = 2 * u - width, V = 2 * v - height; - return wrap((g.c * U + g.d * V + height) / 2, height) * width + - wrap((g.a * U + g.b * V + width) / 2, width); - } - // The top-left tile of the image of a w x h footprint anchored at (x, y): the image of a - // rectangle is a rectangle, so it is the image's lowest corner on each axis. - std::pair anchor(int e, int x, int y, int w, int h) const - { - const Isometry &g = elements[e]; - const int U0 = 2 * x - width, V0 = 2 * y - height; - const int U1 = 2 * (x + w) - width, V1 = 2 * (y + h) - height; - const int X = std::min(g.a * U0 + g.b * V0, g.a * U1 + g.b * V1); - const int Y = std::min(g.c * U0 + g.d * V0, g.c * U1 + g.d * V1); - return {wrap((X + width) / 2, width), wrap((Y + height) / 2, height)}; - } -}; - -// The symmetry that gives every colony identical ground: a half turn for two colonies, a -// quarter turn for four on a square map, the two mirrors for four on a rectangular map (where a -// quarter turn doesn't map the map onto itself), and all eight symmetries of a square for -// eight. Elements are listed in order round the centre, so neighbouring colonies are -// neighbouring team numbers. Empty for colony counts no symmetry serves. -Symmetry symmetryFor(int width, int height, int teams) -{ - Symmetry s; - s.width = width; - s.height = height; - const bool square = width == height; - if (teams == 2) - s.elements = {{1, 0, 0, 1}, {-1, 0, 0, -1}}; - else if (teams == 4 && square) - s.elements = {{1, 0, 0, 1}, {0, -1, 1, 0}, {-1, 0, 0, -1}, {0, 1, -1, 0}}; - else if (teams == 4) - s.elements = {{1, 0, 0, 1}, {-1, 0, 0, 1}, {-1, 0, 0, -1}, {1, 0, 0, -1}}; - else if (teams == 8 && square) - s.elements = {{1, 0, 0, 1}, {0, 1, 1, 0}, {0, -1, 1, 0}, {-1, 0, 0, 1}, - {-1, 0, 0, -1}, {0, -1, -1, 0}, {0, 1, -1, 0}, {1, 0, 0, -1}}; - return s; -} - // Continuous positions relative to the map centre, in tiles. The centre is an undermap corner, // so corners sit on integers and tile centres on half-integers. struct Point @@ -205,7 +144,7 @@ Arena arenaFor(const GenerationRequest &r) a.centre = 4.0 + o.centreSize / 100.0 * 0.75 * std::min(a.width, a.height); a.moat = o.moatWidth; a.causewayWidth = o.causewayWidth; - a.symmetry = symmetryFor(a.width, a.height, a.teams); + a.symmetry = MapGeneration::pointSymmetry(a.width, a.height, a.teams); return a; } @@ -358,68 +297,6 @@ struct Layout int orchardPhase, orchardPattern; }; -// Colony 0's feature stamped onto every image of it: an entry is set when any image of its -// tile (or corner) lies in the feature. A union doesn't depend on the order an orbit is -// visited in, so the result is exactly symmetric. -std::vector stamp(const Symmetry &s, const std::vector &feature, - bool corners) -{ - std::vector result(feature.size(), 0); - for (int y = 0; y < s.height; ++y) - for (int x = 0; x < s.width; ++x) - for (int e = 0; e < s.order(); ++e) - if (feature[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]) - { - result[size_t(y) * s.width + x] = 1; - break; - } - return result; -} - -// Plain noise summed over every orbit, as integers so the sum is exact in any order. -std::vector orbitNoise(GenerationContext &context, const Symmetry &s, - const std::string &stream, float smoothing, bool corners) -{ - const int w = s.width, h = s.height; - HeightMap noise(w, h, context.stream(stream)); - noise.makePlain(smoothing); - std::vector raw(size_t(w) * h), summed(size_t(w) * h, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - // Noise as integers (12 bits) so the sum over an orbit is exact: floating-point sums in - // different orders could differ in the last bit and break the symmetry. - raw[size_t(y) * w + x] = int(noise(x, y) * 4096); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - for (int e = 0; e < s.order(); ++e) - summed[size_t(y) * w + x] += - raw[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]; - return summed; -} - -// The share (0 to 1) of eligible entries with the highest (or lowest) values, as a mask. Ties -// at the cut-off are all kept, so the members of an orbit, which have equal values, are never -// split. -std::vector topShare(const std::vector &value, - const std::vector &eligible, double share, - bool highest) -{ - std::vector pool; - for (size_t i = 0; i < value.size(); ++i) - if (eligible[i]) - pool.push_back(value[i]); - std::vector mask(value.size(), 0); - const size_t count = - std::min(pool.size(), size_t(std::lround(std::max(0.0, share) * pool.size()))); - if (!count) - return mask; - std::sort(pool.begin(), pool.end()); - const int threshold = highest ? pool[pool.size() - count] : pool[count - 1]; - for (size_t i = 0; i < value.size(); ++i) - mask[i] = eligible[i] && (highest ? value[i] >= threshold : value[i] <= threshold); - return mask; -} - // Whether a point lies on one of colony 0's causeways, extended past the island's shore by // `inner` and past the outer shore by `outer`. bool onCauseway(const Arena &a, const Layout &l, Point p, double inner, double outer, @@ -475,7 +352,7 @@ bool carvePaths(const Arena &a, const Layout &l, Terrain &t) for (int i : walk) route[size_t(i)] = 1; } - t.paths = stamp(a.symmetry, route, true); + t.paths = stampOrbits(a.symmetry, route, true); for (int v = 0; v < h; ++v) for (int u = 0; u < w; ++u) { @@ -534,12 +411,12 @@ bool buildTerrain(Map &map, GenerationContext &context, const Arena &a, const La pond[i] = toPond <= kPondRadius; gate[i] = onCauseway(a, l, p, 1.5, 1.5, a.causewayWidth / 2); } - homeDisc = stamp(s, homeDisc, true); - homeZone = stamp(s, homeZone, true); - t.ponds = stamp(s, pond, true); + homeDisc = stampOrbits(s, homeDisc, true); + homeZone = stampOrbits(s, homeZone, true); + t.ponds = stampOrbits(s, pond, true); // Without a moat the orchard island joins the land round it, and no causeway is needed; the // moat's width still spaces the homes. - t.causeways = o.moat ? stamp(s, gate, true) : std::vector(n, 0); + t.causeways = o.moat ? stampOrbits(s, gate, true) : std::vector(n, 0); t.moat.assign(n, 0); std::vector lakeEligible(n, 0); @@ -707,8 +584,8 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou homeClear[i] = torusDistance(w, h, p, l.home) <= kHomeRadius + 1.5; landing[i] = onCauseway(a, l, p, 3.0, 4.0, a.causewayWidth / 2 + 2); } - homeClear = stamp(s, homeClear, false); - landing = stamp(s, landing, false); + homeClear = stampOrbits(s, homeClear, false); + landing = stampOrbits(s, landing, false); const auto onPath = [&](int u, int v) { return t.paths[size_t(wrap(v, h)) * w + wrap(u, w)] != 0; }; for (int y = 0; y < h; ++y) @@ -959,27 +836,10 @@ bool furnish(Game &game, GenerationContext &context, const Arena &a, const Layou for (int x = 0; x < w; ++x) if (plan[size_t(y) * w + x] >= 0) map.setResource(x, y, plan[size_t(y) * w + x], 1); - // The engine draws each deposit's amount and look from the gameplay RNG. Every orbit takes - // its lowest-indexed tile's, so deposits are symmetric in size as well as type. - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const int i = y * w + x; - int first = i; - for (int e = 1; e < s.order(); ++e) - first = std::min(first, s.tile(e, x, y)); - if (first == i) - continue; - const Resource source = map.getResource(size_t(first)); - Resource &target = map.getResource(size_t(i)); - if (source.type != target.type) - { - context.detail = "deposits differ across the orbit of (" + std::to_string(x) + - ", " + std::to_string(y) + ")"; - return false; - } - target = source; - } + // The engine draws each deposit's amount and look from the gameplay RNG; equalising gives every + // orbit its lowest-indexed tile's, so deposits are symmetric in size as well as type. + if (!MapGeneration::equaliseDeposits(map, s, context.detail)) + return false; return true; } @@ -1022,19 +882,6 @@ bool generate(Game &game, GenerationContext &context) furnish(game, context, a, l, o, t); } -// The class of a tile's graphic (Map::lookup): grass, grass and sand, sand, sand and water, -// water. It depends only on which terrains a tile's corners hold, not on which corner holds -// which, so it is unchanged by any symmetry. -int terrainClass(Uint16 terrain) -{ - return terrain < 16 ? 0 : terrain < 128 ? 1 : terrain < 144 ? 2 : terrain < 256 ? 3 : 4; -} - -std::string at(int x, int y) -{ - return " at (" + std::to_string(x) + ", " + std::to_string(y) + ")"; -} - // The arena's guarantees, checked on the finished world rather than trusted: every symmetry of // the request maps undermap corners, tile terrain, deposits (type and amount), buildings and // units onto themselves with the colonies permuted one to one; those permutations carry any @@ -1047,78 +894,8 @@ std::string validateWorld(const Game &game, const GenerationContext &context) const int w = map.getW(), h = map.getH(), teams = context.request.nbTeams; const Arena a = arenaFor(context.request); const Symmetry &s = a.symmetry; - if (s.order() != teams) - return "No symmetry gives " + std::to_string(teams) + " colonies the same ground."; - std::vector> permutations; - for (int e = 1; e < s.order(); ++e) - { - const std::string under = " is not symmetric under symmetry " + std::to_string(e) + "."; - std::vector image(size_t(teams), -1); - const auto relate = [&](int from, int to) - { - if (from < 0 || from >= teams || to < 0 || to >= teams) - return false; - if (image[size_t(from)] < 0) - image[size_t(from)] = to; - return image[size_t(from)] == to; - }; - for (int v = 0; v < h; ++v) - for (int u = 0; u < w; ++u) - { - const int c = s.corner(e, u, v); - if (map.getUMTerrain(u, v) != map.getUMTerrain(c % w, c / w)) - return "Undermap corner" + at(u, v) + under; - } - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const int q = s.tile(e, x, y), qx = q % w, qy = q / w; - if (terrainClass(map.getTerrain(x, y)) != terrainClass(map.getTerrain(qx, qy))) - return "Terrain" + at(x, y) + under; - const Resource &ra = map.getResource(x, y), &rb = map.getResource(qx, qy); - if (ra.type != rb.type || ra.amount != rb.amount) - return "Deposit" + at(x, y) + under; - const Uint16 ba = map.getBuilding(x, y), bb = map.getBuilding(qx, qy); - if ((ba == NOGBID) != (bb == NOGBID)) - return "Building" + at(x, y) + under; - if (ba != NOGBID && (!relate(Building::GIDtoTeam(ba), Building::GIDtoTeam(bb)) || - game.teams[Building::GIDtoTeam(ba)] - ->myBuildings[Building::GIDtoID(ba)] - ->type != game.teams[Building::GIDtoTeam(bb)] - ->myBuildings[Building::GIDtoID(bb)] - ->type)) - return "Building" + at(x, y) + under; - const Uint16 ua = map.getGroundUnit(x, y), ub = map.getGroundUnit(qx, qy); - if ((ua == NOGUID) != (ub == NOGUID)) - return "Unit" + at(x, y) + under; - if (ua != NOGUID && - (!relate(Unit::GIDtoTeam(ua), Unit::GIDtoTeam(ub)) || - game.teams[Unit::GIDtoTeam(ua)]->myUnits[Unit::GIDtoID(ua)]->typeNum != - game.teams[Unit::GIDtoTeam(ub)]->myUnits[Unit::GIDtoID(ub)]->typeNum)) - return "Unit" + at(x, y) + under; - } - std::vector hit(size_t(teams), 0); - for (int team = 0; team < teams; ++team) - { - const int to = image[size_t(team)]; - if (to < 0 || hit[size_t(to)]) - return "Symmetry " + std::to_string(e) + " does not map colonies one to one."; - hit[size_t(to)] = 1; - } - permutations.push_back(image); - } - std::vector carried{0}; - std::vector seen(size_t(teams), 0); - seen[0] = 1; - for (size_t head = 0; head < carried.size(); ++head) - for (const auto &image : permutations) - if (!seen[size_t(image[size_t(carried[head])])]) - { - seen[size_t(image[size_t(carried[head])])] = 1; - carried.push_back(image[size_t(carried[head])]); - } - if (int(carried.size()) != teams) - return "The symmetries do not carry colony 0 onto every other colony."; + if (const std::string broken = MapGeneration::orbitMismatch(game, s, teams); !broken.empty()) + return broken; static const char *const targets[] = {"wheat", "wood", "cherries", "oranges", "prunes", "the orchard"}; diff --git a/src/map/generator/generators/WatershedGenerator.cpp b/src/map/generator/generators/WatershedGenerator.cpp index 9592e9af3..236cf48fd 100644 --- a/src/map/generator/generators/WatershedGenerator.cpp +++ b/src/map/generator/generators/WatershedGenerator.cpp @@ -10,6 +10,7 @@ #include "Resources.h" #include "Roads.h" #include "Settlements.h" +#include "Sketch.h" #include "Topology.h" #include "Unit.h" #include @@ -1181,13 +1182,6 @@ std::vector stampTerrain(const Layout &layout, const WatershedOpt return terrain; } -bool vertexTileWater(const std::vector &t, int w, int h, int x, int y) -{ - const int x1 = (x + 1) % w, y1 = (y + 1) % h; - return t[size_t(y) * w + x] == WATER && t[size_t(y) * w + x1] == WATER && - t[size_t(y1) * w + x] == WATER && t[size_t(y1) * w + x1] == WATER; -} - // Every ford interrupts a real channel - open water runs on in line just upstream and downstream // of it - is open from bank to bank along three parallel lines, and reaches dry land at both // ends. `water(x, y)` answers for a tile. @@ -1295,18 +1289,8 @@ Tiles tileView(const std::vector &terrain, int w, int h) t.w = w; t.h = h; const size_t n = size_t(w) * h; - t.water.assign(n, 0); - t.grass.assign(n, 0); - for (int y = 0; y < h; ++y) - for (int x = 0; x < w; ++x) - { - const size_t i = size_t(y) * w + x; - const int x1 = (x + 1) % w, y1 = (y + 1) % h; - const unsigned char a = terrain[i], b = terrain[size_t(y) * w + x1]; - const unsigned char c = terrain[size_t(y1) * w + x], d = terrain[size_t(y1) * w + x1]; - t.water[i] = a == WATER && b == WATER && c == WATER && d == WATER; - t.grass[i] = a == GRASS && b == GRASS && c == GRASS && d == GRASS; - } + t.water = pureTiles(terrain, Torus{w, h}, WATER); + t.grass = pureTiles(terrain, Torus{w, h}, GRASS); t.waterSteps = stepsFrom(Torus{w, h}, t.water); std::vector land(n), notGrass(n); for (size_t i = 0; i < n; ++i) @@ -1855,7 +1839,8 @@ bool generate(Game &game, GenerationContext &context) context.stage = "watershed terrain"; const std::vector terrain = stampTerrain(layout, o, context); - const auto stampedWater = [&](int x, int y) { return vertexTileWater(terrain, w, h, x, y); }; + const std::vector stampedTiles = pureTiles(terrain, Torus{w, h}, WATER); + const auto stampedWater = [&](int x, int y) { return stampedTiles[size_t(y) * w + x] != 0; }; context.detail = checkFords(layout, stampedWater); if (context.detail.empty()) context.detail = checkChannels(layout, stampedWater); diff --git a/src/map/generator/shared/Biomes.cpp b/src/map/generator/shared/Biomes.cpp new file mode 100644 index 000000000..642682a67 --- /dev/null +++ b/src/map/generator/shared/Biomes.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Biomes.h" +#include "FertilityField.h" +#include "GenerationContext.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Map.h" +#include "Morphology.h" +#include "Planting.h" +#include +#include +namespace MapGeneration +{ +BiomeKit fertilePlain() +{ + BiomeKit kit; + kit.name = "fertile plain"; + kit.pondsPer1000 = 2; + kit.pondTiles = 70; + kit.farmPerMille = 450; + kit.outcropsPer1000 = 2; + kit.grovesPer1000 = 1; + return kit; +} + +BiomeKit stoneFortress() +{ + BiomeKit kit; + kit.name = "stone fortress"; + kit.pondsPer1000 = 1; + kit.pondTiles = 40; + kit.wallThickness = 2; + kit.farmPerMille = 300; + kit.outcropsPer1000 = 8; + kit.grovesPer1000 = 1; + return kit; +} + +BiomeKit orchardIsland() +{ + BiomeKit kit; + kit.name = "orchard island"; + kit.pondsPer1000 = 1; + kit.pondTiles = 260; + kit.farmPerMille = 250; + kit.outcropsPer1000 = 2; + kit.orchardIsland = true; + return kit; +} + +BiomeKit forest() +{ + BiomeKit kit; + kit.name = "forest"; + kit.pondsPer1000 = 2; + kit.pondTiles = 50; + kit.farmPerMille = 250; + kit.woodPercent = 60; + kit.outcropsPer1000 = 2; + kit.grovesPer1000 = 2; + kit.coverPercent = 55; + return kit; +} + +namespace +{ +// A kit's worth per tile before normalising, from the start scorer's weights: fertility (0.25) from +// how much of the ground its ponds water (a pond waters about thirty tiles square), wheat, wood and +// their depth (0.56) from the farmland on that watered ground, room (0.10) from the open ground left, +// stone (0.09) for having any, and a judgement for fruit, which the scorer doesn't measure but which +// pulls enemy units across (GAME_RULES_FOR_MAP_DESIGN.md). +double rawWorth(const BiomeKit &kit) +{ + const double pondShare = kit.pondsPer1000 * kit.pondTiles / 1000.0; + const double watered = std::min(1.0, kit.pondsPer1000 * 900 / 1000.0); + const double farm = kit.farmPerMille / 1000.0 * watered; + const double open = std::max(0.0, 1 - pondShare - farm - kit.coverPercent / 100.0 * (1 - farm)); + const double stone = kit.wallThickness > 0 || kit.outcropsPer1000 > 0 ? 1 : 0; + const double fruit = std::min(1.0, kit.grovesPer1000 * 0.05 + (kit.orchardIsland ? 0.3 : 0)); + return 0.25 * watered + 0.56 * farm + 0.10 * open + 0.09 * stone + 0.2 * fruit; +} +} // namespace + +double biomeWorth(const BiomeKit &kit) +{ + return rawWorth(kit) / rawWorth(fertilePlain()); +} + +BiomeTerrain sketchBiome(TerrainSketch &sketch, const Torus &t, + const std::vector ®ion, + const std::vector &doors, const BiomeKit &kit, + GenerationContext &context, const std::string &stream) +{ + const int n = t.size(); + BiomeTerrain terrain; + terrain.water.assign(size_t(n), 0); + terrain.wall.assign(size_t(n), 0); + terrain.island.assign(size_t(n), 0); + int tiles = 0; + for (unsigned char r : region) + tiles += r; + // Ponds keep four tiles inside the region (past the wall), so their beaches stay inside it. + const std::vector inner = erode(t, region, 4 + kit.wallThickness); + std::vector candidates; + for (int i = 0; i < n; ++i) + if (inner[i]) + candidates.push_back(i); + const int ponds = kit.pondsPer1000 > 0 ? std::max(1, tiles * kit.pondsPer1000 / 1000) : 0; + const double pondRadius = std::sqrt(std::max(1, kit.pondTiles) / kPi); + const int spacing2 = int(std::lround(9 * pondRadius * pondRadius)); + std::vector seeds; + for (int p = 0; p < ponds && !candidates.empty(); ++p) + for (int attempt = 0; attempt < 50; ++attempt) + { + const int at = candidates[context.bounded(stream, std::uint32_t(candidates.size()))]; + bool apart = true; + for (int s : seeds) + apart = apart && t.dist2(at % t.w, at / t.w, s % t.w, s / t.w) >= spacing2; + if (apart) + { + seeds.push_back(at); + break; + } + } + std::vector queued(size_t(n), 0); + constexpr double kIslandRadius = 4.5; + for (size_t p = 0; p < seeds.size(); ++p) + { + const int seed = seeds[p]; + const bool island = kit.orchardIsland && p == 0; + const int target = + kit.pondTiles + (island ? int(std::lround(kPi * kIslandRadius * kIslandRadius)) : 0); + growWater( + t, terrain.water, seed, target, [&](int i) { return inner[i] != 0; }, [&](int i) + { return t.dist2(seed % t.w, seed / t.w, i % t.w, i / t.w); }, queued, int(p) + 1); + if (island) + for (int i = 0; i < n; ++i) + if (terrain.water[i] && t.dist2(seed % t.w, seed / t.w, i % t.w, i / t.w) <= + kIslandRadius * kIslandRadius) + { + terrain.water[i] = 0; + terrain.island[i] = 1; + } + } + for (int i = 0; i < n; ++i) + if (terrain.water[i]) + sketch[i] = WATER; + if (kit.wallThickness > 0) + { + const std::vector depth = clearance(t, region); + for (int i = 0; i < n; ++i) + terrain.wall[i] = region[i] && depth[i] <= kit.wallThickness && !doors[i]; + } + return terrain; +} + +void furnishBiome(Map &map, const Torus &t, GenerationContext &context, + const std::vector ®ion, const BiomeTerrain &terrain, + const BiomeKit &kit, const std::vector &keepClear, + const std::string &stream) +{ + const int n = t.size(); + for (int i = 0; i < n; ++i) + if (terrain.wall[i] && !keepClear[i] && map.isResourceAllowed(i % t.w, i / t.w, STONE)) + map.setResource(i % t.w, i / t.w, STONE, 1); + // Outcrops and groves are clumps a tile across (placeResourceClump), so everything keeps a tile in + // from the ground's edge, the wall, the island and the clear ground. + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = region[i] && !terrain.wall[i] && !terrain.island[i] && !keepClear[i]; + const std::vector inside = erode(t, ground, 1); + const auto eligible = [&](int i) { return inside[i] && clearGround(map, i % t.w, i / t.w); }; + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector patch = periodicNoise(t.w, t.h, 12, context.stream(stream + "-patch")); + const std::vector split = periodicNoise(t.w, t.h, 6, context.stream(stream + "-split")); + int fertile = 0; + for (int i = 0; i < n; ++i) + fertile += eligible(i) && fertility.at(i % t.w, i / t.w) > 0; + const int farm = fertile * kit.farmPerMille / 1000; + const std::string stoneStream = stream + "-stone", fruitStream = stream + "-fruit"; + furnishGround( + map, t, context, fertility, eligible, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int area) + { + return GroundAmounts{farm * (100 - kit.woodPercent) / 100, farm * kit.woodPercent / 100, + area * kit.outcropsPer1000 / 1000, + area * kit.grovesPer1000 / 1000}; + }, + stoneStream.c_str(), fruitStream.c_str()); + int fruit = 0; + for (int i = 0; i < n; ++i) + if (terrain.island[i] && !keepClear[i] && map.isResourceAllowed(i % t.w, i / t.w, CHERRY)) + map.setResource(i % t.w, i / t.w, CHERRY + fruit++ % 3, 1); + if (kit.coverPercent <= 0) + return; + const std::vector cover = periodicNoise(t.w, t.h, 10, context.stream(stream + "-cover")); + std::vector open; + for (int i = 0; i < n; ++i) + if (eligible(i)) + open.push_back(cover[i]); + if (open.empty()) + return; + const int level = percentile(open, 100 - std::clamp(kit.coverPercent, 0, 100)); + plantCover(map, t, region, WOOD, [&](int i) { return eligible(i) && cover[i] >= level; }); +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Biomes.h b/src/map/generator/shared/Biomes.h new file mode 100644 index 000000000..a3a7b9733 --- /dev/null +++ b/src/map/generator/shared/Biomes.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include "Sketch.h" +#include +#include +class Map; +struct GenerationContext; +namespace MapGeneration +{ +// A kind of land as data: how much water it holds, whether stone rings it, how much of it is farmland, +// forest, outcrops and fruit. A map that gives colonies different land of equal worth, rather than the +// same land turned round, deals each colony a kit, sizes each colony's ground by the kit's worth +// (growTerritories' worth, Territories.h), sketches the terrain and furnishes it with the same two calls +// whatever the kit. +// +// The worth is an estimate for sharing out ground, not a measurement: it adds up what each layer is +// usually worth to a young colony per tile, from the start scorer's weights (StartQuality.h). Tune a +// map's kits with the fairness tournament (docs/map-generators/FAIRNESS_TOURNAMENT.md), and measure +// the finished starts with scoreStarts, before trusting it. + +struct BiomeKit +{ + std::string name; + int pondsPer1000 = 0; // ponds per 1000 tiles of ground (at least one when above 0) + int pondTiles = 0; // each pond's size in tiles + int wallThickness = 0; // a ring of stone round the ground this thick; 0 for none + int farmPerMille = 0; // of the ground's fertile tiles, the share under wheat and wood + int woodPercent = 33; // of that farmland, the share that is wood + int outcropsPer1000 = 0; // one-tile stone outcrops per 1000 tiles + int grovesPer1000 = 0; // one-tile fruit groves per 1000 tiles + int coverPercent = 0; // of the open ground left, the share grown over with wood + bool orchardIsland = false; // the first pond holds an island of all three fruits +}; + +/// The kits a map can start from. +BiomeKit fertilePlain(); // big ponds, wide farmland, open ground +BiomeKit stoneFortress(); // a thick stone ring and outcrops, poor water +BiomeKit orchardIsland(); // a lake with an island of all three fruits, little farmland +BiomeKit forest(); // most of the ground under wood, ponds to keep it growing + +/// The estimated worth of one tile of ground dealt this kit, relative to a fertile plain's 1. +double biomeWorth(const BiomeKit &); + +/// What sketchBiome laid, for furnishBiome and a validator. +struct BiomeTerrain +{ + std::vector water; // pond tiles + std::vector wall; // the stone ring + std::vector island; // the orchard island +}; + +/// The kit's terrain in `region`: ponds grown round (growWater) from seeds at least four tiles inside +/// the region, one of them with an island in the middle when the kit has an orchard, and the stone +/// ring on the region's rim (the tiles within `wallThickness` of ground outside it) except on `doors`. +/// Water goes into the sketch; lay beaches afterwards. Draws from `stream`. +BiomeTerrain sketchBiome(TerrainSketch &, const Torus &, const std::vector ®ion, + const std::vector &doors, const BiomeKit &, + GenerationContext &, const std::string &stream); + +/// The kit's deposits on a written map: stone on the ring, farmland on the most fertile open ground in +/// patches (furnishGround), outcrops and groves, all three fruits on the island, and cover over the +/// share of what is left open, all a tile in from the ground's edge. Nothing goes on `keepClear` (a +/// swarm's clearing, a road). Draws from streams named after `stream`. +void furnishBiome(Map &, const Torus &, GenerationContext &, + const std::vector ®ion, const BiomeTerrain &, const BiomeKit &, + const std::vector &keepClear, const std::string &stream); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Channels.cpp b/src/map/generator/shared/Channels.cpp new file mode 100644 index 000000000..7e3913983 --- /dev/null +++ b/src/map/generator/shared/Channels.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Channels.h" +#include "Map.h" +#include "TerrainType.h" +#include "Topology.h" +#include "Walls.h" +#include +namespace MapGeneration +{ +int widestChannelTowersCross(int level, int depth) +{ + const int range = kTowerRange[std::clamp(level, 1, 3) - 1]; + return std::max(0, range - bankToBank(0) - (std::max(1, depth) - 1)); +} + +int narrowestChannelTowersMiss(int level) +{ + return widestChannelTowersCross(level, 1) + 1; +} + +std::vector beachTiles(const TerrainSketch &sketch, const Torus &t) +{ + const std::vector grass = pureTiles(sketch, t, GRASS); + const std::vector water = pureTiles(sketch, t, WATER); + std::vector beach(sketch.size(), 0); + for (size_t i = 0; i < beach.size(); ++i) + beach[i] = !grass[i] && !water[i]; + return beach; +} + +std::vector beachTiles(const Map &map, const Torus &t) +{ + std::vector beach(size_t(t.size()), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + beach[size_t(y) * t.w + x] = !map.isGrass(x, y) && !map.isWater(x, y); + return beach; +} + +int bridgeAcross(TerrainSketch &sketch, const Torus &t, ShapePoint from, ShapePoint to, + double halfWidth) +{ + std::vector deck(sketch.size(), 0); + strokePath(deck, t, {{from.x, from.y, halfWidth}, {to.x, to.y, halfWidth}}); + int laid = 0; + for (size_t i = 0; i < sketch.size(); ++i) + if (deck[i] && sketch[i] == WATER) + { + sketch[i] = SAND; + ++laid; + } + return laid; +} + +std::vector crossingsPerLabel(const Torus &t, const std::vector &bridges, + const std::vector &labelled, int labels) +{ + const std::vector piece = connectedRegions(bridges, t.w, t.h, true, GridNeighbors::Eight); + int pieces = 0; + for (int p : piece) + pieces = std::max(pieces, p + 1); + std::vector> touches(size_t(std::max(0, labels)), + std::vector(size_t(pieces), 0)); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int p = piece[size_t(y) * t.w + x]; + if (p < 0) + continue; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int label = labelled[t.at(x + dx, y + dy)]; + if (label >= 0 && label < labels) + touches[label][p] = 1; + } + } + std::vector count(size_t(std::max(0, labels)), 0); + for (int label = 0; label < labels; ++label) + count[label] = int(std::count(touches[label].begin(), touches[label].end(), 1)); + return count; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Channels.h b/src/map/generator/shared/Channels.h new file mode 100644 index 000000000..11a8e9e71 --- /dev/null +++ b/src/map/generator/shared/Channels.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Drawing.h" +#include "Grid.h" +#include "Sketch.h" +#include +class Map; +namespace MapGeneration +{ +// Water between two pieces of land, sized for what it is meant to do: a canal towers shoot across +// from the first minute, a moat no tower can reach over, a strait with a few sand bridges. Every +// generator that parts land with water has done the same beach arithmetic by hand; it lives here once. +// +// The arithmetic. A straight channel `w` undermap corners wide gets a sand corner either side from +// layBeaches, and a tile is pure grass only when its four corners are, so the channel spoils w + 3 +// tiles of grass across (kChannelSpoiledTiles), of which w - 1 are pure water. A tower's footprint on +// the last grass tile of one bank is w + 4 tiles (Chebyshev) from the first grass tile of the other, +// and a tower of range r hits what is r tiles away or nearer (towerReach, Walls.h). + +/// The tiles of grass a channel spoils beyond its water corners. +constexpr int kChannelSpoiledTiles = 3; + +/// The distance from a tower on one bank's last grass tile to the other bank's first grass tile, for a +/// straight channel `waterCorners` wide. +inline int bankToBank(int waterCorners) +{ + return waterCorners + kChannelSpoiledTiles + 1; +} + +/// The widest channel (in water corners) a tower of `level` (1 to 3) on one bank still shoots across, +/// reaching `depth` tiles of grass into the far bank (1 = its first tile). 0 when even a single water +/// corner is too wide. +int widestChannelTowersCross(int level, int depth = 1); + +/// The narrowest channel (in water corners) a tower of `level` cannot shoot across at all. +int narrowestChannelTowersMiss(int level); + +/// A sketch's beach as the game will draw it: every tile that is neither pure grass nor pure water, +/// the walkable, unbuildable rim round water. Towers stand against it to cover a canal +/// (TowerRequest::against). +std::vector beachTiles(const TerrainSketch &, const Torus &); + +/// The same on a finished map. +std::vector beachTiles(const Map &, const Torus &); + +/// A sand bridge laid straight across water from `from` to `to`: every water corner within +/// `halfWidth` of the line becomes sand (strokePath over corners). A tile is water only when all four +/// of its corners are, so a bridge one corner wide already carries units on the two tiles either side +/// of it. Run after layBeaches (a bridge's own sand needs no beach). Returns the corners changed. +int bridgeAcross(TerrainSketch &, const Torus &, ShapePoint from, ShapePoint to, double halfWidth); + +/// How many separate bridges (eight-connected pieces of `bridges`) touch each label's ground, for the +/// labels 0 to `labels` - 1: the check that every colony got as many ways across as every other. +std::vector crossingsPerLabel(const Torus &, const std::vector &bridges, + const std::vector &labelled, int labels); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Contact.cpp b/src/map/generator/shared/Contact.cpp new file mode 100644 index 000000000..fb36fafe2 --- /dev/null +++ b/src/map/generator/shared/Contact.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Contact.h" +#include "Map.h" +#include +#include +#include +#include +#include +#include +namespace MapGeneration +{ +int stepCost(const Map &map, int x, int y, const StepCosts &costs) +{ + if (map.isWater(x, y)) + return costs.water; + if (map.getBuilding(x, y) != NOGBID) + return costs.building; + if (map.isResource(x, y)) + { + const int type = map.getResource(x, y).type; + return type == STONE || (type >= CHERRY && type <= CHERRY + 2) ? costs.eternal + : costs.clearable; + } + return costs.open; +} + +std::vector costsFrom(const Map &map, const Torus &t, const std::vector &sources, + const StepCosts &costs, GridNeighbors neighbours) +{ + const int n = t.w * t.h; + std::vector step(n), cost(n, INT_MAX); + for (int i = 0; i < n; ++i) + step[i] = stepCost(map, i % t.w, i / t.w, costs); + using Entry = std::pair; + std::priority_queue, std::greater> heap; + for (int i : sources) + if (cost[i] > 0) + { + cost[i] = 0; + heap.push({0, i}); + } + while (!heap.empty()) + { + const auto [c, i] = heap.top(); + heap.pop(); + if (c > cost[i]) + continue; + const int x = i % t.w, y = i / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if ((!dx && !dy) || (neighbours == GridNeighbors::Cardinal && dx && dy)) + continue; + const int m = t.at(x + dx, y + dy); + if (step[m] < 0 || c + step[m] >= cost[m]) + continue; + cost[m] = c + step[m]; + heap.push({cost[m], m}); + } + } + for (int &c : cost) + if (c == INT_MAX) + c = -1; + return cost; +} + +std::vector ContactReport::nearestRival() const +{ + std::vector nearest(cost.size(), -1); + for (size_t from = 0; from < cost.size(); ++from) + for (size_t to = 0; to < cost.size(); ++to) + if (from != to && cost[from][to] >= 0 && + (nearest[from] < 0 || cost[from][to] < nearest[from])) + nearest[from] = cost[from][to]; + return nearest; +} + +int ContactReport::spread() const +{ + return costSpread(nearestRival()); +} + +ContactReport contactMatrix(const Map &map, int teams, const StepCosts &costs) +{ + const Torus t(map); + const auto units = unitTilesByTeam(map, teams); + ContactReport report; + report.cost.assign(size_t(teams), std::vector(size_t(teams), -1)); + for (int from = 0; from < teams; ++from) + { + const std::vector field = costsFrom(map, t, units[from], costs); + for (int to = 0; to < teams; ++to) + { + int best = from == to ? 0 : -1; + if (from != to) + for (int i : units[to]) + if (field[i] >= 0 && (best < 0 || field[i] < best)) + best = field[i]; + report.cost[from][to] = best; + } + } + return report; +} + +std::vector costsToTarget(const Map &map, int teams, const std::vector &targets, + const StepCosts &costs) +{ + const Torus t(map); + const auto units = unitTilesByTeam(map, teams); + std::vector result(size_t(teams), -1); + for (int team = 0; team < teams; ++team) + { + const std::vector field = costsFrom(map, t, units[team], costs); + for (size_t i = 0; i < targets.size(); ++i) + if (targets[i] && field[i] >= 0 && (result[team] < 0 || field[i] < result[team])) + result[team] = field[i]; + } + return result; +} + +int costSpread(const std::vector &costs) +{ + if (costs.empty()) + return 0; + if (std::find(costs.begin(), costs.end(), -1) != costs.end()) + return -1; + const auto [lo, hi] = std::minmax_element(costs.begin(), costs.end()); + return *hi - *lo; +} + +std::string unevenCosts(const std::vector &costs, int tolerance, const std::string &what) +{ + for (size_t team = 0; team < costs.size(); ++team) + if (costs[team] < 0) + return "Colony " + std::to_string(team) + " cannot reach " + what + "."; + const int spread = costSpread(costs); + if (spread <= tolerance) + return ""; + const size_t worst = size_t(std::max_element(costs.begin(), costs.end()) - costs.begin()); + return "Colony " + std::to_string(worst) + " is " + std::to_string(spread) + " further from " + + what + " than the nearest colony (tolerance " + std::to_string(tolerance) + ")."; +} + +std::vector equalCostSites(const std::vector> &costs, + const std::vector &eligible, int target, + int tolerance) +{ + const int teams = int(costs.size()); + std::vector sites(size_t(teams), -1); + for (int k = 0; k < teams; ++k) + { + int bestGap = tolerance + 1; + for (size_t i = 0; i < eligible.size(); ++i) + { + const int own = costs[k][i]; + if (!eligible[i] || own < 0) + continue; + const int gap = std::abs(own - target); + if (gap >= bestGap) + continue; + bool nearest = true; + for (int j = 0; j < teams && nearest; ++j) + nearest = j == k || costs[j][i] < 0 || costs[j][i] > own; + if (nearest) + { + bestGap = gap; + sites[k] = int(i); + } + } + } + return sites; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Contact.h b/src/map/generator/shared/Contact.h new file mode 100644 index 000000000..d23f39d17 --- /dev/null +++ b/src/map/generator/shared/Contact.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include "Topology.h" +#include +#include +class Map; +namespace MapGeneration +{ +// How far colonies are from each other and from what a map promises them, by what it costs to get +// there rather than by steps alone. Walls.h's walkSpread counts plain walking steps to one target; +// here a cost model says what a step onto each kind of tile costs, so the same measure answers "how +// far to walk", "how much forest to chop through", or "how far once units can swim", and a map that is +// only fair under one of those (a forest map, a canal map) can prove it. + +/// What stepping onto a tile costs, by what is on it; -1 means the step cannot be taken. Clearable +/// deposits are wheat, wood and algae (workers can clear them); eternal ones are stone and fruit. +/// Water is checked first, then buildings, then deposits. +struct StepCosts +{ + int open = 1; + int clearable = -1; + int eternal = -1; + int water = -1; + int building = -1; + + /// Units walking today: open ground only. + static StepCosts walking() { return {1, -1, -1, -1, -1}; } + /// Workers cutting their way through: a clearable deposit costs `clear` steps. + static StepCosts chopping(int clear = 4) { return {1, clear, -1, -1, -1}; } + /// Units that have learnt to swim: water costs `swim` steps. + static StepCosts swimming(int swim = 1) { return {1, -1, -1, swim, -1}; } +}; + +/// The cost of stepping onto tile (x, y) of a finished map, -1 when it cannot be entered. +int stepCost(const Map &, int x, int y, const StepCosts &); + +/// The cheapest cost from any source tile to every tile (0 at a source, -1 where nothing reaches), +/// eight-connected on the torus unless told otherwise. A source is reached whatever stands on it. +std::vector costsFrom(const Map &, const Torus &, const std::vector &sources, + const StepCosts &, GridNeighbors = GridNeighbors::Eight); + +/// Every colony's cost to reach every other colony's units (row = from, column = to; 0 on the +/// diagonal, -1 when unreachable), measured from each colony's unit tiles. +struct ContactReport +{ + std::vector> cost; + /// Each colony's cost to its nearest rival, -1 when none can be reached. + std::vector nearestRival() const; + /// The largest minus the smallest nearest-rival cost; -1 when some colony reaches no rival. + int spread() const; +}; +ContactReport contactMatrix(const Map &, int teams, const StepCosts &); + +/// Every colony's cost, from its units, to the nearest tile of `targets` (-1 when none is reached). +std::vector costsToTarget(const Map &, int teams, const std::vector &targets, + const StepCosts &); + +/// The largest minus the smallest of a list of costs, -1 when any is -1 (unreachable), 0 when empty. +int costSpread(const std::vector &costs); + +/// A validator's check that every colony reaches `what` within `tolerance` of every other under a cost +/// model: empty when it does, otherwise which colony fell short and by how much. +std::string unevenCosts(const std::vector &costs, int tolerance, const std::string &what); + +/// One site per colony at the same cost from home: for each colony k (its row of `costs`, a costsFrom +/// field per colony), the tile `eligible` allows whose cost from k is nearest `target` and lower than +/// every other colony's cost to it (so it lies on k's side), within `tolerance` of the target; the lowest +/// index wins a tie. -1 for a colony with no such tile. Hidden groves in a forest at an equal chop from +/// every home (StepCosts::chopping), a prize equally deep in every colony's rock. +std::vector equalCostSites(const std::vector> &costs, + const std::vector &eligible, int target, + int tolerance); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Drawing.cpp b/src/map/generator/shared/Drawing.cpp index cd9328b4e..32c430581 100644 --- a/src/map/generator/shared/Drawing.cpp +++ b/src/map/generator/shared/Drawing.cpp @@ -155,6 +155,58 @@ std::vector bentPath(ShapePoint from, double heading, double length return bezierPath(from, control, to, fromHalfWidth, toHalfWidth, segments); } +std::vector wanderingPath(const Torus &t, ShapePoint from, ShapePoint to, + double halfWidth, double wander, double widthJitter, + std::mt19937 &random) +{ + const auto nearest = [](double d, int period) + { + d = std::fmod(d, double(period)); + if (d > period / 2.0) + d -= period; + else if (d < -period / 2.0) + d += period; + return d; + }; + const double dx = nearest(to.x - from.x, t.w), dy = nearest(to.y - from.y, t.h); + const double length = std::hypot(dx, dy); + // A draw in [-1, 1) straight from the generator's bits: std::uniform_real_distribution differs + // between standard libraries. + const auto unit = [](std::mt19937 &r) { return (double(r()) - 2147483648.0) / 2147483648.0; }; + // Three harmonics of a sine that vanishes at both ends for the sideways push (weaker as they get + // finer), and three phases for the width. + double bend[3], phase[3]; + for (double &b : bend) + b = unit(random); + for (double &p : phase) + p = unit(random) * kPi; + const int segments = std::max(1, int(std::ceil(length))); + const double nx = length > 0 ? -dy / length : 0, ny = length > 0 ? dx / length : 0; + std::vector path; + path.reserve(size_t(segments) + 1); + for (int i = 0; i <= segments; ++i) + { + const double s = double(i) / segments; + double side = 0, swell = 0; + for (int h = 0; h < 3; ++h) + { + side += bend[h] * std::sin(kPi * (h + 1) * s) / (h + 1); + swell += std::sin(2 * kPi * (h + 1) * s + phase[h]) / 3; + } + const double offset = wander * side / (1 + 1 / 2.0 + 1 / 3.0); + path.push_back({from.x + dx * s + nx * offset, from.y + dy * s + ny * offset, + std::max(0.5, halfWidth * (1 + widthJitter * swell))}); + } + return path; +} + +void carveCorridor(std::vector &mask, const Torus &t, ShapePoint from, ShapePoint to, + double halfWidth, double wander, double widthJitter, std::mt19937 &random, + unsigned char value) +{ + strokePath(mask, t, wanderingPath(t, from, to, halfWidth, wander, widthJitter, random), value); +} + namespace { // The distance from a point to a path's centre line, less the path's half width at the nearest diff --git a/src/map/generator/shared/Drawing.h b/src/map/generator/shared/Drawing.h index 1d3248b60..f63c05e3f 100644 --- a/src/map/generator/shared/Drawing.h +++ b/src/map/generator/shared/Drawing.h @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace MapGeneration { @@ -50,6 +51,21 @@ std::vector bezierPath(ShapePoint from, ShapePoint control, ShapePo std::vector bentPath(ShapePoint from, double heading, double length, double bend, double fromHalfWidth, double toHalfWidth, int segments); +/// A path that wanders from `from` to `to` the short way round the torus: points about a tile apart, +/// pushed sideways by up to `wander` tiles (three harmonics with random amplitudes, zero at both ends, +/// so it leaves and arrives exactly where asked), and a half width of `halfWidth` swelling and +/// narrowing by up to `widthJitter` of itself. `to` is placed beside `from` (its nearest copy), so +/// points may run past the seam, as strokePath expects. Six draws from `random`. A tunnel between two +/// chambers, a lane between two plazas, a trail that doesn't look ruled. +std::vector wanderingPath(const Torus &, ShapePoint from, ShapePoint to, + double halfWidth, double wander, double widthJitter, + std::mt19937 &random); + +/// Strokes a wanderingPath into `mask` with `value`: a corridor carved from one point to another. +void carveCorridor(std::vector &mask, const Torus &, ShapePoint from, ShapePoint to, + double halfWidth, double wander, double widthJitter, std::mt19937 &random, + unsigned char value = 1); + /// The narrowest gap between two stroked paths: the least distance from any point of either to /// the other's centre line, less both half widths there. Negative where they overlap. Points of /// `a` nearer than `skip` tiles to its first point are ignored, so a branch can be measured diff --git a/src/map/generator/shared/Farmland.cpp b/src/map/generator/shared/Farmland.cpp index 873dcf487..cb0c91f32 100644 --- a/src/map/generator/shared/Farmland.cpp +++ b/src/map/generator/shared/Farmland.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "Farmland.h" +#include "Morphology.h" #include "Map.h" #include "Resources.h" #include "Drawing.h" @@ -256,16 +257,12 @@ std::vector growFarmFields(const Torus &t, const std::vector // within itself), which removes such strips and keeps the rest. for (size_t s = 0; s < seeds.size(); ++s) { - std::vector outside(n, 0); + std::vector inside(n, 0); for (int i = 0; i < n; ++i) - outside[i] = labels[i] != int(s) && homeOf[i] != owners[s]; - const std::vector fromOutside = stepsFrom(t, outside); - std::vector core(n, 0); + inside[i] = labels[i] == int(s) || homeOf[i] == owners[s]; + const std::vector opened = openMask(t, inside, kOpening); for (int i = 0; i < n; ++i) - core[i] = !outside[i] && fromOutside[i] > kOpening; - const std::vector fromCore = stepsFrom(t, core); - for (int i = 0; i < n; ++i) - if (labels[i] == int(s) && (fromCore[i] < 0 || fromCore[i] > kOpening)) + if (labels[i] == int(s) && !opened[i]) labels[i] = -1; } // Trimming a field to its gaps can leave slivers cut off from the rest; a field keeps only the diff --git a/src/map/generator/shared/GraphMaze.cpp b/src/map/generator/shared/GraphMaze.cpp index 4c06e784c..06e543e10 100644 --- a/src/map/generator/shared/GraphMaze.cpp +++ b/src/map/generator/shared/GraphMaze.cpp @@ -1,10 +1,40 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "GraphMaze.h" #include "GenerationContext.h" +#include #include namespace MapGeneration { -bool pocketsFit(const Tessellation &g, const std::vector &pocket) +CellGraph cellGraph(const Tessellation &tiling) +{ + CellGraph g; + for (const auto &cell : tiling.cells) + g.cellEdges.push_back(cell.edges); + for (const auto &edge : tiling.edges) + g.edgeCells.push_back({edge.cells[0], edge.cells[1]}); + g.distance2 = [&tiling](int a, int b) { return tiling.distance2(a, b); }; + return g; +} + +CellGraph cellGraph(const Torus &t, const std::vector &sites, + const std::vector> &neighbours) +{ + CellGraph g; + g.cellEdges.assign(sites.size(), {}); + for (int a = 0; a < int(neighbours.size()); ++a) + for (int b : neighbours[a]) + if (a < b) + { + g.cellEdges[a].push_back(int(g.edgeCells.size())); + g.cellEdges[b].push_back(int(g.edgeCells.size())); + g.edgeCells.push_back({a, b}); + } + g.distance2 = [t, sites](int a, int b) + { return (long long)t.dist2(sites[a].x, sites[a].y, sites[b].x, sites[b].y); }; + return g; +} + +bool pocketsFit(const CellGraph &g, const std::vector &pocket) { int start = -1, freeCells = 0; for (int cell = 0; cell < g.cellCount(); ++cell) @@ -16,14 +46,14 @@ bool pocketsFit(const Tessellation &g, const std::vector &pocket) continue; } bool door = false; - for (int edge : g.cells[cell].edges) + for (int edge : g.cellEdges[cell]) door = door || !pocket[g.other(edge, cell)]; if (!door) return false; } if (!freeCells) return false; - std::vector seen(g.cells.size(), 0); + std::vector seen(size_t(g.cellCount()), 0); std::vector stack{start}; seen[start] = 1; int reached = 1; @@ -31,7 +61,7 @@ bool pocketsFit(const Tessellation &g, const std::vector &pocket) { const int cell = stack.back(); stack.pop_back(); - for (int edge : g.cells[cell].edges) + for (int edge : g.cellEdges[cell]) { const int next = g.other(edge, cell); if (!pocket[next] && !seen[next]) @@ -45,11 +75,11 @@ bool pocketsFit(const Tessellation &g, const std::vector &pocket) return reached == freeCells; } -std::vector spreadPockets(const Tessellation &g, int count) +std::vector spreadPockets(const CellGraph &g, int count) { - std::vector isPocket(g.cells.size(), 0); + std::vector isPocket(size_t(g.cellCount()), 0); std::vector pockets; - std::vector nearest(g.cells.size(), 0); + std::vector nearest(size_t(g.cellCount()), 0); for (int k = 0; k < count; ++k) { std::vector order; @@ -79,9 +109,8 @@ std::vector spreadPockets(const Tessellation &g, int count) return pockets; } -bool carveSpanningTree(const Tessellation &g, GenerationContext &context, - const std::string &stream, const std::vector &blocked, - std::vector &open) +bool carveSpanningTree(const CellGraph &g, GenerationContext &context, const std::string &stream, + const std::vector &blocked, std::vector &open) { std::vector free; for (int cell = 0; cell < g.cellCount(); ++cell) @@ -97,7 +126,7 @@ bool carveSpanningTree(const Tessellation &g, GenerationContext &context, { const int cell = stack.back(); choices.clear(); - for (int edge : g.cells[cell].edges) + for (int edge : g.cellEdges[cell]) if (!visited[g.other(edge, cell)]) choices.push_back(edge); if (choices.empty()) @@ -114,7 +143,7 @@ bool carveSpanningTree(const Tessellation &g, GenerationContext &context, return std::all_of(visited.begin(), visited.end(), [](unsigned char v) { return v != 0; }); } -std::vector openPocketDoors(const Tessellation &g, GenerationContext &context, +std::vector openPocketDoors(const CellGraph &g, GenerationContext &context, const std::string &stream, const std::vector &pockets, const std::vector &isPocket, std::vector &open) @@ -124,7 +153,7 @@ std::vector openPocketDoors(const Tessellation &g, GenerationContext &conte for (int pocket : pockets) { choices.clear(); - for (int edge : g.cells[pocket].edges) + for (int edge : g.cellEdges[pocket]) if (!isPocket[g.other(edge, pocket)]) choices.push_back(edge); if (choices.empty()) @@ -139,7 +168,7 @@ std::vector openPocketDoors(const Tessellation &g, GenerationContext &conte return doors; } -void openLoops(const Tessellation &g, GenerationContext &context, const std::string &stream, +void openLoops(const CellGraph &g, GenerationContext &context, const std::string &stream, const std::vector &blocked, int percent, std::vector &open) { @@ -150,8 +179,8 @@ void openLoops(const Tessellation &g, GenerationContext &context, const std::str if (blocked[cell]) continue; ++freeCells; - for (int edge : g.cells[cell].edges) - if (g.edges[edge].cells[0] == cell && !open[edge] && !blocked[g.other(edge, cell)]) + for (int edge : g.cellEdges[cell]) + if (g.edgeCells[edge][0] == cell && !open[edge] && !blocked[g.other(edge, cell)]) closed.push_back(edge); } context.shuffle(closed.begin(), closed.end(), stream); @@ -160,17 +189,17 @@ void openLoops(const Tessellation &g, GenerationContext &context, const std::str open[closed[i]] = 1; } -std::vector deadEnds(const Tessellation &g, const std::vector &open, +std::vector deadEnds(const CellGraph &g, const std::vector &open, const std::vector &blocked, std::vector &exitEdge) { std::vector ends; - exitEdge.assign(g.cells.size(), -1); + exitEdge.assign(size_t(g.cellCount()), -1); for (int cell = 0; cell < g.cellCount(); ++cell) { if (blocked[cell]) continue; int degree = 0, exit = -1; - for (int edge : g.cells[cell].edges) + for (int edge : g.cellEdges[cell]) if (open[edge]) { ++degree; @@ -184,4 +213,40 @@ std::vector deadEnds(const Tessellation &g, const std::vector &pocket) +{ + return pocketsFit(cellGraph(g), pocket); +} + +std::vector spreadPockets(const Tessellation &g, int count) +{ + return spreadPockets(cellGraph(g), count); +} + +bool carveSpanningTree(const Tessellation &g, GenerationContext &context, const std::string &stream, + const std::vector &blocked, std::vector &open) +{ + return carveSpanningTree(cellGraph(g), context, stream, blocked, open); +} + +std::vector openPocketDoors(const Tessellation &g, GenerationContext &context, + const std::string &stream, const std::vector &pockets, + const std::vector &isPocket, + std::vector &open) +{ + return openPocketDoors(cellGraph(g), context, stream, pockets, isPocket, open); +} + +void openLoops(const Tessellation &g, GenerationContext &context, const std::string &stream, + const std::vector &blocked, int percent, + std::vector &open) +{ + openLoops(cellGraph(g), context, stream, blocked, percent, open); +} + +std::vector deadEnds(const Tessellation &g, const std::vector &open, + const std::vector &blocked, std::vector &exitEdge) +{ + return deadEnds(cellGraph(g), open, blocked, exitEdge); +} } // namespace MapGeneration diff --git a/src/map/generator/shared/GraphMaze.h b/src/map/generator/shared/GraphMaze.h index f92a2f8b8..8d755b6b7 100644 --- a/src/map/generator/shared/GraphMaze.h +++ b/src/map/generator/shared/GraphMaze.h @@ -1,48 +1,93 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include "Points.h" #include "Tessellation.h" +#include +#include #include #include struct GenerationContext; namespace MapGeneration { -// A maze carved through a Tessellation's cells: which edges are open passages, which stay walls. -// Every function here works on the cell graph alone, so the same maze logic runs on squares, -// hexagons or warped cells. Some cells can be set aside as pockets - colonies' homes - that the +// A maze carved through a graph of cells: which edges are open passages, which stay walls. Every +// function here works on the cell graph alone, so the same maze logic runs on squares, hexagons or +// warped cells (a Tessellation), or on any irregular cells a design drew: Voronoi cells round +// scattered sites (Points.h), chambers joined by tunnels, plazas joined by streets. Some cells can be set aside as pockets - colonies' homes - that the // tree never passes through, each opened later by a single door so it is a cul-de-sac. `open` holds // one flag per edge. +/// The graph the maze functions work on: each cell's edges in a fixed order, each edge's two cells +/// (its owner first), and the squared distance between two cells' centres. Edges are identities, not +/// pairs, so two cells may share more than one. +struct CellGraph +{ + std::vector> cellEdges; + std::vector> edgeCells; + std::function distance2; + + int cellCount() const { return int(cellEdges.size()); } + int other(int edge, int cell) const + { + return edgeCells[edge][0] == cell ? edgeCells[edge][1] : edgeCells[edge][0]; + } +}; + +/// A tessellation's cells as a graph, edges numbered as the tessellation numbers them. The graph +/// refers to the tessellation for distances, so it must not outlive it. +CellGraph cellGraph(const Tessellation &); + +/// Scattered sites' cells as a graph: one edge per neighbouring pair (neighbours as siteNeighbours +/// lists them), owned by the lower site, numbered in order of the lower then the higher site, and +/// distances the short way round the torus in tiles squared. +CellGraph cellGraph(const Torus &, const std::vector &sites, + const std::vector> &neighbours); + /// Whether a set of pocket cells leaves a layout a maze can be carved in: every pocket borders at /// least one other cell to put its door in, and the other cells are all joined together. -bool pocketsFit(const Tessellation &, const std::vector &pocket); +bool pocketsFit(const CellGraph &, const std::vector &pocket); /// `count` pocket cells spread as far apart as the torus allows, as a pattern fixed by the tiling /// alone, so a validator can check exactly what generation will use: farthest-point spreading /// between cell centres from cell 0, taking at each step the farthest cell that still leaves /// pocketsFit (the lowest index on a tie). Empty if the tiling can't hold them. -std::vector spreadPockets(const Tessellation &, int count); +std::vector spreadPockets(const CellGraph &, int count); /// Opens a spanning tree of the cells not `blocked` with a recursive backtracker from a random /// start: long winding passages with comparatively few, deep dead ends. Returns whether every such /// cell was reached. -bool carveSpanningTree(const Tessellation &, GenerationContext &, const std::string &stream, +bool carveSpanningTree(const CellGraph &, GenerationContext &, const std::string &stream, const std::vector &blocked, std::vector &open); /// Opens one door into each pocket, through a random edge onto a cell that isn't a pocket, and /// returns each pocket's door edge in the order given. -std::vector openPocketDoors(const Tessellation &, GenerationContext &, const std::string &stream, +std::vector openPocketDoors(const CellGraph &, GenerationContext &, const std::string &stream, const std::vector &pockets, const std::vector &isPocket, std::vector &open); /// Opens extra walls between cells that aren't `blocked`: `percent` of those cells' count, chosen /// at random from the closed edges between them, giving second routes to flank along. -void openLoops(const Tessellation &, GenerationContext &, const std::string &stream, +void openLoops(const CellGraph &, GenerationContext &, const std::string &stream, const std::vector &blocked, int percent, std::vector &open); /// The cells, other than `blocked` ones, with exactly one open edge, and that edge for each (by /// cell index; -1 elsewhere). +std::vector deadEnds(const CellGraph &, const std::vector &open, + const std::vector &blocked, std::vector &exitEdge); + +/// The same functions on a tessellation's own graph (cellGraph). +bool pocketsFit(const Tessellation &, const std::vector &pocket); +std::vector spreadPockets(const Tessellation &, int count); +bool carveSpanningTree(const Tessellation &, GenerationContext &, const std::string &stream, + const std::vector &blocked, std::vector &open); +std::vector openPocketDoors(const Tessellation &, GenerationContext &, + const std::string &stream, const std::vector &pockets, + const std::vector &isPocket, + std::vector &open); +void openLoops(const Tessellation &, GenerationContext &, const std::string &stream, + const std::vector &blocked, int percent, + std::vector &open); std::vector deadEnds(const Tessellation &, const std::vector &open, const std::vector &blocked, std::vector &exitEdge); } // namespace MapGeneration diff --git a/src/map/generator/shared/Growth.cpp b/src/map/generator/shared/Growth.cpp new file mode 100644 index 000000000..ee39e20ef --- /dev/null +++ b/src/map/generator/shared/Growth.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Growth.h" +#include "Morphology.h" +#include "TerrainType.h" +namespace MapGeneration +{ +Fertility::Field cropGrowthField(const TerrainSketch &sketch, const Torus &t) +{ + const std::vector water = pureTiles(sketch, t, WATER); + const std::vector sand = pureTiles(sketch, t, SAND); + Fertility::Field field; + field.rebuild(t.w, t.h, std::vector(water.begin(), water.end()), + std::vector(sand.begin(), sand.end())); + return field; +} + +double wateredShare(const Fertility::Field &field, const std::vector ®ion, + std::uint32_t minimum) +{ + const int w = field.getW(); + long long tiles = 0, watered = 0; + for (size_t i = 0; i < region.size(); ++i) + if (region[i]) + { + ++tiles; + watered += field.at(int(i % w), int(i / w)) >= minimum; + } + return tiles ? double(watered) / double(tiles) : 0.0; +} + +int wetTiles(const Fertility::Field &field, const std::vector ®ion) +{ + const int w = field.getW(); + int wet = 0; + for (size_t i = 0; i < region.size(); ++i) + wet += region[i] && field.at(int(i % w), int(i / w)) > 0; + return wet; +} + +std::vector dryZone(const Torus &t, const std::vector ®ion) +{ + return dilate(t, region, kCropProbeReach); +} + +int drainWithin(TerrainSketch &sketch, const std::vector &zone) +{ + int drained = 0; + for (size_t i = 0; i < sketch.size(); ++i) + if (zone[i] && sketch[i] == WATER) + { + sketch[i] = GRASS; + ++drained; + } + return drained; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Growth.h b/src/map/generator/shared/Growth.h new file mode 100644 index 000000000..108242b09 --- /dev/null +++ b/src/map/generator/shared/Growth.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "FertilityField.h" +#include "Grid.h" +#include "Sketch.h" +#include +#include +namespace MapGeneration +{ +// Where wheat and wood will grow back, known while the map is still a sketch. +// +// A crop spreads when a probe up to 15 tiles away on each axis finds pure water and the probe mirrored +// through the tile finds no pure sand (Map::growResources; Fertility::Field computes the exact chance +// for every tile). Planting.h's algaeGrowthChance answers the same question for algae on a finished +// map; these answer it for crops on a TerrainSketch, before anything is written, so a design can size +// its farmland to its water, keep a forest from ever growing back, or prove a region stays dry. + +/// How far, on each axis, the engine's crop growth probe reaches for water. +constexpr int kCropProbeReach = 15; + +/// Every tile's crop growth chance on Fertility::kScale, from the sketch as the game will draw it +/// (pure water and pure sand tiles, pureTiles). Unlike Fertility::forMap it is not gated on deposits +/// being able to reach a tile: this is where crops would regrow if planted. +Fertility::Field cropGrowthField(const TerrainSketch &, const Torus &); + +/// The share (0 to 1) of `region`'s tiles whose chance is at least `minimum`; 0 for an empty region. +double wateredShare(const Fertility::Field &, const std::vector ®ion, + std::uint32_t minimum = 1); + +/// How many of `region`'s tiles have any chance at all: a design that promises a region stays dry +/// (a forest that never grows back) checks this is 0. +int wetTiles(const Fertility::Field &, const std::vector ®ion); + +/// The tiles where water would give some tile of `region` a chance to regrow: every tile within +/// kCropProbeReach on each axis. Keep water out of it and the region stays dry. +std::vector dryZone(const Torus &, const std::vector ®ion); + +/// Turns every water corner of the sketch inside `zone` to grass, and returns how many it changed. Run +/// it before layBeaches, since beaches are drawn from the water that is left. +int drainWithin(TerrainSketch &, const std::vector &zone); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Homes.h b/src/map/generator/shared/Homes.h index 4d47ffe4b..c5b6cffa0 100644 --- a/src/map/generator/shared/Homes.h +++ b/src/map/generator/shared/Homes.h @@ -7,14 +7,16 @@ #include "Grid.h" #include "Planting.h" #include "Resources.h" +#include "Territories.h" #include #include #include #include namespace MapGeneration { -// Round homes: a home that stands alone as a round patch of ground turned to face out from the map's -// middle, its door on the inner side. The swarm stands in from the middle towards the door, and the +// Round homes: a home that stands alone as a round patch of ground turned to face along `axis`, its door +// on the side opposite: out from the map's middle on a ring of homes (Carousel, Switchbacks), or away +// from the way in on a map with no middle. The swarm stands in from the middle towards the door, and the // home's starter kit lies within a short walk of it. A round home may carry ponds of its own - one out // behind the swarm and a second on a flank - for a map that gives its homes no other water; every pond // keeps a gap of land to the home's edge, so where the home is a coast its beach never meets the sea's. @@ -46,7 +48,7 @@ inline bool homeHasRoom(double homeRadius) } /// Stamps a round home: `visit(tile)` for every tile of the `home` outline centred at `centre` and -/// turned to `axis` (the heading out from the map's middle), and `ponds` (0 to 2) of the `pond` outline +/// turned to `axis` (the heading away from its door; out from the map's middle on a ring), and `ponds` (0 to 2) of the `pond` outline /// into `pondMask`, turned the same way; `pond` may be null when there are none. Returns where the kit /// goes: the first pond's centre, or the home's middle without ponds. template @@ -104,6 +106,54 @@ void plantHomeKit(Map &map, const Torus &t, GenerationContext &context, ShapePoi plantKit(map, t, context, kit, eligible); } +/// A home in ground of any shape: a Voronoi cell, a chamber, a town block, a territory. The swarm stands +/// the same walk from the home's way in as every other colony's (siteAtDepth over walking steps from the +/// `door` tiles through `region`), on the roomiest such tile; `axis` points from the door towards the +/// swarm, so plantHomeKit and homeSwarmSite read it as they read a round home's. -1 site when no tile is +/// `depth` (give or take `spread`) steps in with at least `minimumRoom` steps to the region's edge or to +/// `keepClear`. +struct RegionHome +{ + int site = -1; // the swarm's centre tile + MapGeneratorPoint swarm{0, 0}; // its footprint's top-left tile, as placeSettlement measures + double axis = 0; // from the door towards the swarm + ShapePoint kitCentre{0, 0}; // kHomeSwarmReach further along the axis: where the kit goes +}; +inline RegionHome regionHome(const Torus &t, const std::vector ®ion, + const std::vector &door, int depth, int spread, int minimumRoom, + const std::vector *keepClear = nullptr) +{ + RegionHome home; + if (door.empty()) + return home; + const std::vector steps = stepsFrom(t, tileMask(t, door), region); + std::vector blocked(region.size(), 0); + for (size_t i = 0; i < region.size(); ++i) + blocked[i] = !region[i] || (keepClear && (*keepClear)[i]); + std::vector room = stepsFrom(t, blocked); + std::vector depthIn(steps); + for (size_t i = 0; i < region.size(); ++i) + if (!region[i]) + depthIn[i] = -1; + home.site = siteAtDepth(depthIn, room, depth, minimumRoom, spread); + if (home.site < 0) + return home; + const int sx = home.site % t.w, sy = home.site / t.w; + // The door's middle, the short way round from the site. + double ox = 0, oy = 0; + for (int d : door) + { + ox += t.offsetX(sx, d % t.w); + oy += t.offsetY(sy, d / t.w); + } + ox /= double(door.size()); + oy /= double(door.size()); + home.axis = (ox == 0 && oy == 0) ? 0 : std::atan2(-oy, -ox); + home.swarm = MapGeneratorPoint(sx - 2, sy - 2); + home.kitCentre = polarPoint(sx, sy, kHomeSwarmReach, home.axis); + return home; +} + /// What a stretch of ground carries (a home beyond its starter kit, a commons, a plaza), given how many /// tiles it has. struct GroundAmounts diff --git a/src/map/generator/shared/Morphology.cpp b/src/map/generator/shared/Morphology.cpp new file mode 100644 index 000000000..ab4668586 --- /dev/null +++ b/src/map/generator/shared/Morphology.cpp @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Morphology.h" +#include +#include +namespace MapGeneration +{ +namespace +{ +// Whether any of the 2r + 1 entries centred on each position of a cyclic line is set, by a running +// count, so the cost doesn't grow with the radius. +void slideAny(const std::vector &line, int radius, std::vector &out) +{ + const int n = int(line.size()); + out.assign(line.size(), 0); + if (2 * radius + 1 >= n) + { + const bool any = std::any_of(line.begin(), line.end(), [](unsigned char v) { return v; }); + std::fill(out.begin(), out.end(), any); + return; + } + int count = 0; + for (int d = -radius; d <= radius; ++d) + count += line[((d % n) + n) % n] != 0; + for (int i = 0; i < n; ++i) + { + out[i] = count > 0; + count -= line[((i - radius) % n + n) % n] != 0; + count += line[(i + radius + 1) % n] != 0; + } +} + +// One-dimensional squared distance transform of a cyclic line (Felzenszwalb and Huttenlocher's lower +// envelope), where f holds 0 on sources and a huge value elsewhere. The line is laid out three times +// so every position sees its nearest source the short way round. +void distanceLine(const std::vector &f, std::vector &out) +{ + constexpr std::int64_t kHuge = std::int64_t(1) << 50; + const int n = int(f.size()), m = 3 * n; + std::vector v(m); + std::vector z(m + 1); + const auto value = [&](int q) { return f[q % n]; }; + int k = -1; + for (int q = 0; q < m; ++q) + { + if (value(q) >= kHuge) + continue; + if (k < 0) + { + k = 0; + v[0] = q; + z[0] = -1e30; + z[1] = 1e30; + continue; + } + double s; + for (;;) + { + const int p = v[k]; + s = double((value(q) + std::int64_t(q) * q) - (value(p) + std::int64_t(p) * p)) / + (2.0 * (q - p)); + if (s > z[k]) + break; + --k; // z[0] is minus infinity, so this stops at the first parabola + } + ++k; + v[k] = q; + z[k] = s; + z[k + 1] = 1e30; + } + out.assign(n, kHuge); + if (k < 0) + return; + int j = 0; + for (int q = n; q < 2 * n; ++q) + { + while (z[j + 1] < q) + ++j; + const std::int64_t d = q - v[j]; + out[q - n] = d * d + value(v[j]); + } +} +} // namespace + +std::vector dilate(const Torus &t, const std::vector &mask, + int radius) +{ + if (radius <= 0) + return mask; + std::vector rows(mask.size()), result(mask.size()); + std::vector line, out; + line.resize(t.w); + for (int y = 0; y < t.h; ++y) + { + for (int x = 0; x < t.w; ++x) + line[x] = mask[size_t(y) * t.w + x]; + slideAny(line, radius, out); + for (int x = 0; x < t.w; ++x) + rows[size_t(y) * t.w + x] = out[x]; + } + line.resize(t.h); + for (int x = 0; x < t.w; ++x) + { + for (int y = 0; y < t.h; ++y) + line[y] = rows[size_t(y) * t.w + x]; + slideAny(line, radius, out); + for (int y = 0; y < t.h; ++y) + result[size_t(y) * t.w + x] = out[y]; + } + return result; +} + +std::vector erode(const Torus &t, const std::vector &mask, int radius) +{ + std::vector inverse(mask.size()); + for (size_t i = 0; i < mask.size(); ++i) + inverse[i] = !mask[i]; + std::vector grown = dilate(t, inverse, radius); + for (auto &v : grown) + v = !v; + return grown; +} + +std::vector openMask(const Torus &t, const std::vector &mask, + int radius) +{ + return dilate(t, erode(t, mask, radius), radius); +} + +std::vector closeMask(const Torus &t, const std::vector &mask, + int radius) +{ + return erode(t, dilate(t, mask, radius), radius); +} + +std::vector distanceSquaredTo(const Torus &t, const std::vector &mask) +{ + constexpr std::int64_t kHuge = std::int64_t(1) << 50; + const size_t n = mask.size(); + if (std::none_of(mask.begin(), mask.end(), [](unsigned char v) { return v; })) + return std::vector(n, -1); + std::vector field(n), line, out; + for (size_t i = 0; i < n; ++i) + field[i] = mask[i] ? 0 : kHuge; + line.resize(t.w); + for (int y = 0; y < t.h; ++y) + { + for (int x = 0; x < t.w; ++x) + line[x] = field[size_t(y) * t.w + x]; + distanceLine(line, out); + for (int x = 0; x < t.w; ++x) + field[size_t(y) * t.w + x] = out[x]; + } + line.resize(t.h); + for (int x = 0; x < t.w; ++x) + { + for (int y = 0; y < t.h; ++y) + line[y] = field[size_t(y) * t.w + x]; + distanceLine(line, out); + for (int y = 0; y < t.h; ++y) + field[size_t(y) * t.w + x] = out[y]; + } + return field; +} + +std::vector dilateRound(const Torus &t, const std::vector &mask, + double radius) +{ + const std::vector d2 = distanceSquaredTo(t, mask); + std::vector result(mask.size(), 0); + for (size_t i = 0; i < mask.size(); ++i) + result[i] = d2[i] >= 0 && double(d2[i]) <= radius * radius; + return result; +} + +std::vector clearance(const Torus &t, const std::vector &mask) +{ + std::vector outside(mask.size()); + for (size_t i = 0; i < mask.size(); ++i) + outside[i] = !mask[i]; + std::vector steps = stepsFrom(t, outside); + const int far = std::max(t.w, t.h) / 2; + for (size_t i = 0; i < mask.size(); ++i) + steps[i] = !mask[i] ? 0 : steps[i] < 0 ? far : steps[i]; + return steps; +} + +std::vector dropSmallRegions(const Torus &t, const std::vector &mask, + int minimumTiles, GridNeighbors neighbours) +{ + const std::vector region = connectedRegions(mask, t.w, t.h, true, neighbours); + int regions = 0; + for (int r : region) + regions = std::max(regions, r + 1); + std::vector size(size_t(regions), 0); + for (int r : region) + if (r >= 0) + ++size[size_t(r)]; + std::vector kept(mask.size(), 0); + for (size_t i = 0; i < mask.size(); ++i) + kept[i] = region[i] >= 0 && size[size_t(region[i])] >= minimumTiles; + return kept; +} + +int widestWalkClearance(const Torus &t, const std::vector &mask, + const std::vector &sources, const std::vector &goal) +{ + const std::vector room = clearance(t, mask); + // Best bottleneck so far per tile; a max-heap on it (widest-path Dijkstra). + std::vector best(mask.size(), 0); + std::priority_queue> heap; + for (int i : sources) + if (mask[i] && room[i] > best[i]) + { + best[i] = room[i]; + heap.push({best[i], i}); + } + while (!heap.empty()) + { + const auto [width, i] = heap.top(); + heap.pop(); + if (width < best[i]) + continue; + if (goal[i]) + return width; + const int x = i % t.w, y = i / t.w; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int m = t.at(x + dx, y + dy); + const int through = std::min(width, room[m]); + if (mask[m] && through > best[m]) + { + best[m] = through; + heap.push({through, m}); + } + } + } + return 0; +} + +int narrowestPassage(const Torus &t, const std::vector &mask, + const std::vector &sources, const std::vector &goal) +{ + const int c = widestWalkClearance(t, mask, sources, goal); + return c > 0 ? 2 * c - 1 : 0; +} + +std::vector slivers(const Torus &t, const std::vector &mask, + int minimumClearance) +{ + const std::vector kept = openMask(t, mask, std::max(0, minimumClearance - 1)); + std::vector thin(mask.size(), 0); + for (size_t i = 0; i < mask.size(); ++i) + thin[i] = mask[i] && !kept[i]; + return thin; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Morphology.h b/src/map/generator/shared/Morphology.h new file mode 100644 index 000000000..cff4dd46c --- /dev/null +++ b/src/map/generator/shared/Morphology.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include "Topology.h" +#include +#include +namespace MapGeneration +{ +// Mask arithmetic on the torus: growing and shrinking regions, closing gaps, dropping specks, and +// measuring how wide ground is. Every function wraps, works on whole tiles and uses integers only, so +// the same mask comes out on every platform. +// +// A design uses these to keep what it drew playable: a tunnel no narrower than a unit column, a +// corridor a tower can't close, no scrap of grass too small to build on. Widths here are in tiles +// across a passage: a tile with clearance c (Chebyshev steps to the nearest tile outside the mask) +// sits in ground at least 2c - 1 tiles across. + +/// Every tile within `radius` Chebyshev steps (a square) of a mask tile. Radius 0 copies the mask. +std::vector dilate(const Torus &, const std::vector &mask, + int radius); + +/// Every mask tile whose whole square of `radius` lies in the mask. +std::vector erode(const Torus &, const std::vector &mask, int radius); + +/// Erode then dilate: drops parts of the mask narrower than 2 * radius + 1, keeps the rest's outline. +std::vector openMask(const Torus &, const std::vector &mask, + int radius); + +/// Dilate then erode: fills gaps and notches narrower than 2 * radius + 1. +std::vector closeMask(const Torus &, const std::vector &mask, + int radius); + +/// Squared Euclidean distance from every tile to the nearest mask tile, the short way round; 0 on the +/// mask, and -1 everywhere when the mask is empty. Exact (lower envelopes of parabolas per axis). +std::vector distanceSquaredTo(const Torus &, const std::vector &mask); + +/// Every tile within `radius` tiles (a disc, Euclidean) of a mask tile. +std::vector dilateRound(const Torus &, const std::vector &mask, + double radius); + +/// Chebyshev steps from every mask tile to the nearest tile outside it (1 on the rim); 0 off the mask. +/// A mask with nothing outside it gets the half of the longer side everywhere. +std::vector clearance(const Torus &, const std::vector &mask); + +/// The mask with every connected part (with the given neighbours, across the wrap) smaller than +/// `minimumTiles` removed. Pass the inverted mask to fill pockets instead. +std::vector dropSmallRegions(const Torus &, const std::vector &mask, + int minimumTiles, + GridNeighbors neighbours = GridNeighbors::Eight); + +/// The widest walk from any source to any goal through the mask, eight-connected: the largest +/// clearance c such that some walk keeps to tiles of clearance at least c, so its narrowest point is at +/// least 2c - 1 tiles across. 0 when no walk exists. Sources and goals must be mask tiles to count. +int widestWalkClearance(const Torus &, const std::vector &mask, + const std::vector &sources, const std::vector &goal); + +/// widestWalkClearance as a passage width in tiles (2c - 1), 0 when no walk exists: the check a +/// validator makes that a designed tunnel, lane or street still takes a column of units. +int narrowestPassage(const Torus &, const std::vector &mask, + const std::vector &sources, const std::vector &goal); + +/// Every mask tile lying in ground narrower than an eroded square of `minimumClearance` survives: +/// the tiles openMask(mask, minimumClearance - 1) removes, the slivers a design should widen or give +/// up. +std::vector slivers(const Torus &, const std::vector &mask, + int minimumClearance); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Orbits.cpp b/src/map/generator/shared/Orbits.cpp new file mode 100644 index 000000000..e88e2b714 --- /dev/null +++ b/src/map/generator/shared/Orbits.cpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Orbits.h" +#include "Building.h" +#include "BuildingType.h" +#include "Game.h" +#include "GenerationContext.h" +#include "HeightMap.h" +#include "Map.h" +#include "Unit.h" +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +int wrap(int v, int n) +{ + v %= n; + return v < 0 ? v + n : v; +} + +// The class of a tile's graphic (Map::lookup): grass, grass and sand, sand, sand and water, water. It +// depends only on which terrains a tile's corners hold, not on which corner holds which, so it is +// unchanged by any symmetry. +int terrainClass(Uint16 terrain) +{ + return terrain < 16 ? 0 : terrain < 128 ? 1 : terrain < 144 ? 2 : terrain < 256 ? 3 : 4; +} + +std::string at(int x, int y) +{ + return " at (" + std::to_string(x) + ", " + std::to_string(y) + ")"; +} +} // namespace + +int Symmetry::tile(int e, int x, int y) const +{ + const Isometry &g = elements[e]; + const int X = 2 * x + 1 - width, Y = 2 * y + 1 - height; + return wrap((g.c * X + g.d * Y + height - 1) / 2 + g.ty, height) * width + + wrap((g.a * X + g.b * Y + width - 1) / 2 + g.tx, width); +} + +int Symmetry::corner(int e, int u, int v) const +{ + const Isometry &g = elements[e]; + const int U = 2 * u - width, V = 2 * v - height; + return wrap((g.c * U + g.d * V + height) / 2 + g.ty, height) * width + + wrap((g.a * U + g.b * V + width) / 2 + g.tx, width); +} + +std::pair Symmetry::anchor(int e, int x, int y, int w, int h) const +{ + const Isometry &g = elements[e]; + const int U0 = 2 * x - width, V0 = 2 * y - height; + const int U1 = 2 * (x + w) - width, V1 = 2 * (y + h) - height; + const int X = std::min(g.a * U0 + g.b * V0, g.a * U1 + g.b * V1); + const int Y = std::min(g.c * U0 + g.d * V0, g.c * U1 + g.d * V1); + return {wrap((X + width) / 2 + g.tx, width), wrap((Y + height) / 2 + g.ty, height)}; +} + +int Symmetry::orbitKey(int x, int y) const +{ + int first = y * width + x; + for (int e = 1; e < order(); ++e) + first = std::min(first, tile(e, x, y)); + return first; +} + +Symmetry pointSymmetry(int width, int height, int teams) +{ + Symmetry s; + s.width = width; + s.height = height; + const bool square = width == height; + if (teams == 2) + s.elements = {{1, 0, 0, 1}, {-1, 0, 0, -1}}; + else if (teams == 4 && square) + s.elements = {{1, 0, 0, 1}, {0, -1, 1, 0}, {-1, 0, 0, -1}, {0, 1, -1, 0}}; + else if (teams == 4) + s.elements = {{1, 0, 0, 1}, {-1, 0, 0, 1}, {-1, 0, 0, -1}, {1, 0, 0, -1}}; + else if (teams == 8 && square) + s.elements = {{1, 0, 0, 1}, {0, 1, 1, 0}, {0, -1, 1, 0}, {-1, 0, 0, 1}, + {-1, 0, 0, -1}, {0, -1, -1, 0}, {0, 1, -1, 0}, {1, 0, 0, -1}}; + return s; +} + +Symmetry translationSymmetry(int width, int height, int teams, long long *spacing2) +{ + Symmetry best; + best.width = width; + best.height = height; + if (teams < 2 || width <= 0 || height <= 0 || (long long)width * height % teams) + return best; + // Every subgroup of the torus's translations, in Hermite normal form: generated by (p, 0) and + // (s, q), where p divides the width, q the height, 0 <= s < p, and q steps of the second generator + // land back on a multiple of the first. Its order is (width / p) * (height / q). + long long bestSpacing = -1; + const Torus t(width, height); + for (int p = 1; p <= width; ++p) + { + if (width % p) + continue; + for (int q = 1; q <= height; ++q) + { + if (height % q || (long long)(width / p) * (height / q) != teams) + continue; + const int columns = width / p, rows = height / q; + for (int s = 0; s < p; ++s) + { + if ((long long)s * rows % p) + continue; + long long spacing = LLONG_MAX; + for (int j = 0; j < rows; ++j) + for (int i = 0; i < columns; ++i) + if (i || j) + { + const int dx = t.offsetX(0, i * p + j * s), dy = t.offsetY(0, j * q); + spacing = std::min(spacing, (long long)dx * dx + (long long)dy * dy); + } + if (spacing <= bestSpacing) + continue; + bestSpacing = spacing; + best.elements.clear(); + for (int j = 0; j < rows; ++j) + for (int i = 0; i < columns; ++i) + best.elements.push_back({1, 0, 0, 1, wrap(i * p + j * s, width), j * q}); + } + } + } + if (spacing2) + *spacing2 = bestSpacing; + return best; +} + +LatticeSites latticeSites(int width, int height, int teams, double x0, double y0) +{ + LatticeSites result; + if (teams < 1) + return result; + const Symmetry exact = translationSymmetry(width, height, teams); + if (exact.order() == teams) + { + result.exact = true; + for (const Isometry &g : exact.elements) + result.sites.push_back({std::fmod(x0 + g.tx, width), std::fmod(y0 + g.ty, height)}); + return result; + } + // Rows of equal length: of the ways to split the colonies into equal rows, the one whose row + // spacing is nearest a hexagonal lattice's (0.866 of the spacing along a row). + int rows = 1; + double bestError = 1e18; + for (int r = 1; r <= teams; ++r) + { + if (teams % r) + continue; + const double along = double(width) / (teams / r), across = double(height) / r; + const double error = std::fabs(std::log(across / (0.866 * along))); + if (error < bestError - 1e-12) + { + bestError = error; + rows = r; + } + } + const int columns = teams / rows; + const double along = double(width) / columns, across = double(height) / rows; + // Each row is shifted from the last by the whole number of steps, over the rows, nearest half a + // step, so that after all the rows the shift is whole steps and the lattice wraps seamlessly. + const double shift = along * std::round(rows / 2.0) / rows; + for (int j = 0; j < rows; ++j) + for (int i = 0; i < columns; ++i) + result.sites.push_back( + {std::fmod(x0 + i * along + j * shift, width), std::fmod(y0 + j * across, height)}); + return result; +} + +std::vector stampOrbits(const Symmetry &s, const std::vector &feature, + bool corners) +{ + std::vector result(feature.size(), 0); + for (int y = 0; y < s.height; ++y) + for (int x = 0; x < s.width; ++x) + for (int e = 0; e < s.order(); ++e) + if (feature[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]) + { + result[size_t(y) * s.width + x] = 1; + break; + } + return result; +} + +std::vector orbitSum(const Symmetry &s, const std::vector &raw, bool corners) +{ + std::vector summed(raw.size(), 0); + for (int y = 0; y < s.height; ++y) + for (int x = 0; x < s.width; ++x) + for (int e = 0; e < s.order(); ++e) + summed[size_t(y) * s.width + x] += + raw[size_t(corners ? s.corner(e, x, y) : s.tile(e, x, y))]; + return summed; +} + +std::vector orbitNoise(GenerationContext &context, const Symmetry &s, + const std::string &stream, float smoothing, bool corners) +{ + const int w = s.width, h = s.height; + HeightMap noise(w, h, context.stream(stream)); + noise.makePlain(smoothing); + std::vector raw(size_t(w) * h); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + // Noise as integers (12 bits) so the sum over an orbit is exact: floating-point sums in + // different orders could differ in the last bit and break the symmetry. + raw[size_t(y) * w + x] = int(noise(x, y) * 4096); + return orbitSum(s, raw, corners); +} + +std::vector topShare(const std::vector &value, + const std::vector &eligible, double share, + bool highest) +{ + std::vector pool; + for (size_t i = 0; i < value.size(); ++i) + if (eligible[i]) + pool.push_back(value[i]); + std::vector mask(value.size(), 0); + const size_t count = + std::min(pool.size(), size_t(std::lround(std::max(0.0, share) * pool.size()))); + if (!count) + return mask; + std::sort(pool.begin(), pool.end()); + const int threshold = highest ? pool[pool.size() - count] : pool[count - 1]; + for (size_t i = 0; i < value.size(); ++i) + mask[i] = eligible[i] && (highest ? value[i] >= threshold : value[i] <= threshold); + return mask; +} + +bool equaliseDeposits(Map &map, const Symmetry &s, std::string &detail) +{ + const int w = s.width, h = s.height; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const int i = y * w + x; + const int first = s.orbitKey(x, y); + if (first == i) + continue; + const Resource source = map.getResource(size_t(first)); + Resource &target = map.getResource(size_t(i)); + if (source.type != target.type) + { + detail = "deposits differ across the orbit of (" + std::to_string(x) + ", " + + std::to_string(y) + ")"; + return false; + } + target = source; + } + return true; +} + +std::string orbitMismatch(const Game &game, const Symmetry &s, int teams, + std::vector> *permutations) +{ + const Map &map = game.map; + const int w = map.getW(), h = map.getH(); + if (s.order() != teams) + return "No symmetry gives " + std::to_string(teams) + " colonies the same ground."; + std::vector> images; + for (int e = 1; e < s.order(); ++e) + { + const std::string under = " is not symmetric under symmetry " + std::to_string(e) + "."; + std::vector image(size_t(teams), -1); + const auto relate = [&](int from, int to) + { + if (from < 0 || from >= teams || to < 0 || to >= teams) + return false; + if (image[size_t(from)] < 0) + image[size_t(from)] = to; + return image[size_t(from)] == to; + }; + for (int v = 0; v < h; ++v) + for (int u = 0; u < w; ++u) + { + const int c = s.corner(e, u, v); + if (map.getUMTerrain(u, v) != map.getUMTerrain(c % w, c / w)) + return "Undermap corner" + at(u, v) + under; + } + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + const int q = s.tile(e, x, y), qx = q % w, qy = q / w; + if (terrainClass(map.getTerrain(x, y)) != terrainClass(map.getTerrain(qx, qy))) + return "Terrain" + at(x, y) + under; + const Resource &ra = map.getResource(x, y), &rb = map.getResource(qx, qy); + if (ra.type != rb.type || ra.amount != rb.amount) + return "Deposit" + at(x, y) + under; + const Uint16 ba = map.getBuilding(x, y), bb = map.getBuilding(qx, qy); + if ((ba == NOGBID) != (bb == NOGBID)) + return "Building" + at(x, y) + under; + if (ba != NOGBID && (!relate(Building::GIDtoTeam(ba), Building::GIDtoTeam(bb)) || + game.teams[Building::GIDtoTeam(ba)] + ->myBuildings[Building::GIDtoID(ba)] + ->type != game.teams[Building::GIDtoTeam(bb)] + ->myBuildings[Building::GIDtoID(bb)] + ->type)) + return "Building" + at(x, y) + under; + const Uint16 ua = map.getGroundUnit(x, y), ub = map.getGroundUnit(qx, qy); + if ((ua == NOGUID) != (ub == NOGUID)) + return "Unit" + at(x, y) + under; + if (ua != NOGUID && + (!relate(Unit::GIDtoTeam(ua), Unit::GIDtoTeam(ub)) || + game.teams[Unit::GIDtoTeam(ua)]->myUnits[Unit::GIDtoID(ua)]->typeNum != + game.teams[Unit::GIDtoTeam(ub)]->myUnits[Unit::GIDtoID(ub)]->typeNum)) + return "Unit" + at(x, y) + under; + } + std::vector hit(size_t(teams), 0); + for (int team = 0; team < teams; ++team) + { + const int to = image[size_t(team)]; + if (to < 0 || hit[size_t(to)]) + return "Symmetry " + std::to_string(e) + " does not map colonies one to one."; + hit[size_t(to)] = 1; + } + images.push_back(image); + } + std::vector carried{0}; + std::vector seen(size_t(teams), 0); + seen[0] = 1; + for (size_t head = 0; head < carried.size(); ++head) + for (const auto &image : images) + if (!seen[size_t(image[size_t(carried[head])])]) + { + seen[size_t(image[size_t(carried[head])])] = 1; + carried.push_back(image[size_t(carried[head])]); + } + if (int(carried.size()) != teams) + return "The symmetries do not carry colony 0 onto every other colony."; + if (permutations) + *permutations = images; + return ""; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Orbits.h b/src/map/generator/shared/Orbits.h new file mode 100644 index 000000000..6ee56082b --- /dev/null +++ b/src/map/generator/shared/Orbits.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Geometry.h" +#include "Grid.h" +#include +#include +#include +class Game; +class Map; +struct GenerationContext; +namespace MapGeneration +{ +// Fairness by symmetry: a map every colony sees the same way because a group of the torus's own +// symmetries carries each colony's ground exactly onto every other's. A wedge frame (Wedge.h) turns +// one design round the middle and rasterizes every copy separately, so copies agree to within a tile; +// a symmetry here maps tiles onto tiles, so copies agree exactly, deposits and all. +// +// Two kinds of group are offered. Point symmetries turn or mirror the map about its centre (a half +// turn for 2 colonies, quarter turns or mirrors for 4, all eight for 8; Symmetric arena). Translation +// symmetries slide the whole map by whole tiles onto itself: colonies on a lattice across the torus, +// no centre at all, every colony with the same neighbourhood in every direction, and a rectangular map +// served as well as a square one. Map sides are powers of two, so a translation group's order is too: +// 2, 4, 8, 16... colonies. Other counts have no exact group; latticeSites still spaces them evenly and +// the generator shares out the ground with growTerritories (Territories.h). + +/// A symmetry as a signed permutation matrix acting on doubled centred coordinates, then a move by +/// (tx, ty) whole tiles. Tile (x, y) is (2x + 1 - W, 2y + 1 - H) and undermap corner (u, v) is +/// (2u - W, 2v - H). A tile's terrain comes from its four corners (Map::regenerateMap), and the same +/// matrix maps a tile's corners onto its image's corners, so the tile rotation (x, y) -> (W-1-y, x) is +/// the corner rotation (u, v) -> (W-v, u). A translation moves tiles and corners alike. +struct Isometry +{ + int a, b, c, d; + int tx = 0, ty = 0; +}; + +/// A group of symmetries of a width x height torus. elements[0] is the identity, and colony k is +/// meant to start on elements[k]'s image of colony 0's home. +struct Symmetry +{ + int width = 0, height = 0; + std::vector elements; + + int order() const { return int(elements.size()); } + /// The index of the image of tile (x, y) under element e. + int tile(int e, int x, int y) const; + /// The index of the image of undermap corner (u, v) under element e. + int corner(int e, int u, int v) const; + /// The top-left tile of the image of a w x h footprint anchored at (x, y): the image of a + /// rectangle is a rectangle, so it is the image's lowest corner on each axis. + std::pair anchor(int e, int x, int y, int w, int h) const; + /// The tile a symmetry carries (x, y) to with the lowest index: one name for a whole orbit. + int orbitKey(int x, int y) const; +}; + +/// The point symmetry that gives every colony identical ground: a half turn for two colonies, a +/// quarter turn for four on a square map, the two mirrors for four on a rectangular map (where a +/// quarter turn doesn't map the map onto itself), and all eight symmetries of a square for eight. +/// Elements are listed in order round the centre, so neighbouring colonies are neighbouring team +/// numbers. Empty for colony counts no point symmetry serves. +Symmetry pointSymmetry(int width, int height, int teams); + +/// The translation group of `teams` elements whose nearest two copies of a tile lie farthest apart: +/// colonies on the roomiest lattice the torus holds, square, staggered like hexagons, or sheared. +/// Elements are listed row by row of the lattice. Empty when no group of that order exists (the +/// order must divide the tile count, a power of two) or `teams` is below 2. `spacing2`, if given, +/// receives the squared distance between the nearest two copies. +Symmetry translationSymmetry(int width, int height, int teams, long long *spacing2 = nullptr); + +/// Where `teams` colonies go on a lattice across the torus, one per site, starting from (x0, y0). +/// `exact` when translationSymmetry serves the count, and then site k is element k's image of site 0; +/// otherwise equal rows of evenly spaced sites, as many rows as brings the spacing between rows +/// nearest a hexagonal lattice's, each shifted about half a step from the last (by whole steps over all +/// the rows, so the wrap stays seamless): the ground between them is nearly even, and growTerritories +/// shares it out exactly. +struct LatticeSites +{ + std::vector sites; + bool exact = false; +}; +LatticeSites latticeSites(int width, int height, int teams, double x0, double y0); + +/// Colony 0's feature stamped onto every image of it: an entry is set when any image of its tile (or +/// corner) lies in the feature. A union doesn't depend on the order an orbit is visited in, so the +/// result is exactly symmetric. +std::vector stampOrbits(const Symmetry &, const std::vector &feature, + bool corners); + +/// Any integer field summed over every orbit. Integers, so the sum is exact in any order and every +/// member of an orbit gets the same value. +std::vector orbitSum(const Symmetry &, const std::vector &raw, bool corners); + +/// Plain noise (HeightMap::makePlain with `smoothing`) from a named stream, as 12-bit integers summed +/// over every orbit: a field every symmetry leaves unchanged. +std::vector orbitNoise(GenerationContext &, const Symmetry &, const std::string &stream, + float smoothing, bool corners); + +/// The share (0 to 1) of eligible entries with the highest (or lowest) values, as a mask. Ties at the +/// cut-off are all kept, so the members of an orbit, which have equal values, are never split. +std::vector topShare(const std::vector &value, + const std::vector &eligible, double share, + bool highest); + +/// The engine draws each deposit's amount and look from the gameplay RNG (Map::setResource). Every +/// orbit takes its lowest-indexed tile's, so deposits are symmetric in size as well as type. Fails, +/// naming the tile in `detail`, when an orbit's deposits differ in type. +bool equaliseDeposits(Map &, const Symmetry &, std::string &detail); + +/// Whether a finished world is exactly symmetric: every element maps undermap corners, tile graphics +/// (by class), deposits (type and amount), buildings and units onto themselves, with the colonies +/// permuted one to one, and those permutations carry colony 0 onto every other colony. Empty when it +/// is; otherwise what broke, and where. `permutations`, if given, receives each non-identity element's +/// colony permutation. +std::string orbitMismatch(const Game &, const Symmetry &, int teams, + std::vector> *permutations = nullptr); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Patterns.cpp b/src/map/generator/shared/Patterns.cpp new file mode 100644 index 000000000..b0af52f8e --- /dev/null +++ b/src/map/generator/shared/Patterns.cpp @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Patterns.h" +#include "LatticeNoise.h" +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +// A box blur of radius r along one axis of the torus, by a running sum; the mean rounds towards minus +// infinity so it is exact integers everywhere. +void blurLine(const std::vector &in, std::vector &out, int r) +{ + const int n = int(in.size()); + out.assign(in.size(), 0); + if (r <= 0) + { + out = in; + return; + } + const int span = 2 * r + 1; + std::int64_t sum = 0; + for (int d = -r; d <= r; ++d) + sum += in[((d % n) + n) % n]; + for (int i = 0; i < n; ++i) + { + const std::int64_t q = sum >= 0 ? sum / span : -((-sum + span - 1) / span); + out[i] = int(q); + sum -= in[((i - r) % n + n) % n]; + sum += in[(i + r + 1) % n]; + } +} + +std::vector boxBlur(const Torus &t, const std::vector &field, int rx, int ry) +{ + std::vector rows(field.size()), result(field.size()), line, out; + line.resize(t.w); + for (int y = 0; y < t.h; ++y) + { + for (int x = 0; x < t.w; ++x) + line[x] = field[size_t(y) * t.w + x]; + blurLine(line, out, rx); + for (int x = 0; x < t.w; ++x) + rows[size_t(y) * t.w + x] = out[x]; + } + line.resize(t.h); + for (int x = 0; x < t.w; ++x) + { + for (int y = 0; y < t.h; ++y) + line[y] = rows[size_t(y) * t.w + x]; + blurLine(line, out, ry); + for (int y = 0; y < t.h; ++y) + result[size_t(y) * t.w + x] = out[y]; + } + return result; +} + +// Two box passes, close to a Gaussian. +std::vector smooth(const Torus &t, const std::vector &field, int rx, int ry) +{ + return boxBlur(t, boxBlur(t, field, rx, ry), rx, ry); +} +} // namespace + +std::vector turingPattern(const Torus &t, const TuringStyle &style, std::mt19937 &rng) +{ + const int wavelength = std::max(4, style.wavelength); + std::vector field = periodicNoise(t.w, t.h, std::max(2, wavelength / 2), rng); + for (int &v : field) + v -= 32768; + // Two box passes of radius r spread about as far as a Gaussian of deviation 0.8 r, and a difference + // of two such blurs, one twice as wide, picks out waves a few radii long. Near radii of a tenth of + // the wavelength and far radii of a fifth give bands within about 15% of the wavelength (measured + // on 128-tile maps from 8 to 32 tiles). + const auto radius = [&](int stretch, double share) + { return std::max(1, int(std::lround(wavelength * share * std::max(10, stretch) / 100.0))); }; + const int nearX = radius(style.stretchX, 1 / 10.5), nearY = radius(style.stretchY, 1 / 10.5); + const int farX = std::max(nearX + 1, radius(style.stretchX, 2 / 10.5)); + const int farY = std::max(nearY + 1, radius(style.stretchY, 2 / 10.5)); + constexpr int kGain = 6; + for (int pass = 0; pass < style.iterations; ++pass) + { + const std::vector nearBlur = smooth(t, field, nearX, nearY); + const std::vector farBlur = smooth(t, field, farX, farY); + std::int64_t total = 0; + for (size_t i = 0; i < field.size(); ++i) + { + // Activation where the near blur beats the far one, inhibition where it doesn't: in + // proportion, so crests grow smoothly rather than into flat plateaus. + field[i] += int(std::int64_t(nearBlur[i] - farBlur[i]) * kGain / 4); + total += field[i]; + } + // Recentre on the mean and rescale to the peak, so the field neither drifts nor saturates. + const int mean = int(total / std::int64_t(field.size())); + int peak = 1; + for (int &v : field) + { + v -= mean; + peak = std::max(peak, std::abs(v)); + } + for (int &v : field) + v = int(std::int64_t(v) * 32768 / peak); + } + return field; +} + +std::vector stripePhase(const Torus &t, const StripeStyle &style, std::mt19937 &rng) +{ + const std::int64_t area = std::int64_t(t.w) * t.h; + const std::vector warp = + style.warpPercent ? fractalNoise(t.w, t.h, std::max(2, style.warpPeriod), 3, rng) + : std::vector(size_t(area), 32768); + std::vector phase(static_cast(area)); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const size_t i = size_t(y) * t.w + x; + // (acrossX * x / w + acrossY * y / h) whole turns, in 65536ths, exactly. + std::int64_t turns = + (std::int64_t(style.acrossX) * x * t.h + std::int64_t(style.acrossY) * y * t.w) * + 65536 / area; + turns += std::int64_t(warp[i] - 32768) * style.warpPercent / 100; + phase[i] = int(((turns % 65536) + 65536) % 65536); + } + return phase; +} + +double stripeSpacing(const Torus &t, const StripeStyle &style) +{ + const double kx = double(style.acrossX) / t.w, ky = double(style.acrossY) / t.h; + const double k = std::hypot(kx, ky); + return k > 0 ? 1.0 / k : double(std::max(t.w, t.h)); +} + +double stripeNormal(const Torus &t, const StripeStyle &style) +{ + return std::atan2(double(style.acrossY) / t.h, double(style.acrossX) / t.w); +} + +double stripeHeading(const Torus &t, const StripeStyle &style) +{ + return stripeNormal(t, style) + kPi / 2; +} + +std::vector upwindSteps(const Torus &t, const std::vector &mask, int dx, int dy, + int reach) +{ + const int n = t.w * t.h, length = std::max(std::abs(dx), std::abs(dy)); + std::vector steps(size_t(n), -1); + if (!length) + { + for (int i = 0; i < n; ++i) + steps[i] = mask[i] ? 0 : -1; + return steps; + } + // The upwind offset k Chebyshev steps back, rounded half away from zero, in integers. + const auto back = [&](int k, int d) + { + const int num = 2 * k * d; + return -(num >= 0 ? (num + length) / (2 * length) : -((-num + length) / (2 * length))); + }; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (mask[i]) + { + steps[i] = 0; + continue; + } + for (int k = 1; k <= reach; ++k) + if (mask[t.at(x + back(k, dx), y + back(k, dy))]) + { + steps[i] = k; + break; + } + } + return steps; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Patterns.h b/src/map/generator/shared/Patterns.h new file mode 100644 index 000000000..89b780d62 --- /dev/null +++ b/src/map/generator/shared/Patterns.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Drawing.h" +#include "Grid.h" +#include +#include +#include +namespace MapGeneration +{ +// Whole-map patterns with a grain: labyrinths that grow themselves, parallel stripes that wrap the +// torus exactly at any slant, the shadow a ridge casts downwind, and curves that follow a field. +// LatticeNoise.h gives smooth isotropic noise; these give structure. The iterated and stepped ones +// are integer arithmetic, since an error repeated over many passes would compound. + +/// A Turing pattern: stripes and spots that grow out of noise by local activation and wider +/// inhibition, the way a fingerprint, a zebra's coat or a brain coral does. Each pass blurs the field +/// over a near and a far box, adds the difference (near minus far) to every tile, then recentres and +/// rescales the field to its own peak; after enough passes the field settles into bands about +/// `wavelength` tiles from crest to crest (within about 15%). A stretch above 100 on one axis widens both +/// blurs along it, so the bands line up across that axis (a grain). Values are integers in +/// [-32768, 32768]; threshold at the median for a labyrinth of equal stripes, or near a tail for spots +/// (percentile, LatticeNoise.h). Starts from periodicNoise, so it tiles the torus. +struct TuringStyle +{ + int wavelength = 16; + int stretchX = 100, stretchY = 100; + int iterations = 20; +}; +std::vector turingPattern(const Torus &, const TuringStyle &, std::mt19937 &); + +/// Parallel stripes that wrap the torus exactly: a phase in [0, 65536) per tile that climbs by one +/// whole turn `acrossX` times along the width and `acrossY` times down the height, so every stripe +/// meets itself across both seams at any slant those whole numbers allow. Stripes run across the +/// direction (acrossX / width, acrossY / height), about stripeSpacing tiles apart. `warpPercent` bends +/// them with fractal noise `warpPeriod` tiles across, by up to that share of one stripe spacing. +struct StripeStyle +{ + int acrossX = 3, acrossY = 1; + int warpPeriod = 32; + int warpPercent = 20; +}; +std::vector stripePhase(const Torus &, const StripeStyle &, std::mt19937 &); + +/// The distance between neighbouring stripes, in tiles. +double stripeSpacing(const Torus &, const StripeStyle &); + +/// The heading (radians) along the stripes, and the heading across them towards rising phase. +double stripeHeading(const Torus &, const StripeStyle &); +double stripeNormal(const Torus &, const StripeStyle &); + +/// How far a phase lies from the nearest crest (phase 0), in [0, 32768]: a ridge `share` of the spacing +/// wide is every tile under share * 32768. +inline int stripeDistance(int phase) +{ + return phase < 32768 ? phase : 65536 - phase; +} + +/// The shadow a mask casts downwind: for every tile, the number of steps upwind (against the direction +/// (dx, dy), in Chebyshev steps along the line) to the nearest mask tile, 0 on the mask, -1 when none +/// lies within `reach`. The wind is given as whole numbers, so every step lands on a tile the same way +/// on every platform: (1, 0) blows east, (2, 1) east-south-east. What lies close behind a ridge is dry. +std::vector upwindSteps(const Torus &, const std::vector &mask, int dx, int dy, + int reach); + +/// A curve that follows a field of headings: from `start`, `steps` steps of `stepLength` tiles, each +/// along `headingAt(x, y)` (radians, sampled at the wrapped position) at the midpoint of the step, with +/// every point's half width `halfWidth`. Positions keep climbing past the seam rather than wrapping, as +/// strokePath expects. Rivers down a slope, ridges along a grain, dunes across the wind. +template +std::vector traceStreamline(const Torus &t, ShapePoint start, HeadingAt headingAt, + int steps, double stepLength, double halfWidth) +{ + const auto wrapped = [&](double v, int n) + { + double r = std::fmod(v, double(n)); + return r < 0 ? r + n : r; + }; + std::vector path{{start.x, start.y, halfWidth}}; + double x = start.x, y = start.y; + for (int i = 0; i < steps; ++i) + { + const double h0 = headingAt(wrapped(x, t.w), wrapped(y, t.h)); + const double mx = x + 0.5 * stepLength * std::cos(h0), + my = y + 0.5 * stepLength * std::sin(h0); + const double h = headingAt(wrapped(mx, t.w), wrapped(my, t.h)); + x += stepLength * std::cos(h); + y += stepLength * std::sin(h); + path.push_back({x, y, halfWidth}); + } + return path; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Planting.h b/src/map/generator/shared/Planting.h index 30e77a42a..3e078f3d8 100644 --- a/src/map/generator/shared/Planting.h +++ b/src/map/generator/shared/Planting.h @@ -139,6 +139,27 @@ void plantFields(Map &map, const Torus &t, std::vector tiles, int wheat, in map.setResource(tiles[k] % t.w, tiles[k] / t.w, k < wheatShare ? CORN : WOOD, 1); } +/// Dense cover: one tile of `type` on every tile of `region` that `eligible(tile)` allows and the engine +/// accepts, in index order. A forest that fills a continent, a wheat plain, the undergrowth of a +/// swamp; what stays open is whatever `eligible` refuses (roads, clearings, a pattern's gaps). Every +/// deposit is a clearable wall until workers cut it, so a map built on cover should say what keeps +/// its colonies connected (openColonyRoutes, Roads.h). Returns how many tiles it planted. +template +int plantCover(Map &map, const Torus &t, const std::vector ®ion, int type, + Eligible eligible) +{ + int planted = 0; + for (int i = 0; i < t.w * t.h; ++i) + { + const int x = i % t.w, y = i / t.w; + if (!region[i] || !eligible(i) || !map.isResourceAllowed(x, y, type)) + continue; + map.setResource(x, y, type, 1); + ++planted; + } + return planted; +} + /// Clumps round a circle, the same at every angle: at each of `angles` (radians) round (cx, cy), a /// clump of `type` and `clumpRadius` grown from the eligible tile nearest the point on the circle of /// `radius`, searched within `within` tiles. A prize or an outcrop designed once per colony lands the diff --git a/src/map/generator/shared/Points.cpp b/src/map/generator/shared/Points.cpp new file mode 100644 index 000000000..b09c45b41 --- /dev/null +++ b/src/map/generator/shared/Points.cpp @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Points.h" +#include "GenerationContext.h" +#include "LatticeNoise.h" +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +// Bucket rows or columns to search around bucket c: all of them when a window would wrap onto +// itself. +std::vector bucketWindow(int c, int count, int radius) +{ + std::vector result; + if (count <= 2 * radius + 1) + { + for (int i = 0; i < count; ++i) + result.push_back(i); + } + else + { + for (int d = -radius; d <= radius; ++d) + result.push_back(((c + d) % count + count) % count); + } + return result; +} +} // namespace + +std::vector spreadPoints(const Torus &t, int spacing, GenerationContext &context, + const std::string &stream, int minimumPercent, int dartsPerSite) +{ + const int minimum = std::max(4, spacing * minimumPercent / 100); + const std::int64_t expected = + std::max(2, std::int64_t(t.w) * t.h / (std::int64_t(spacing) * spacing)); + const int gx = std::max(1, t.w / minimum), gy = std::max(1, t.h / minimum); + std::vector> columns(gx), rows(gy); + for (int c = 0; c < gx; ++c) + columns[c] = bucketWindow(c, gx, 1); + for (int r = 0; r < gy; ++r) + rows[r] = bucketWindow(r, gy, 1); + std::vector> buckets(size_t(gx) * gy); + std::vector sites; + for (std::int64_t attempt = 0; attempt < expected * dartsPerSite; ++attempt) + { + const int x = int(context.bounded(stream, t.w)); + const int y = int(context.bounded(stream, t.h)); + const int bx = x * gx / t.w, by = y * gy / t.h; + bool clear = true; + for (int row : rows[by]) + { + for (int column : columns[bx]) + { + for (int s : buckets[size_t(row) * gx + column]) + if (t.dist2(x, y, sites[s].x, sites[s].y) < minimum * minimum) + { + clear = false; + break; + } + if (!clear) + break; + } + if (!clear) + break; + } + if (!clear) + continue; + buckets[size_t(by) * gx + bx].push_back(int(sites.size())); + sites.push_back({x, y}); + } + return sites; +} + +std::vector nearestSiteLabels(const Torus &t, const std::vector &sites, int spacing, + GenerationContext &context, const std::string &stream, + int warpPeriodPercent, int warpPercent) +{ + const int period = std::max(1, spacing * warpPeriodPercent / 100); + const std::vector warpX = fractalNoise(t.w, t.h, period, 3, context.stream(stream)); + const std::vector warpY = fractalNoise(t.w, t.h, period, 3, context.stream(stream)); + const std::int64_t amplitude = std::int64_t(spacing) * 16 * warpPercent / 100; + const int W = t.w * 16, H = t.h * 16; + const int gx = std::max(1, t.w / spacing), gy = std::max(1, t.h / spacing); + std::vector> buckets(size_t(gx) * gy), columns(gx), rows(gy); + for (size_t s = 0; s < sites.size(); ++s) + buckets[size_t(sites[s].y * gy / t.h) * gx + sites[s].x * gx / t.w].push_back(int(s)); + for (int c = 0; c < gx; ++c) + columns[c] = bucketWindow(c, gx, 2); + for (int r = 0; r < gy; ++r) + rows[r] = bucketWindow(r, gy, 2); + std::vector label(size_t(t.w) * t.h, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const size_t i = size_t(y) * t.w + x; + int px = x * 16 + 8 + int((warpX[i] - 32768) * amplitude / 32768); + int py = y * 16 + 8 + int((warpY[i] - 32768) * amplitude / 32768); + px = ((px % W) + W) % W; + py = ((py % H) + H) % H; + const int bx = int(std::int64_t(px) * gx / W), by = int(std::int64_t(py) * gy / H); + std::int64_t best = INT64_MAX; + int nearest = 0; + for (int row : rows[by]) + for (int column : columns[bx]) + for (int s : buckets[size_t(row) * gx + column]) + { + int dx = std::abs(px - (sites[s].x * 16 + 8)), + dy = std::abs(py - (sites[s].y * 16 + 8)); + dx = std::min(dx, W - dx); + dy = std::min(dy, H - dy); + const std::int64_t d = std::int64_t(dx) * dx + std::int64_t(dy) * dy; + if (d < best || (d == best && s < nearest)) + { + best = d; + nearest = s; + } + } + label[i] = nearest; + } + return label; +} + +std::vector relaxPoints(const Torus &t, std::vector sites, int iterations) +{ + const int n = int(sites.size()); + for (int round = 0; round < iterations && n > 0; ++round) + { + std::vector sumX(n, 0), sumY(n, 0), count(n, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + int nearest = 0, best = INT32_MAX; + for (int s = 0; s < n; ++s) + { + const int d = t.dist2(x, y, sites[s].x, sites[s].y); + if (d < best) + { + best = d; + nearest = s; + } + } + // Offsets from the site, so a cell across the wrap averages the short way round. + sumX[nearest] += t.offsetX(sites[nearest].x, x); + sumY[nearest] += t.offsetY(sites[nearest].y, y); + ++count[nearest]; + } + for (int s = 0; s < n; ++s) + if (count[s]) + { + // Round half away from zero, in integers. + const auto mean = [&](std::int64_t sum) + { + return int(sum >= 0 ? (2 * sum + count[s]) / (2 * count[s]) + : -((-2 * sum + count[s]) / (2 * count[s]))); + }; + sites[s].x = t.x(sites[s].x + mean(sumX[s])); + sites[s].y = t.y(sites[s].y + mean(sumY[s])); + } + } + return sites; +} + +std::vector> siteNeighbours(const Torus &t, const std::vector &labels, + int sites) +{ + std::vector> graph(size_t(std::max(0, sites))); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int a = labels[size_t(y) * t.w + x]; + for (int b : {labels[t.at(x + 1, y)], labels[t.at(x, y + 1)]}) + if (a != b && a >= 0 && b >= 0 && a < sites && b < sites) + { + graph[a].push_back(b); + graph[b].push_back(a); + } + } + for (auto &list : graph) + { + std::sort(list.begin(), list.end()); + list.erase(std::unique(list.begin(), list.end()), list.end()); + } + return graph; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Points.h b/src/map/generator/shared/Points.h new file mode 100644 index 000000000..4607ab8c9 --- /dev/null +++ b/src/map/generator/shared/Points.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include +#include +struct GenerationContext; +namespace MapGeneration +{ +// Scattered sites on the torus and the cells they own: basins between ridges (Stone highlands), +// chambers in rock, plazas in a town, villages on a polder. Everything here is integer arithmetic on +// whole tiles and sixteenths of a tile, so the same seed draws the same cells on every platform. +// Tessellation.h is the regular counterpart (square and hexagon lattices); these are irregular. + +struct Site +{ + int x, y; +}; + +/// Sites by dart throwing on the torus: a dart is kept when it lies at least `minimumPercent` of +/// `spacing` (and at least 4 tiles) from every site kept before it. `dartsPerSite` darts are thrown per +/// expected site (one per spacing squared, at least 2), enough to saturate the map, after which nearly +/// every dart lands too close. Draws come from the named stream, two per dart. +std::vector spreadPoints(const Torus &, int spacing, GenerationContext &, + const std::string &stream, int minimumPercent = 83, + int dartsPerSite = 60); + +/// Every tile's nearest site, measured from a warped copy of the tile so cell borders wander instead of +/// running straight. The warp is two fractal noise fields (three octaves, `warpPeriodPercent` of the +/// spacing across, both drawn from `stream`) moving each tile by up to `warpPercent` of the spacing. +/// Distances are in sixteenths of a tile; a tie goes to the lower site index. `spacing` also sizes the +/// search buckets, so sites must be at least about `spacing` apart for the search to stay local (as +/// spreadPoints leaves them), and warpPercent 0 gives plain Voronoi cells. +std::vector nearestSiteLabels(const Torus &, const std::vector &sites, int spacing, + GenerationContext &, const std::string &stream, + int warpPeriodPercent = 150, int warpPercent = 30); + +/// Lloyd relaxation on the torus: each site moves to the centroid of the tiles nearest it (measured the +/// short way round from the site), `iterations` times, so cells grow more even in size. Cheap enough for +/// a few dozen sites; the search is exhaustive. +std::vector relaxPoints(const Torus &, std::vector sites, int iterations); + +/// The site graph of a labelling: two sites are neighbours when their cells touch (four-connected, +/// across the wrap), listed in ascending order. +std::vector> siteNeighbours(const Torus &, const std::vector &labels, + int sites); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Roads.cpp b/src/map/generator/shared/Roads.cpp index 655ad3963..bb504910c 100644 --- a/src/map/generator/shared/Roads.cpp +++ b/src/map/generator/shared/Roads.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "Roads.h" +#include "GenerationContext.h" #include "Map.h" #include #include @@ -79,4 +80,84 @@ bool openRoad(Map &map, const Torus &t, const std::vector &sources, map.setNoResource(i % t.w, i / t.w, 1); return true; } +bool openColonyRoutes(Map &map, const GenerationContext &context, const Torus &t, + const StepCosts &costs) +{ + const int n = t.w * t.h, teams = context.request.nbTeams; + if (teams < 2) + return false; + const auto openAt = [&](int i) + { + const int x = i % t.w, y = i / t.w; + return !map.isWater(x, y) && !map.isResource(x, y) && map.getBuilding(x, y) == NOGBID; + }; + // The open tiles round a colony's swarm. + const auto doorstep = [&](int team) + { + std::vector tiles; + for (int dy = -1; dy <= 4; ++dy) + for (int dx = -1; dx <= 4; ++dx) + if (dy == -1 || dy == 4 || dx == -1 || dx == 4) + { + const int i = t.at(context.bootX[team] + dx, context.bootY[team] + dy); + if (openAt(i)) + tiles.push_back(i); + } + return tiles; + }; + bool changed = false, forded = false; + for (int team = 1; team < teams; ++team) + { + std::vector open(n), source(n, 0); + for (int i = 0; i < n; ++i) + open[i] = openAt(i); + for (int i : doorstep(0)) + source[i] = 1; + const std::vector walk = stepsFrom(t, source, open); + std::vector target(n, 0); + bool arrived = false; + for (int i : doorstep(team)) + { + target[i] = 1; + arrived = arrived || walk[i] >= 0; + } + if (arrived) + continue; + // The cheapest way from anything colony 0 can reach to this colony's doorstep. + std::vector reachable; + for (int i = 0; i < n; ++i) + if (walk[i] >= 0) + reachable.push_back(i); + const std::vector route = + cheapestWalk(t, GridNeighbors::Cardinal, reachable, target, + [&](int, int to, int, int) + { + return map.getBuilding(to % t.w, to / t.w) != NOGBID + ? -1 + : stepCost(map, to % t.w, to / t.w, costs); + }); + for (int i : route) + { + const int x = i % t.w, y = i / t.w; + if (map.isWater(x, y)) + { + // One sand corner turns this tile and its three other tiles into walkable shore. + map.setUMTerrain(x, y, SAND); + for (int dy = -1; dy <= 0; ++dy) + for (int dx = -1; dx <= 0; ++dx) + if (map.isResource(t.x(x + dx), t.y(y + dy))) + map.setNoResource(t.x(x + dx), t.y(y + dy), 1); + forded = true; + } + else if (map.isResource(x, y)) + { + map.setNoResource(x, y, 1); + } + changed = true; + } + if (forded) + map.rebuildTerrain(); + } + return changed; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Roads.h b/src/map/generator/shared/Roads.h index bb8b836e3..4b96ec912 100644 --- a/src/map/generator/shared/Roads.h +++ b/src/map/generator/shared/Roads.h @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include "Contact.h" #include "Grid.h" #include "Topology.h" #include @@ -8,6 +9,7 @@ #include #include class Map; +struct GenerationContext; namespace MapGeneration { /// The cheapest walk on the torus by any step cost: from any source tile (at cost 0) to any @@ -84,4 +86,11 @@ std::vector cheapestRoute(const Torus &, const std::vector &sources, bool openRoad(Map &, const Torus &, const std::vector &sources, const std::vector &goal, const std::vector *alsoBlocked = nullptr); +/// Every colony must be able to walk to colony 0 at the start. Where a map's growth, water or walls box +/// one in, the cheapest way from anything colony 0's doorstep reaches to that colony's doorstep (the +/// open ring round its swarm) is opened under `costs`, four-connected: deposits on it are cleared, and +/// water on it becomes a sand ford (one sand corner per water tile, clearing deposits on the four tiles +/// it spoils). The map may look odd there; it does not fail. Returns whether anything changed; the +/// terrain is rebuilt when a ford was laid. Everglades' backstop, and any map whose design can close. +bool openColonyRoutes(Map &, const GenerationContext &, const Torus &, const StepCosts &costs); } // namespace MapGeneration diff --git a/src/map/generator/shared/Room.cpp b/src/map/generator/shared/Room.cpp new file mode 100644 index 000000000..0caf733e8 --- /dev/null +++ b/src/map/generator/shared/Room.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Room.h" +#include "Map.h" +namespace MapGeneration +{ +std::vector buildableTiles(const Map &map) +{ + const Torus t(map); + std::vector open(size_t(t.size()), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + open[size_t(y) * t.w + x] = map.isFreeForBuilding(x, y); + return open; +} + +std::vector buildAnchors(const Torus &t, const std::vector &buildable, + int size) +{ + // Rows first: how many buildable tiles run rightwards from each tile (capped at size), then + // whether `size` rows below each tile all run that far. + const int n = t.size(); + std::vector run(size_t(n), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + int length = 0; + while (length < size && buildable[t.at(x + length, y)]) + ++length; + run[size_t(y) * t.w + x] = length; + } + std::vector anchors(size_t(n), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + bool fits = true; + for (int dy = 0; dy < size && fits; ++dy) + fits = run[t.at(x, y + dy)] >= size; + anchors[size_t(y) * t.w + x] = fits; + } + return anchors; +} + +int buildSites(const Torus &t, const std::vector &buildable, + const std::vector ®ion, int size) +{ + std::vector usable(buildable.size(), 0); + for (size_t i = 0; i < usable.size(); ++i) + usable[i] = buildable[i] && region[i]; + const std::vector anchors = buildAnchors(t, usable, size); + int count = 0; + for (unsigned char a : anchors) + count += a; + return count; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Room.h b/src/map/generator/shared/Room.h new file mode 100644 index 000000000..297eea6c8 --- /dev/null +++ b/src/map/generator/shared/Room.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Grid.h" +#include +#include +#include +class Map; +namespace MapGeneration +{ +// Building room as something a design decides, not only something the start scorer finds afterwards. +// A swarm, an inn or a barracks needs a 4x4 footprint of pure grass with nothing on it +// (Map::isFreeForBuilding); StartQuality counts such footprints round each colony on the finished map. +// These count them on a design, so a map can make room scarce on purpose (chambers in rock, blocks in a +// town) and still give every colony exactly as much. + +/// The size of the footprint building room is counted in: a swarm's. +constexpr int kBuildFootprint = 4; + +/// Every tile a building can stand on today: pure grass, no deposit, no building, no unit. +std::vector buildableTiles(const Map &); + +/// The top-left tiles of every `size` x `size` footprint lying wholly on `buildable`, across the wrap. +std::vector buildAnchors(const Torus &, const std::vector &buildable, + int size = kBuildFootprint); + +/// How many `size` x `size` footprints lie wholly on tiles that are both `buildable` and in `region`. +int buildSites(const Torus &, const std::vector &buildable, + const std::vector ®ion, int size = kBuildFootprint); + +/// Grows `region` until it holds `target` footprints of buildable ground: always taking the +/// four-connected frontier tile `eligible` allows with the lowest `key(tile)` (then the lowest index), +/// never more than `maximumTiles` added. A chamber dug out of rock until it fits exactly what the design +/// promised. Returns the footprints it holds when it stopped. +template +int growUntilSites(const Torus &t, std::vector ®ion, + const std::vector &buildable, + const std::vector &eligible, int target, int maximumTiles, + Key key, int size = kBuildFootprint); +} // namespace MapGeneration + +#include +template +int MapGeneration::growUntilSites(const Torus &t, std::vector ®ion, + const std::vector &buildable, + const std::vector &eligible, int target, + int maximumTiles, Key key, int size) +{ + const auto usable = [&](int i) { return region[i] && buildable[i]; }; + // Whether the footprint anchored at (ax, ay) lies wholly on usable ground. + const auto complete = [&](int ax, int ay) + { + for (int dy = 0; dy < size; ++dy) + for (int dx = 0; dx < size; ++dx) + if (!usable(t.at(ax + dx, ay + dy))) + return false; + return true; + }; + int sites = buildSites(t, buildable, region, size); + using Entry = std::pair; + std::priority_queue, std::greater> frontier; + std::vector queued(region.size(), 0); + const auto offer = [&](int i) + { + const int x = i % t.w, y = i / t.w; + for (const auto &s : kCardinalSteps) + { + const int n = t.at(x + s[0], y + s[1]); + if (!region[n] && !queued[n] && eligible[n]) + { + queued[n] = 1; + frontier.push({std::int64_t(key(n)), n}); + } + } + }; + for (int i = 0; i < t.size(); ++i) + if (region[i]) + offer(i); + for (int added = 0; sites < target && added < maximumTiles && !frontier.empty(); ++added) + { + const int i = frontier.top().second; + frontier.pop(); + region[i] = 1; + if (buildable[i]) + { + const int x = i % t.w, y = i / t.w; + for (int ay = y - size + 1; ay <= y; ++ay) + for (int ax = x - size + 1; ax <= x; ++ax) + sites += complete(ax, ay); + } + offer(i); + } + return sites; +} diff --git a/src/map/generator/shared/Sketch.cpp b/src/map/generator/shared/Sketch.cpp index 80ca2945e..2296d978c 100644 --- a/src/map/generator/shared/Sketch.cpp +++ b/src/map/generator/shared/Sketch.cpp @@ -39,6 +39,31 @@ int countTiles(const TerrainSketch &terrain, TerrainType type) return int(std::count(terrain.begin(), terrain.end(), (unsigned char)type)); } +std::vector tileCorners(const Torus &t, const std::vector &tiles) +{ + std::vector corners(tiles.size(), 0); + for (int i = 0; i < t.size(); ++i) + if (tiles[i]) + { + const int x = i % t.w, y = i / t.w; + corners[i] = corners[t.at(x + 1, y)] = corners[t.at(x, y + 1)] = + corners[t.at(x + 1, y + 1)] = 1; + } + return corners; +} + +std::vector pureTiles(const TerrainSketch &terrain, const Torus &t, TerrainType type) +{ + std::vector pure(terrain.size(), 0); + const unsigned char want = (unsigned char)type; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + pure[size_t(y) * t.w + x] = + terrain[t.at(x, y)] == want && terrain[t.at(x + 1, y)] == want && + terrain[t.at(x, y + 1)] == want && terrain[t.at(x + 1, y + 1)] == want; + return pure; +} + std::vector raiseIslands(TerrainSketch &terrain, const Torus &t, GenerationContext &context, const IslandPlacement &placement) { diff --git a/src/map/generator/shared/Sketch.h b/src/map/generator/shared/Sketch.h index 7c78397f1..5563c1c73 100644 --- a/src/map/generator/shared/Sketch.h +++ b/src/map/generator/shared/Sketch.h @@ -28,6 +28,16 @@ void writeUndermap(Map &, const TerrainSketch &); int countTiles(const TerrainSketch &, TerrainType); +/// The undermap corners of every tile in `tiles`: the reverse of pureTiles, the vertices a design must +/// set for those tiles to take a terrain of their own. +std::vector tileCorners(const Torus &, const std::vector &tiles); + +/// The tiles whose four undermap corners all hold `type` (Map::regenerateMap reads tile (x, y) from +/// corners (x, y), (x + 1, y), (x, y + 1) and (x + 1, y + 1)): what the sketch will draw as pure grass, +/// sand or water. Buildings and deposits need pure grass, and the engine's growth tests read pure +/// water and pure sand, so this is the sketch as the game will see it. +std::vector pureTiles(const TerrainSketch &, const Torus &, TerrainType type); + /// Decorative patches of sand inside the land, away from any shore: of the grass tiles `eligible` /// allows that lie at least `inland` steps from water, the `share` (0 to 1) with the highest /// `noiseAt(tile)` turn to sand. Sampling smooth noise gives a few rounded patches rather than diff --git a/src/map/generator/shared/Territories.h b/src/map/generator/shared/Territories.h index 2028d915a..0676635b4 100644 --- a/src/map/generator/shared/Territories.h +++ b/src/map/generator/shared/Territories.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "Grid.h" +#include "Morphology.h" #include "Sketch.h" #include "Topology.h" #include @@ -195,10 +196,10 @@ inline void separateTerritories(const Torus &t, std::vector &labels, int ga } } } - const std::vector fromBorder = stepsFrom(t, border); - const int half = (gap + 1) / 2; + // Every tile fewer than half the gap from the border goes. + const std::vector band = dilate(t, border, (gap + 1) / 2 - 1); for (int i = 0; i < t.size(); ++i) - if (labels[i] >= 0 && fromBorder[i] >= 0 && fromBorder[i] < half) + if (labels[i] >= 0 && band[i] && (gap + 1) / 2 > 0) labels[i] = -1; } diff --git a/src/map/generator/shared/Towers.cpp b/src/map/generator/shared/Towers.cpp index 7307e3970..053b55e21 100644 --- a/src/map/generator/shared/Towers.cpp +++ b/src/map/generator/shared/Towers.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "Towers.h" +#include "Morphology.h" #include "Game.h" #include "GenerationContext.h" #include "Settlements.h" @@ -209,14 +210,8 @@ int evenTowerPlan(TowerPlan &plan) std::vector roomyGround(const Torus &t, const std::vector &open, int room) { - std::vector closed(t.size(), 0); - for (int i = 0; i < t.size(); ++i) - closed[i] = !open[i]; - const std::vector fromClosed = stepsFrom(t, closed); - std::vector roomy(t.size(), 0); - for (int i = 0; i < t.size(); ++i) - roomy[i] = open[i] && (fromClosed[i] < 0 || fromClosed[i] >= room); - return roomy; + // `room` steps from anything closed is a whole square of radius room - 1 open round the tile. + return erode(t, open, room - 1); } bool settleStartingTowers(Game &game, GenerationContext &context, TowerPlan &plan, int level, diff --git a/src/map/generator/shared/Walls.cpp b/src/map/generator/shared/Walls.cpp index f411af829..18c0d22c4 100644 --- a/src/map/generator/shared/Walls.cpp +++ b/src/map/generator/shared/Walls.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "Walls.h" #include "Map.h" +#include "Sketch.h" #include "TerrainType.h" #include namespace MapGeneration @@ -231,4 +232,28 @@ WalkSpread walkSpread(const Map &map, const Torus &t, const std::vector islandSeaMargin(const Map &map, const Torus &t, + const std::vector &ponds, + const std::vector &roadTile, + const std::vector &otherSand) +{ + std::vector notBeach = roadTile; + const std::vector sandTiles = roadTiles(t, otherSand); + for (int i = 0; i < t.size(); ++i) + notBeach[i] = notBeach[i] || sandTiles[i]; + return seaMargin(map, t, seaVertices(map, t, ponds), notBeach); +} + +std::vector sealedIslandStone(const Map &map, const Torus &t, + const std::vector &margin, + const std::vector &land, + const std::vector &designed) +{ + std::vector stone = sealCoasts(map, t, margin, land); + const DesignedStone wall = designedStone(map, t, designed); + for (int i = 0; i < t.size(); ++i) + if (wall.stone[i]) + stone[i] = 1; + return stone; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Walls.h b/src/map/generator/shared/Walls.h index 8fd027f7d..43da74158 100644 --- a/src/map/generator/shared/Walls.h +++ b/src/map/generator/shared/Walls.h @@ -39,6 +39,22 @@ std::vector sealCoasts(const Map &, const Torus &, const std::vector &margin, const std::vector &wallable); +/// The sea's margin for a design of walled land in the sea with inland ponds and sand roads (Carousel, +/// Switchbacks): the sea is every water vertex but the `ponds`', and neither a road's spoiled tiles +/// (`roadTile`) nor those of any other designed sand line (`otherSand`, as vertices, such as a farm plot's +/// ring) count as beach, so none of them carries the margin inland. +std::vector islandSeaMargin(const Map &, const Torus &, + const std::vector &ponds, + const std::vector &roadTile, + const std::vector &otherSand); + +/// Such a design's stone once the terrain is laid: every coast of `land` on the margin sealed +/// (sealCoasts), and every tile of the `designed` walls that the beaches left grass (designedStone). +std::vector sealedIslandStone(const Map &, const Torus &, + const std::vector &margin, + const std::vector &land, + const std::vector &designed); + /// A wall between labelled regions: a tile is wall when any of its eight neighbours has a lower /// label. A tile left off the wall then touches only its own label or wall, so no unit can step, /// even diagonally, from one region into another, and the wall is one tile thick. Negative labels diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 5527865cf..2dbc0d961 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -8,6 +8,7 @@ #include "IntBuildingType.h" #include "LegacyGenerationDescriptor.h" #include "MapGeneratorFrameworkChecks.h" +#include "MapGeneratorLandscapeChecks.h" #include "MapGeneratorToolkitChecks.h" #include "NewMapScreen.h" #include "Race.h" @@ -88,6 +89,7 @@ class MapGeneratorDefaultsTest globalsInit(); frameworkChecks(); ToolkitChecks::toolkitChecks(); + LandscapeChecks::landscapeChecks(); GenerationService service; for (int method : GeneratorRegistry::builtins().methods()) { @@ -301,7 +303,7 @@ class MapGeneratorDefaultsTest screen.descriptor.option("test-shape") == 1 && screen.descriptor.option("test-switch") == 1); edit(screen, "Smoothing", 8); - assert(registry.selectionIndex(101) == 24); + assert(registry.selectionIndex(101) == int(GeneratorRegistry::builtins().methods().size())); const auto playable = registry.methods(false); assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); Game generated(nullptr); diff --git a/test/MapGeneratorLandscapeChecks.h b/test/MapGeneratorLandscapeChecks.h new file mode 100644 index 000000000..0a7c2fe2f --- /dev/null +++ b/test/MapGeneratorLandscapeChecks.h @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +// Contracts of the landscape toolkit (symmetry groups, mask arithmetic, crop growth on a sketch, cost +// models, scattered sites and their graphs, patterns, wandering paths, channels, building room, homes +// in any region and biome kits), each on a small map built by hand. Part of the toolkit checks. +#include "Biomes.h" +#include "Channels.h" +#include "Contact.h" +#include "Drawing.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GraphMaze.h" +#include "Grid.h" +#include "Growth.h" +#include "Homes.h" +#include "Morphology.h" +#include "Orbits.h" +#include "Patterns.h" +#include "Points.h" +#include "Roads.h" +#include "Room.h" +#include "Sketch.h" +#include "Walls.h" +#include +#include +#include +#include +#include +#include +#include + +namespace LandscapeChecks +{ +using namespace MapGeneration; + +inline int count(const std::vector &mask) +{ + return int(std::count_if(mask.begin(), mask.end(), [](unsigned char v) { return v != 0; })); +} + +inline void landscapeGrass(Game &game, int wDec, int hDec) +{ + game.map.setSize(wDec, hDec); + game.map.setGame(&game); + game.map.makeHomogenMap(GRASS); +} + +// Orbits: the point symmetries are the arena's; translation groups exist only for orders dividing the +// tile count, pick the roomiest lattice, and carry orbits onto themselves; stamping and summing are +// orbit invariant; topShare keeps ties; equaliseDeposits and orbitMismatch see a broken deposit. +inline void orbitChecks() +{ + assert(pointSymmetry(64, 64, 4).order() == 4 && pointSymmetry(64, 32, 8).order() == 0); + long long spacing2 = 0; + const Symmetry lattice = translationSymmetry(64, 64, 4, &spacing2); + assert(lattice.order() == 4 && spacing2 == 32 * 32); + assert(translationSymmetry(64, 64, 3).order() == 0 && + translationSymmetry(64, 64, 1).order() == 0); + const Symmetry wide = translationSymmetry(64, 32, 8, &spacing2); + assert(wide.order() == 8 && spacing2 >= 16 * 16); + std::set> moves; + for (const Isometry &g : wide.elements) + assert(g.a == 1 && g.d == 1 && moves.insert({g.tx, g.ty}).second); + const Symmetry sheared = translationSymmetry(64, 64, 2); + assert(sheared.order() == 2); + for (const Symmetry &s : {lattice, wide, pointSymmetry(64, 64, 8)}) + for (int y = 0; y < s.height; y += 3) + for (int x = 0; x < s.width; x += 5) + for (int e = 0; e < s.order(); ++e) + { + const int image = s.tile(e, x, y); + assert(s.orbitKey(image % s.width, image / s.width) == s.orbitKey(x, y)); + } + + const LatticeSites three = latticeSites(64, 64, 3, 5, 7); + assert(!three.exact && three.sites.size() == 3); + const LatticeSites four = latticeSites(64, 64, 4, 5, 7); + assert(four.exact && four.sites.size() == 4 && four.sites[0].x == 5 && four.sites[0].y == 7); + + std::vector feature(size_t(64) * 64, 0); + feature[size_t(7) * 64 + 3] = 1; + assert(count(stampOrbits(lattice, feature, false)) == 4); + std::vector raw(size_t(64) * 64); + for (size_t i = 0; i < raw.size(); ++i) + raw[i] = int(i * 7919 % 1000); + const std::vector summed = orbitSum(lattice, raw, false); + for (int e = 0; e < lattice.order(); ++e) + assert(summed[size_t(lattice.tile(e, 3, 7))] == summed[size_t(7) * 64 + 3]); + std::vector all(raw.size(), 1); + const std::vector top = topShare(summed, all, 0.25, true); + for (size_t i = 0; i < raw.size(); ++i) + assert(top[i] == top[size_t(lattice.orbitKey(int(i % 64), int(i / 64)))]); + + Game game(nullptr); + landscapeGrass(game, 6, 6); + for (int e = 0; e < lattice.order(); ++e) + { + const int i = lattice.tile(e, 10, 12); + game.map.setResource(i % 64, i / 64, WOOD, 1); + } + std::string detail; + assert(equaliseDeposits(game.map, lattice, detail) && detail.empty()); + game.map.setResource(40, 40, CORN, 1); + assert(!equaliseDeposits(game.map, lattice, detail) && !detail.empty()); + game.map.setNoResource(40, 40, 1); + game.map.setResource(40, 40, STONE, 1); + assert(orbitMismatch(game, lattice, 4).find("Deposit") == 0); +} + +// Morphology: dilate and erode undo each other on a square across the seam; the distance transform +// matches brute force; clearance measures a corridor; the widest walk finds a pinch; small regions go. +inline void morphologyChecks() +{ + const Torus t(32, 24); + std::vector dot(t.size(), 0); + dot[t.at(0, 0)] = 1; + const std::vector square = dilate(t, dot, 2); + assert(count(square) == 25 && square[t.at(30, 22)] && !square[t.at(3, 0)]); + assert(erode(t, square, 2) == dot && openMask(t, square, 2) == square); + assert(count(dilateRound(t, dot, 3)) == 29); + std::mt19937 rng(9); + std::vector mask(t.size(), 0); + for (int k = 0; k < 12; ++k) + mask[rng() % t.size()] = 1; + const std::vector d2 = distanceSquaredTo(t, mask); + for (int i = 0; i < t.size(); ++i) + { + std::int64_t best = -1; + for (int j = 0; j < t.size(); ++j) + if (mask[j]) + { + const std::int64_t d = t.dist2(i % t.w, i / t.w, j % t.w, j / t.w); + best = best < 0 ? d : std::min(best, d); + } + assert(d2[i] == best); + } + // A corridor five tiles across along the width, pinched to one tile at x = 16. + std::vector corridor(t.size(), 0); + for (int x = 0; x < t.w; ++x) + for (int y = 10; y < 15; ++y) + corridor[t.at(x, y)] = x != 16 || y == 12; + assert(clearance(t, corridor)[t.at(5, 12)] == 3 && clearance(t, corridor)[t.at(5, 10)] == 1); + std::vector goal(t.size(), 0); + goal[t.at(20, 12)] = 1; + assert(narrowestPassage(t, corridor, {t.at(12, 12)}, goal) == 5); // the long way round + std::vector blocked = corridor; + for (int y = 10; y < 15; ++y) + blocked[t.at(26, y)] = y == 12; + assert(narrowestPassage(t, blocked, {t.at(12, 12)}, goal) == 1); + std::vector specks = dot; + for (int y = 4; y < 9; ++y) + for (int x = 4; x < 9; ++x) + specks[t.at(x, y)] = 1; + assert(count(dropSmallRegions(t, specks, 5)) == 25); + assert(count(slivers(t, corridor, 2)) > 0 && !slivers(t, corridor, 2)[t.at(5, 12)]); +} + +// Growth: a pond waters what is near it and nothing more than its probe reaches; draining the dry +// zone of a region leaves it dry. +inline void growthChecks() +{ + const Torus t(64, 64); + TerrainSketch sketch(t.size(), GRASS); + for (int y = 28; y < 36; ++y) + for (int x = 28; x < 36; ++x) + sketch[t.at(x, y)] = WATER; + layBeaches(sketch, t); + Fertility::Field field = cropGrowthField(sketch, t); + assert(field.at(26, 31) > 0 && field.at(0, 0) == 0); + std::vector far(t.size(), 0), near(t.size(), 0); + far[t.at(0, 0)] = far[t.at(63, 63)] = 1; + near[t.at(25, 31)] = 1; + assert(wetTiles(field, far) == 0 && wetTiles(field, near) == 1); + assert(wateredShare(field, near) == 1.0 && wateredShare(field, far) == 0.0); + assert(dryZone(t, near)[t.at(40, 31)] && !dryZone(t, near)[t.at(41, 31)]); + std::vector region(t.size(), 0); + region[t.at(24, 31)] = 1; + TerrainSketch drained(t.size(), GRASS); + for (int y = 28; y < 36; ++y) + for (int x = 28; x < 36; ++x) + drained[t.at(x, y)] = WATER; + assert(drainWithin(drained, dryZone(t, region)) == 64); + layBeaches(drained, t); + assert(wetTiles(cropGrowthField(drained, t), region) == 0); +} + +// Contact: a wood wall stops walkers and costs choppers; costSpread and unevenCosts read the costs; +// equalCostSites takes each colony's own side; openColonyRoutes fords a strait between two colonies. +inline void contactChecks() +{ + Game game(nullptr); + landscapeGrass(game, 5, 5); + Map &map = game.map; + const Torus t(map); + for (int y = 0; y < t.h; ++y) + map.setResource(16, y, WOOD, 1); + assert(stepCost(map, 16, 3, StepCosts::walking()) == -1 && + stepCost(map, 16, 3, StepCosts::chopping(5)) == 5); + // The wall wraps, so the torus is still open round the other side: walkers go round. + const std::vector walk = costsFrom(map, t, {t.at(10, 3)}, StepCosts::walking()); + const std::vector chop = costsFrom(map, t, {t.at(10, 3)}, StepCosts::chopping(5)); + assert(walk[t.at(22, 3)] == 20 && chop[t.at(22, 3)] == 12 + 4); + assert(costSpread({3, 7, 5}) == 4 && costSpread({3, -1}) == -1 && costSpread({}) == 0); + assert(unevenCosts({3, 7}, 4, "the hub").empty() && !unevenCosts({3, 9}, 4, "the hub").empty()); + const std::vector fromA = costsFrom(map, t, {t.at(4, 4)}, StepCosts::walking()); + const std::vector fromB = costsFrom(map, t, {t.at(28, 4)}, StepCosts::walking()); + std::vector eligible(t.size(), 1); + const std::vector sites = equalCostSites({fromA, fromB}, eligible, 6, 0); + assert(sites.size() == 2 && fromA[sites[0]] == 6 && fromB[sites[1]] == 6); + assert(fromA[sites[0]] < fromB[sites[0]] && fromB[sites[1]] < fromA[sites[1]]); + + Game strait(nullptr); + landscapeGrass(strait, 6, 5); + const Torus s(strait.map); + TerrainSketch sketch(s.size(), GRASS); + for (int y = 0; y < s.h; ++y) + for (int x = 0; x < s.w; ++x) + if ((x >= 20 && x < 26) || (x >= 52 && x < 58)) + sketch[s.at(x, y)] = WATER; + layBeaches(sketch, s); + writeUndermap(strait.map, sketch); + GenerationRequest request; + request.nbTeams = 2; + GenerationContext context(request); + context.bootX[0] = 8; + context.bootY[0] = 12; + context.bootX[1] = 38; + context.bootY[1] = 12; + assert(openColonyRoutes(strait.map, context, s, StepCosts{1, 3, 8, 25, -1})); + const std::vector after = + stepsFrom(s, tileMask(s, {s.at(7, 11)}), walkableTiles(strait.map)); + assert(after[s.at(42, 12)] >= 0); + assert(!openColonyRoutes(strait.map, context, s, StepCosts{1, 3, 8, 25, -1})); +} + +// Points: darts keep their spacing; unwarped labels are the brute-force nearest site; relaxation keeps +// every site and evens the cells; the site graph is symmetric and a maze carved on it spans it. +inline void pointChecks() +{ + const Torus t(96, 64); + GenerationRequest request; + request.seed = 12; + GenerationContext context(request), again(request); + const std::vector sites = spreadPoints(t, 20, context, "sites"); + assert(sites.size() >= 8 && spreadPoints(t, 20, again, "sites").size() == sites.size()); + for (size_t a = 0; a < sites.size(); ++a) + for (size_t b = a + 1; b < sites.size(); ++b) + assert(t.dist2(sites[a].x, sites[a].y, sites[b].x, sites[b].y) >= 16 * 16); + const std::vector labels = nearestSiteLabels(t, sites, 20, context, "warp", 150, 0); + for (int i = 0; i < t.size(); i += 7) + { + int best = 0; + for (size_t s = 1; s < sites.size(); ++s) + { + const int x = i % t.w, y = i / t.w; + if (t.dist2(x, y, sites[s].x, sites[s].y) < t.dist2(x, y, sites[best].x, sites[best].y)) + best = int(s); + } + const int x = i % t.w, y = i / t.w; + assert(t.dist2(x, y, sites[labels[i]].x, sites[labels[i]].y) == + t.dist2(x, y, sites[best].x, sites[best].y)); + } + const auto spread = [&](const std::vector &s) + { + const std::vector l = nearestSiteLabels(t, s, 20, context, "warp", 150, 0); + std::vector size(s.size(), 0); + for (int v : l) + ++size[v]; + return *std::max_element(size.begin(), size.end()) - + *std::min_element(size.begin(), size.end()); + }; + const std::vector relaxed = relaxPoints(t, sites, 3); + assert(relaxed.size() == sites.size() && spread(relaxed) <= spread(sites)); + const auto graph = siteNeighbours(t, labels, int(sites.size())); + for (size_t a = 0; a < graph.size(); ++a) + { + assert(!graph[a].empty()); + for (int b : graph[a]) + assert(std::count(graph[b].begin(), graph[b].end(), int(a)) == 1); + } + const CellGraph cells = cellGraph(t, sites, graph); + std::vector blocked(sites.size(), 0), open(cells.edgeCells.size(), 0); + assert(carveSpanningTree(cells, context, "maze", blocked, open)); + assert(count(open) == int(sites.size()) - 1); + assert(spreadPockets(cells, 2).size() == 2); +} + +// Patterns: Turing fields repeat per seed, fill both signs and settle near their wavelength; stripes +// wrap exactly across both seams; the upwind shadow counts steps against the wind; a streamline in a +// constant field is straight. +inline void patternChecks() +{ + const Torus t(128, 128); + std::mt19937 a(3), b(3); + TuringStyle style; + style.wavelength = 16; + const std::vector field = turingPattern(t, style, a); + assert(field == turingPattern(t, style, b)); + int crossings = 0, positive = 0; + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int v = field[size_t(t.at(x, y))]; + assert(v >= -32768 && v <= 32768); + positive += v > 0; + crossings += (v > 0) != (field[size_t(t.at(x + 1, y))] > 0); + } + const double period = 2.0 * t.size() / std::max(1, crossings); + assert(positive > t.size() / 5 && positive < t.size() * 4 / 5 && period > 10 && period < 26); + + StripeStyle stripes; + stripes.acrossX = 3; + stripes.acrossY = 2; + stripes.warpPercent = 0; + const Torus r(64, 32); + const std::vector phase = stripePhase(r, stripes, a); + // One step along x climbs 3/64 of a turn, one step down 2/32: the same across the seam. + for (int y = 0; y < r.h; ++y) + { + const int step = + ((phase[size_t(r.at(0, y))] - phase[size_t(r.at(r.w - 1, y))]) % 65536 + 65536) % 65536; + assert(std::abs(step - 3 * 65536 / 64) <= 1); + } + assert(std::abs(stripeSpacing(r, stripes) - 1 / std::hypot(3.0 / 64, 2.0 / 32)) < 1e-9); + assert(stripeDistance(100) == 100 && stripeDistance(65436) == 100); + + std::vector ridge(r.size(), 0); + for (int y = 0; y < r.h; ++y) + ridge[size_t(r.at(10, y))] = 1; + const std::vector shadow = upwindSteps(r, ridge, 1, 0, 8); + assert(shadow[size_t(r.at(10, 4))] == 0 && shadow[size_t(r.at(13, 4))] == 3 && + shadow[size_t(r.at(19, 4))] == -1 && shadow[size_t(r.at(7, 4))] == -1); + const std::vector line = + traceStreamline(r, {60, 5}, [](double, double) { return 0.0; }, 10, 1.0, 2.0); + assert(line.size() == 11 && std::abs(line.back().x - 70) < 1e-9 && line.back().y == 5); +} + +// Wandering paths leave and arrive where asked, even the short way across a seam, and a carved +// corridor joins its ends. +inline void wanderChecks() +{ + const Torus t(64, 64); + std::mt19937 rng(5); + const std::vector path = wanderingPath(t, {60, 10}, {6, 12}, 1.5, 6, 0.3, rng); + assert(path.front().x == 60 && path.front().y == 10); + assert(std::abs(path.back().x - 70) < 1e-9 && std::abs(path.back().y - 12) < 1e-9); + assert(path.size() == 12); // a point about every tile: ceil(hypot(10, 2)) segments + std::vector mask(t.size(), 0); + carveCorridor(mask, t, {10, 40}, {50, 20}, 1.5, 8, 0.2, rng); + assert(mask[t.at(10, 40)] && mask[t.at(50, 20)]); + assert(stepsFrom(t, tileMask(t, {t.at(10, 40)}), mask)[t.at(50, 20)] > 0); +} + +// Channels: a channel w corners wide spoils w + 3 tiles of grass and puts its banks bankToBank(w) +// apart, which towerReach agrees with; the widest crossable channel is in range and the next is not; +// a bridge carries units across and is counted once per side. +inline void channelChecks() +{ + const Torus t(64, 8); + for (int w = 1; w <= 8; ++w) + { + TerrainSketch sketch(t.size(), GRASS); + for (int y = 0; y < t.h; ++y) + for (int x = 20; x < 20 + w; ++x) + sketch[t.at(x, y)] = WATER; + layBeaches(sketch, t); + const std::vector grass = pureTiles(sketch, t, GRASS); + const std::vector beach = beachTiles(sketch, t); + int spoiled = 0; + for (int x = 0; x < t.w; ++x) + spoiled += !grass[t.at(x, 3)]; + assert(spoiled == w + kChannelSpoiledTiles); + assert(count(beach) == (spoiled - std::max(0, w - 1)) * t.h); + std::vector west(t.size(), 0), east(t.size(), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + west[t.at(x, y)] = grass[t.at(x, y)] && x < 20; + east[t.at(x, y)] = grass[t.at(x, y)] && x >= 20 + w && x < 30 + w; + } + assert(towerReach(t, west, east) == bankToBank(w)); + } + for (int level = 1; level <= 3; ++level) + { + const int widest = widestChannelTowersCross(level); + assert(widest >= 1 && bankToBank(widest) <= kTowerRange[level - 1] && + bankToBank(narrowestChannelTowersMiss(level)) > kTowerRange[level - 1]); + } + assert(widestChannelTowersCross(3, 3) == widestChannelTowersCross(3) - 2 && + widestChannelTowersCross(1, 3) == 0); + + TerrainSketch sketch(t.size(), GRASS); + for (int y = 0; y < t.h; ++y) + for (int x = 20; x < 26; ++x) + sketch[t.at(x, y)] = WATER; + layBeaches(sketch, t); + assert(bridgeAcross(sketch, t, {18, 4}, {28, 4}, 0.5) == 6); + const std::vector water = pureTiles(sketch, t, WATER); + std::vector open(t.size(), 0), bridge(t.size(), 0); + for (int i = 0; i < t.size(); ++i) + open[i] = !water[i]; + for (int x = 20; x < 26; ++x) + bridge[t.at(x, 4)] = 1; + // The wrap joins the banks round the far side too, so only the bridge's own row is checked. + std::vector near(t.size(), 0); + for (int y = 0; y < t.h; ++y) + for (int x = 16; x < 30; ++x) + near[t.at(x, y)] = open[t.at(x, y)]; + assert(stepsFrom(t, tileMask(t, {t.at(17, 4)}), near)[t.at(28, 4)] > 0); + std::vector sides(t.size(), -1); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + sides[t.at(x, y)] = x < 20 ? 0 : x >= 26 ? 1 : -1; + assert(crossingsPerLabel(t, bridge, sides, 2) == std::vector({1, 1})); +} + +// Room: footprints wrap, a blocked tile removes the sixteen that cover it, a region bounds them, and a +// chamber grows until it holds what it was asked for. +inline void roomChecks() +{ + const Torus t(16, 16); + std::vector buildable(t.size(), 1), all(t.size(), 1); + assert(count(buildAnchors(t, buildable)) == 256); + buildable[t.at(0, 0)] = 0; + assert(count(buildAnchors(t, buildable)) == 256 - 16); + std::vector region(t.size(), 0); + for (int y = 4; y < 12; ++y) + for (int x = 4; x < 12; ++x) + region[t.at(x, y)] = 1; + assert(buildSites(t, buildable, region) == 25); + std::vector chamber(t.size(), 0); + for (int y = 6; y < 10; ++y) + for (int x = 6; x < 10; ++x) + chamber[t.at(x, y)] = 1; + const int grown = growUntilSites(t, chamber, buildable, all, 9, 200, + [&](int i) { return t.dist2(i % t.w, i / t.w, 8, 8); }); + assert(grown >= 9 && buildSites(t, buildable, chamber) == grown && count(chamber) < 60); + + Game game(nullptr); + landscapeGrass(game, 4, 4); + assert(count(buildableTiles(game.map)) == 256); + game.map.setResource(3, 3, WOOD, 1); + assert(count(buildableTiles(game.map)) == 255); +} + +// A home in a rectangle with its door on the left: the swarm stands its depth in, the axis points away +// from the door, and the footprint is centred on the site. +inline void regionHomeChecks() +{ + const Torus t(64, 64); + std::vector region(t.size(), 0); + for (int y = 10; y < 30; ++y) + for (int x = 10; x < 50; ++x) + region[t.at(x, y)] = 1; + std::vector door; + for (int y = 18; y < 22; ++y) + door.push_back(t.at(10, y)); + const RegionHome home = regionHome(t, region, door, 12, 1, 4); + assert(home.site >= 0); + const int sx = home.site % t.w, sy = home.site / t.w; + assert(std::abs(sx - 22) <= 1 && sy >= 14 && sy <= 25); + assert(std::cos(home.axis) > 0.7 && home.swarm.x == sx - 2 && home.swarm.y == sy - 2); + assert(home.kitCentre.x > sx); + assert(regionHome(t, region, {}, 12, 1, 4).site < 0); +} + +// Biome kits: worth is relative to a fertile plain; an orchard island rises out of its lake; a +// fortress rings its ground in stone except at the door; a forest covers most of its open ground. +inline void biomeChecks() +{ + assert(std::abs(biomeWorth(fertilePlain()) - 1) < 1e-12); + assert(biomeWorth(stoneFortress()) > 0 && biomeWorth(forest()) > 0 && + biomeWorth(orchardIsland()) > 0); + for (const BiomeKit &kit : {fertilePlain(), stoneFortress(), orchardIsland(), forest()}) + { + Game game(nullptr); + landscapeGrass(game, 7, 7); + const Torus t(game.map); + std::vector region(t.size(), 0), doors(t.size(), 0), keep(t.size(), 0); + for (int y = 20; y < 108; ++y) + for (int x = 20; x < 108; ++x) + region[t.at(x, y)] = 1; + for (int y = 60; y < 68; ++y) + doors[t.at(20, y)] = doors[t.at(21, y)] = 1; + GenerationRequest request; + request.seed = 31; + GenerationContext context(request); + TerrainSketch sketch(t.size(), GRASS); + const BiomeTerrain terrain = sketchBiome(sketch, t, region, doors, kit, context, "biome"); + assert(count(terrain.water) > 0); + for (int i = 0; i < t.size(); ++i) + assert(!terrain.water[i] || region[i]); + if (kit.orchardIsland) + { + assert(count(terrain.island) > 0); + const std::vector ring = dilate(t, terrain.island, 1); + for (int i = 0; i < t.size(); ++i) + assert(!ring[i] || terrain.island[i] || terrain.water[i]); + } + assert((count(terrain.wall) > 0) == (kit.wallThickness > 0)); + for (int i = 0; i < t.size(); ++i) + assert(!terrain.wall[i] || !doors[i]); + layBeaches(sketch, t); + writeUndermap(game.map, sketch); + furnishBiome(game.map, t, context, region, terrain, kit, keep, "biome"); + int stone = 0, wood = 0, fruit = 0, outside = 0; + for (int i = 0; i < t.size(); ++i) + { + const Resource &r = game.map.getResource(i % t.w, i / t.w); + if (r.type == NO_RES_TYPE) + continue; + outside += !region[i]; + stone += r.type == STONE && terrain.wall[i]; + wood += r.type == WOOD; + fruit += r.type >= CHERRY && r.type <= CHERRY + 2 && terrain.island[i]; + } + assert(outside == 0); + assert(kit.wallThickness == 0 || stone > count(terrain.wall) / 2); + assert(!kit.orchardIsland || fruit > 0); + assert(kit.coverPercent == 0 || wood > count(region) / 4); + } +} + +inline void landscapeChecks() +{ + orbitChecks(); + morphologyChecks(); + growthChecks(); + contactChecks(); + pointChecks(); + patternChecks(); + wanderChecks(); + channelChecks(); + roomChecks(); + regionHomeChecks(); + biomeChecks(); + puts( + "PASS landscape toolkit: symmetry groups, morphology, crop growth, cost models and routes, " + "sites and cell graphs, patterns, wandering paths, channels, building room, region homes, " + "biome kits"); +} +} // namespace LandscapeChecks diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 71303ae8c..87fceff0f 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -9,6 +9,10 @@ #include "MapGenerator.h" #include "Race.h" #include "Unit.h" +#include "Contact.h" +#include "FertilityField.h" +#include "Grid.h" +#include "Room.h" #include "StartQuality.h" #include "Utilities.h" #include @@ -325,6 +329,57 @@ int saveRotatedMaps(Game &game, const GenerationResult &result, const Generation return consistent && placed && roundTrip && idempotent ? 0 : 6; } } // namespace +// Writes one of the study's overlays (see the dump option) as "w h" then a row of integers per line. +static bool writeOverlay(Game &game, int teams, const std::string &kind, const std::string &path) +{ + using namespace MapGeneration; + const Map &map = game.map; + const Torus t(map); + std::vector value(size_t(t.size()), 0); + if (kind == "growth") + { + const Fertility::Field field = Fertility::forMap(map, false); + for (int i = 0; i < t.size(); ++i) + value[i] = int(std::min(255, field.at(i % t.w, i / t.w) * 255 / 8000)); + } + else if (kind == "sites") + { + const std::vector anchors = buildAnchors(t, buildableTiles(map)); + for (int i = 0; i < t.size(); ++i) + value[i] = anchors[i]; + } + else if (kind == "chop" || kind == "owner") + { + const auto units = unitTilesByTeam(map, teams); + std::vector best(size_t(t.size()), -1), owner(size_t(t.size()), -1); + for (int team = 0; team < teams; ++team) + { + const std::vector cost = costsFrom(map, t, units[team], StepCosts::chopping(4)); + for (int i = 0; i < t.size(); ++i) + if (cost[i] >= 0 && (best[i] < 0 || cost[i] < best[i])) + { + best[i] = cost[i]; + owner[i] = team; + } + } + value = kind == "chop" ? best : owner; + } + else + return false; + FILE *f = std::fopen(path.c_str(), "w"); + if (!f) + return false; + std::fprintf(f, "%d %d\n", t.w, t.h); + for (int y = 0; y < t.h; ++y) + { + for (int x = 0; x < t.w; ++x) + std::fprintf(f, "%d ", value[size_t(y) * t.w + x]); + std::fprintf(f, "\n"); + } + std::fclose(f); + return true; +} + int main(int argc, char **argv) { if (argc == 2 && std::string(argv[1]) == "--catalog") @@ -395,7 +450,7 @@ int main(int argc, char **argv) bool tuning = false; bool headroom = false; bool quality = false; - std::string dump, savePrefix, mapName; + std::string dump, savePrefix, mapName, overlay; int candidates = 0, rotations = 1; std::map aliases = {{"smooth", "smoothing"}, {"craters", "lake-density"}, @@ -426,9 +481,12 @@ int main(int argc, char **argv) if (eq == std::string::npos) return 2; std::string id = arg.substr(0, eq); - if (id == "dump" || id == "save" || id == "name") + if (id == "dump" || id == "save" || id == "name" || id == "overlay") { - (id == "dump" ? dump : id == "save" ? savePrefix : mapName) = arg.substr(eq + 1); + (id == "dump" ? dump + : id == "save" ? savePrefix + : id == "name" ? mapName + : overlay) = arg.substr(eq + 1); continue; } if (id == "candidates" || id == "rotations") @@ -787,6 +845,12 @@ int main(int argc, char **argv) std::fprintf(f, "\n"); } std::fclose(f); + // An overlay beside the dump, one integer per tile, for tools/render_map.py to tint the map + // by: growth (the crop growth chance, 0-255), sites (1 where a 4x4 building fits today), + // chop (every tile's cheapest cost from the nearest colony, clearing wheat and wood as it + // goes; -1 unreachable) or owner (which colony that is; -1 unreachable). + if (!overlay.empty() && !writeOverlay(game, descriptor.nbTeams, overlay, dump + ".overlay")) + return 3; } if (!savePrefix.empty()) return saveRotatedMaps(game, result, descriptor, savePrefix, diff --git a/tools/new_map_generator.py b/tools/new_map_generator.py new file mode 100644 index 000000000..8fd1d32f1 --- /dev/null +++ b/tools/new_map_generator.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +"""Scaffold a new map generator that builds, registers and passes its own validation. + + tools/new_map_generator.py marches "Marches" + +Writes src/map/generator/generators/Generator.{h,cpp} from the designed-generator +shape (design, generate, validateWorld; docs/map-generators/ADDING_A_GENERATOR.md), with +colonies on the roomiest lattice the torus holds (Orbits.h), a round home and pond for +each, the starter kit and the crop guarantee, and three controls to start from. Then: + + * adds its definition to GeneratorRegistry::builtins() and its source to src/SConscript, + * adds its translation keys to data/texts.keys.txt and, with the English text as a + placeholder, to every table (translate them before merging), + * picks the next unused legacy id (or --legacy-id). + +What is left is the map itself and the verification the docs ask for: + + scons release=1 -j12 map-generator-golden-test map-generator-defaults-test map-generator-study + build/src/MapGeneratorGoldenTest --update # records its golden rows + tools/render_map.py sheet --out artifacts/ +""" +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +GENERATORS = ROOT / 'src' / 'map' / 'generator' / 'generators' +REGISTRY = ROOT / 'src' / 'map' / 'generator' / 'core' / 'GeneratorRegistry.cpp' +SCONSCRIPT = ROOT / 'src' / 'SConscript' +TEXTS = ROOT / 'data' + +HEADER = '''// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct {Name}Options +{{ + int homeSize; + int wheat, wood; // percentages of the default amounts + explicit {Name}Options(const GenerationRequest &r); +}}; +GeneratorDefinition {lower}Definition(); +''' + +SOURCE = '''// SPDX-License-Identifier: GPL-3.0-or-later +#include "{Name}Generator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "Orbits.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Settlements.h" +#include "Sketch.h" +#include +#include +#include +using namespace MapGeneration; + +// {Display}: TODO say what the landscape is, in a sentence a player would recognise. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). TODO: where contact happens and +// when, what remote ground pays, what keeps the map traversable. +// +// Scaffolded by tools/new_map_generator.py: colonies on the roomiest lattice the torus holds, each with +// a round home and a pond. Replace the design with the map's own. +namespace +{{ + +// A home's pond takes this share of its radius. +constexpr double kPondShare = 0.25; + +struct Layout +{{ + Torus t{{1, 1}}; + TerrainSketch terrain; + std::vector homeOf; // the colony whose home a tile is, or -1 + std::vector homes; // each colony's home centre + std::vector kits; // where each home's kit goes + double homeRadius = 0; + std::string failure; +}}; + +// The whole layout as a pure function of the request and the context's streams: validateWorld builds it +// again and checks the finished map against it. +Layout design(const GenerationRequest &request, GenerationContext &context) +{{ + const {Name}Options o(request); + Layout L; + L.t = {{1 << request.wDec, 1 << request.hDec}}; + const Torus &t = L.t; + const int teams = request.nbTeams; + L.terrain.assign(t.size(), GRASS); + L.homeOf.assign(t.size(), -1); + L.homeRadius = o.homeSize; + // Colonies on a lattice (Orbits.h): exact copies of one another when the count has a translation + // group, evenly spaced rows otherwise. + const LatticeSites lattice = + latticeSites(t.w, t.h, teams, context.bounded("{id}-layout", std::uint32_t(t.w)), + context.bounded("{id}-layout", std::uint32_t(t.h))); + for (size_t a = 0; a < lattice.sites.size(); ++a) + for (size_t b = a + 1; b < lattice.sites.size(); ++b) + {{ + const double dx = t.offsetX(int(lattice.sites[a].x), int(lattice.sites[b].x)); + const double dy = t.offsetY(int(lattice.sites[a].y), int(lattice.sites[b].y)); + if (dx * dx + dy * dy < (2 * L.homeRadius + 8) * (2 * L.homeRadius + 8)) + {{ + L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; + return L; + }} + }} + if (!homeHasRoom(L.homeRadius)) + {{ + L.failure = "The homes are too small; use a bigger home size."; + return L; + }} + const RadialShape home(L.homeRadius, 0.2, context, "{id}-home"); + const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "{id}-pond"); + std::vector water(t.size(), 0); + for (int k = 0; k < teams; ++k) + {{ + const ShapePoint centre = lattice.sites[k]; + L.homes.push_back(centre); + L.kits.push_back(stampRoundHome(t, centre, 0.0, home, L.homeRadius, 1, &pond, water, + [&](int i) {{ L.homeOf[i] = k; }})); + }} + for (int i = 0; i < t.size(); ++i) + if (water[i]) + L.terrain[i] = WATER; + layBeaches(L.terrain, t); + return L; +}} + +bool generate(Game &game, GenerationContext &context) +{{ + context.stage = "{id} layout"; + const {Name}Options o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + {{ + context.detail = L.failure; + return false; + }} + Map &map = game.map; + const Torus &t = L.t; + const int teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "{id} terrain"; + writeUndermap(map, L.terrain); + + context.stage = "{id} colonies"; + const auto homeMask = [&](int team) + {{ + std::vector ground(size_t(t.size()), 0); + for (int i = 0; i < t.size(); ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }}; + const auto anchor = [&](int team) {{ return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }}; + if (!settleColonies(game, context, "{id}-starts", homeMask, anchor)) + return false; + + context.stage = "{id} resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantHomeKit(map, t, context, L.kits[k], 0.0, L.homeRadius, 12, 12, [&](int i) + {{ return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }}); + secureStartingCrops(game, context, t); + reopenCrampedStarts(game, context, {{o.wheat, o.wood, 100, 100, 100}}); + return true; +}} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + if (const std::string mismatch = designMismatch(L, game.map, "{display_lower}"); !mismatch.empty()) + return mismatch; + return walkFromFirstColony(game.map, context.request.nbTeams, "the map", "").error; +}} +}} // namespace + +{Name}Options::{Name}Options(const GenerationRequest &r) + : homeSize(r.option("home-size")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")) +{{ +}} + +GeneratorDefinition {lower}Definition() +{{ + return {{"{id}", + {legacy}, + "{Display}", + 1, + false, + {{{{"home-size", "Home size", 10, 24, 1, 14, ControlGroup::Layout}}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount")}}, + generate, + true, + nullptr, + validateWorld}}; +}} +''' + + +def camel(identifier): + return ''.join(part.capitalize() for part in identifier.split('-')) + + +def used_legacy_ids(): + ids = set() + for path in GENERATORS.glob('*.cpp'): + for match in re.finditer(r'return\s*\{\s*"[a-z0-9-]+",\s*(\d+),', path.read_text()): + ids.add(int(match.group(1))) + return ids + + +def append_keys(keys): + listing = (TEXTS / 'texts.list.txt').read_text().split() + key_file = ROOT / listing[0] + existing = set(key_file.read_text().split('\n')) + fresh = [k for k in keys if f'[{k}]' not in existing] + if not fresh: + return + with key_file.open('a') as f: + for k in fresh: + f.write(f'[{k}]\n') + for name in listing[1:]: + table = ROOT / name + text = table.read_text() + if text and not text.endswith('\n'): + text += '\n' + text += ''.join(f'[{k}]\n{k}\n' for k in fresh) + table.write_text(text) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('id', help='stable string id, lower-case words joined by hyphens') + parser.add_argument('display', help='display name, which is also its translation key') + parser.add_argument('--legacy-id', type=int, help='numeric id (default: the next unused)') + args = parser.parse_args() + if not re.fullmatch(r'[a-z][a-z0-9]*(-[a-z0-9]+)*', args.id): + sys.exit('the id is lower-case words joined by hyphens, like "tidal-flats"') + name = camel(args.id) + header, source = GENERATORS / f'{name}Generator.h', GENERATORS / f'{name}Generator.cpp' + if header.exists() or source.exists(): + sys.exit(f'{source.name} already exists') + used = used_legacy_ids() + legacy = args.legacy_id if args.legacy_id is not None else max(used) + 1 + if legacy in used: + sys.exit(f'legacy id {legacy} is taken; ids are never reused') + lower = name[0].lower() + name[1:] + fields = dict(Name=name, lower=lower, id=args.id, Display=args.display, + display_lower=args.display.lower(), legacy=legacy) + header.write_text(HEADER.format(**fields)) + source.write_text(SOURCE.format(**fields)) + + registry = REGISTRY.read_text() + last_include = list(re.finditer(r'#include "[A-Za-z]+Generator\.h"\n', registry))[-1] + registry = registry[:last_include.end()] + f'#include "{name}Generator.h"\n' + registry[last_include.end():] + # New generators join the designed ones, ahead of the legacy families. + marker = 'ruggedArchipelagoDefinition()' + if marker not in registry: + sys.exit('GeneratorRegistry.cpp no longer lists ruggedArchipelagoDefinition(); register by hand') + registry = registry.replace(marker, f'{lower}Definition(), {marker}', 1) + REGISTRY.write_text(registry) + + sconscript = SCONSCRIPT.read_text() + anchor = 'map/generator/generators/WatershedGenerator.cpp\n' + if anchor not in sconscript: + sys.exit('src/SConscript no longer lists WatershedGenerator.cpp; add the source by hand') + SCONSCRIPT.write_text(sconscript.replace(anchor, anchor + f'map/generator/generators/{name}Generator.cpp\n', 1)) + + append_keys([args.display, 'Home size', 'Too many colonies for this map; use a bigger map or fewer colonies.', + 'The homes are too small; use a bigger home size.']) + print(f'Created {header.relative_to(ROOT)} and {source.relative_to(ROOT)} (legacy id {legacy}).') + print('Registered it, added its source to src/SConscript and its keys to the translation tables') + print('(English placeholders: translate them). Next:') + print(' scons release=1 -j12 map-generator-golden-test map-generator-defaults-test map-generator-study') + print(' build/src/MapGeneratorGoldenTest --update') + print(f' tools/render_map.py sheet {args.id} --out artifacts/{args.id}') + + +if __name__ == '__main__': + main() diff --git a/tools/render_map.py b/tools/render_map.py new file mode 100644 index 000000000..ec4227e67 --- /dev/null +++ b/tools/render_map.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Render generated maps to PNG, alone or as a comparison sheet. + +Every map comes from build/src/MapGeneratorStudy's dump (build it with +`scons release=1 map-generator-study`), so what is drawn is exactly what the +generator made. No third-party modules: the PNG is written with zlib. + +One map: + tools/render_map.py one coral --seed 7 --size 256 --out artifacts/coral.png + tools/render_map.py one maze --set cell-shape=1 --set teams=6 --overlay sites + +A sheet: every generator given, beside each other, at every size given, with +an HTML page captioning each image (the rule for a new generator is to compare +it with its nearest neighbours at 128, 256 and 512 before showing it): + tools/render_map.py sheet coral spider-web --sizes 128 256 512 --out artifacts/sheet + +Generators are named by their stable string id or numeric legacy id +(`MapGeneratorStudy --catalog` lists both). Overlays tint the map by a measure +the study writes beside the dump: growth (crop growth chance), sites (where a +4x4 building fits), chop (cost from the nearest colony, clearing crops) and +owner (which colony that nearest colony is). +""" +import argparse +import html +import json +import os +import struct +import subprocess +import sys +import tempfile +import zlib +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +STUDY = ROOT / 'build' / 'src' / 'MapGeneratorStudy' + +# The dump's classes: grass, sand, water, mixed shore, wheat, wood, stone, building, fruit, algae. +PALETTE = { + 0: (86, 160, 64), 1: (222, 204, 140), 2: (40, 92, 168), 3: (196, 178, 118), + 4: (236, 200, 60), 5: (24, 96, 36), 6: (150, 150, 150), 7: (220, 40, 40), + 8: (210, 60, 170), 9: (60, 190, 170), +} +OWNER_COLOURS = [ + (230, 25, 75), (60, 180, 75), (255, 225, 25), (0, 130, 200), (245, 130, 48), + (145, 30, 180), (70, 240, 240), (240, 50, 230), (210, 245, 60), (250, 190, 212), + (0, 128, 128), (220, 190, 255), +] + + +def write_png(path, width, height, pixels): + """pixels: a bytes-like of width * height * 3 RGB values.""" + raw = bytearray() + for y in range(height): + raw.append(0) + raw += pixels[y * width * 3:(y + 1) * width * 3] + + def chunk(kind, data): + body = kind + data + return struct.pack('>I', len(data)) + body + struct.pack('>I', zlib.crc32(body) & 0xffffffff) + + png = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0)) + png += chunk(b'IDAT', zlib.compress(bytes(raw), 9)) + chunk(b'IEND', b'') + Path(path).parent.mkdir(parents=True, exist_ok=True) + Path(path).write_bytes(png) + + +def read_grid(path): + lines = Path(path).read_text().split('\n') + width, height = map(int, lines[0].split()) + grid = [list(map(int, lines[1 + y].split())) for y in range(height)] + return width, height, grid + + +def catalog(): + out = subprocess.run([str(STUDY), '--catalog'], capture_output=True, text=True, check=True, cwd=ROOT) + return json.loads(out.stdout) + + +def resolve(name, entries): + for entry in entries: + if str(entry['method']) == str(name) or entry['id'] == name: + return entry + sys.exit(f'unknown generator {name!r}; MapGeneratorStudy --catalog lists them') + + +def exponent(size): + e = int(size).bit_length() - 1 + if 1 << e != int(size): + sys.exit(f'map sizes are powers of two, not {size}') + return e + + +def generate(entry, seed, width, height, settings, overlay, workdir): + dump = Path(workdir) / f"{entry['id']}-{width}x{height}-{seed}.dump" + profile = Path(workdir) / 'profile' + profile.mkdir(exist_ok=True) + command = [str(STUDY), str(entry['method']), str(seed), str(profile), 'quality', + f'width={exponent(width)}', f'height={exponent(height)}', f'dump={dump}'] + command += [f'{k}={v}' for k, v in settings] + if overlay: + command.append(f'overlay={overlay}') + result = subprocess.run(command, capture_output=True, text=True, cwd=ROOT) + quality = next((l for l in result.stdout.splitlines() if l.startswith('QUALITY')), '') + if result.returncode != 0 or not dump.exists(): + return None, (result.stderr.strip().splitlines() or ['failed'])[-1], quality + return dump, '', quality + + +def render(dump, overlay, scale): + width, height, grid = read_grid(dump) + tint = None + if overlay: + _, _, tint = read_grid(str(dump) + '.overlay') + peak = max(max(row) for row in tint) or 1 + pixels = bytearray() + for y in range(height): + row = bytearray() + for x in range(width): + r, g, b = PALETTE.get(grid[y][x], (0, 0, 0)) + if tint is not None: + v = tint[y][x] + if overlay == 'owner': + if v >= 0: + orr, og, ob = OWNER_COLOURS[v % len(OWNER_COLOURS)] + r, g, b = (r + orr) // 2, (g + og) // 2, (b + ob) // 2 + elif overlay == 'sites': + if v: + r, g, b = (r + 255) // 2, (g + 255) // 2, b // 2 + else: + share = 0 if v < 0 else v / peak + if overlay == 'chop': + share = 1 - share if v >= 0 else 0 + r, g, b = int(r * (1 - share) + 255 * share), int(g * (1 - share)), int(b * (1 - share)) + row += bytes((r, g, b)) * scale + pixels += row * scale + return width * scale, height * scale, pixels + + +def settings_of(pairs): + out = [] + for pair in pairs or []: + key, _, value = pair.partition('=') + if not value: + sys.exit(f'--set takes key=value, not {pair!r}') + out.append((key, value)) + return out + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = parser.add_subparsers(dest='mode', required=True) + for mode in ('one', 'sheet'): + p = sub.add_parser(mode) + p.add_argument('generators', nargs='+' if mode == 'sheet' else 1) + p.add_argument('--seed', type=int, default=1) + p.add_argument('--set', action='append', metavar='KEY=VALUE', + help='a control by id, or teams=/workers=; repeatable') + p.add_argument('--overlay', choices=['growth', 'sites', 'chop', 'owner']) + p.add_argument('--scale', type=int, default=0, help='pixels per tile (default: fit ~512 px)') + if mode == 'one': + p.add_argument('--size', type=int, default=256) + p.add_argument('--height', type=int) + p.add_argument('--out', required=True) + else: + p.add_argument('--sizes', type=int, nargs='+', default=[128, 256, 512]) + p.add_argument('--out', required=True, help='a directory: PNGs and index.html') + args = parser.parse_args() + if not STUDY.exists(): + sys.exit(f'{STUDY} is missing; run: scons release=1 map-generator-study') + entries = catalog() + settings = settings_of(args.set) + with tempfile.TemporaryDirectory() as workdir: + if args.mode == 'one': + entry = resolve(args.generators[0], entries) + height = args.height or args.size + dump, error, quality = generate(entry, args.seed, args.size, height, settings, args.overlay, workdir) + if not dump: + sys.exit(f"{entry['id']}: {error}") + scale = args.scale or max(1, 512 // max(args.size, height)) + w, h, pixels = render(dump, args.overlay, scale) + write_png(args.out, w, h, pixels) + print(f'{args.out} {quality}') + return + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + cells = [] + for name in args.generators: + entry = resolve(name, entries) + row = [] + for size in args.sizes: + dump, error, quality = generate(entry, args.seed, size, size, settings, args.overlay, workdir) + image = f"{entry['id']}-{size}.png" + if dump: + scale = args.scale or max(1, 512 // size) + w, h, pixels = render(dump, args.overlay, scale) + write_png(out / image, w, h, pixels) + row.append((size, image, quality)) + else: + row.append((size, None, error)) + print(entry['id'], size, 'ok' if dump else error) + cells.append((entry, row)) + page = ['Map sheet', + '', + f'

seed {args.seed}; settings {html.escape(str(settings))}; overlay {args.overlay}

', + '' + ''.join(f'' for s in args.sizes) + ''] + for entry, row in cells: + page.append(f"") + for size, image, note in row: + body = f'' if image else '
failed
' + page.append(f'
') + page.append('') + page.append('
{s}
{html.escape(entry['id'])}{body}
{html.escape(note)}
') + (out / 'index.html').write_text('\n'.join(page)) + print(out / 'index.html') + + +if __name__ == '__main__': + main() From 6f30581c3c86cdb9a9ec189b67b81295b78b748d Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 18:02:04 -0400 Subject: [PATCH 115/136] Add nine landscape generators on the landscape toolkit Marches (25), Fingerprint (26), Rain shadow (27), Old growth (28), Canals (29), Polder (30), Old town (31), Anthill (32) and Patchwork (33): the round-5 designs, each a design()/generate()/validateWorld generator built from the shared modules (Orbits lattices, Points, Patterns, Morphology, Growth, Contact, Channels, Room, Biomes, GraphMaze over any cell graph, Homes on any axis). Every constant and default carries the reasoning and the measurement behind it. - Marches: homelands on a translation lattice, wooded border bands, a watering hole per border stretch, orchards at junctions. - Fingerprint: integer Turing-pattern labyrinth in water or stone, pools at spaced trough bottoms. - Rain shadow: exactly wrapping slanted stone ridges with staggered passes, windward pools, lee sand. - Old growth: dry forest that never regrows, far lakes, hidden groves at equal cutting cost; validated by cutting cost, trails optional. - Canals: warped blocks, canals a level-2 tower shoots across, bridges on a tree plus a few, starting towers on the banks. - Polder: the whole torus in seamless crop/ditch rows with dykes, villages and hamlets. - Old town: walled city of stone blocks and grass streets, two-block home plazas, cathedral orchard, farm rows outside. - Anthill: solid stone carved into chambers and wandering tunnels, queen chambers grown to an equal building-site count. - Patchwork: territories by biome worth, four kits dealt in turn. Toolkit additions used by them: latticePeriod (lattice-periodic warp noise that keeps exact copies rough), windowMinimum, alongStripes, stampRoundHomes and plantOpenHomeKit, nearestSiteLabels from caller warp fields; BiomeKit ponds are now per 10,000 tiles. Verified on macOS arm64: every lobby cell (128/2,4; 256/2-12; 512/4,12; 512x256/6; three seeds) generates for all nine; golden table 264 rows (72 new), 0 failures; --sweep 0 failing combinations; defaults test with toolkit and landscape checks; CustomGameSetupHarness; strict translation check (new labels carry English placeholders in every table); client build. Rendered at 128/256/512 beside each other with tools/render_map.py; none has been played yet. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 90 ++++ data/texts.br.txt | 90 ++++ data/texts.ca.txt | 90 ++++ data/texts.cz.txt | 90 ++++ data/texts.de.txt | 90 ++++ data/texts.dk.txt | 90 ++++ data/texts.en.txt | 90 ++++ data/texts.eo.txt | 90 ++++ data/texts.es.txt | 90 ++++ data/texts.eu.txt | 90 ++++ data/texts.fa.txt | 90 ++++ data/texts.fi.txt | 90 ++++ data/texts.fr.txt | 90 ++++ data/texts.gr.txt | 90 ++++ data/texts.hu.txt | 90 ++++ data/texts.id.txt | 90 ++++ data/texts.it.txt | 90 ++++ data/texts.ja.txt | 90 ++++ data/texts.keys.txt | 45 ++ data/texts.ko.txt | 90 ++++ data/texts.nl.txt | 90 ++++ data/texts.pl.txt | 90 ++++ data/texts.pt.txt | 90 ++++ data/texts.ro.txt | 90 ++++ data/texts.ru.txt | 90 ++++ data/texts.si.txt | 90 ++++ data/texts.sk.txt | 90 ++++ data/texts.sr.txt | 90 ++++ data/texts.sv.txt | 90 ++++ data/texts.tr.txt | 90 ++++ data/texts.uk.txt | 90 ++++ data/texts.vi.txt | 90 ++++ data/texts.zh-cn.txt | 90 ++++ data/texts.zh-tw.txt | 90 ++++ .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 233 ++++++++- src/SConscript | 9 + src/map/generator/core/GeneratorRegistry.cpp | 50 +- .../generator/generators/AnthillGenerator.cpp | 359 ++++++++++++++ .../generator/generators/AnthillGenerator.h | 11 + .../generator/generators/CanalsGenerator.cpp | 429 +++++++++++++++++ .../generator/generators/CanalsGenerator.h | 11 + .../generators/FingerprintGenerator.cpp | 297 ++++++++++++ .../generators/FingerprintGenerator.h | 11 + .../generator/generators/MarchesGenerator.cpp | 428 +++++++++++++++++ .../generator/generators/MarchesGenerator.h | 12 + .../generators/OldGrowthGenerator.cpp | 397 ++++++++++++++++ .../generator/generators/OldGrowthGenerator.h | 12 + .../generator/generators/OldTownGenerator.cpp | 443 ++++++++++++++++++ .../generator/generators/OldTownGenerator.h | 11 + .../generators/PatchworkGenerator.cpp | 348 ++++++++++++++ .../generator/generators/PatchworkGenerator.h | 11 + .../generator/generators/PolderGenerator.cpp | 296 ++++++++++++ .../generator/generators/PolderGenerator.h | 12 + .../generators/RainShadowGenerator.cpp | 387 +++++++++++++++ .../generators/RainShadowGenerator.h | 11 + src/map/generator/shared/Biomes.cpp | 22 +- src/map/generator/shared/Biomes.h | 4 +- src/map/generator/shared/Homes.h | 36 ++ src/map/generator/shared/Morphology.cpp | 54 +++ src/map/generator/shared/Morphology.h | 6 + src/map/generator/shared/Orbits.cpp | 13 + src/map/generator/shared/Orbits.h | 7 + src/map/generator/shared/Patterns.cpp | 17 + src/map/generator/shared/Patterns.h | 7 + src/map/generator/shared/Points.cpp | 7 + src/map/generator/shared/Points.h | 7 + test/map-generator-golden.txt | 72 +++ 67 files changed, 7022 insertions(+), 23 deletions(-) create mode 100644 src/map/generator/generators/AnthillGenerator.cpp create mode 100644 src/map/generator/generators/AnthillGenerator.h create mode 100644 src/map/generator/generators/CanalsGenerator.cpp create mode 100644 src/map/generator/generators/CanalsGenerator.h create mode 100644 src/map/generator/generators/FingerprintGenerator.cpp create mode 100644 src/map/generator/generators/FingerprintGenerator.h create mode 100644 src/map/generator/generators/MarchesGenerator.cpp create mode 100644 src/map/generator/generators/MarchesGenerator.h create mode 100644 src/map/generator/generators/OldGrowthGenerator.cpp create mode 100644 src/map/generator/generators/OldGrowthGenerator.h create mode 100644 src/map/generator/generators/OldTownGenerator.cpp create mode 100644 src/map/generator/generators/OldTownGenerator.h create mode 100644 src/map/generator/generators/PatchworkGenerator.cpp create mode 100644 src/map/generator/generators/PatchworkGenerator.h create mode 100644 src/map/generator/generators/PolderGenerator.cpp create mode 100644 src/map/generator/generators/PolderGenerator.h create mode 100644 src/map/generator/generators/RainShadowGenerator.cpp create mode 100644 src/map/generator/generators/RainShadowGenerator.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 0fc49b091..8d6889608 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2074,3 +2074,93 @@ OpenGL غير متوفر في هذا الإصدار. سداسيات [Warp] التواء +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.br.txt b/data/texts.br.txt index 9d4f437aa..a052c9c27 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -2072,3 +2072,93 @@ Karrezioù C'hwec'hkornegoù [Warp] Distummañ +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 5dff90df0..fdaca11f1 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2084,3 +2084,93 @@ Quadrats Hexàgons [Warp] Deformació +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 8da285feb..598891132 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2076,3 +2076,93 @@ Tvar buněk Šestiúhelníky [Warp] Deformace +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.de.txt b/data/texts.de.txt index e6429fb75..23b57ff84 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2076,3 +2076,93 @@ Quadrate Sechsecke [Warp] Verzerrung +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.dk.txt b/data/texts.dk.txt index fdf8bf114..4ae3ba17d 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2152,3 +2152,93 @@ Kvadrater Sekskanter [Warp] Forvrængning +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.en.txt b/data/texts.en.txt index ca00f3ade..91c385bbe 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2066,3 +2066,93 @@ Squares Hexagons [Warp] Warp +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.eo.txt b/data/texts.eo.txt index f81f87350..38197a7b2 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2074,3 +2074,93 @@ Kvadratoj Sesanguloj [Warp] Deformado +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.es.txt b/data/texts.es.txt index f72941dd2..700dc17a0 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2076,3 +2076,93 @@ Cuadrados Hexágonos [Warp] Deformación +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 084da1ad6..f383eeacf 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2086,3 +2086,93 @@ Karratuak Hexagonoak [Warp] Deformazioa +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 39130a295..2e104258b 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2074,3 +2074,93 @@ OpenGL در این بیلد موجود نیست. شش‌ضلعی‌ها [Warp] پیچش +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 020959f5c..3fe78f836 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2076,3 +2076,93 @@ Neliöt Kuusikulmiot [Warp] Vääristymä +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 143f6c69f..16a447a63 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2086,3 +2086,93 @@ Carrés Hexagones [Warp] Déformation +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.gr.txt b/data/texts.gr.txt index fe616c8c9..26e1f97f7 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2146,3 +2146,93 @@ Cortex Εξάγωνα [Warp] Παραμόρφωση +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 4a0ca9d0d..3ea571d50 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2076,3 +2076,93 @@ Négyzetek Hatszögek [Warp] Torzítás +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.id.txt b/data/texts.id.txt index d27b0a9fe..2cd1bdb31 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2072,3 +2072,93 @@ Persegi Segi enam [Warp] Distorsi +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.it.txt b/data/texts.it.txt index ec5d61772..8faa58912 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2138,3 +2138,93 @@ Quadrati Esagoni [Warp] Deformazione +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.ja.txt b/data/texts.ja.txt index d4b31217b..4605735cb 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2072,3 +2072,93 @@ Globulation 2 のカーソルを表示します。 六角形 [Warp] ゆがみ +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.keys.txt b/data/texts.keys.txt index a17498252..88ad56537 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1031,3 +1031,48 @@ [Squares] [Hexagons] [Warp] +[Marches] +[The homes are too small; use a bigger home size.] +[March width] +[Orchards] +[Fingerprint] +[Wavelength] +[Pattern] +[Labyrinth] +[Channels] +[Barrier] +[Water] +[Grain] +[Rain shadow] +[Ridges] +[Slant] +[Ridge thickness] +[Pass spacing] +[Lee width] +[Old growth] +[Canals] +[Forest density] +[Hidden groves] +[Starting trails] +[Block size] +[Canal width] +[Extra bridges] +[Polder] +[Old town] +[Anthill] +[Patchwork] +[Row angle] +[Straight] +[Diagonal] +[Dyke spacing] +[Village size] +[Hamlets] +[City size] +[Street width] +[Plazas] +[Gates] +[Chamber spacing] +[Chamber size] +[Queen room] +[Tunnel width] +[Loops] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 9e1d07a66..a3fc4e493 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2072,3 +2072,93 @@ Globulation 2 커서를 표시합니다. 육각형 [Warp] 뒤틀림 +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 54ab87504..7d2affb7d 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2100,3 +2100,93 @@ Vierkanten Zeshoeken [Warp] Vervorming +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.pl.txt b/data/texts.pl.txt index b9f8cc678..954369de8 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2076,3 +2076,93 @@ Kwadraty Sześciokąty [Warp] Zniekształcenie +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 9adbe3f69..b029a939a 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2082,3 +2082,93 @@ Quadrados Hexágonos [Warp] Deformação +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 8db9ced3b..486a6b5c8 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2076,3 +2076,93 @@ Pătrate Hexagoane [Warp] Deformare +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 744cec4b4..e9bbb69ea 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2074,3 +2074,93 @@ OpenGL недоступен в этой сборке. Шестиугольники [Warp] Искажение +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.si.txt b/data/texts.si.txt index 63d489c2a..678f38292 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2076,3 +2076,93 @@ Kvadrati Šestkotniki [Warp] Popačenje +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 5b11d1abe..414b7593e 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2084,3 +2084,93 @@ Tvar buniek Šesťuholníky [Warp] Deformácia +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 4278cf799..de6cbb77d 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2076,3 +2076,93 @@ OpenGL није доступан у овој верзији. Шестоуглови [Warp] Изобличење +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.sv.txt b/data/texts.sv.txt index e3aaa496b..8371ef65d 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2086,3 +2086,93 @@ Kvadrater Hexagoner [Warp] Förvrängning +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 3574ffb14..ce9df7111 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2084,3 +2084,93 @@ Kareler Altıgenler [Warp] Bükülme +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.uk.txt b/data/texts.uk.txt index e2bc81d99..365889a49 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2072,3 +2072,93 @@ OpenGL недоступний у цій збірці. Шестикутники [Warp] Викривлення +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.vi.txt b/data/texts.vi.txt index f118baa46..d637cbc65 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2072,3 +2072,93 @@ Hình vuông Lục giác [Warp] Biến dạng +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index d0a8a5749..978622773 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2072,3 +2072,93 @@ OpenGL 在此版本中不可用。 六边形 [Warp] 扭曲 +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 6ba3634d7..dc56410e0 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2148,3 +2148,93 @@ OpenGL 在此版本中不可用。 六邊形 [Warp] 扭曲 +[Marches] +Marches +[The homes are too small; use a bigger home size.] +The homes are too small; use a bigger home size. +[March width] +March width +[Orchards] +Orchards +[Fingerprint] +Fingerprint +[Wavelength] +Wavelength +[Pattern] +Pattern +[Labyrinth] +Labyrinth +[Channels] +Channels +[Barrier] +Barrier +[Water] +Water +[Grain] +Grain +[Rain shadow] +Rain shadow +[Ridges] +Ridges +[Slant] +Slant +[Ridge thickness] +Ridge thickness +[Pass spacing] +Pass spacing +[Lee width] +Lee width +[Old growth] +Old growth +[Canals] +Canals +[Forest density] +Forest density +[Hidden groves] +Hidden groves +[Starting trails] +Starting trails +[Block size] +Block size +[Canal width] +Canal width +[Extra bridges] +Extra bridges +[Polder] +Polder +[Old town] +Old town +[Anthill] +Anthill +[Patchwork] +Patchwork +[Row angle] +Row angle +[Straight] +Straight +[Diagonal] +Diagonal +[Dyke spacing] +Dyke spacing +[Village size] +Village size +[Hamlets] +Hamlets +[City size] +City size +[Street width] +Street width +[Plazas] +Plazas +[Gates] +Gates +[Chamber spacing] +Chamber spacing +[Chamber size] +Chamber size +[Queen room] +Queen room +[Tunnel width] +Tunnel width +[Loops] +Loops diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 3541218fc..2074fe733 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -63,9 +63,10 @@ them so the ranking rewards what it is meant to be. ## The designed generator -Fourteen generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, -City states, Tidal flats, Everglades, Spider web, Coral, Carousel, Amphitheatre, Switchbacks) follow one shape, and the nine newest of them -are little more than a sequence of shared stages: +Twenty-three generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, +City states, Tidal flats, Everglades, Spider web, Coral, Carousel, Amphitheatre, Switchbacks, and the nine +landscape generators Marches, Fingerprint, Rain shadow, Old growth, Canals, Polder, Old town, Anthill and +Patchwork) follow one shape, and the newest of them are little more than a sequence of shared stages: 1. `design(request, context)` computes the whole layout from the request and the context's named streams without touching the map, and returns it with a `failure` string when the @@ -112,6 +113,15 @@ reused after a generator is retired. | `carousel` | 22 | Carousel | Its own — see below | | `amphitheatre` | 23 | Amphitheatre | Its own — see below | | `switchbacks` | 24 | Switchbacks | Its own — see below | +| `marches` | 25 | Marches | Its own — see below | +| `fingerprint` | 26 | Fingerprint | Its own — see below | +| `rain-shadow` | 27 | Rain shadow | Its own — see below | +| `old-growth` | 28 | Old growth | Its own — see below | +| `canals` | 29 | Canals | Its own — see below | +| `polder` | 30 | Polder | Its own — see below | +| `old-town` | 31 | Old town | Its own — see below | +| `anthill` | 32 | Anthill | Its own — see below | +| `patchwork` | 33 | Patchwork | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -910,6 +920,223 @@ next leg up. Every home reaches a walled wheat farm on either flank. trails shut and with only the middle legs shut (so no leg can be skipped), a level-1 tower at home reaching the first leg and one on the plateau reaching the last, and even walks to the plateau. +## Marches + +Homelands on a lattice across the whole torus, with no centre and no prize in the middle. Every colony's +home lies in its own territory, and between every two neighbouring territories runs a band of wild +border country, the marches: wooded, with a watering hole on every stretch of border and an orchard of +all three fruits wherever three or four territories meet. A colony has a frontier with every neighbour, +and its choice is which border to push. + +- **Lattice.** Homes on the roomiest translation lattice the torus holds (`latticeSites`, `Orbits`): + exact copies of one another for 2, 4, 8 or 16 colonies, evenly staggered rows otherwise. Nothing is + designed round a middle, so a rectangular map is served as well as a square one. +- **Homelands.** Every tile's nearest home after a warp (`nearestSiteLabels`). On an exact lattice the + warp noise is laid on cells the lattice's moves are whole multiples of (`latticePeriod`), so the + homelands are exact copies too; summing plain noise over the orbits instead averaged the roughness + away and left the borders ruled straight. `border-roughness` (0-100, 40) bends a border by up to 30% + of the home spacing at full; `march-width` (6-24, 12) is the band either side of the border. +- **Fit.** A home disc must stay inside its homeland however far the warp bends the border: on a crowded + map the homes shrink (`home-size`, 10-24, 14) to what is left, then the marches narrow (never below 4), + and only then is the map refused. +- **Prizes.** One watering hole (a pool of radius 3 and 24 tiles of wheat) at the middle of every + separate stretch of border between two homelands, found as the 8-connected groups of border tiles per + pair of neighbours; an orchard of the three fruits with a quarry at every junction of three or more + homelands, junctions within three tiles merged (`orchards`, on). The marches carry wood over 55% of + their ground in noise patches (the gaps are the natural ways through) and never regrow it, being far + from water. +- **Kits and fields.** A wheat and wood kit with a quarry at every home's pond (`plantOpenHomeKit`); + ambient farmland as a share of each homeland's *fertile* ground, the ring round its pond, not of its + area (4% of the area buried the home under six hundred tiles of crops). Where the woods close a + colony in, `openColonyRoutes` cuts the cheapest way, never a ford. +- **Checked, not assumed.** The design rebuilt, every home's pond present, every colony walkable from + the first. + +## Fingerprint + +An organic labyrinth: a Turing pattern grown over the whole torus (`turingPattern`, `Patterns`) +draws long curving bands that fork, merge and dead-end the way a fingerprint's ridges do, and the bands +become the barriers. + +- **Pattern.** `wavelength` (12-40, 20) is the crest-to-crest width, so about half of it is corridor; + `grain` (0-100, 0) stretches the blur along one axis so the bands run rather than wander. `pattern` + chooses the barrier's share of the map: Labyrinth 42% (bands and corridors read alike once a water + band's beach has taken a tile of each side), Islands 60%, Channels 28%. +- **Barrier.** Water, so every corridor is beside water and all the land is farmland (a share of the + fertile ground under crops in patches); or Stone, permanent walls with pools in the corridors, the + only fertile ground. The pools lie at the bottoms of the field's troughs: every local minimum with + nothing lower within two wavelengths (`windowMinimum`), grown to 40 tiles along its trough; a + percentile of the field instead gave a hundred tiny pools whose beaches cut the stone into blobs. +- **Homes.** Clearings on a lattice (`latticeSites`), each a rough disc with a pond (`stampRoundHomes`) + and a margin of three tiles of pattern cleared beyond it, shrunk so a band's width of pattern always + runs between neighbours. Fairness is statistical, like Everglades'. +- **Routes.** The pattern owes nobody a way through: `openColonyRoutes` opens the cheapest way from the + first colony to any it cannot walk to, a sand ford across water or a gap cut in the stone. + +## Rain shadow + +A map with a grain: long stone ridges run across the torus, parallel and a valley apart, and the wind +blows across them. The windward foot of every ridge catches the rain (a chain of pools, and the +farmland), the lee side is a band of dry sand where nothing grows or builds, and passes cut every ridge +at intervals staggered from ridge to ridge, so moving along a valley is easy and crossing is slow. + +- **Ridges.** `ridges` (2-8, 4) crossings of the map, tilted by `slant` (0-3, 1) so following one across + the width climbs that many ridges: stripes that wrap the torus exactly (`stripePhase`), so with a slant + every ridge is one spiral, which is what staggers its own passes from one crossing to the next + (straight ridges stagger by parity instead). On a small map ridges are taken away until a valley + holds a home with its margins, the pools and the sand (four suit a 256 map, a 128 map gets two). + `ridge-thickness` (2-6, 3): three tiles is sealed against diagonal steps and within a level-1 tower's + reach across (`towerReach`). +- **Passes.** Every `pass-spacing` tiles (24-96, 48) along the ridge, `pass-width` (3-9, 5) wide, + laid out by the phase along the ridges (`alongStripes`), so they repeat seamlessly. +- **Wind.** Pools run along the windward foot, 12 tiles long every 24, four tiles out from the stone + (three took the ridge's windward row with them: a pool's beach spoils the tiles round it and stone + stands only on pure grass); the lee sand starts two tiles behind the ridge for the same reason and + runs `lee-width` (2-12, 6) tiles, measured downwind from the stone itself (`upwindSteps`) so it stops + where a pass lets the rain through. +- **Homes.** On a lattice, each slid along the wind to the middle of its valley, so every home has the + same ridge behind it and the same foot before it. Crops go on the fertile ground, which the pools make + the windward side of every valley: a third of it under wheat and a fifth under wood. +- **Checked, not assumed.** Every ridge tile that could hold stone does, every pond present, every + colony walkable from the first through the passes. + +## Old growth + +A dry continent under unbroken forest: every colony starts in a clearing with its own pond and kit, +and beyond the clearing stands wood in every direction. There is no other water but a few lakes far from +every home, so almost none of the forest ever grows back: what a colony cuts stays cut, the map opens as +the game goes on, and contact happens only where someone has cut through. + +- **Forest.** Wood on `forest-density` percent (60-100, 90) of the forest ground, the gaps drawn from a + noise field so they are small openings; below the 8-connected site percolation threshold (about 41% + open) the openings never join into a way through, so at any density offered the forest is a wall + that has to be cut (`plantCover`). +- **Lakes.** `lakes` per 128x128 of map (0-4, 1) of `lake-size` tiles (40-160, 90), each at the tile + farthest from every clearing and every lake so far (`distanceSquaredTo`) and grown by distance with a + little noise; ringed with wheat and an orchard of the three fruits, the only ground where wood + regrows and the only fruit not hidden. +- **Hidden groves.** One per colony (`hidden-groves`, on): a pocket cut in the forest at 65% of the + cutting cost half way to the nearest rival, on its own side (`equalCostSites` over + `StepCosts::chopping`), holding a fruit grove, a wheat patch and a stone clump. Costs are measured on + the planted forest, so the groves lie at the same worker-hours from every home. +- **Trails.** `trails` (off) cuts a trail from every colony to the first before the game starts + (`openColonyRoutes`). Without it the colonies start entirely apart, and the validator checks the map by + cutting cost (`contactMatrix`) rather than by walking. +- **Checked, not assumed.** Every pond present; the forest dry beyond the reach of any water (no growth + chance on the finished map's own field where nothing waters it); every colony reachable from the + first, by walking with trails, by cutting without. + +## Canals + +A lagoon city: the map cut into blocks by a grid of narrow canals, every canal just wide enough to stop +a unit and just narrow enough for a tower on one bank to shoot the other, and only a handful of sand +bridges. Towers reach across from the first minute and armies cannot, so where the first towers go is the +opening; once swimming pools are built every canal is a road. + +- **Blocks.** A square tiling of `block-size` (16-40, 24) warped by `warp` (0-100, 40), every edge an + obstacle the warp keeps apart (`warpCorners`), every edge stroked `canal-width` corners of water + (3-5, 3). A canal's water is a corner narrower than the stroke and a diagonal canal is sealed against a + diagonal step only when its water is two tiles thick, which is why the narrowest offered is 3; a + straight canal w corners wide puts the banks' grass w + 4 apart (`Channels`), so 3 is reached by a + level-2 tower and not a level-1. +- **Homes and markets.** The colonies' blocks are the ones farthest apart on the block graph + (`spreadPockets`), each with a pond and a kit; a market block per colony, farthest from every home, + carries an orchard of the three fruits. Fairness is statistical. +- **Bridges.** A tree of shortest block-to-block paths from the first colony's block to every other + colony's, so every colony can be walked to, then `extra-bridges` percent of the blocks' count more at + random (`openLoops`), so most blocks stay islands until someone swims. A bridge is a line of sand + corners across the canal (a tile with a sand corner is no longer pure water) reaching onto both banks. +- **Towers.** `tower-count` towers at `starting-towers` level (default 2 of level 2) and two pads per + colony, on its own bank against the beach (`chooseTowerSites` with `against`), covering the most of + other blocks' land across the canal; none may close the colony's walk to a bridge + (`settleStartingTowers`). +- **Fields.** Every block is fertile (its canal is within the growth probe's reach of all of it), so a + modest share of every block goes under crops in patches, the bridges' landings kept clear; algae in + every canal, sand being everywhere. + +## Polder + +Reclaimed land, all of it: the whole torus laid out in rows of crops with a ditch of water between every +two, sand dykes across the ditches at intervals, small grass villages for the colonies and hamlets +between them. Food is effectively unlimited; the game is logistics, on the dykes and the ditches' beaches, +until someone can swim. + +- **Rows.** Stripes that wrap the torus exactly (`stripePhase`): straight rows 10 of crops and 6 of water + (a period of 16 divides every map side; the yield fit's 10 and 8 would give 18, which does not, for + under 3% of yield), or diagonal rows (`row-angle`) 11 and 6 on a spacing the map sets. Dykes every + `dyke-spacing` tiles (12-48, 24) along the rows, two corners of sand wide, laid by the phase along the + rows (`alongStripes`). +- **Villages.** Grass discs of `village-size` (8-16, 11) on a lattice, shrunk so a whole row and ditch + lie between two; each holds a swarm, its kit and a few buildings and no more. `hamlets` (on) puts a + grass disc of radius 5 half way between neighbouring villages, room for a forward inn and a tower, with + a fruit grove. +- **Fields.** 55% of the fertile row ground under wheat and 15% under wood, in patches; every row tile is + a few tiles from water, so it all regrows. Only crops could close a lane, so `openColonyRoutes` clears + only crops. + +## Old town + +A walled city of stone blocks and narrow grass streets, farmland outside the wall. Streets are buildable +and buildings block walking, so every building a player puts up closes a street: the players build the +city's fortifications themselves, and a tower on a street fires over the block into the next. + +- **City.** A disc of `city-size` percent of the half side (40-90, 70), tiled into blocks of + `block-size` (10-20, 14) warped by `warp` (0-100, 50); the band `street-width` (3-7, 4) along every + cell border is street, everything else in a city cell is a stone block. A wall two thick with `gates` + (2-8, 4) evenly spaced from a random start rings the city (`ringWithGates`), a ring road inside it. +- **Plazas.** Blocks left open, with a fountain pond. A home plaza is two adjoining blocks (a single block + of 14 less its streets, with a fountain in it, has no room for a 4x4 swarm; measured: every settlement + failed), the fountain in one and the swarm in the other, the pairs as far apart as the city allows; + `plazas` (0-4, 2) more per colony farthest from those, given up first when the city is small; and the + cathedral square at the cell holding the centre, with an orchard of the three fruits round its fountain. +- **Fields.** Outside the wall, farm rows along the map's axis (`layFarm` at `bestFarmRows`, bridged + every 16), half their fertile ground under wheat and a sixth under wood; the plazas inside get a light + share so they stay open. Every kit has no quarry: the blocks are stone. +- **Checked, not assumed.** Every fountain present, the wall unbroken but for its gates, every colony + walkable from the first through the streets. + +## Anthill + +Solid stone carved into chambers joined by winding tunnels. Every colony starts in a queen chamber, a +cul-de-sac with one door and a pond; the other chambers are farm chambers with a pond and a field, or +dead-end treasure chambers with fruit and wheat. Stone is everywhere and never runs out, but it cannot be +built on or cleared, so room is the one scarce thing: a chamber holds a few buildings, and growing means +taking the next chamber down the tunnel. + +- **Chambers and tunnels.** Sites `chamber-spacing` apart (20-40, 28; `spreadPoints`), their nearest-site + cells the graph (`cellGraph` over `siteNeighbours`); a spanning tree through every chamber but the + queens' (`carveSpanningTree`), one door into each queen chamber, and `loops` percent (0-60, 20) of + the chambers' count in extra tunnels. Every open edge is a wandering tunnel `tunnel-width` wide (2-4, + 3) between its chambers' middles (`carveCorridor`); every chamber a rough disc of `chamber-size` + (4-8, 5), farm chambers two bigger, each turned by the golden angle so one outline reads as many. +- **Kinds.** Dead ends other than the queens' are treasure chambers; of the rest every other one is a + farm chamber with a pond and a field. A queen chamber's pond lies on the far side from its door and + its swarm towards the door, and it is grown until it holds `queen-room` building sites (20-120, 40: + overlapping 4x4 footprints, the start scorer's measure) with `growUntilSites`, within its own cell + and never onto a pond's beach, so no colony starts with more room than another. +- **Rock.** Stone on every uncarved tile the beaches left pure grass; `openColonyRoutes` clears crops + in a tunnel and cuts stone only as a last resort, at a cost that keeps it to a tile or two. + +## Patchwork + +Every colony starts on a different kind of land: the map is shared out into one territory per colony +and each is dealt a kit from `Biomes` in turn, a fertile plain, a stone fortress, an orchard island or a +forest, so every player plays a different opening. The one map in the catalog that gives up fairness by +construction. + +- **Territories.** Homes on a lattice; `growTerritories` by worth, so a territory dealt a poorer kit gets + proportionally more tiles (`biomeWorth`, an estimate from the start scorer's weights); noise in the + cost bends the borders by `border-roughness` (0-100, 40) and `smoothing` passes (0-4, 2) at radius 3 + straighten them into curves. Whatever was left unclaimed goes to the nearest home. +- **Kits.** Each kit's terrain over its whole territory (`sketchBiome`: ponds, an orchard island, a + fortress ring with a gate towards every neighbouring territory at the rim tile nearest the line between + the two homes), the home's own clearing and pond kept; then its deposits (`furnishBiome`), the amounts + scaling each kit's own shares. Every home gets the same unscaled kit with a quarry, so every opening is + at least viable. +- **Tuning.** `biomeWorth` is not a measurement: run the fairness tournament + ([FAIRNESS_TOURNAMENT.md](FAIRNESS_TOURNAMENT.md)) before trusting the deal. `openColonyRoutes` opens + the cheapest way through crops, and through a ring only where a gate failed. + ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort diff --git a/src/SConscript b/src/SConscript index 3a074cb3b..5d0768c3e 100644 --- a/src/SConscript +++ b/src/SConscript @@ -203,6 +203,15 @@ map/generator/generators/SymmetricArenaGenerator.cpp map/generator/generators/TidalFlatsGenerator.cpp map/generator/generators/UniformGenerator.cpp map/generator/generators/WatershedGenerator.cpp +map/generator/generators/PatchworkGenerator.cpp +map/generator/generators/AnthillGenerator.cpp +map/generator/generators/OldTownGenerator.cpp +map/generator/generators/PolderGenerator.cpp +map/generator/generators/CanalsGenerator.cpp +map/generator/generators/OldGrowthGenerator.cpp +map/generator/generators/RainShadowGenerator.cpp +map/generator/generators/FingerprintGenerator.cpp +map/generator/generators/MarchesGenerator.cpp map/generator/shared/legacy/Distances.cpp map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index d9c3a90cd..6806cfb2b 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -24,6 +24,15 @@ #include "TidalFlatsGenerator.h" #include "UniformGenerator.h" #include "WatershedGenerator.h" +#include "MarchesGenerator.h" +#include "FingerprintGenerator.h" +#include "RainShadowGenerator.h" +#include "OldGrowthGenerator.h" +#include "CanalsGenerator.h" +#include "PolderGenerator.h" +#include "OldTownGenerator.h" +#include "AnthillGenerator.h" +#include "PatchworkGenerator.h" #include #include #include @@ -110,13 +119,38 @@ const GeneratorRegistry &GeneratorRegistry::builtins() // This is also the product-facing catalog order. Keep the most polished and // distinctive playable maps first; numeric IDs remain stable compatibility // identifiers and do not determine presentation order. - static const GeneratorRegistry registry( - {contestedCommonsDefinition(), mazeDefinition(), fjordContinentDefinition(), - shatteredCoastDefinition(), islesDefinition(), watershedDefinition(), - stoneHighlandsDefinition(), symmetricArenaDefinition(), ringWorldDefinition(), - cityStatesDefinition(), tidalFlatsDefinition(), evergladesDefinition(), spiderWebDefinition(), - coralDefinition(), carouselDefinition(), amphitheatreDefinition(), switchbacksDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), - craterLakesDefinition(), islandsDefinition(), swampDefinition(), riverDefinition(), - uniformDefinition()}); + static const GeneratorRegistry registry({contestedCommonsDefinition(), + mazeDefinition(), + fjordContinentDefinition(), + shatteredCoastDefinition(), + islesDefinition(), + watershedDefinition(), + stoneHighlandsDefinition(), + symmetricArenaDefinition(), + ringWorldDefinition(), + cityStatesDefinition(), + tidalFlatsDefinition(), + evergladesDefinition(), + spiderWebDefinition(), + coralDefinition(), + carouselDefinition(), + amphitheatreDefinition(), + switchbacksDefinition(), + marchesDefinition(), + fingerprintDefinition(), + rainShadowDefinition(), + oldGrowthDefinition(), + canalsDefinition(), + polderDefinition(), + oldTownDefinition(), + anthillDefinition(), + patchworkDefinition(), + ruggedArchipelagoDefinition(), + concreteIslandsDefinition(), + craterLakesDefinition(), + islandsDefinition(), + swampDefinition(), + riverDefinition(), + uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/AnthillGenerator.cpp b/src/map/generator/generators/AnthillGenerator.cpp new file mode 100644 index 000000000..0f0365c33 --- /dev/null +++ b/src/map/generator/generators/AnthillGenerator.cpp @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "AnthillGenerator.h" +#include "Drawing.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "GraphMaze.h" +#include "Grid.h" +#include "Morphology.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Points.h" +#include "Resources.h" +#include "Roads.h" +#include "Room.h" +#include "Settlements.h" +#include "Sketch.h" +#include +#include +#include +#include +using namespace MapGeneration; + +// Anthill: a map of solid stone, carved into chambers joined by winding tunnels two or three tiles +// wide. Every colony starts in a queen chamber of its own, a cul-de-sac with a single door and a +// pond; the other chambers are farm chambers with a pond and a field, or dead-end treasure +// chambers with fruit and wheat and nothing else. Stone is everywhere and never runs out, but it +// cannot be built on and cannot be cleared, so the one thing scarce on this map is room: a chamber +// holds a few buildings, not a colony, and growing means taking the next chamber down the tunnel. +// Towers shoot through the rock into neighbouring tunnels. +// +// The chambers are scattered sites (Points.h) with their nearest-site cells as the graph the +// tunnels are carved on (GraphMaze.h): a spanning tree, a door into every queen chamber, and a +// share of extra tunnels for loops. Fairness is statistical, the lobby keeping the best of several +// seeds; every queen chamber is grown to hold the same number of building sites (Room.h), so no +// colony starts with more room than another. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Buildings need pure grass +// and stone is never cleared, so the map's building room is fixed at generation and the fight is +// over chambers; tunnels a column wide are closed by one building or one tower; the ponds in the +// farm chambers are the only water, so a colony that wants its fields to regrow holds a farm +// chamber; and a tower scans square rings with no line of sight, so the rock between two tunnels +// is no cover at all. +namespace +{ + +// The queen chamber's pond (radius 2, a dozen corners of water with a beach a unit walks round) and +// its kit, unscaled whatever the amounts say: wheat and wood either side of the pond. No quarry: the +// walls are stone. +constexpr double kPondRadius = 2.0; +constexpr int kHomeWheat = 12, kHomeWood = 10; +// A farm chamber is this much bigger than an ordinary chamber, to hold its pond and field; its field +// is this much wheat and wood. +constexpr int kFarmExtra = 2, kFarmWheat = 12, kFarmWood = 8; +// A treasure chamber holds a fruit grove of radius 1 (5 tiles) and this much wheat. +constexpr int kTreasureWheat = 8; +// Tunnels wander sideways by this share of the chamber spacing, and swell and narrow by this share +// of their width, so they read as dug rather than drawn. +constexpr double kWanderShare = 0.18, kWidthJitter = 0.25; +// The queen chamber starts this much bigger than an ordinary chamber and grows until it holds +// `queenRoom` building sites, never more than this many tiles. +constexpr int kQueenExtra = 1, kQueenGrowthLimit = 300; + +struct Layout +{ + Torus t{1, 1}; + std::vector sites; + std::vector label; // every tile's nearest site + std::vector homeSite, farmSite, treasureSite; + std::vector open, water; // tunnels and chambers; ponds + std::vector homeOf; + std::vector homes, kits; + std::vector axes; // each home's facing: away from its door + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const AnthillOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + + // The chambers' sites and the graph of neighbouring cells the tunnels follow. + L.sites = spreadPoints(t, o.chamberSpacing, context, "anthill-sites"); + L.label = nearestSiteLabels(t, L.sites, o.chamberSpacing, context, "anthill-warp", 150, 0); + const CellGraph graph = cellGraph(t, L.sites, siteNeighbours(t, L.label, int(L.sites.size()))); + L.homeSite = spreadPockets(graph, teams); + if (int(L.homeSite.size()) != teams || int(L.sites.size()) < teams + 2) + { + L.failure = "Too many colonies for this map; use a bigger map, closer chambers or fewer " + "colonies."; + return L; + } + // The tunnels: a spanning tree through every chamber but the queens', a door into each queen + // chamber, and `loops` percent of the chambers' count in extra tunnels. + std::vector isPocket(L.sites.size(), 0); + for (int site : L.homeSite) + isPocket[site] = 1; + std::vector openEdge(graph.edgeCells.size(), 0); + if (!carveSpanningTree(graph, context, "anthill-maze", isPocket, openEdge)) + { + L.failure = "The chambers could not all be joined; use closer chambers."; + return L; + } + const std::vector doors = + openPocketDoors(graph, context, "anthill-maze", L.homeSite, isPocket, openEdge); + openLoops(graph, context, "anthill-maze", isPocket, o.loops, openEdge); + // Dead ends other than the queens' are treasure chambers; of the rest, every other one is a + // farm chamber (index parity, which is as random as the sites are). + std::vector exitEdge; + const std::vector ends = deadEnds(graph, openEdge, isPocket, exitEdge); + std::vector isEnd(L.sites.size(), 0); + for (int site : ends) + isEnd[site] = 1; + L.treasureSite = ends; + for (int site = 0; site < int(L.sites.size()); ++site) + if (!isPocket[site] && !isEnd[site] && site % 2 == 0) + L.farmSite.push_back(site); + + // Carving: every open edge a wandering tunnel between its two chambers' middles, then every + // chamber a rough disc, queen and farm chambers bigger. + L.open.assign(n, 0); + L.water.assign(n, 0); + std::mt19937 &tunnels = context.stream("anthill-tunnels"); + for (int edge = 0; edge < int(openEdge.size()); ++edge) + if (openEdge[edge]) + { + const Site a = L.sites[graph.edgeCells[edge][0]], b = L.sites[graph.edgeCells[edge][1]]; + carveCorridor(L.open, t, {a.x + 0.5, a.y + 0.5}, {b.x + 0.5, b.y + 0.5}, + o.tunnelWidth / 2.0, kWanderShare * o.chamberSpacing, kWidthJitter, + tunnels); + } + const RadialShape chamber(o.chamberSize, 0.3, context, "anthill-chambers"); + const RadialShape farm(o.chamberSize + kFarmExtra, 0.3, context, "anthill-chambers"); + const RadialShape queen(o.chamberSize + kQueenExtra, 0.25, context, "anthill-chambers"); + const RadialShape pond(kPondRadius, 0.3, context, "anthill-ponds"); + std::vector kind(L.sites.size(), 0); // 0 chamber, 1 farm, 2 queen + for (int site : L.farmSite) + kind[site] = 1; + for (int site : L.homeSite) + kind[site] = 2; + for (int site = 0; site < int(L.sites.size()); ++site) + { + // Each chamber turned by a different angle, so one outline reads as many. + const double turn = site * 2.399963; // the golden angle + const RadialShape &shape = kind[site] == 2 ? queen : kind[site] == 1 ? farm : chamber; + fillShape(L.open, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, shape, turn); + } + // Ponds: in every farm chamber at its middle; in every queen chamber on the far side from the + // door, with the swarm towards the door, so the kit lies between them and the way out is clear. + for (int site : L.farmSite) + fillShape(L.water, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, pond, 0.0); + L.homeOf.assign(n, -1); + for (int k = 0; k < teams; ++k) + { + const int site = L.homeSite[k]; + const Site s = L.sites[site]; + const ShapePoint centre{s.x + 0.5, s.y + 0.5}; + // The door's direction: towards the chamber across the door edge. + const int across = graph.other(doors[k], site); + const double axis = std::atan2(-double(t.offsetY(s.y, L.sites[across].y)), + -double(t.offsetX(s.x, L.sites[across].x))); + const double pondOut = o.chamberSize + kQueenExtra - kPondRadius - 2.5; + const ShapePoint pondAt = polarPoint(centre.x, centre.y, pondOut, axis); + fillShape(L.water, t, pondAt.x, pondAt.y, pond, 0.0); + L.homes.push_back(centre); + L.kits.push_back(pondAt); + L.axes.push_back(axis); + } + // The queen chambers grow until each holds `queenRoom` building sites: the tiles nearest the + // middle first, within the chamber's own cell so it never meets a neighbour, and never onto water + // or the beach a pond will get (two tiles round it). + const std::vector shore = dilate(t, L.water, 2); + std::vector buildable(n, 0); + for (int i = 0; i < n; ++i) + buildable[i] = !shore[i]; + for (int k = 0; k < teams; ++k) + { + const int site = L.homeSite[k]; + std::vector region(n, 0), eligible(n, 0); + for (int i = 0; i < n; ++i) + { + region[i] = L.open[i] && L.label[i] == site; + eligible[i] = L.label[i] == site && !shore[i]; + } + growUntilSites(t, region, buildable, eligible, o.queenRoom, kQueenGrowthLimit, [&](int i) + { return t.dist2(L.sites[site].x, L.sites[site].y, i % t.w, i / t.w); }); + for (int i = 0; i < n; ++i) + if (region[i]) + L.open[i] = 1; + } + for (int i = 0; i < n; ++i) + if (L.water[i]) + L.open[i] = 1; + for (int k = 0; k < teams; ++k) + for (int i = 0; i < n; ++i) + if (L.open[i] && L.label[i] == L.homeSite[k]) + L.homeOf[i] = k; + return L; +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "anthill layout"; + const AnthillOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "anthill terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + if (L.water[i]) + terrain[i] = WATER; + layBeaches(terrain, t); + writeUndermap(map, terrain); + // The rock: stone on every tile that is not carved, wherever pure grass allows it. + std::vector rock(n, 0); + for (int i = 0; i < n; ++i) + if (!L.open[i] && map.isResourceAllowed(i % t.w, i / t.w, STONE)) + { + map.setResource(i % t.w, i / t.w, STONE, 1); + rock[i] = 1; + } + + context.stage = "anthill colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w) && !rock[i]; + return ground; + }; + // The swarm stands between the chamber's middle and its door. + const auto anchor = [&](int team) + { + const ShapePoint p = polarPoint(L.homes[team].x, L.homes[team].y, 2.0, L.axes[team] + kPi); + return MapGeneratorPoint(int(std::lround(p.x)) - 2, int(std::lround(p.y)) - 2); + }; + if (!settleColonies(game, context, "anthill-starts", homeMask, anchor)) + return false; + + context.stage = "anthill resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + { + const auto eligible = [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + // Wheat and wood either side of the pond, towards the swarm. + const KitFrame frame{int(std::lround(L.kits[k].x)), int(std::lround(L.kits[k].y)), + L.axes[k]}; + plantKit(map, t, context, + {frame.at(-2, -(kPondRadius + 3), 6), frame.at(-2, kPondRadius + 3, 6), + frame.at(0, 0, 0), kHomeWheat, kHomeWood, -1}, + eligible); + } + // Farm chambers: a field round the pond; treasure chambers: a fruit grove and a wheat patch. + for (int site : L.farmSite) + { + const Site s = L.sites[site]; + const auto here = [&](int i) + { return L.open[i] && L.label[i] == site && clearGround(map, i % t.w, i / t.w); }; + if (const int seed = seedNear(t, s.x - 4, s.y, 6, here); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kFarmWheat, o.wheat)), here); + if (const int seed = seedNear(t, s.x + 4, s.y, 6, here); seed >= 0) + growPatch(map, t, seed, WOOD, int(scaledCount(kFarmWood, o.wood)), here); + } + for (int site : L.treasureSite) + { + const Site s = L.sites[site]; + const auto here = [&](int i) + { return L.open[i] && L.label[i] == site && clearGround(map, i % t.w, i / t.w); }; + if (scaledCount(1, o.fruit) > 0 && here(t.at(s.x, s.y))) + placeResourceClump(map, context, MapGeneratorPoint(s.x, s.y), + CHERRY + int(context.bounded("anthill-fruit", 3)), 1); + if (const int seed = seedNear(t, s.x - 3, s.y - 3, 5, here); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kTreasureWheat, o.wheat)), here); + } + seedAlgae(map, context, t, "anthill-algae", o.algae, AlgaeBand::anyWater(12)); + secureStartingCrops(game, context, t, 24, 32, 0, &rock); + reopenCrampedStarts(game, context, {o.wheat, o.wood, 100, o.algae, o.fruit}, 24, 32, 0, &rock); + // The tunnels join every chamber; only crops in a tunnel could close one, and only those are + // cleared. Stone is cut only as a last resort, at a cost that keeps it to a tile or two. + context.stage = "anthill routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, 12, -1, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "anthill"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (int k = 0; k < context.request.nbTeams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s queen chamber has lost its pond."; + } + return walkFromFirstColony(map, context.request.nbTeams, "the anthill", "through the tunnels") + .error; +} +} // namespace + +AnthillOptions::AnthillOptions(const GenerationRequest &r) + : chamberSpacing(r.option("chamber-spacing")), chamberSize(r.option("chamber-size")), + queenRoom(r.option("queen-room")), tunnelWidth(r.option("tunnel-width")), + loops(r.option("loops")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition anthillDefinition() +{ + return { + "anthill", + 32, + "Anthill", + 1, + false, + // Chambers 28 apart give a 256 map about a hundred of them; a chamber of radius 5 holds + // three or four buildings; a queen chamber grown to 40 building sites (overlapping 4x4 + // footprints, the start scorer's measure) holds a swarm, an inn and a couple more; tunnels + // three wide take a column of units and are closed by one tower; a fifth of the chambers' + // count in loops keeps a way round most blockades. + {{"chamber-spacing", "Chamber spacing", 20, 40, 2, 28, ControlGroup::Layout}, + {"chamber-size", "Chamber size", 4, 8, 1, 5, ControlGroup::Layout}, + {"queen-room", "Queen room", 20, 120, 10, 40, ControlGroup::Layout}, + {"tunnel-width", "Tunnel width", 2, 4, 1, 3, ControlGroup::Terrain}, + {"loops", "Loops", 0, 60, 10, 20, ControlGroup::Terrain}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/AnthillGenerator.h b/src/map/generator/generators/AnthillGenerator.h new file mode 100644 index 000000000..a92100bf1 --- /dev/null +++ b/src/map/generator/generators/AnthillGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct AnthillOptions +{ + int chamberSpacing, chamberSize, queenRoom, tunnelWidth, loops; + int wheat, wood, algae, fruit; // percentages of the default amounts + explicit AnthillOptions(const GenerationRequest &r); +}; +GeneratorDefinition anthillDefinition(); diff --git a/src/map/generator/generators/CanalsGenerator.cpp b/src/map/generator/generators/CanalsGenerator.cpp new file mode 100644 index 000000000..4e2d60d14 --- /dev/null +++ b/src/map/generator/generators/CanalsGenerator.cpp @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CanalsGenerator.h" +#include "Channels.h" +#include "Farmland.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "GraphMaze.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Tessellation.h" +#include "Towers.h" +#include +#include +#include +#include +using namespace MapGeneration; + +// Canals: a lagoon city. The map is cut into blocks by a grid of narrow canals, warped so the blocks +// are irregular, and every canal is just wide enough to stop a unit and just narrow enough for a +// tower on one bank to shoot the other. Every colony starts on a block of its own with a pond and a +// kit, and a handful of sand bridges join the blocks: only the bridges a tree needs to connect the +// colonies' blocks, plus a few more at random, so most blocks are islands until someone can swim. +// Towers reach across the canals from the first minute and armies cannot, so where the first towers +// go is the opening; once swimming pools are built every canal is a road and the map turns inside +// out. +// +// The city has no centre: the colonies' blocks are the ones farthest apart on the block graph +// (spreadPockets), market blocks with orchards lie farthest from those, and fairness is statistical +// (the lobby keeps the best-scoring of several seeds). +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Water blocks walking until +// a colony can swim, so the canals are a timing rule; a tower scans square rings with no line of +// sight, and a straight canal w corners wide puts the banks' grass w + 4 tiles apart (Channels.h), +// so the default canal of 3 is reached by a level-2 tower (range 7) and not by a level-1 (range 5). +// Every bank has sand beside water within reach of every tile, so algae grows everywhere and crops +// regrow everywhere; the fight is over bridges and banks, not fields. +namespace +{ + +// Every home's starting kit, unscaled whatever the amounts say. +constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// A bridge is a line of sand corners kBridgeHalfWidth wide across the canal (a 1-corner line already +// carries units two tiles wide, since a tile with a sand corner is no longer pure water), reaching +// this far past the canal's edge onto each bank so it lands on grass, not beach. +constexpr double kBridgeLanding = 2.5; +// Market blocks, per home block: blocks farthest from every home carry an orchard of all three fruits. +constexpr int kMarketsPerHome = 1; +// Tower pads beside each colony's starting towers, and the spacing between a colony's sites. +constexpr int kTowerPads = 2, kTowerSpacing = 6; + +struct Layout +{ + Torus t{1, 1}; + Tessellation g; + std::vector cell; // every tile's block + std::vector homeCell, marketCell; + std::vector homeOf; // the home block a tile is in, or -1 + std::vector canal, bridge, water, land; + std::vector homes, kits; + double homeRadius = 0; + std::string failure; +}; + +// A subtile point in tile units. +ShapePoint tilePoint(SubtilePoint p) +{ + return {p.x / 16.0, p.y / 16.0}; +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const CanalsOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + + // The blocks: a square tiling, warped. Every edge is a canal, so every edge is an obstacle the + // warp keeps apart: opposite sides of a block stay at least the canal plus eight tiles apart, so + // a block always keeps a few tiles of grass across its narrowest, and a block's centre keeps at + // least a fifth of the block from its sides, so a pond fits. + L.g = squareTessellation(t.w, t.h, o.blockSize); + if (L.g.columns < 2 || L.g.rows < 2) + { + L.failure = "The canals need at least two blocks across and down; use a bigger map or " + "smaller blocks."; + return L; + } + const std::vector walls(L.g.edges.size(), 1); + warpCorners(L.g, warpLimit(L.g) * o.warp / 100, walls, o.canalWidth + 8, o.blockSize / 5, + context, "canals-warp"); + L.cell = labelTiles(L.g); + if (L.cell.empty()) + { + L.failure = "The blocks could not be labelled."; + return L; + } + + // The home blocks: as far apart on the block graph as the tiling allows; then the market blocks, + // each the farthest block from every home and market so far. + const CellGraph graph = cellGraph(L.g); + L.homeCell = spreadPockets(graph, teams); + if (int(L.homeCell.size()) != teams) + { + L.failure = "Too many colonies for this many blocks; use a bigger map, smaller blocks or " + "fewer colonies."; + return L; + } + std::vector taken(L.g.cellCount(), 0); + for (int cell : L.homeCell) + taken[cell] = 1; + for (int market = 0; market < kMarketsPerHome * teams; ++market) + { + int best = -1; + long long bestGap = -1; + for (int cell = 0; cell < L.g.cellCount(); ++cell) + { + if (taken[cell]) + continue; + long long gap = -1; + for (int other : L.homeCell) + gap = gap < 0 ? L.g.distance2(cell, other) + : std::min(gap, L.g.distance2(cell, other)); + for (int other : L.marketCell) + gap = std::min(gap, L.g.distance2(cell, other)); + if (gap > bestGap) + { + bestGap = gap; + best = cell; + } + } + if (best < 0) + break; + taken[best] = 1; + L.marketCell.push_back(best); + } + + // The canals: every edge stroked canalWidth corners wide. A canal's water is canalWidth - 1 tiles + // across, and a diagonal canal is only sealed against a diagonal step when its water is two tiles + // thick, which is why the narrowest canal offered is 3. + L.canal.assign(n, 0); + for (int edge = 0; edge < int(L.g.edges.size()); ++edge) + { + const auto ends = L.g.edgeEnds(edge, L.g.edges[edge].cells[0]); + const ShapePoint a = tilePoint(ends.first), b = tilePoint(ends.second); + strokePath(L.canal, t, {{a.x, a.y, o.canalWidth / 2.0}, {b.x, b.y, o.canalWidth / 2.0}}); + } + + // The bridges: a tree of the shortest block-to-block paths from the first colony's block to every + // other colony's, so every colony can be walked to, and then `extraBridges` percent of the blocks' + // count in further bridges at random (openLoops), which is where the flanking routes come from. + std::vector open(L.g.edges.size(), 0); + if (teams > 1) + { + std::vector parentEdge(L.g.cellCount(), -1), order{L.homeCell[0]}; + std::vector seen(L.g.cellCount(), 0); + seen[L.homeCell[0]] = 1; + for (size_t head = 0; head < order.size(); ++head) + for (int edge : L.g.cells[order[head]].edges) + { + const int next = L.g.other(edge, order[head]); + if (!seen[next]) + { + seen[next] = 1; + parentEdge[next] = edge; + order.push_back(next); + } + } + for (int k = 1; k < teams; ++k) + for (int cell = L.homeCell[k]; parentEdge[cell] >= 0;) + { + open[parentEdge[cell]] = 1; + cell = L.g.other(parentEdge[cell], cell); + } + } + openLoops(graph, context, "canals-bridges", std::vector(L.g.cellCount(), 0), + o.extraBridges, open); + L.bridge.assign(n, 0); + for (int edge = 0; edge < int(L.g.edges.size()); ++edge) + { + if (!open[edge]) + continue; + const int owner = L.g.edges[edge].cells[0]; + const auto ends = L.g.edgeEnds(edge, owner); + const ShapePoint a = tilePoint(ends.first), b = tilePoint(ends.second); + const ShapePoint middle{(a.x + b.x) / 2, (a.y + b.y) / 2}; + // Across the canal: from the owner's centre to the neighbour's, through the edge's middle. + const ShapePoint from = tilePoint(L.g.cells[owner].centre), + to = tilePoint(L.g.centreAcross(edge, owner)); + const double dx = to.x - from.x, dy = to.y - from.y, length = std::hypot(dx, dy); + const double reach = o.canalWidth / 2.0 + kBridgeLanding; + strokePath( + L.bridge, t, + {{middle.x - dx / length * reach, middle.y - dy / length * reach, kBridgeHalfWidth}, + {middle.x + dx / length * reach, middle.y + dy / length * reach, kBridgeHalfWidth}}); + } + + // Homes: a pond at the middle of every home block, and a kit round it. The block's "radius" for + // the kit is what is left between its centre and its canal. + L.homeRadius = std::max(6.0, o.blockSize / 2.0 - o.canalWidth / 2.0 - 3); + L.water = L.canal; + L.homeOf.assign(n, -1); + L.land.assign(n, 0); + for (int i = 0; i < n; ++i) + L.land[i] = !L.canal[i]; + for (int k = 0; k < teams; ++k) + L.homes.push_back(tilePoint(L.g.cells[L.homeCell[k]].centre)); + const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "canals-pond"); + for (int k = 0; k < teams; ++k) + { + L.kits.push_back( + stampRoundHome(t, L.homes[k], 0.0, pond, L.homeRadius, 1, &pond, L.water, [](int) {})); + for (int i = 0; i < n; ++i) + if (L.cell[i] == L.homeCell[k] && L.land[i]) + L.homeOf[i] = k; + } + return L; +} + +// The towers every colony starts with: on its own block's bank (against the beach), covering the most +// of other blocks' land across the canal, none within range of another colony's swarm; open pads +// beside them for more. +TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &context, + const CanalsOptions &o, const std::vector &bank) +{ + const Torus &t = L.t; + const int n = t.size(), teams = int(L.homes.size()); + const std::vector reserved = swarmSurroundings(t, context); + std::vector owner(n, -1); + std::vector buildable(n, 0), target(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + if (map.isWater(x, y)) + continue; + // Land on other blocks belongs to "everyone else": worth shooting at from any colony's bank. + owner[i] = L.homeOf[i] >= 0 ? L.homeOf[i] : teams; + target[i] = 1; + buildable[i] = + L.homeOf[i] >= 0 && map.isGrass(x, y) && !reserved[i] && !map.isResource(x, y); + } + TowerRequest request = startingTowerRequest(o.towers, o.towerCount, kTowerPads, kTowerSpacing); + request.otherWeight = 1; + request.ownWeight = 0; + request.against = &bank; + return chooseTowerSites(t, owner, buildable, target, swarmSurroundings(t, context, 0), teams, + request); +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "canals layout"; + const CanalsOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "canals terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + if (L.water[i]) + terrain[i] = WATER; + layBeaches(terrain, t); + for (int i = 0; i < n; ++i) + if (L.bridge[i] && L.canal[i]) + terrain[i] = SAND; + writeUndermap(map, terrain); + + context.stage = "canals colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; + if (!settleColonies(game, context, "canals-starts", homeMask, anchor)) + return false; + + // Towers on the banks. No tower or pad may close a colony's walk to a bridge, and every colony + // keeps as many as the fewest got. + context.stage = "canals towers"; + const std::vector bank = beachTiles(map, t); + TowerPlan towers = planTowers(map, L, context, o, bank); + std::vector bridges(n, 0); + bool anyBridge = false; + for (int i = 0; i < n; ++i) + if (L.bridge[i] && L.canal[i] && !map.isWater(i % t.w, i / t.w)) + bridges[i] = anyBridge = true; + if (!settleStartingTowers(game, context, towers, o.towers, o.towers > 0 && o.towerCount > 0, + anyBridge ? &bridges : nullptr)) + return false; + const std::vector pads = towerFootprints(t, towers); + + context.stage = "canals resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantOpenHomeKit(map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, + kHomeQuarry, + [&](int i) + { + return L.homeOf[i] == k && !reserved[i] && !pads[i] && + clearGround(map, i % t.w, i / t.w); + }); + // Every block is fertile (its canal is within the growth probe's reach of all of it), so the + // ambient farmland is a modest share of every block, in patches, the home blocks included; the + // bridges' landings stay clear so no field closes a bridge. + const std::vector landings = dilate(t, bridges, 2); + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("canals-patch")); + const std::vector split = periodicNoise(t.w, t.h, 5, context.stream("canals-split")); + const auto eligible = [&](int i) + { + return L.land[i] && !reserved[i] && !pads[i] && !landings[i] && + clearGround(map, i % t.w, i / t.w); + }; + int fertile = 0; + for (int i = 0; i < n; ++i) + fertile += eligible(i) && fertility.at(i % t.w, i / t.w) > 0; + furnishGround( + map, t, context, fertility, eligible, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int area) + { + return GroundAmounts{int(scaledCount(fertile * 12 / 100, o.wheat)), + int(scaledCount(fertile * 8 / 100, o.wood)), + int(scaledCount(area / 900, o.stone)), 0}; + }, + "canals-stone", "canals-fruit"); + // The markets: an orchard of all three fruits round each market block's middle. + if (scaledCount(1, o.fruit) > 0) + for (int cell : L.marketCell) + { + const ShapePoint centre = tilePoint(L.g.cells[cell].centre); + const double spin = context.bounded("canals-fruit", 3600) / 3600.0 * 2 * kPi; + plantOrchard(map, t, context, centre.x, centre.y, 3.5, {spin}, 4.5, 6, 1, + [&](int i) { return L.cell[i] == cell && eligible(i); }); + } + seedAlgae(map, context, t, "canals-algae", o.algae, AlgaeBand::anyWater(25)); + secureStartingCrops(game, context, t); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}); + // The bridges join every colony to the first; only deposits could close the way, so only those + // are cleared, never a canal. + context.stage = "canals routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, 8, -1, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "canals"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (int k = 0; k < context.request.nbTeams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s block has lost its pond."; + } + return walkFromFirstColony(map, context.request.nbTeams, "the city", "over the bridges").error; +} +} // namespace + +CanalsOptions::CanalsOptions(const GenerationRequest &r) + : blockSize(r.option("block-size")), canalWidth(r.option("canal-width")), + warp(r.option("warp")), extraBridges(r.option("extra-bridges")), + towers(r.option("starting-towers")), towerCount(r.option("tower-count")), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition canalsDefinition() +{ + return { + "canals", + 29, + "Canals", + 1, + false, + // Blocks of 24 give a 256 map about a hundred blocks; a canal of 3 corners (two tiles of + // water) is sealed against diagonal steps and is reached by a level-2 tower, which is what + // the colonies start with; a fifth again in extra bridges keeps most blocks islands. + {{"block-size", "Block size", 16, 40, 2, 24, ControlGroup::Layout}, + {"canal-width", "Canal width", 3, 5, 1, 3, ControlGroup::Terrain}, + {"warp", "Warp", 0, 100, 10, 40, ControlGroup::Terrain}, + {"extra-bridges", "Extra bridges", 0, 100, 10, 20, ControlGroup::Layout}, + {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + {"tower-count", "Towers per colony", 1, 4, 1, 2, ControlGroup::Layout}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/CanalsGenerator.h b/src/map/generator/generators/CanalsGenerator.h new file mode 100644 index 000000000..4122f02ce --- /dev/null +++ b/src/map/generator/generators/CanalsGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct CanalsOptions +{ + int blockSize, canalWidth, warp, extraBridges, towers, towerCount; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit CanalsOptions(const GenerationRequest &r); +}; +GeneratorDefinition canalsDefinition(); diff --git a/src/map/generator/generators/FingerprintGenerator.cpp b/src/map/generator/generators/FingerprintGenerator.cpp new file mode 100644 index 000000000..ce9b44d54 --- /dev/null +++ b/src/map/generator/generators/FingerprintGenerator.cpp @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "FingerprintGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Orbits.h" +#include "Patterns.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Fingerprint: an organic labyrinth. A Turing pattern grown over the whole torus (Patterns.h) draws +// long curving bands that fork, merge and dead-end the way the ridges of a fingerprint or the +// stripes of a zebra do, and the bands become the barriers: water, so every corridor of land runs +// beside water and all of it is farmland; or stone, permanent walls with pools in the corridors. +// The wavelength sets how wide the corridors are, the pattern how open the map is (an even +// labyrinth, islands of land in water, or channels of water through land), and the grain stretches +// the bands along one axis so they run rather than wander. +// +// Nothing is symmetric: the pattern is one field over the map and the homes are clearings on a +// lattice through it, so fairness is statistical and the lobby keeps the best-scoring of several +// seeds. Where the bands close a colony off, the cheapest way through is opened as a ford (or a +// gap cut in the stone), so every colony can be walked to from the first. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Wheat and wood regrow only +// near water, so a water fingerprint is fertile everywhere and the fight is over the corridors, not +// the fields; a stone fingerprint is the opposite, its pools the only fertile ground. Corridors that +// fork and dead-end give ambushes and cul-de-sacs no grid has, and reads as country rather than as +// a maze. +namespace +{ + +// Every home's starting kit, unscaled whatever the amounts say: wheat and wood beside the pond, +// and a quarry of radius 2, which on a water map is the colony's only stone until it finds an +// outcrop. +constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// The pattern is cleared this far beyond a home's rough disc, so its beach never reaches the +// swarm's ground and a band of the pattern always runs between neighbouring clearings. +constexpr double kClearingMargin = 3.0; +// How much of the map the barrier covers, in percent, for each pattern: Labyrinth, Islands, Channels. +// The field is symmetric about its mean, so 50% would give bands and corridors of equal width; a +// water band spoils a further tile of grass on each side with its beach (Channels.h), so 42% water +// leaves corridors about as wide as the bands read. 60% turns the corridors into strings of islands +// in water; 28% leaves channels of water through wide land. +constexpr int kBarrierShare[3] = {42, 60, 28}; +// On a stone map the pools lie at the bottoms of the field's troughs, which run along the middles +// of the corridors: every local minimum with nothing lower within two wavelengths becomes a pool +// (windowMinimum, Morphology.h), grown to kPoolTiles by the field's value so it fills its trough. +// Two wavelengths apart gives about a dozen pools on a 256 map at the default wavelength, each +// watering the corridor round it; a percentile of the field instead gave a hundred tiny pools whose +// beaches cut the stone into blobs. +constexpr int kPoolTiles = 40; + +struct Layout +{ + Torus t{1, 1}; + double homeRadius = 0; + std::vector homes, kits; + std::vector homeOf; + std::vector water, stone, clearing; + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const FingerprintOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + L.homeRadius = o.homeSize; + L.homes = + latticeSites(t.w, t.h, teams, context.bounded("fingerprint-layout", std::uint32_t(t.w)), + context.bounded("fingerprint-layout", std::uint32_t(t.h))) + .sites; + double spacing = std::min(t.w, t.h); + for (size_t a = 0; a < L.homes.size(); ++a) + for (size_t b = a + 1; b < L.homes.size(); ++b) + spacing = + std::min(spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), + t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + // Clearings shrink to keep a band's width of pattern between neighbours; the smallest home that + // holds a pond and a kit is the floor. + L.homeRadius = + std::min(L.homeRadius, std::floor(spacing / 2 - kClearingMargin - o.wavelength / 2.0)); + if (!homeHasRoom(L.homeRadius)) + { + L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; + return L; + } + + // The pattern, and the barrier as the share of it above a level. + TuringStyle style; + style.wavelength = o.wavelength; + style.stretchX = 100 + o.grain * 2; + std::vector field = turingPattern(t, style, context.stream("fingerprint-pattern")); + const int level = percentile(field, 100 - kBarrierShare[std::clamp(o.pattern, 0, 2)]); + std::vector barrier(n, 0); + for (int i = 0; i < n; ++i) + barrier[i] = field[i] >= level; + // Every home is a clearing in the pattern, with its pond. + L.clearing.assign(n, 0); + for (const ShapePoint &home : L.homes) + for (int i = 0; i < n; ++i) + if (std::hypot(t.offsetX(int(home.x), i % t.w), t.offsetY(int(home.y), i / t.w)) <= + L.homeRadius + kClearingMargin) + L.clearing[i] = 1; + for (int i = 0; i < n; ++i) + if (L.clearing[i]) + barrier[i] = 0; + L.water.assign(n, 0); + L.stone.assign(n, 0); + if (o.barrier == 0) + L.water = barrier; + else + { + L.stone = barrier; + // Pools in the corridors: one at every trough bottom with nothing lower within two + // wavelengths, grown along the trough. The only water, so the only fertile ground. + const std::vector lowest = windowMinimum(t, field, 2 * o.wavelength); + std::vector queued(n, 0); + int pools = 0; + for (int i = 0; i < n; ++i) + if (field[i] == lowest[i] && !L.clearing[i] && !L.water[i]) + growWater( + t, L.water, i, kPoolTiles, [&](int j) { return !L.clearing[j]; }, + [&](int j) + { + return std::int64_t(field[j]) + + t.dist2(i % t.w, i / t.w, j % t.w, j / t.w) * 50; + }, + queued, ++pools); + // A pool's beach takes the stone off its shore, so no stone stands within two of a pool. + const std::vector shore = dilate(t, L.water, 2); + for (int i = 0; i < n; ++i) + if (shore[i]) + L.stone[i] = 0; + } + L.homeOf.assign(n, -1); + const RadialShape home(L.homeRadius, 0.15, context, "fingerprint-home"); + const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "fingerprint-pond"); + L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); + return L; +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "fingerprint layout"; + const FingerprintOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "fingerprint terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + if (L.water[i]) + terrain[i] = WATER; + layBeaches(terrain, t); + writeUndermap(map, terrain); + for (int i = 0; i < n; ++i) + if (L.stone[i] && map.isResourceAllowed(i % t.w, i / t.w, STONE)) + map.setResource(i % t.w, i / t.w, STONE, 1); + + context.stage = "fingerprint colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; + if (!settleColonies(game, context, "fingerprint-starts", homeMask, anchor)) + return false; + + context.stage = "fingerprint resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantOpenHomeKit( + map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, + [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + // The ambient layer over all the land: a share of the fertile ground under crops, in patches, with + // outcrops and groves; on a water map that is nearly everything. + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector patch = periodicNoise(t.w, t.h, 10, context.stream("fingerprint-patch")); + const std::vector split = periodicNoise(t.w, t.h, 5, context.stream("fingerprint-split")); + const auto eligible = [&](int i) + { return L.homeOf[i] < 0 && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + int fertile = 0; + for (int i = 0; i < n; ++i) + fertile += eligible(i) && fertility.at(i % t.w, i / t.w) > 0; + furnishGround( + map, t, context, fertility, eligible, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int area) + { + return GroundAmounts{int(scaledCount(fertile * 9 / 100, o.wheat)), + int(scaledCount(fertile * 6 / 100, o.wood)), + int(scaledCount(area / 1000, o.stone)), + int(scaledCount(area / 1800, o.fruit))}; + }, + "fingerprint-stone", "fingerprint-fruit"); + seedAlgae(map, context, t, "fingerprint-algae", o.algae, AlgaeBand::shallows(1, 6, 60)); + secureStartingCrops(game, context, t, 24, 32, 0, &L.stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, + &L.stone); + // The pattern owes nobody a way through: where it closes a colony off, the cheapest way is opened, + // a ford across water or a gap cut through the stone. + context.stage = "fingerprint routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, 8, 25, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "fingerprint"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (int k = 0; k < context.request.nbTeams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s clearing has lost its pond."; + } + return walkFromFirstColony(map, context.request.nbTeams, "the fingerprint", + "through the pattern") + .error; +} +} // namespace + +FingerprintOptions::FingerprintOptions(const GenerationRequest &r) + : wavelength(r.option("wavelength")), pattern(r.option("pattern")), + barrier(r.option("barrier")), grain(r.option("grain")), homeSize(r.option("home-size")), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition fingerprintDefinition() +{ + return {"fingerprint", + 26, + "Fingerprint", + 1, + false, + {{"wavelength", "Wavelength", 12, 40, 2, 20, ControlGroup::Terrain}, + GeneratorControl::choice("pattern", "Pattern", {"Labyrinth", "Islands", "Channels"}, 0, + ControlGroup::Terrain), + GeneratorControl::choice("barrier", "Barrier", {"Water", "Stone"}, 0, + ControlGroup::Terrain), + {"grain", "Grain", 0, 100, 10, 0, ControlGroup::Terrain}, + {"home-size", "Home size", 10, 20, 1, 12, ControlGroup::Layout}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/FingerprintGenerator.h b/src/map/generator/generators/FingerprintGenerator.h new file mode 100644 index 000000000..28436ab46 --- /dev/null +++ b/src/map/generator/generators/FingerprintGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct FingerprintOptions +{ + int wavelength, pattern, barrier, grain, homeSize; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit FingerprintOptions(const GenerationRequest &r); +}; +GeneratorDefinition fingerprintDefinition(); diff --git a/src/map/generator/generators/MarchesGenerator.cpp b/src/map/generator/generators/MarchesGenerator.cpp new file mode 100644 index 000000000..b0e9d4afe --- /dev/null +++ b/src/map/generator/generators/MarchesGenerator.cpp @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MarchesGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Orbits.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Points.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Topology.h" +#include "Walls.h" +#include +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Marches: homelands on a lattice across the whole torus, with no centre and no prize in the middle. +// Every colony's home lies in its own territory, and between every two neighbouring territories runs +// a band of wild border country, the marches: wooded, with a watering hole on every stretch of +// border and an orchard of all three fruits wherever three (or four) territories meet. A colony has +// a frontier with every neighbour, and its choice is which border to push. +// +// Fairness comes from sliding the map onto itself rather than turning it: the homes sit on the +// roomiest translation lattice the torus holds (Orbits.h), so with 2, 4, 8 or 16 colonies every +// colony's neighbourhood is an exact copy of every other's (the border warp is summed over the +// lattice's orbits so the territories match too); other counts get evenly staggered rows and +// statistical fairness. A rectangular map is served as well as a square one, since nothing is +// designed round a middle. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Every home has its own pond, +// so its fields regrow without leaving home; the marches' woods block walking until cut, so the +// border is crossed where someone chose to cut it; a watering hole is the only water on a border and +// so the only place a forward field grows; and an orchard of all three fruits at a junction is the +// strongest prize the game has, shared by three rivals at once. +namespace +{ + +// Every home's starting kit, unscaled whatever the amounts say, so every start is viable: wheat and +// wood patches beside the pond (12 tiles each is a few visits' worth, and they regrow beside the +// water) and a quarry of radius 2 (13 tiles of stone, which never runs out). +constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// The land a home disc keeps from the edge of the marches: room for the disc's own rough outline +// (15% wobble of a 14-tile radius is 2 tiles) and a watering hole's pool and beach (4 tiles) never to +// meet the home's pond and beach. +constexpr int kHomeMargin = 5; +// A crowded map narrows the marches before it is refused, but never below this: two tiles of woods +// either side of the border still reads as a border. +constexpr int kNarrowestMarch = 4; +// A watering hole: a pool of radius 3 (about 25 corners of water, a beach round it) and 24 tiles of +// wheat, a forward field the size of a home kit, so holding a border stretch is worth a field. +constexpr double kPoolRadius = 3.0; +constexpr int kPoolWheat = 24; +// How far the border warp can move a border, as a share of the home spacing at full roughness: at +// 30% two neighbouring borders can never cross (they start half a spacing apart), and a home disc +// sized by geometryFor's rule always stays inside its homeland. +constexpr int kWarpPercent = 30; +// The share of the marches under wood at the default wood amount. Wood is planted in patches from a +// noise field (the gaps between patches are the natural ways through); 55% leaves the band clearly +// wooded but always crossable somewhere, and the route backstop cuts a way where it isn't. +constexpr int kWoodedPercent = 55; + +struct Layout +{ + Torus t{1, 1}; + bool exact = false; + double spacing = 0, homeRadius = 0; + int marchWidth = 0; + std::vector homes, kits; + std::vector territory; // every tile's homeland, including the marches + std::vector land; // the homeland a tile is in, or -1 in the marches + std::vector homeOf; // the home disc a tile is in, or -1 + std::vector march, water; + std::vector holes, junctions; // the watering holes' and orchards' tiles + std::string failure; +}; + +// The tiles of the marches where several homelands meet, grouped: each group's tile nearest its +// middle is where its prize goes. +std::vector prizeSites(const Torus &t, const std::vector &mask) +{ + const std::vector label = connectedRegions(mask, t.w, t.h, true, GridNeighbors::Eight); + std::vector> groups; + for (int i = 0; i < t.size(); ++i) + if (label[i] >= 0) + { + if (label[i] >= int(groups.size())) + groups.resize(label[i] + 1); + groups[label[i]].push_back(i); + } + std::vector sites; + for (const std::vector &tiles : groups) + { + const int first = tiles.front(); + double sx = 0, sy = 0; + for (int i : tiles) + { + sx += t.offsetX(first % t.w, i % t.w); + sy += t.offsetY(first / t.w, i / t.w); + } + const int mx = t.x(first % t.w + int(std::lround(sx / tiles.size()))); + const int my = t.y(first / t.w + int(std::lround(sy / tiles.size()))); + int best = first; + for (int i : tiles) + if (t.dist2(mx, my, i % t.w, i / t.w) < t.dist2(mx, my, best % t.w, best / t.w)) + best = i; + sites.push_back(best); + } + return sites; +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const MarchesOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + L.homeRadius = o.homeSize; + + // The homes, on the roomiest lattice the torus holds. + const LatticeSites lattice = + latticeSites(t.w, t.h, teams, context.bounded("marches-layout", std::uint32_t(t.w)), + context.bounded("marches-layout", std::uint32_t(t.h))); + L.exact = lattice.exact; + L.homes = lattice.sites; + L.spacing = std::min(t.w, t.h); + for (size_t a = 0; a < L.homes.size(); ++a) + for (size_t b = a + 1; b < L.homes.size(); ++b) + L.spacing = + std::min(L.spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), + t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + // A home disc must stay inside its homeland however far the warp bends the border towards it: on + // a crowded map the homes shrink to fit, then the marches narrow, before the map is refused. + const int warpPercent = o.borderRoughness * kWarpPercent / 100; + const double border = L.spacing * (0.5 - warpPercent / 100.0); + L.marchWidth = o.marchWidth; + L.homeRadius = std::min(L.homeRadius, std::floor(border - L.marchWidth / 2.0 - kHomeMargin)); + while (!homeHasRoom(L.homeRadius) && L.marchWidth > kNarrowestMarch) + { + L.marchWidth -= 2; + L.homeRadius = + std::min(o.homeSize, std::floor(border - L.marchWidth / 2.0 - kHomeMargin)); + } + if (!homeHasRoom(L.homeRadius)) + { + L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; + return L; + } + + // Homelands: every tile's nearest home after a warp that bends the borders. On an exact lattice + // the warp noise is laid on cells the lattice's moves are whole multiples of (latticePeriod), so + // it is the same at every colony's image of a tile and the homelands are exact copies of one + // another; summing plain noise over the orbits instead would average the roughness away and leave + // the borders ruled straight. Otherwise the noise's cells are one and a half spacings across, big + // enough to bend a border as a whole rather than fray it. + std::vector sites; + for (const ShapePoint &p : L.homes) + sites.push_back({int(std::lround(p.x)), int(std::lround(p.y))}); + const int period = L.exact ? latticePeriod(translationSymmetry(t.w, t.h, teams)) + : std::max(4, int(L.spacing) * 3 / 2); + const std::vector warpX = + fractalNoise(t.w, t.h, period, 3, context.stream("marches-warp")); + const std::vector warpY = + fractalNoise(t.w, t.h, period, 3, context.stream("marches-warp")); + L.territory = + nearestSiteLabels(t, sites, std::max(4, int(L.spacing)), warpX, warpY, warpPercent); + + // The marches: the band along every border, both sides alike. + std::vector border2(n, 0); + for (int i = 0; i < n; ++i) + for (const auto &s : kCardinalSteps) + if (L.territory[t.at(i % t.w + s[0], i / t.w + s[1])] != L.territory[i]) + border2[i] = 1; + L.march = dilateRound(t, border2, L.marchWidth / 2.0); + L.land.assign(n, -1); + for (int i = 0; i < n; ++i) + if (!L.march[i]) + L.land[i] = L.territory[i]; + + // The homes: a round clearing (the whole map is grass, so only its pond shows) at every site. + L.homeOf.assign(n, -1); + L.water.assign(n, 0); + const RadialShape home(L.homeRadius, 0.15, context, "marches-home"); + const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "marches-pond"); + L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); + + // Watering holes: one on every stretch of border between two homelands, at its middle. On the + // torus two homelands may share several separate stretches; each gets its own. + std::map, std::vector> stretches; + for (int i = 0; i < n; ++i) + for (const auto &s : kCardinalSteps) + { + const int other = L.territory[t.at(i % t.w + s[0], i / t.w + s[1])]; + if (other > L.territory[i]) + { + auto &mask = stretches[{L.territory[i], other}]; + if (mask.empty()) + mask.assign(n, 0); + mask[i] = 1; + } + } + for (const auto &entry : stretches) + for (int site : prizeSites(t, entry.second)) + L.holes.push_back(site); + // Junctions: wherever three or more homelands meet within a tile; junctions within a few tiles + // of each other (a four-way corner the warp split in two) are one orchard. + std::vector junction(n, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + int seen[9], count = 0; + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + const int label = L.territory[t.at(x + dx, y + dy)]; + if (std::find(seen, seen + count, label) == seen + count) + seen[count++] = label; + } + junction[size_t(y) * t.w + x] = count >= 3; + } + if (o.orchards) + L.junctions = prizeSites(t, dilate(t, junction, 3)); + // A pool at every watering hole. + const RadialShape pool(kPoolRadius, 0.3, context, "marches-pool"); + for (int hole : L.holes) + fillShape(L.water, t, hole % t.w + 0.5, hole / t.w + 0.5, pool, 0.0); + return L; +} + +// The deposits: every home's kit, farmland on each homeland's fertile ground, woods through the +// marches, wheat round every watering hole, and an orchard with a quarry at every junction. +void furnish(Map &map, const Layout &L, GenerationContext &context, const MarchesOptions &o, + int teams) +{ + const Torus &t = L.t; + const int n = t.size(); + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantOpenHomeKit( + map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, + [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector patch = periodicNoise(t.w, t.h, 12, context.stream("marches-patch")); + const std::vector split = periodicNoise(t.w, t.h, 6, context.stream("marches-split")); + // A homeland's only water is its pond, so its fertile ground (where the engine's growth probe + // finds water, within 15 tiles) is the ring round the home; the ambient farmland is a share of + // that ring, not of the homeland, or it would bury the home: 12% wheat and 8% wood of about a + // thousand fertile tiles is a few patches, in the top half of a noise field so they clump. + for (int k = 0; k < teams; ++k) + { + const auto eligible = [&](int i) + { return L.land[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + int fertile = 0; + for (int i = 0; i < n; ++i) + fertile += eligible(i) && fertility.at(i % t.w, i / t.w) > 0; + furnishGround( + map, t, context, fertility, eligible, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int area) + { + return GroundAmounts{int(scaledCount(fertile * 12 / 100, o.wheat)), + int(scaledCount(fertile * 8 / 100, o.wood)), + int(scaledCount(area / 900, o.stone)), + int(scaledCount(area / 2500, o.fruit))}; + }, + "marches-stone", "marches-fruit"); + } + // The marches' woods: wood over the wooded share of the band, in patches from a noise field + // (the gaps between patches are the natural ways through), and the odd outcrop. Far from water, + // none of it grows back: a way cut through the marches stays cut. + std::vector band, levels; + const std::vector woods = periodicNoise(t.w, t.h, 9, context.stream("marches-woods")); + const auto wild = [&](int i) + { return L.march[i] && !L.water[i] && clearGround(map, i % t.w, i / t.w); }; + for (int i = 0; i < n; ++i) + if (L.march[i]) + { + band.push_back(i); + levels.push_back(woods[i]); + } + const int wooded = std::clamp(int(scaledCount(kWoodedPercent, o.wood)), 0, 100); + const int level = percentile(levels, 100 - wooded); + plantCover(map, t, L.march, WOOD, [&](int i) { return wild(i) && woods[i] >= level; }); + scatterClumps(context, t, band, int(scaledCount(int(band.size()) / 1200, o.stone)), + "marches-outcrops", wild, + [&](MapGeneratorPoint p) { placeResourceClump(map, context, p, STONE, 1); }); + // Wheat round every watering hole, on the first grass past the pool's beach. + for (int hole : L.holes) + { + const int hx = hole % t.w, hy = hole / t.w; + const auto near = [&](int i) { return wild(i) && t.dist2(hx, hy, i % t.w, i / t.w) <= 64; }; + if (const int seed = seedNear(t, hx, hy, 8, near); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kPoolWheat, o.wheat)), near); + } + // An orchard of all three fruits round every junction, with a quarry in the middle. + for (int junction : L.junctions) + { + const int jx = junction % t.w, jy = junction / t.w; + const auto near = [&](int i) + { return wild(i) && t.dist2(jx, jy, i % t.w, i / t.w) <= 100; }; + if (scaledCount(1, o.stone) > 0 && near(junction)) + placeResourceClump(map, context, MapGeneratorPoint(jx, jy), STONE, 1); + if (scaledCount(1, o.fruit) > 0) + { + const double spin = context.bounded("marches-fruit", 3600) / 3600.0 * 2 * kPi; + plantOrchard(map, t, context, jx, jy, 4.5, {spin}, 4.5, 4, 1, near); + } + } +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "marches layout"; + const MarchesOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "marches terrain"; + TerrainSketch terrain(t.size(), GRASS); + for (int i = 0; i < t.size(); ++i) + if (L.water[i]) + terrain[i] = WATER; + layBeaches(terrain, t); + writeUndermap(map, terrain); + + context.stage = "marches colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(t.size()), 0); + for (int i = 0; i < t.size(); ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; + if (!settleColonies(game, context, "marches-starts", homeMask, anchor)) + return false; + + context.stage = "marches resources"; + furnish(map, L, context, o, teams); + seedAlgae(map, context, t, "marches-algae", o.algae, AlgaeBand::anyWater(30)); + secureStartingCrops(game, context, t); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}); + // The marches are wooded and the fields grow; where they close a colony in, the cheapest cut + // through the woods is opened, never a ford. + context.stage = "marches routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, -1, -1, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "marches"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (int k = 0; k < context.request.nbTeams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s home has lost its pond."; + } + return walkFromFirstColony(map, context.request.nbTeams, "the marches", "through the marches") + .error; +} +} // namespace + +MarchesOptions::MarchesOptions(const GenerationRequest &r) + : homeSize(r.option("home-size")), marchWidth(r.option("march-width")), + borderRoughness(r.option("border-roughness")), orchards(r.option("orchards") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition marchesDefinition() +{ + return {"marches", + 25, + "Marches", + 1, + false, + {{"home-size", "Home size", 10, 24, 1, 14, ControlGroup::Layout}, + {"march-width", "March width", 6, 24, 2, 12, ControlGroup::Terrain}, + {"border-roughness", "Border roughness", 0, 100, 10, 40, ControlGroup::Terrain}, + // Off, the junctions carry no orchard and the watering holes are the only prizes. + GeneratorControl::toggle("orchards", "Orchards", true, ControlGroup::Layout), + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/MarchesGenerator.h b/src/map/generator/generators/MarchesGenerator.h new file mode 100644 index 000000000..6a8f11dfa --- /dev/null +++ b/src/map/generator/generators/MarchesGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct MarchesOptions +{ + int homeSize, marchWidth, borderRoughness; + bool orchards; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit MarchesOptions(const GenerationRequest &r); +}; +GeneratorDefinition marchesDefinition(); diff --git a/src/map/generator/generators/OldGrowthGenerator.cpp b/src/map/generator/generators/OldGrowthGenerator.cpp new file mode 100644 index 000000000..3c4ebaacf --- /dev/null +++ b/src/map/generator/generators/OldGrowthGenerator.cpp @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "OldGrowthGenerator.h" +#include "Contact.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Growth.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Orbits.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Old growth: a dry continent under unbroken forest. Every colony starts in a clearing with its own +// pond and kit, and beyond the clearing stands wood in every direction. There is no other water but +// a few lakes far from every home, so almost none of the forest ever grows back (wood regrows only +// where the engine's growth probe, 15 tiles each way, finds water): what a colony cuts stays cut, and +// the map opens as the game goes on, the opposite of a swamp that grows shut. Contact happens only +// where someone has cut through, so where a colony's workers clear is where its front will be. +// +// Deep in the forest, at the same cutting distance from every home and on each home's own side, a +// hidden grove waits: a pocket of fruit, wheat and stone that rewards the colony that cuts to it +// first. The lakes are the other prize: the only ground where wood regrows, ringed with wheat and +// an orchard, and as far from every home as the map allows. +// +// Homes sit on a lattice (Orbits.h) and everything else is measured from them, so fairness is by +// construction where the lattice is exact and statistical otherwise (the lobby keeps the best of +// several seeds). With the trails switch a cut trail joins every colony to the first from the +// start; without it (the default) the colonies start entirely apart, and the validator checks the +// map by cutting cost rather than by walking. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Deposits block movement +// and building, wood is clearable, and clearing is slow work, so the forest is a wall that any +// colony can breach anywhere at a price in worker-hours; that makes the front a choice. Wood far +// from water never regrows, so every cut is permanent and the map's history is written in it; wood +// near the home pond does regrow, so a colony never runs out. The lakes' orchards are the only +// fruit not hidden. +namespace +{ + +// Every home's starting kit, unscaled whatever the amounts say: wheat and wood beside the pond +// (the wood regrows there, being beside water), and a quarry. +constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// The forest starts this far beyond a home's rough disc, so the clearing's edge is open ground to +// build against and the pond's beach never meets a tree. +constexpr double kClearingMargin = 3.0; +// Lakes keep this far from every clearing and from each other's shores, so a lake's regrowth never +// reaches a home's forest edge (the growth probe reaches 15 tiles) and two lakes read as two. +constexpr int kLakeGap = 20; +// A hidden grove lies at this share of the cutting cost half way to the nearest rival: nearer its +// own home than anyone else's, but deep enough that reaching it is a decision. Its pocket is a +// clearing of this radius. +constexpr int kGroveDepthPercent = 65; +constexpr double kGroveRadius = 3.5; +// What a hidden grove holds: a fruit grove of radius 1 (5 tiles), a wheat patch and a stone clump. +constexpr int kGroveWheat = 16, kGroveStone = 1; +// What a lake holds: wheat round its shore and an orchard of all three fruits. +constexpr int kLakeWheat = 40; + +struct Layout +{ + Torus t{1, 1}; + double homeRadius = 0, spacing = 0; + std::vector homes, kits; + std::vector homeOf; + std::vector water, clearing, forest, lake; + std::vector lakeCentres; + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const OldGrowthOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + + // Homes on the roomiest lattice, shrunk to leave a stretch of forest between neighbours at least + // as wide as a clearing, so no two clearings ever touch. + L.homes = latticeSites(t.w, t.h, teams, context.bounded("growth-layout", std::uint32_t(t.w)), + context.bounded("growth-layout", std::uint32_t(t.h))) + .sites; + L.spacing = std::min(t.w, t.h); + for (size_t a = 0; a < L.homes.size(); ++a) + for (size_t b = a + 1; b < L.homes.size(); ++b) + L.spacing = + std::min(L.spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), + t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + L.homeRadius = std::min(o.homeSize, std::floor(L.spacing / 3 - kClearingMargin)); + if (!homeHasRoom(L.homeRadius)) + { + L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; + return L; + } + L.clearing.assign(n, 0); + for (const ShapePoint &home : L.homes) + for (int i = 0; i < n; ++i) + if (std::hypot(t.offsetX(int(home.x), i % t.w), t.offsetY(int(home.y), i / t.w)) <= + L.homeRadius + kClearingMargin) + L.clearing[i] = 1; + L.homeOf.assign(n, -1); + L.water.assign(n, 0); + const RadialShape home(L.homeRadius, 0.15, context, "growth-home"); + const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "growth-pond"); + L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); + + // Lakes: each at the tile farthest from every clearing and every lake so far (the exact + // distance transform, Morphology.h), grown to `lakeSize` tiles by distance from its seed with a + // little noise in the key so the outline is a lake's, not a disc's. `lakes` is a count per + // 128x128 of map, so a 256 map gets four times as many as a 128. + L.lake.assign(n, 0); + const int lakes = int(std::lround(o.lakes * double(n) / (128.0 * 128.0))); + std::vector keepClear = dilate(t, L.clearing, kLakeGap); + const std::vector ripple = periodicNoise(t.w, t.h, 6, context.stream("growth-lakes")); + std::vector queued(n, 0); + for (int lake = 0; lake < lakes; ++lake) + { + const std::vector far = distanceSquaredTo(t, keepClear); + int seed = -1; + for (int i = 0; i < n; ++i) + if (!keepClear[i] && (seed < 0 || far[i] > far[seed])) + seed = i; + if (seed < 0) + break; + const int grown = growWater( + t, L.water, seed, o.lakeSize, [&](int i) { return !keepClear[i]; }, + [&](int i) + { + const double d = + std::sqrt(double(t.dist2(seed % t.w, seed / t.w, i % t.w, i / t.w))); + return std::int64_t(d * 1000) + std::int64_t(ripple[i]) * 2500 / 65536; + }, + queued, lake + 1); + if (grown <= 0) + break; + L.lakeCentres.push_back(seed); + for (int i = 0; i < n; ++i) + if (L.water[i] && !L.clearing[i]) + L.lake[i] = 1; + keepClear = dilate(t, L.clearing, kLakeGap); + const std::vector shores = dilate(t, L.lake, kLakeGap); + for (int i = 0; i < n; ++i) + keepClear[i] = keepClear[i] || shores[i]; + } + // The forest: everything that is not a clearing or water. + L.forest.assign(n, 0); + for (int i = 0; i < n; ++i) + L.forest[i] = !L.clearing[i] && !L.water[i]; + return L; +} + +// Hidden groves: one per colony, at kGroveDepthPercent of the cutting cost half way to its nearest +// rival, on its own side of the forest (equalCostSites, Contact.h). Each is a pocket cut in the +// forest holding a fruit grove, a wheat patch and a stone clump. Costs are measured on the planted +// forest, so the groves lie at the same worker-hours from every home whatever the forest's gaps. +void hideGroves(Map &map, const Layout &L, GenerationContext &context, const OldGrowthOptions &o, + int teams) +{ + const Torus &t = L.t; + const int n = t.size(); + const auto units = unitTilesByTeam(map, teams); + std::vector> costs; + for (int k = 0; k < teams; ++k) + costs.push_back(costsFrom(map, t, units[k], StepCosts::chopping(4))); + // Half way to the nearest rival, in cutting cost: the least cost at which some other colony's + // field meets this one's, over all colonies, so every grove is measured against the same front. + std::int64_t frontier = 0; + int met = 0; + for (int k = 0; k < teams; ++k) + for (int j = k + 1; j < teams; ++j) + { + int least = -1; + for (int i = 0; i < n; ++i) + if (costs[k][i] >= 0 && costs[j][i] >= 0) + { + const int here = std::max(costs[k][i], costs[j][i]); + if (least < 0 || here < least) + least = here; + } + if (least >= 0) + { + frontier += least; + ++met; + } + } + if (!met) + return; + const int target = int(frontier / met * kGroveDepthPercent / 100); + std::vector deep(n, 0); + for (int i = 0; i < n; ++i) + deep[i] = L.forest[i] && map.getResource(i % t.w, i / t.w).type == WOOD; + const std::vector sites = equalCostSites(costs, deep, target, std::max(8, target / 4)); + for (int k = 0; k < teams; ++k) + { + if (sites[k] < 0) + continue; + const int gx = sites[k] % t.w, gy = sites[k] / t.w; + // The pocket: wood cut within kGroveRadius of the site. + for (int dy = -4; dy <= 4; ++dy) + for (int dx = -4; dx <= 4; ++dx) + if (dx * dx + dy * dy <= kGroveRadius * kGroveRadius && + map.getResource(t.x(gx + dx), t.y(gy + dy)).type == WOOD) + map.setNoResource(t.x(gx + dx), t.y(gy + dy), 1); + const auto pocket = [&](int i) + { return t.dist2(gx, gy, i % t.w, i / t.w) <= 16 && clearGround(map, i % t.w, i / t.w); }; + if (scaledCount(1, o.fruit) > 0) + placeResourceClump(map, context, MapGeneratorPoint(gx, gy), + CHERRY + int(context.bounded("growth-fruit", 3)), 1); + if (const int seed = seedNear(t, gx - 2, gy - 2, 4, pocket); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kGroveWheat, o.wheat)), pocket); + if (scaledCount(1, o.stone) > 0) + if (const int seed = seedNear(t, gx + 2, gy + 2, 4, pocket); seed >= 0) + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), STONE, + kGroveStone); + } +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "old growth layout"; + const OldGrowthOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "old growth terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + if (L.water[i]) + terrain[i] = WATER; + layBeaches(terrain, t); + writeUndermap(map, terrain); + + context.stage = "old growth colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; + if (!settleColonies(game, context, "growth-starts", homeMask, anchor)) + return false; + + context.stage = "old growth resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantOpenHomeKit( + map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, + [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + // The lakes' prizes go down before the forest, so they stand in the open on the shore: wheat all + // round the water and an orchard of the three fruits a few tiles out. + for (int centre : L.lakeCentres) + { + const int cx = centre % t.w, cy = centre / t.w; + const auto shore = [&](int i) + { + return L.forest[i] && t.dist2(cx, cy, i % t.w, i / t.w) <= 400 && + clearGround(map, i % t.w, i / t.w); + }; + if (const int seed = seedNear(t, cx, cy, 20, shore); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kLakeWheat, o.wheat)), shore); + if (scaledCount(1, o.fruit) > 0) + { + const double spin = context.bounded("growth-fruit", 3600) / 3600.0 * 2 * kPi; + const double radius = std::sqrt(o.lakeSize / kPi) + 4; + plantOrchard(map, t, context, cx, cy, radius, {spin}, 4.5, 6, 1, shore); + } + } + // The forest: wood on `forestDensity` percent of the forest ground, the gaps drawn from a noise + // field so they are small openings rather than lanes. Below the 8-connected site percolation + // threshold (about 41% open) the openings never join up into a way through, so at any density + // the control offers (60% and above) the forest is still a wall that has to be cut. + const std::vector gaps = periodicNoise(t.w, t.h, 5, context.stream("growth-gaps")); + std::vector levels; + for (int i = 0; i < n; ++i) + if (L.forest[i]) + levels.push_back(gaps[i]); + const int level = percentile(levels, 100 - o.forestDensity); + plantCover(map, t, L.forest, WOOD, [&](int i) + { return gaps[i] >= level && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + if (o.hiddenGroves) + hideGroves(map, L, context, o, teams); + seedAlgae(map, context, t, "growth-algae", o.algae, AlgaeBand::anyWater(30)); + secureStartingCrops(game, context, t); + reopenCrampedStarts(game, context, {o.wheat, 100, o.stone, o.algae, o.fruit}); + // With trails, a cut trail joins every colony to the first: the cheapest cut through the wood. + if (o.trails) + { + context.stage = "old growth trails"; + openColonyRoutes(map, context, t, StepCosts{1, 3, -1, -1, -1}); + } + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + const OldGrowthOptions o(context.request); + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "old growth"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + const int teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s clearing has lost its pond."; + } + // The forest is dry: nothing but the home ponds and the lakes waters it, so wood beyond their + // reach never regrows. Checked on the finished map's own growth field. + const Fertility::Field fertility = Fertility::forMap(map, false); + std::vector wateredBy = dilate(t, L.water, kCropProbeReach); + for (int i = 0; i < t.size(); ++i) + if (L.forest[i] && !wateredBy[i] && fertility.at(i % t.w, i / t.w) > 0) + return "The forest regrows away from any water at (" + std::to_string(i % t.w) + ", " + + std::to_string(i / t.w) + ")."; + // Every colony is reachable from the first: by walking with trails, by cutting without. + if (o.trails) + return walkFromFirstColony(map, teams, "the forest", "along the trails").error; + const ContactReport contact = contactMatrix(map, teams, StepCosts::chopping(4)); + for (int k = 1; k < teams; ++k) + if (contact.cost[0][k] < 0) + return "Colony " + std::to_string(k) + " cannot be cut to from colony 0."; + return ""; +} +} // namespace + +OldGrowthOptions::OldGrowthOptions(const GenerationRequest &r) + : forestDensity(r.option("forest-density")), lakes(r.option("lakes")), + lakeSize(r.option("lake-size")), homeSize(r.option("home-size")), + hiddenGroves(r.option("hidden-groves") != 0), trails(r.option("trails") != 0), + wheat(r.option("wheat-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition oldGrowthDefinition() +{ + return { + "old-growth", + 28, + "Old growth", + 1, + false, + // 90% cover reads as unbroken forest with the odd glade; one lake per 128x128 of 90 + // tiles (four on a 256 map, each a few days' cutting from any home) keeps them rare enough + // to be prizes; the forest carries no wood amount, since the forest is the map. + {{"forest-density", "Forest density", 60, 100, 5, 90, ControlGroup::Terrain}, + {"lakes", "Lakes", 0, 4, 1, 1, ControlGroup::Terrain}, + {"lake-size", "Lake size", 40, 160, 10, 90, ControlGroup::Terrain}, + {"home-size", "Home size", 10, 20, 1, 13, ControlGroup::Layout}, + GeneratorControl::toggle("hidden-groves", "Hidden groves", true, ControlGroup::Layout), + // On, a trail is cut from every colony to the first before the game starts. + GeneratorControl::toggle("trails", "Starting trails", false, ControlGroup::Layout), + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/OldGrowthGenerator.h b/src/map/generator/generators/OldGrowthGenerator.h new file mode 100644 index 000000000..5e7b3146d --- /dev/null +++ b/src/map/generator/generators/OldGrowthGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct OldGrowthOptions +{ + int forestDensity, lakes, lakeSize, homeSize; + bool hiddenGroves, trails; + int wheat, stone, algae, fruit; // percentages of the default amounts + explicit OldGrowthOptions(const GenerationRequest &r); +}; +GeneratorDefinition oldGrowthDefinition(); diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp new file mode 100644 index 000000000..a981ad146 --- /dev/null +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "OldTownGenerator.h" +#include "Farmland.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Tessellation.h" +#include +#include +#include +#include +using namespace MapGeneration; + +// Old town: a walled city of stone blocks and narrow grass streets, with farmland outside the wall. +// The city is a warped grid of irregular blocks (Tessellation.h); every block is solid stone, every +// street between blocks is grass, so the streets form a dense grid with a flanking route round every +// corner, unlike a maze's single ways. Some blocks are plazas instead: open grass with a fountain +// pond, and every colony starts in a plaza of its own; the plaza at the city's centre is the +// cathedral square, with an orchard of all three fruits round its fountain. A wall of stone with a +// few gates rings the city, and outside it the land is laid out in farm rows. +// +// Streets are buildable and buildings block walking, so every building a player puts up closes a +// street: the players build the city's fortifications and chokepoints themselves, and a tower on a +// street fires over the block into the next street. The fields outside are where the food is; the +// streets are where the fight is. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Stone blocks are permanent +// and unbuildable, so the city's shape is fixed and only the streets can change hands; crops regrow +// near water, and the only water inside the wall is the fountains, so a colony that wants to eat +// must farm the plazas or go out through a gate. A wall stops walking but not shooting, so the +// blocks are also cover for towers. +namespace +{ + +// Every home's starting kit, unscaled whatever the amounts say: wheat and wood beside the fountain. +// No quarry: the blocks are stone, the nearest of them a few steps from every door. +constexpr int kHomeWheat = 14, kHomeWood = 12; +// The wall is this thick (stone, sealed against diagonal steps) and stands this far outside the +// outermost blocks, leaving a ring road inside it; gates are this wide. +constexpr int kWallThickness = 2, kRingRoad = 4, kGateHalfWidth = 3; +// Outside the wall the farm rows keep this margin of grass from it and run along the map's axis. +constexpr int kFarmMargin = 4, kFarmRim = 2, kFarmBridges = 16; +// A fountain's radius: a small pond with a beach, the width of a street to walk round. +constexpr double kFountainRadius = 2.5; + +struct Layout +{ + Torus t{1, 1}; + Tessellation g; + double cx = 0, cy = 0, cityRadius = 0, homeRadius = 0; + std::vector cell; + std::vector homeCell, annexCell, + plazaCell; // each colony's fountain block and swarm block, then the other plazas + std::vector axes; // each home's facing: from the fountain to the swarm + int cathedral = -1; + std::vector homeOf; + std::vector city, block, street, wall, water, farmSand, farmRegion; + std::vector homes, kits; + std::string failure; +}; + +ShapePoint tilePoint(SubtilePoint p) +{ + return {p.x / 16.0, p.y / 16.0}; +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const OldTownOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + L.cx = t.w / 2.0; + L.cy = t.h / 2.0; + // The city is a disc of `citySize` percent of the half side; the wall stands at its edge. + L.cityRadius = o.citySize / 100.0 * std::min(t.w, t.h) / 2.0; + + // The blocks: a square tiling warped into irregular polygons. Every edge is a street, and the + // warp keeps streets that share no corner a street's width and a house apart. + L.g = squareTessellation(t.w, t.h, o.blockSize); + const std::vector walls(L.g.edges.size(), 1); + warpCorners(L.g, warpLimit(L.g) * o.warp / 100, walls, o.streetWidth + 6, o.blockSize / 4, + context, "town-warp"); + L.cell = labelTiles(L.g); + if (L.cell.empty()) + { + L.failure = "The blocks could not be labelled."; + return L; + } + // The city's cells: those whose centre lies inside the wall's ring road. + std::vector inCity(L.g.cellCount(), 0); + int cityCells = 0; + for (int cell = 0; cell < L.g.cellCount(); ++cell) + { + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + const double d = std::hypot(t.offsetX(int(L.cx), int(c.x)), t.offsetY(int(L.cy), int(c.y))); + inCity[cell] = d + o.blockSize / 2.0 < L.cityRadius - kWallThickness - kRingRoad; + cityCells += inCity[cell]; + } + // Plazas: the colonies' plazas as far apart among the city's cells as the city allows + // (farthest-point spreading by centre distance, the cathedral square taken first so no colony + // starts on it), then `plazas` per colony farthest from those. + L.cathedral = L.cell[t.at(int(L.cx), int(L.cy))]; + if (cityCells < teams + 1) + { + L.failure = + "The city has too few blocks for this many colonies; use a bigger city, smaller " + "blocks or fewer colonies."; + return L; + } + { + // Farthest-point spreading over the city's cells, the cathedral square taken first so no + // colony starts on it. A home plaza is two blocks, not one: a single block of 14 less its + // streets is 10 tiles across, and with a fountain in the middle no 4x4 swarm fits beside it + // (measured: every settlement failed); so each home takes a neighbouring block as well, the + // fountain in the first and the swarm in the second. Extra plazas are given up first when the + // city is small. + std::vector nearest(L.g.cellCount(), 0); + std::vector taken(L.g.cellCount(), 0); + taken[L.cathedral] = 1; + for (int cell = 0; cell < L.g.cellCount(); ++cell) + nearest[cell] = L.g.distance2(cell, L.cathedral); + const int extra = std::min(o.plazas * teams, std::max(0, (cityCells - 1 - 2 * teams) / 2)); + for (int k = 0; k < teams + extra; ++k) + { + int best = -1; + for (int cell = 0; cell < L.g.cellCount(); ++cell) + if (inCity[cell] && !taken[cell] && (best < 0 || nearest[cell] > nearest[best])) + best = cell; + if (best < 0) + break; + taken[best] = 1; + if (k < teams) + { + // The home's second block: its city neighbour nearest the cathedral, so the swarm + // stands on the side towards the middle of town. + int annex = -1; + for (int edge : L.g.cells[best].edges) + { + const int next = L.g.other(edge, best); + if (inCity[next] && !taken[next] && + (annex < 0 || + L.g.distance2(next, L.cathedral) < L.g.distance2(annex, L.cathedral))) + annex = next; + } + if (annex < 0) + break; + taken[annex] = 1; + L.homeCell.push_back(best); + L.annexCell.push_back(annex); + for (int cell = 0; cell < L.g.cellCount(); ++cell) + nearest[cell] = std::min(nearest[cell], L.g.distance2(cell, annex)); + } + else + L.plazaCell.push_back(best); + for (int cell = 0; cell < L.g.cellCount(); ++cell) + nearest[cell] = std::min(nearest[cell], L.g.distance2(cell, best)); + } + if (int(L.homeCell.size()) != teams) + { + L.failure = "The city has too few blocks for this many colonies; use a bigger city, " + "smaller blocks or fewer colonies."; + return L; + } + } + + // Streets: the band along every cell border inside the city; blocks: the rest of the city's + // cells, except the plazas; the wall: a ring with gates; the ring road between them. + L.city.assign(n, 0); + L.wall.assign(n, 0); + std::vector border(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + const double d = std::hypot(t.offsetX(int(L.cx), x), t.offsetY(int(L.cy), y)); + L.city[i] = d < L.cityRadius - kWallThickness; + for (const auto &s : kCardinalSteps) + if (L.cell[t.at(x + s[0], y + s[1])] != L.cell[i]) + border[i] = 1; + } + L.street = dilateRound(t, border, o.streetWidth / 2.0); + const double gateSpin = context.bounded("town-gates", 3600) / 3600.0 * 2 * kPi; + std::vector gates; + for (int gate = 0; gate < o.gates; ++gate) + gates.push_back(gateSpin + 2 * kPi * gate / o.gates); + ringWithGates(t, L.cx, L.cy, L.cityRadius - kWallThickness / 2.0, kWallThickness / 2.0, gates, + kGateHalfWidth, [&](int i, int gate) { L.wall[i] = gate < 0; }); + std::vector plaza(L.g.cellCount(), 0); + plaza[L.cathedral] = 1; + for (int cell : L.homeCell) + plaza[cell] = 1; + for (int cell : L.annexCell) + plaza[cell] = 1; + for (int cell : L.plazaCell) + plaza[cell] = 1; + L.block.assign(n, 0); + for (int i = 0; i < n; ++i) + L.block[i] = + L.city[i] && inCity[L.cell[i]] && !L.street[i] && !plaza[L.cell[i]] && !L.wall[i]; + // Fountains: a pond at the middle of every plaza; the colonies' kits round theirs. + L.water.assign(n, 0); + L.homeOf.assign(n, -1); + L.homeRadius = std::max(6.0, o.blockSize / 2.0 - o.streetWidth / 2.0 - 1); + const RadialShape fountain(kFountainRadius, 0.3, context, "town-fountain"); + for (int k = 0; k < teams; ++k) + { + const ShapePoint fountainAt = tilePoint(L.g.cells[L.homeCell[k]].centre); + const ShapePoint swarmAt = tilePoint(L.g.centreAcross( + [&] + { + for (int edge : L.g.cells[L.homeCell[k]].edges) + if (L.g.other(edge, L.homeCell[k]) == L.annexCell[k]) + return edge; + return L.g.cells[L.homeCell[k]].edges[0]; + }(), + L.homeCell[k])); + L.homes.push_back(swarmAt); + L.kits.push_back(fountainAt); + L.axes.push_back(std::atan2(swarmAt.y - fountainAt.y, swarmAt.x - fountainAt.x) + kPi); + fillShape(L.water, t, fountainAt.x, fountainAt.y, fountain, 0.0); + for (int i = 0; i < n; ++i) + if (L.cell[i] == L.homeCell[k] || L.cell[i] == L.annexCell[k]) + L.homeOf[i] = k; + } + for (int cell : L.plazaCell) + { + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + fillShape(L.water, t, c.x, c.y, fountain, 0.0); + } + { + const ShapePoint c = tilePoint(L.g.cells[L.cathedral].centre); + fillShape(L.water, t, c.x, c.y, fountain, 0.0); + } + // Outside the wall: farm rows over everything beyond the wall's margin, along the map's axis + // with the row through the centre a crop row, bridged every kFarmBridges tiles. + L.farmRegion.assign(n, 0); + for (int i = 0; i < n; ++i) + { + const double d = std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); + L.farmRegion[i] = d > L.cityRadius + kFarmMargin; + } + TerrainSketch rows(n, GRASS); + const Farm farm = layFarm(rows, t, L.farmRegion, 0.0, {L.cx, L.cy}, kFarmRim, bestFarmRows(0.0), + nullptr, kFarmBridges, true); + L.farmSand = farm.sand; + for (int i = 0; i < n; ++i) + if (farm.water[i]) + L.water[i] = 1; + return L; +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "old town layout"; + const OldTownOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "old town terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + terrain[i] = L.water[i] ? WATER : L.farmSand[i] ? SAND : GRASS; + layBeaches(terrain, t); + writeUndermap(map, terrain); + // The blocks and the wall are stone, wherever the beaches left pure grass. + for (int i = 0; i < n; ++i) + if ((L.block[i] || L.wall[i]) && map.isResourceAllowed(i % t.w, i / t.w, STONE)) + map.setResource(i % t.w, i / t.w, STONE, 1); + + context.stage = "old town colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) + { + return MapGeneratorPoint(int(std::lround(L.homes[team].x)) - 2, + int(std::lround(L.homes[team].y)) - 2); + }; + if (!settleColonies(game, context, "town-starts", homeMask, anchor)) + return false; + + context.stage = "old town resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantHomeKit( + map, t, context, L.kits[k], L.axes[k], L.homeRadius, kHomeWheat, kHomeWood, [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + // The cathedral square's orchard, round its fountain. + if (scaledCount(1, o.fruit) > 0) + { + const ShapePoint c = tilePoint(L.g.cells[L.cathedral].centre); + const double spin = context.bounded("town-fruit", 3600) / 3600.0 * 2 * kPi; + plantOrchard(map, t, context, c.x, c.y, kFountainRadius + 3.5, {spin}, 4.5, 4, 1, + [&](int i) + { + return L.cell[i] == L.cathedral && !L.street[i] && + clearGround(map, i % t.w, i / t.w); + }); + } + // The fields outside: half the fertile row ground under wheat and a sixth under wood, in patches; + // inside the city only the plazas are fertile (their fountains), and get a light share so they + // stay open to build on. + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("town-patch")); + const std::vector split = periodicNoise(t.w, t.h, 4, context.stream("town-split")); + const auto fields = [&](int i) + { return L.farmRegion[i] && !L.farmSand[i] && clearGround(map, i % t.w, i / t.w); }; + int fertile = 0; + for (int i = 0; i < n; ++i) + fertile += fields(i) && fertility.at(i % t.w, i / t.w) > 0; + furnishGround( + map, t, context, fertility, fields, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int area) + { + return GroundAmounts{int(scaledCount(fertile * 50 / 100, o.wheat)), + int(scaledCount(fertile * 16 / 100, o.wood)), + int(scaledCount(area / 2000, o.stone)), + int(scaledCount(area / 3000, o.fruit))}; + }, + "town-stone", "town-fruit"); + const auto plazas = [&](int i) + { + return L.city[i] && !L.block[i] && !L.wall[i] && !L.street[i] && L.homeOf[i] < 0 && + clearGround(map, i % t.w, i / t.w); + }; + int plazaFertile = 0; + for (int i = 0; i < n; ++i) + plazaFertile += plazas(i) && fertility.at(i % t.w, i / t.w) > 0; + furnishGround( + map, t, context, fertility, plazas, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int) + { + return GroundAmounts{int(scaledCount(plazaFertile * 15 / 100, o.wheat)), + int(scaledCount(plazaFertile * 10 / 100, o.wood)), 0, 0}; + }, + "town-stone", "town-fruit"); + seedAlgae(map, context, t, "town-algae", o.algae, AlgaeBand::anyWater(50)); + std::vector stone(n, 0); + for (int i = 0; i < n; ++i) + stone[i] = L.block[i] || L.wall[i]; + secureStartingCrops(game, context, t, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, + &stone); + // The streets join every plaza and the gates join the city to the fields; only crops could close + // a street, so only crops are cleared. + context.stage = "old town routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, -1, -1, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "old town"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (int k = 0; k < context.request.nbTeams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s plaza has lost its fountain."; + } + for (int i = 0; i < t.size(); ++i) + if (L.wall[i] && map.isGrass(i % t.w, i / t.w) && + map.getResource(i % t.w, i / t.w).type != STONE) + return "The city wall has a gap at (" + std::to_string(i % t.w) + ", " + + std::to_string(i / t.w) + ")."; + return walkFromFirstColony(map, context.request.nbTeams, "the city", "through the streets") + .error; +} +} // namespace + +OldTownOptions::OldTownOptions(const GenerationRequest &r) + : citySize(r.option("city-size")), blockSize(r.option("block-size")), + streetWidth(r.option("street-width")), warp(r.option("warp")), plazas(r.option("plazas")), + gates(r.option("gates")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition oldTownDefinition() +{ + return {"old-town", + 31, + "Old town", + 1, + false, + // A city of 70% of the half side leaves a belt of fields round it; blocks of 14 with + // streets of 4 give a 256 map about a hundred blocks and streets a column of units wide + // that one building closes; four gates, one to a side. + {{"city-size", "City size", 40, 90, 5, 70, ControlGroup::Layout}, + {"block-size", "Block size", 10, 20, 1, 14, ControlGroup::Layout}, + {"street-width", "Street width", 3, 7, 1, 4, ControlGroup::Terrain}, + {"warp", "Warp", 0, 100, 10, 50, ControlGroup::Terrain}, + {"plazas", "Plazas", 0, 4, 1, 2, ControlGroup::Layout}, + {"gates", "Gates", 2, 8, 1, 4, ControlGroup::Layout}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/OldTownGenerator.h b/src/map/generator/generators/OldTownGenerator.h new file mode 100644 index 000000000..2a0ab0139 --- /dev/null +++ b/src/map/generator/generators/OldTownGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct OldTownOptions +{ + int citySize, blockSize, streetWidth, warp, plazas, gates; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit OldTownOptions(const GenerationRequest &r); +}; +GeneratorDefinition oldTownDefinition(); diff --git a/src/map/generator/generators/PatchworkGenerator.cpp b/src/map/generator/generators/PatchworkGenerator.cpp new file mode 100644 index 000000000..3f6882572 --- /dev/null +++ b/src/map/generator/generators/PatchworkGenerator.cpp @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "PatchworkGenerator.h" +#include "Biomes.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Orbits.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include "Territories.h" +#include +#include +#include +#include +using namespace MapGeneration; + +// Patchwork: every colony starts on a different kind of land. The map is shared out into one +// territory per colony, and each territory is dealt a kit from Biomes.h in turn: a fertile plain +// (big ponds, wide fields, open ground), a stone fortress (a ring of stone with gates, outcrops, +// poor water), an orchard island (a lake with an island of all three fruits, little farmland) or a +// forest (most of the ground under wood, ponds to keep it growing). Every player plays a different +// opening: the plain builds wide, the fortress holds, the island pulls hungry enemies across with +// its fruit, the forest cuts. +// +// This is the one map in the catalog that gives up fairness by construction. What it does instead is +// share the ground out by worth: a territory dealt a poorer kit gets proportionally more tiles +// (growTerritories' worth, from biomeWorth's estimate), and the start scorer ranks the seeds the +// lobby rolls. biomeWorth is an estimate from the scorer's weights, not a measurement: the kits are +// to be tuned with the fairness tournament (docs/map-generators/FAIRNESS_TOURNAMENT.md). +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Crops regrow near water, so +// the plain out-produces everyone and must be attacked before it does; stone is a wall no one +// clears, so the fortress's gates are its whole defence; an inn stocked with all three fruits pulls +// hungry enemy units across, so the island's orchard is a weapon; wood is a wall until cut, so the +// forest colony chooses where its front is. +namespace +{ + +// Every home's starting kit, unscaled whatever the amounts say, so every opening is at least +// viable: wheat and wood beside the home's own pond, and a quarry. +constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// The home's clearing keeps this much beyond its rough disc free of the kit's cover, ponds and ring. +constexpr double kClearingMargin = 3.0; +// A fortress's gates: one towards every neighbouring territory, this wide. +constexpr int kGateRadius = 3; +// Territory borders wander with noise cells half a home spacing across, weighted so a border can +// bend by a few tiles but never wraps round a home. +constexpr int kBorderCostScale = 700; + +struct Layout +{ + Torus t{1, 1}; + double homeRadius = 0; + std::vector homes, kits; + std::vector kitOf; // which BiomeKit each colony was dealt + std::vector territory, homeOf; + std::vector> regions, doors; + std::vector biomes; + std::vector water, clearing; + TerrainSketch sketch; + std::string failure; +}; + +const std::vector &kits() +{ + static const std::vector all{fertilePlain(), stoneFortress(), orchardIsland(), + forest()}; + return all; +} + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const PatchworkOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + + // Homes on a lattice, and the kits dealt round from a random first. + L.homes = latticeSites(t.w, t.h, teams, context.bounded("patch-layout", std::uint32_t(t.w)), + context.bounded("patch-layout", std::uint32_t(t.h))) + .sites; + double spacing = std::min(t.w, t.h); + for (size_t a = 0; a < L.homes.size(); ++a) + for (size_t b = a + 1; b < L.homes.size(); ++b) + spacing = + std::min(spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), + t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + L.homeRadius = std::min(o.homeSize, std::floor(spacing / 3 - kClearingMargin)); + if (!homeHasRoom(L.homeRadius)) + { + L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; + return L; + } + const int first = int(context.bounded("patch-deal", std::uint32_t(kits().size()))); + std::vector worth; + for (int k = 0; k < teams; ++k) + { + L.kitOf.push_back((first + k) % int(kits().size())); + worth.push_back(biomeWorth(kits()[L.kitOf[k]])); + } + + // Territories by worth (growTerritories): the colony whose ground is worth least per tile takes + // the next tile, so it ends with more of them; noise in the cost bends the borders, and a few + // smoothing passes straighten them into curves. + std::vector> seeds; + for (const ShapePoint &home : L.homes) + seeds.push_back({t.at(int(home.x), int(home.y))}); + const std::vector noise = + fractalNoise(t.w, t.h, std::max(4, int(spacing) / 2), 3, context.stream("patch-borders")); + const int roughness = o.borderRoughness * kBorderCostScale / 100; + L.territory = growTerritories( + t, std::vector(n, 1), seeds, [&](int i) + { return int(std::int64_t(noise[i]) * roughness / 65536); }, &worth) + .labels; + std::vector keep(n, 0); + for (const auto &seed : seeds) + keep[seed[0]] = 1; + if (o.smoothing > 0) + smoothLabels(t, L.territory, o.smoothing, keep, 3); + // Every tile must belong to someone: whatever smoothing or growth left unclaimed goes to the + // nearest home. + for (int i = 0; i < n; ++i) + if (L.territory[i] < 0) + { + int best = 0; + for (int k = 1; k < teams; ++k) + if (t.dist2(i % t.w, i / t.w, int(L.homes[k].x), int(L.homes[k].y)) < + t.dist2(i % t.w, i / t.w, int(L.homes[best].x), int(L.homes[best].y))) + best = k; + L.territory[i] = best; + } + L.regions.assign(teams, std::vector(n, 0)); + for (int i = 0; i < n; ++i) + L.regions[L.territory[i]][i] = 1; + + // Gates: for a fortress, a door in its ring towards every neighbouring territory, at the rim tile + // nearest the line between the two homes. Computed for every kit, harmlessly. + L.doors.assign(teams, std::vector(n, 0)); + for (int k = 0; k < teams; ++k) + { + std::vector neighbour(teams, 0); + for (int i = 0; i < n; ++i) + if (L.territory[i] == k) + for (const auto &s : kCardinalSteps) + { + const int other = L.territory[t.at(i % t.w + s[0], i / t.w + s[1])]; + if (other != k) + neighbour[other] = 1; + } + std::vector gates; + for (int j = 0; j < teams; ++j) + { + if (!neighbour[j]) + continue; + const double mx = L.homes[k].x + t.offsetX(int(L.homes[k].x), int(L.homes[j].x)) / 2.0; + const double my = L.homes[k].y + t.offsetY(int(L.homes[k].y), int(L.homes[j].y)) / 2.0; + int best = -1; + for (int i = 0; i < n; ++i) + { + if (L.territory[i] != k) + continue; + bool rim = false; + for (const auto &s : kCardinalSteps) + rim = rim || L.territory[t.at(i % t.w + s[0], i / t.w + s[1])] != k; + if (rim && (best < 0 || t.dist2(int(mx), int(my), i % t.w, i / t.w) < + t.dist2(int(mx), int(my), best % t.w, best / t.w))) + best = i; + } + if (best >= 0) + gates.push_back(best); + } + L.doors[k] = dilate(t, tileMask(t, gates), kGateRadius); + } + + // The homes' clearings and ponds, then each kit's own terrain in its territory: a kit's ponds + // and ring keep out of the clearing, whose pond is the home's. + L.clearing.assign(n, 0); + for (const ShapePoint &home : L.homes) + for (int i = 0; i < n; ++i) + if (std::hypot(t.offsetX(int(home.x), i % t.w), t.offsetY(int(home.y), i / t.w)) <= + L.homeRadius + kClearingMargin) + L.clearing[i] = 1; + L.homeOf.assign(n, -1); + L.water.assign(n, 0); + const RadialShape home(L.homeRadius, 0.15, context, "patch-home"); + const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "patch-pond"); + L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); + // Each kit's terrain over its whole territory (so a fortress's ring runs round the territory, + // not round the home clearing); a kit's pond that landed in the clearing is drained, leaving the + // home its own pond. + L.sketch.assign(n, GRASS); + const std::vector homeWater = L.water; + for (int k = 0; k < teams; ++k) + { + L.biomes.push_back(sketchBiome(L.sketch, t, L.regions[k], L.doors[k], kits()[L.kitOf[k]], + context, "patch-biome-" + std::to_string(k))); + for (int i = 0; i < n; ++i) + { + if (L.clearing[i] && !homeWater[i]) + { + L.biomes[k].water[i] = 0; + L.sketch[i] = GRASS; + } + if (L.biomes[k].water[i]) + L.water[i] = 1; + } + } + for (int i = 0; i < n; ++i) + if (L.water[i]) + L.sketch[i] = WATER; + return L; +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "patchwork layout"; + const PatchworkOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "patchwork terrain"; + TerrainSketch terrain = L.sketch; + layBeaches(terrain, t); + writeUndermap(map, terrain); + + context.stage = "patchwork colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; + if (!settleColonies(game, context, "patch-starts", homeMask, anchor)) + return false; + + context.stage = "patchwork resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantOpenHomeKit( + map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, + [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + // Each kit's deposits over its territory, the home's clearing kept open. The amounts scale each + // kit's own shares. + std::vector stone(n, 0); + for (int k = 0; k < teams; ++k) + { + BiomeKit kit = kits()[L.kitOf[k]]; + kit.farmPerMille = int(scaledCount(kit.farmPerMille, (o.wheat + o.wood) / 2)); + kit.outcropsPer1000 = int(scaledCount(kit.outcropsPer1000, o.stone)); + kit.grovesPer1000 = int(scaledCount(kit.grovesPer1000, o.fruit)); + kit.coverPercent = std::min(100, int(scaledCount(kit.coverPercent, o.wood))); + std::vector keepClear(n, 0), ground(n, 0); + for (int i = 0; i < n; ++i) + { + keepClear[i] = L.clearing[i] || reserved[i]; + ground[i] = L.regions[k][i]; + stone[i] = stone[i] || L.biomes[k].wall[i]; + } + furnishBiome(map, t, context, ground, L.biomes[k], kit, keepClear, + "patch-" + std::to_string(k)); + } + seedAlgae(map, context, t, "patch-algae", o.algae, AlgaeBand::anyWater(30)); + secureStartingCrops(game, context, t, 24, 32, 0, &stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, + &stone); + // The kits' cover and rings can close a colony in; the cheapest way through is opened, through + // crops for preference, through a ring only when a gate failed. + context.stage = "patchwork routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, 12, -1, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "patchwork"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (int k = 0; k < context.request.nbTeams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s home has lost its pond."; + } + return walkFromFirstColony(map, context.request.nbTeams, "the patchwork", "across the borders") + .error; +} +} // namespace + +PatchworkOptions::PatchworkOptions(const GenerationRequest &r) + : homeSize(r.option("home-size")), borderRoughness(r.option("border-roughness")), + smoothing(r.option("smoothing")), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition patchworkDefinition() +{ + return {"patchwork", + 33, + "Patchwork", + 1, + false, + {{"home-size", "Home size", 10, 20, 1, 13, ControlGroup::Layout}, + {"border-roughness", "Border roughness", 0, 100, 10, 40, ControlGroup::Terrain}, + {"smoothing", "Smoothing", 0, 4, 1, 2, ControlGroup::Terrain}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/PatchworkGenerator.h b/src/map/generator/generators/PatchworkGenerator.h new file mode 100644 index 000000000..56fc54097 --- /dev/null +++ b/src/map/generator/generators/PatchworkGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct PatchworkOptions +{ + int homeSize, borderRoughness, smoothing; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit PatchworkOptions(const GenerationRequest &r); +}; +GeneratorDefinition patchworkDefinition(); diff --git a/src/map/generator/generators/PolderGenerator.cpp b/src/map/generator/generators/PolderGenerator.cpp new file mode 100644 index 000000000..0a24b37bb --- /dev/null +++ b/src/map/generator/generators/PolderGenerator.cpp @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "PolderGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Orbits.h" +#include "Patterns.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include +#include +#include +#include +using namespace MapGeneration; + +// Polder: reclaimed land, all of it. The whole torus is laid out in long rows of crops with a +// ditch of water between every two, the way a real farm is (Farmland.h's row widths: crops wide +// enough to fill, water close enough that every crop regrows), and sand dykes cross the ditches at +// regular intervals. Every colony starts in a small grass village on the rows, and hamlets of grass +// lie between the villages for forward inns. Food is effectively unlimited: the game is logistics. +// Units walk only where there is no crop and no water - along the ditches' beaches, over the dykes, +// through the villages - so hungry units walk a long way unless the inns are placed well, fights +// happen on the dykes, and a colony that can swim ignores the dykes altogether. +// +// The rows and the dykes are stripe fields that wrap the torus exactly (Patterns.h), so the polder +// has no seam; the villages sit on a lattice (Orbits.h). Fairness is statistical: every village sees +// the same rows, but which dyke it stands beside is where it fell. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Crops block movement and +// building, so a field is a wall until it is harvested and a village is the only room to build; +// water within a few tiles of every crop keeps every field regrowing, so the map never runs short; +// sand beside a ditch is where everyone walks. Swimming turns every ditch into a road: the second +// half of the game is a different map. +namespace +{ + +// Every village's starting kit, unscaled whatever the amounts say: wheat and wood at the village's +// edge (they regrow there, the ditches being a few tiles away) and a quarry, the only stone the +// polder has besides the odd outcrop. +constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// Row widths, across the rows in tiles. Straight rows: 10 of crops and 6 of water, a period of 16 so +// the pattern divides every map side and wraps without a seam (the yield fit's 10 and 8 would give +// 18, which does not, and the difference in yield is under 3%). Diagonal rows: 11 of crops and 6 of +// water on a perpendicular spacing set by the map, about 17 on a 256 map. +constexpr int kStraightCrops = 10, kStraightPeriod = 16; +constexpr int kDiagonalCrops = 11, kDiagonalStep = 24; +// A dyke is a line of sand this many corners wide across the rows: two corners make one whole tile +// of sand with a walkable half tile either side, a lane two or three units wide. +constexpr int kDykeCorners = 2; +// Hamlets are grass discs of this radius half way between neighbouring villages: room for an inn and +// a tower, not for a swarm. +constexpr double kHamletRadius = 5.0; + +struct Layout +{ + Torus t{1, 1}; + StripeStyle rows, along; + double spacing = 0, crops = 0, villageRadius = 0; + std::vector homes, kits, hamlets; + std::vector homeOf; + std::vector water, dyke, village, hamlet; + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const PolderOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + + // The rows: straight rows run across the map with `h / 16` of them from top to bottom; diagonal + // rows climb as many times across the width as down the height. Both wrap exactly. + L.rows.warpPercent = 0; + if (o.rowAngle == 0) + { + L.rows.acrossX = 0; + L.rows.acrossY = t.h / kStraightPeriod; + L.crops = kStraightCrops; + } + else + { + L.rows.acrossX = std::max(1, t.w / kDiagonalStep); + L.rows.acrossY = std::max(1, t.h / kDiagonalStep); + L.crops = kDiagonalCrops; + } + L.spacing = stripeSpacing(t, L.rows); + L.along = alongStripes(t, L.rows); + const std::vector phase = stripePhase(t, L.rows, context.stream("polder-rows")); + const std::vector along = stripePhase(t, L.along, context.stream("polder-rows")); + + // The villages: on a lattice, shrunk so a whole row of crops and ditch always lies between two. + L.homes = latticeSites(t.w, t.h, teams, context.bounded("polder-layout", std::uint32_t(t.w)), + context.bounded("polder-layout", std::uint32_t(t.h))) + .sites; + double nearest = std::min(t.w, t.h); + for (size_t a = 0; a < L.homes.size(); ++a) + for (size_t b = a + 1; b < L.homes.size(); ++b) + nearest = + std::min(nearest, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), + t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + L.villageRadius = std::min(o.villageSize, std::floor((nearest - L.spacing) / 2)); + if (L.villageRadius < 8) + { + L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; + return L; + } + L.village.assign(n, 0); + L.homeOf.assign(n, -1); + L.water.assign(n, 0); + const RadialShape village(L.villageRadius, 0.15, context, "polder-village"); + L.kits = + stampRoundHomes(t, L.homes, 0.0, village, L.villageRadius, 0, nullptr, L.water, L.homeOf); + for (int i = 0; i < n; ++i) + L.village[i] = L.homeOf[i] >= 0; + // Hamlets: a small grass disc half way between every two villages that are nearest neighbours. + L.hamlet.assign(n, 0); + if (o.hamlets) + { + const RadialShape hamlet(kHamletRadius, 0.2, context, "polder-hamlet"); + for (size_t a = 0; a < L.homes.size(); ++a) + for (size_t b = a + 1; b < L.homes.size(); ++b) + { + const double dx = t.offsetX(int(L.homes[a].x), int(L.homes[b].x)); + const double dy = t.offsetY(int(L.homes[a].y), int(L.homes[b].y)); + if (std::hypot(dx, dy) > nearest * 1.2) + continue; + const ShapePoint middle{std::fmod(L.homes[a].x + dx / 2 + t.w, t.w), + std::fmod(L.homes[a].y + dy / 2 + t.h, t.h)}; + L.hamlets.push_back(middle); + fillShape(L.hamlet, t, middle.x, middle.y, hamlet, 0.0); + } + } + + // Water in every ditch, except through the villages and hamlets, and the dykes across it: a line + // of sand corners every `dykeSpacing` tiles along the rows. + const int cropUnits = int(L.crops / L.spacing * 65536); + const double alongLength = stripeSpacing(t, L.along); + const int dykes = std::max(1, int(std::lround(alongLength / o.dykeSpacing))); + const int dykePeriod = 65536 / dykes, dykeHalf = int(kDykeCorners / 2.0 / alongLength * 65536); + L.dyke.assign(n, 0); + for (int i = 0; i < n; ++i) + { + if (L.village[i] || L.hamlet[i]) + continue; + const int offset = along[i] % dykePeriod; + const bool onDyke = offset < dykeHalf || dykePeriod - offset < dykeHalf; + if (phase[i] >= cropUnits) + (onDyke ? L.dyke[i] : L.water[i]) = 1; + } + return L; +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "polder layout"; + const PolderOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "polder terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + terrain[i] = L.water[i] ? WATER : L.dyke[i] ? SAND : GRASS; + layBeaches(terrain, t); + writeUndermap(map, terrain); + + context.stage = "polder colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) + { return homeSwarmSite(L.homes[team], 0.0, L.villageRadius); }; + if (!settleColonies(game, context, "polder-starts", homeMask, anchor)) + return false; + + context.stage = "polder resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantOpenHomeKit( + map, t, context, L.kits[k], 0.0, L.villageRadius, kHomeWheat, kHomeWood, kHomeQuarry, + [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + // The fields: crops on the rows' grass, nearly all of it. Wheat on 55% of the fertile row ground + // and wood on 15%, in patches, so the rows are mostly under crop (this is the polder) while the + // gaps between patches leave room to build a mill or a tower on a row; the villages and hamlets + // stay open. Every row tile is within a few tiles of water, so it all regrows. + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("polder-patch")); + const std::vector split = periodicNoise(t.w, t.h, 4, context.stream("polder-split")); + const auto rowGround = [&](int i) + { return !L.village[i] && !L.hamlet[i] && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + int fertile = 0; + for (int i = 0; i < n; ++i) + fertile += rowGround(i) && fertility.at(i % t.w, i / t.w) > 0; + furnishGround( + map, t, context, fertility, rowGround, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int area) + { + return GroundAmounts{int(scaledCount(fertile * 55 / 100, o.wheat)), + int(scaledCount(fertile * 15 / 100, o.wood)), + int(scaledCount(area / 2500, o.stone)), 0}; + }, + "polder-stone", "polder-fruit"); + // A grove of one fruit in every hamlet, so a forward inn there has something to stock. + if (scaledCount(1, o.fruit) > 0) + for (const ShapePoint &hamlet : L.hamlets) + if (const int seed = + seedNear(t, int(hamlet.x), int(hamlet.y), 3, [&](int i) + { return L.hamlet[i] && clearGround(map, i % t.w, i / t.w); }); + seed >= 0) + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), + CHERRY + int(context.bounded("polder-fruit", 3)), 1); + seedAlgae(map, context, t, "polder-algae", o.algae, AlgaeBand::anyWater(60)); + secureStartingCrops(game, context, t); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}); + // The beaches along the ditches and the dykes across them join everything; the crops could still + // close a lane, so the cheapest way through crops is opened, never through water. + context.stage = "polder routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, -1, -1, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "polder"); !mismatch.empty()) + return mismatch; + return walkFromFirstColony(map, context.request.nbTeams, "the polder", "along the dykes").error; +} +} // namespace + +PolderOptions::PolderOptions(const GenerationRequest &r) + : rowAngle(r.option("row-angle")), dykeSpacing(r.option("dyke-spacing")), + villageSize(r.option("village-size")), hamlets(r.option("hamlets") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition polderDefinition() +{ + return { + "polder", + 30, + "Polder", + 1, + false, + // A dyke every 24 tiles is a lane every one and a half rows' walk; villages of radius 11 + // hold a swarm, its kit and a few more buildings and no more. + {GeneratorControl::choice("row-angle", "Row angle", {"Straight", "Diagonal"}, 0, + ControlGroup::Terrain), + {"dyke-spacing", "Dyke spacing", 12, 48, 4, 24, ControlGroup::Terrain}, + {"village-size", "Village size", 8, 16, 1, 11, ControlGroup::Layout}, + GeneratorControl::toggle("hamlets", "Hamlets", true, ControlGroup::Layout), + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/PolderGenerator.h b/src/map/generator/generators/PolderGenerator.h new file mode 100644 index 000000000..989f85dcb --- /dev/null +++ b/src/map/generator/generators/PolderGenerator.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct PolderOptions +{ + int rowAngle, dykeSpacing, villageSize; + bool hamlets; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit PolderOptions(const GenerationRequest &r); +}; +GeneratorDefinition polderDefinition(); diff --git a/src/map/generator/generators/RainShadowGenerator.cpp b/src/map/generator/generators/RainShadowGenerator.cpp new file mode 100644 index 000000000..c6c0f8208 --- /dev/null +++ b/src/map/generator/generators/RainShadowGenerator.cpp @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "RainShadowGenerator.h" +#include "FertilityField.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "Grid.h" +#include "Homes.h" +#include "LatticeNoise.h" +#include "Morphology.h" +#include "Orbits.h" +#include "Patterns.h" +#include "Pipeline.h" +#include "Planting.h" +#include "Resources.h" +#include "Roads.h" +#include "Settlements.h" +#include "Sketch.h" +#include +#include +#include +#include +#include +using namespace MapGeneration; + +// Rain shadow: a map with a grain. Long stone ridges run right across the torus, parallel and a +// valley apart, and the wind blows across them from one side. The windward foot of every ridge +// catches the rain: a chain of pools runs along it and the farmland grows there. The lee side is +// dry: a band of sand behind every ridge where nothing grows and nothing can be built. Every ridge +// is cut by passes at regular intervals, staggered from ridge to ridge, so moving along a valley is +// easy and crossing the ridges is slow. +// +// A colony's home sits in the middle of a valley: its lush side faces the windward foot of the next +// ridge, and behind it lies the dry back of the ridge it shelters under. An attacker crossing a pass +// arrives on barren sand facing a defender standing on fields; a defender who sallies out crosses +// the same sand. Nothing here is turned round a centre: the ridges are stripes that wrap the torus +// exactly at their slant (Patterns.h), the homes sit on a lattice (Orbits.h) snapped to the middle +// of their valleys, and fairness comes from every valley being the same valley. +// +// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Stone is a wall no unit +// crosses and nobody clears, so the passes are doors worth holding, and being staggered no straight +// road runs across the map. Crops regrow only near water and never beside sand, so the windward +// pools decide where the fields are and the lee sand is a desert even though it is walkable: an +// army on it is far from any inn. Sand also carries no building, so a forward base has to wait for +// the next valley. +namespace +{ + +// Every home's starting kit, unscaled whatever the amounts say: wheat and wood beside the home's own +// pond, and a quarry, though stone is everywhere on this map. +constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// The ground a home clearing keeps from the ridges, the sand and the pools on either side: its own +// pond's beach (2 tiles) and a lane to walk round the home (2 more). +constexpr int kHomeMargin = 4; +// The ridge's own wobble, as a share of the valley spacing: enough that the ridges read as +// mountains rather than ruled lines, little enough that a valley never pinches shut (the pools and +// homes are placed by the warped phase, so they follow the wobble). +constexpr int kRidgeWarpPercent = 12; +// Ridge wobble noise cells, in tiles: features a few times longer than a pass, so a ridge sways +// rather than shivers. +constexpr int kRidgeWarpPeriod = 40; +// The windward foot: pools start this many tiles out from the ridge's stone and run this many tiles +// deep into the valley. Stone stands only on pure grass, a pool's beach is a ring of sand corners +// one out from its water, and a corner spoils the four tiles round it, so a pool three tiles out +// would take the ridge's windward row with it (measured: the ridge came out one row thick); four +// tiles leaves all three rows standing. +constexpr int kFootGap = 4, kFootDepth = 3; +// Pools along the foot: each this long along the ridge, every this many tiles. A pool 12 long and 3 +// deep is about 36 corners of water, enough for the engine's growth probe to find from a good part +// of the valley; the 12-tile gaps between pools are where the valley's beach lets units walk along +// the foot. +constexpr int kPoolLength = 12, kPoolSpacing = 24; + +struct Layout +{ + Torus t{1, 1}; + StripeStyle across, along; // phase across the ridges (0 on a ridge) and along them + int ridges = 0; // crossings of the map, after any taken away to fit + double spacing = 0; // between ridges, in tiles + double homeRadius = 0; + int windX = 0, windY = 0; // the wind's direction in whole steps + std::vector homes, kits; + std::vector homeOf; + std::vector stone, water, sand, clearing; + std::string failure; +}; + +Layout design(const GenerationRequest &request, GenerationContext &context) +{ + const RainShadowOptions o(request); + Layout L; + L.t = {1 << request.wDec, 1 << request.hDec}; + const Torus &t = L.t; + const int n = t.size(), teams = std::max(1, request.nbTeams); + + // The ridges: `ridges` crossings of the map from top to bottom, tilted so that following one + // across the width climbs `slant` ridges. With a slant every ridge is one long spiral round the + // torus, which is what keeps the passes staggered from one crossing to the next. The valley + // between two ridges must hold a home with its margins, the windward pools and the lee sand: on a + // small map the ridges are taken away one at a time until the valleys are wide enough (four + // ridges suit a 256 map; a 128 map gets two), then the homes shrink to what is left, and only then + // is the map refused. + L.across.acrossX = o.slant; + L.across.warpPeriod = kRidgeWarpPeriod; + L.across.warpPercent = kRidgeWarpPercent; + const double taken = kFootGap + kFootDepth + o.leeWidth; + const double smallest = homePondRadius(o.homeSize) + kHomePondGap + kHomeSwarmRoom; + for (L.ridges = o.ridges; L.ridges > 2; --L.ridges) + { + L.across.acrossY = L.ridges; + if ((stripeSpacing(t, L.across) - o.ridgeThickness - taken) / 2 - kHomeMargin >= smallest) + break; + } + L.across.acrossY = L.ridges; + L.along = alongStripes(t, L.across); + L.spacing = stripeSpacing(t, L.across); + const double alongLength = stripeSpacing(t, L.along); + // Phase units: 65536 is one spacing across, or one turn along. + const auto acrossUnits = [&](double tiles) { return int(tiles / L.spacing * 65536); }; + const auto alongUnits = [&](double tiles) { return int(tiles / alongLength * 65536); }; + const double valley = L.spacing - o.ridgeThickness; + L.homeRadius = std::min(o.homeSize, std::floor((valley - taken) / 2 - kHomeMargin)); + if (!homeHasRoom(L.homeRadius)) + { + L.failure = + "The valleys are too narrow for the homes; use fewer ridges, thinner ridges or a " + "bigger map."; + return L; + } + + const std::vector phase = stripePhase(t, L.across, context.stream("rain-ridges")); + const std::vector along = stripePhase(t, L.along, context.stream("rain-ridges")); + // The wind blows towards rising phase: its direction is the across field's normal, as whole steps. + { + const int g = std::gcd(std::max(1, o.slant * t.h), L.ridges * t.w); + L.windX = o.slant * t.h / g; + L.windY = L.ridges * t.w / g; + } + + // Stone on the ridge's band of phase, except at the passes. Passes are cut every `passSpacing` + // tiles along the ridge (as many whole passes as fit in one turn along it, never fewer than one), + // and on straight ridges every other ridge's passes are shifted by half a spacing, so no pass + // lines up with the one on the next ridge; a slanted ridge staggers its own passes, since each + // crossing of the map shifts the along phase by a fraction of a turn. + const int ridgeHalf = acrossUnits(o.ridgeThickness / 2.0); + const int passes = std::max(1, int(std::lround(alongLength / o.passSpacing))); + const int passPeriod = 65536 / passes, passHalf = alongUnits(o.passWidth / 2.0); + L.stone.assign(n, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int i = y * t.w + x; + if (stripeDistance(phase[i]) >= ridgeHalf) + continue; + int shift = 0; + if (o.slant == 0) + shift = ((L.ridges * y / t.h) % 2) * passPeriod / 2; + const int offset = ((along[i] + shift) % passPeriod + passPeriod) % passPeriod; + const bool pass = offset < passHalf || passPeriod - offset < passHalf; + L.stone[i] = !pass; + } + + // Windward pools: a chain along the foot of every ridge on the side the wind comes from, which + // is the side of falling phase (the wind blows towards rising phase, so it arrives at the ridge + // from below its phase). Each pool runs kPoolLength along the ridge every kPoolSpacing, and + // kFootGap to kFootGap + kFootDepth out from the stone. Lee sand: the band on the other side, as + // far as leeWidth, measured downwind from the stone itself (upwindSteps against the wind) so the + // sand stops where a pass lets the rain through. + L.water.assign(n, 0); + L.sand.assign(n, 0); + const int footNear = ridgeHalf + acrossUnits(kFootGap), + footFar = footNear + acrossUnits(kFootDepth); + const int poolPeriod = alongUnits(kPoolSpacing), poolHalf = alongUnits(kPoolLength / 2.0); + for (int i = 0; i < n; ++i) + { + const int below = 65536 - phase[i]; // how far below the next ridge's crest + if (below >= footNear && below < footFar) + { + const int offset = along[i] % std::max(1, poolPeriod); + if (offset < poolHalf || poolPeriod - offset < poolHalf) + L.water[i] = 1; + } + } + // The sand starts two tiles behind the stone, not one: a sand corner spoils the tiles round it + // and stone stands only on pure grass, so sand touching the ridge would strip its lee row. + const std::vector shadow = upwindSteps(t, L.stone, L.windX, L.windY, o.leeWidth + 1); + for (int i = 0; i < n; ++i) + L.sand[i] = shadow[i] >= 2; + + // Homes: on a lattice, each slid across its valley to the middle of it (phase half a turn), so + // every home has the same ridge behind it and the same foot before it. The clearing round a home + // has no pools, no sand and no stone. + const LatticeSites lattice = + latticeSites(t.w, t.h, teams, context.bounded("rain-layout", std::uint32_t(t.w)), + context.bounded("rain-layout", std::uint32_t(t.h))); + const double nx = L.windX / std::hypot(L.windX, L.windY), + ny = L.windY / std::hypot(L.windX, L.windY); + L.clearing.assign(n, 0); + for (ShapePoint site : lattice.sites) + { + const int at = t.at(int(site.x), int(site.y)); + // Slide along the wind (the normal) by the phase's shortfall from half a turn. + const double shortfall = (32768 - phase[at]) / 65536.0 * L.spacing; + site.x = std::fmod(site.x + shortfall * nx + t.w, t.w); + site.y = std::fmod(site.y + shortfall * ny + t.h, t.h); + L.homes.push_back(site); + for (int i = 0; i < n; ++i) + if (std::hypot(t.offsetX(int(site.x), i % t.w), t.offsetY(int(site.y), i / t.w)) <= + L.homeRadius + kHomeMargin) + L.clearing[i] = 1; + } + double nearest = std::min(t.w, t.h); + for (size_t a = 0; a < L.homes.size(); ++a) + for (size_t b = a + 1; b < L.homes.size(); ++b) + nearest = + std::min(nearest, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), + t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + if (nearest < 2 * (L.homeRadius + kHomeMargin)) + { + L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; + return L; + } + for (int i = 0; i < n; ++i) + if (L.clearing[i]) + L.water[i] = L.sand[i] = L.stone[i] = 0; + L.homeOf.assign(n, -1); + const RadialShape home(L.homeRadius, 0.15, context, "rain-home"); + const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "rain-pond"); + L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); + return L; +} + +bool generate(Game &game, GenerationContext &context) +{ + context.stage = "rain shadow layout"; + const RainShadowOptions o(context.request); + const Layout L = design(context.request, context); + if (!L.failure.empty()) + { + context.detail = L.failure; + return false; + } + Map &map = game.map; + const Torus &t = L.t; + const int n = t.size(), teams = context.request.nbTeams; + for (int k = 0; k < teams; ++k) + game.addTeam(); + + context.stage = "rain shadow terrain"; + TerrainSketch terrain(n, GRASS); + for (int i = 0; i < n; ++i) + terrain[i] = L.water[i] ? WATER : L.sand[i] ? SAND : GRASS; + layBeaches(terrain, t); + writeUndermap(map, terrain); + // Stone stands only on pure grass; a ridge tile the beaches or the lee sand spoiled is left out, + // which is why the pools keep kFootGap from the stone and the sand lies on the far side. + for (int i = 0; i < n; ++i) + if (L.stone[i] && map.isResourceAllowed(i % t.w, i / t.w, STONE)) + map.setResource(i % t.w, i / t.w, STONE, 1); + + context.stage = "rain shadow colonies"; + const auto homeMask = [&](int team) + { + std::vector ground(size_t(n), 0); + for (int i = 0; i < n; ++i) + ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; + }; + const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; + if (!settleColonies(game, context, "rain-starts", homeMask, anchor)) + return false; + + context.stage = "rain shadow resources"; + const std::vector reserved = swarmSurroundings(t, context); + for (int k = 0; k < teams; ++k) + plantOpenHomeKit( + map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, + [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + // The fields: crops on the fertile ground, which the pools make the windward side of every valley + // (the growth probe reaches 15 tiles from water and refuses beside sand, so the lee band grows + // nothing). A third of the fertile ground under wheat and a fifth under wood is a working + // countryside, in patches so it can be walked and built on; outcrops are few, the ridges being + // stone enough. + const Fertility::Field fertility = Fertility::forMap(map, false); + const std::vector patch = periodicNoise(t.w, t.h, 10, context.stream("rain-patch")); + const std::vector split = periodicNoise(t.w, t.h, 5, context.stream("rain-split")); + const auto eligible = [&](int i) + { + return L.homeOf[i] < 0 && !reserved[i] && !L.stone[i] && clearGround(map, i % t.w, i / t.w); + }; + int fertile = 0; + for (int i = 0; i < n; ++i) + fertile += eligible(i) && fertility.at(i % t.w, i / t.w) > 0; + furnishGround( + map, t, context, fertility, eligible, [&](int i) { return float(patch[i]); }, + [&](int i) { return split[i]; }, + [&](int area) + { + return GroundAmounts{int(scaledCount(fertile * 30 / 100, o.wheat)), + int(scaledCount(fertile * 20 / 100, o.wood)), + int(scaledCount(area / 3000, o.stone)), + int(scaledCount(area / 2000, o.fruit))}; + }, + "rain-stone", "rain-fruit"); + seedAlgae(map, context, t, "rain-algae", o.algae, AlgaeBand::anyWater(30)); + secureStartingCrops(game, context, t, 24, 32, 0, &L.stone); + reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, + &L.stone); + // The passes join every valley, but the fields can close one; the cheapest cut through crops is + // opened, never through stone or water. + context.stage = "rain shadow routes"; + openColonyRoutes(map, context, t, StepCosts{1, 3, -1, -1, -1}); + return true; +} + +std::string validateWorld(const Game &game, const GenerationContext &context) +{ + GenerationContext replay(context.request); + const Layout L = design(context.request, replay); + const Map &map = game.map; + if (const std::string mismatch = designMismatch(L, map, "rain shadow"); !mismatch.empty()) + return mismatch; + const Torus &t = L.t; + for (int k = 0; k < context.request.nbTeams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s home has lost its pond."; + } + // Every designed ridge tile that could hold stone does: the ridges are the map's walls. + for (int i = 0; i < t.size(); ++i) + if (L.stone[i] && map.isGrass(i % t.w, i / t.w) && + map.getResource(i % t.w, i / t.w).type != STONE) + return "A ridge has lost its stone at (" + std::to_string(i % t.w) + ", " + + std::to_string(i / t.w) + ")."; + return walkFromFirstColony(map, context.request.nbTeams, "the valleys", "through the passes") + .error; +} +} // namespace + +RainShadowOptions::RainShadowOptions(const GenerationRequest &r) + : ridges(r.option("ridges")), slant(r.option("slant")), + ridgeThickness(r.option("ridge-thickness")), passSpacing(r.option("pass-spacing")), + passWidth(r.option("pass-width")), leeWidth(r.option("lee-width")), + homeSize(r.option("home-size")), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) +{ +} + +GeneratorDefinition rainShadowDefinition() +{ + return {"rain-shadow", + 27, + "Rain shadow", + 1, + false, + // Four ridges on a 256 map give 64-tile valleys: a 12-tile home, a pass every 48 tiles + // and a lee band of 6 leave a valley wide enough to farm and to fight in. Ridges three + // thick are sealed against diagonal steps and thin enough for a level-1 tower to shoot + // across (Walls.h's towerReach); passes five wide take a column of units and can be + // walled shut by whoever holds them. + {{"ridges", "Ridges", 2, 8, 1, 4, ControlGroup::Terrain}, + {"slant", "Slant", 0, 3, 1, 1, ControlGroup::Terrain}, + {"ridge-thickness", "Ridge thickness", 2, 6, 1, 3, ControlGroup::Terrain}, + {"pass-spacing", "Pass spacing", 24, 96, 8, 48, ControlGroup::Terrain}, + {"pass-width", "Pass width", 3, 9, 1, 5, ControlGroup::Terrain}, + {"lee-width", "Lee width", 2, 12, 1, 6, ControlGroup::Terrain}, + {"home-size", "Home size", 10, 20, 1, 12, ControlGroup::Layout}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; +} diff --git a/src/map/generator/generators/RainShadowGenerator.h b/src/map/generator/generators/RainShadowGenerator.h new file mode 100644 index 000000000..8dc01fa0c --- /dev/null +++ b/src/map/generator/generators/RainShadowGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct RainShadowOptions +{ + int ridges, slant, ridgeThickness, passSpacing, passWidth, leeWidth, homeSize; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts + explicit RainShadowOptions(const GenerationRequest &r); +}; +GeneratorDefinition rainShadowDefinition(); diff --git a/src/map/generator/shared/Biomes.cpp b/src/map/generator/shared/Biomes.cpp index 642682a67..8313deca1 100644 --- a/src/map/generator/shared/Biomes.cpp +++ b/src/map/generator/shared/Biomes.cpp @@ -15,8 +15,10 @@ BiomeKit fertilePlain() { BiomeKit kit; kit.name = "fertile plain"; - kit.pondsPer1000 = 2; - kit.pondTiles = 70; + // Four ponds of ninety tiles per 10000 (six on a quarter of a 256 map): a plain with a few big + // ponds, not a marsh; each pond waters a thirty-tile square round it. + kit.pondsPer10000 = 4; + kit.pondTiles = 90; kit.farmPerMille = 450; kit.outcropsPer1000 = 2; kit.grovesPer1000 = 1; @@ -27,7 +29,7 @@ BiomeKit stoneFortress() { BiomeKit kit; kit.name = "stone fortress"; - kit.pondsPer1000 = 1; + kit.pondsPer10000 = 2; kit.pondTiles = 40; kit.wallThickness = 2; kit.farmPerMille = 300; @@ -40,7 +42,7 @@ BiomeKit orchardIsland() { BiomeKit kit; kit.name = "orchard island"; - kit.pondsPer1000 = 1; + kit.pondsPer10000 = 1; kit.pondTiles = 260; kit.farmPerMille = 250; kit.outcropsPer1000 = 2; @@ -52,8 +54,8 @@ BiomeKit forest() { BiomeKit kit; kit.name = "forest"; - kit.pondsPer1000 = 2; - kit.pondTiles = 50; + kit.pondsPer10000 = 3; + kit.pondTiles = 60; kit.farmPerMille = 250; kit.woodPercent = 60; kit.outcropsPer1000 = 2; @@ -65,14 +67,14 @@ BiomeKit forest() namespace { // A kit's worth per tile before normalising, from the start scorer's weights: fertility (0.25) from -// how much of the ground its ponds water (a pond waters about thirty tiles square), wheat, wood and +// how much of the ground its ponds water (a pond waters about thirty tiles square, 900 tiles), wheat, wood and // their depth (0.56) from the farmland on that watered ground, room (0.10) from the open ground left, // stone (0.09) for having any, and a judgement for fruit, which the scorer doesn't measure but which // pulls enemy units across (GAME_RULES_FOR_MAP_DESIGN.md). double rawWorth(const BiomeKit &kit) { - const double pondShare = kit.pondsPer1000 * kit.pondTiles / 1000.0; - const double watered = std::min(1.0, kit.pondsPer1000 * 900 / 1000.0); + const double pondShare = kit.pondsPer10000 * kit.pondTiles / 10000.0; + const double watered = std::min(1.0, kit.pondsPer10000 * 900 / 10000.0); const double farm = kit.farmPerMille / 1000.0 * watered; const double open = std::max(0.0, 1 - pondShare - farm - kit.coverPercent / 100.0 * (1 - farm)); const double stone = kit.wallThickness > 0 || kit.outcropsPer1000 > 0 ? 1 : 0; @@ -105,7 +107,7 @@ BiomeTerrain sketchBiome(TerrainSketch &sketch, const Torus &t, for (int i = 0; i < n; ++i) if (inner[i]) candidates.push_back(i); - const int ponds = kit.pondsPer1000 > 0 ? std::max(1, tiles * kit.pondsPer1000 / 1000) : 0; + const int ponds = kit.pondsPer10000 > 0 ? std::max(1, tiles * kit.pondsPer10000 / 10000) : 0; const double pondRadius = std::sqrt(std::max(1, kit.pondTiles) / kPi); const int spacing2 = int(std::lround(9 * pondRadius * pondRadius)); std::vector seeds; diff --git a/src/map/generator/shared/Biomes.h b/src/map/generator/shared/Biomes.h index a3a7b9733..21c1e9920 100644 --- a/src/map/generator/shared/Biomes.h +++ b/src/map/generator/shared/Biomes.h @@ -22,7 +22,7 @@ namespace MapGeneration struct BiomeKit { std::string name; - int pondsPer1000 = 0; // ponds per 1000 tiles of ground (at least one when above 0) + int pondsPer10000 = 0; // ponds per 10000 tiles of ground (at least one when above 0) int pondTiles = 0; // each pond's size in tiles int wallThickness = 0; // a ring of stone round the ground this thick; 0 for none int farmPerMille = 0; // of the ground's fertile tiles, the share under wheat and wood @@ -34,7 +34,7 @@ struct BiomeKit }; /// The kits a map can start from. -BiomeKit fertilePlain(); // big ponds, wide farmland, open ground +BiomeKit fertilePlain(); // a few big ponds, wide farmland, open ground BiomeKit stoneFortress(); // a thick stone ring and outcrops, poor water BiomeKit orchardIsland(); // a lake with an island of all three fruits, little farmland BiomeKit forest(); // most of the ground under wood, ponds to keep it growing diff --git a/src/map/generator/shared/Homes.h b/src/map/generator/shared/Homes.h index c5b6cffa0..c7cb235cb 100644 --- a/src/map/generator/shared/Homes.h +++ b/src/map/generator/shared/Homes.h @@ -106,6 +106,42 @@ void plantHomeKit(Map &map, const Torus &t, GenerationContext &context, ShapePoi plantKit(map, t, context, kit, eligible); } +/// Round homes at given sites, all turned to `axis`: each stamped with stampRoundHome (its tiles marked +/// with the home's index in `homeOf`, its `ponds` ponds into `pondMask`), and the point its kit goes +/// returned per home. Homes on a lattice with no centre (Orbits.h's latticeSites) use axis 0, so exact +/// copies of one another stay exact. +inline std::vector stampRoundHomes(const Torus &t, const std::vector &sites, + double axis, const RadialShape &home, double radius, + int ponds, const RadialShape *pond, + std::vector &pondMask, + std::vector &homeOf) +{ + std::vector kits; + for (size_t k = 0; k < sites.size(); ++k) + kits.push_back(stampRoundHome(t, sites[k], axis, home, radius, ponds, pond, pondMask, + [&](int i) { homeOf[i] = int(k); })); + return kits; +} + +/// A round home's starter kit with a quarry, for a home that is not walled in stone: wheat and wood +/// either side of `kitCentre` as plantHomeKit lays them, and a stone clump of `quarry` radius beyond +/// the pond on the far side from the swarm. +template +void plantOpenHomeKit(Map &map, const Torus &t, GenerationContext &context, ShapePoint kitCentre, + double axis, double homeRadius, int wheat, int wood, int quarry, + Eligible eligible) +{ + const KitFrame frame{int(std::lround(kitCentre.x)), int(std::lround(kitCentre.y)), axis}; + const double reach = homePondRadius(homeRadius) * 1.2 + 3; + const Kit kit{frame.at(-0.4 * reach, -reach, 12), + frame.at(-0.4 * reach, reach, 12), + frame.at(reach + 3, 0, 10), + wheat, + wood, + quarry}; + plantKit(map, t, context, kit, eligible); +} + /// A home in ground of any shape: a Voronoi cell, a chamber, a town block, a territory. The swarm stands /// the same walk from the home's way in as every other colony's (siteAtDepth over walking steps from the /// `door` tiles through `region`), on the roomiest such tile; `axis` points from the door towards the diff --git a/src/map/generator/shared/Morphology.cpp b/src/map/generator/shared/Morphology.cpp index ab4668586..159d0a3cc 100644 --- a/src/map/generator/shared/Morphology.cpp +++ b/src/map/generator/shared/Morphology.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "Morphology.h" #include +#include #include namespace MapGeneration { @@ -79,6 +80,35 @@ void distanceLine(const std::vector &f, std::vector out[q - n] = d * d + value(v[j]); } } +// The minimum over the 2r + 1 entries centred on each position of a cyclic line: a monotonic deque +// of candidates, each entry pushed and popped once. +void slideMin(const std::vector &line, int radius, std::vector &out) +{ + const int n = int(line.size()); + out.assign(line.size(), 0); + if (2 * radius + 1 >= n) + { + std::fill(out.begin(), out.end(), *std::min_element(line.begin(), line.end())); + return; + } + std::deque candidates; // positions (unwrapped), values nondecreasing front to back + const auto push = [&](int position) + { + const int v = line[((position % n) + n) % n]; + while (!candidates.empty() && line[((candidates.back() % n) + n) % n] >= v) + candidates.pop_back(); + candidates.push_back(position); + }; + for (int d = -radius; d <= radius; ++d) + push(d); + for (int i = 0; i < n; ++i) + { + out[i] = line[((candidates.front() % n) + n) % n]; + if (candidates.front() == i - radius) + candidates.pop_front(); + push(i + radius + 1); + } +} } // namespace std::vector dilate(const Torus &t, const std::vector &mask, @@ -254,4 +284,28 @@ std::vector slivers(const Torus &t, const std::vector windowMinimum(const Torus &t, const std::vector &field, int radius) +{ + std::vector rows(field.size()), result(field.size()), line, out; + line.resize(t.w); + for (int y = 0; y < t.h; ++y) + { + for (int x = 0; x < t.w; ++x) + line[x] = field[size_t(y) * t.w + x]; + slideMin(line, radius, out); + for (int x = 0; x < t.w; ++x) + rows[size_t(y) * t.w + x] = out[x]; + } + line.resize(t.h); + for (int x = 0; x < t.w; ++x) + { + for (int y = 0; y < t.h; ++y) + line[y] = rows[size_t(y) * t.w + x]; + slideMin(line, radius, out); + for (int y = 0; y < t.h; ++y) + result[size_t(y) * t.w + x] = out[y]; + } + return result; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Morphology.h b/src/map/generator/shared/Morphology.h index cff4dd46c..bd7ba41af 100644 --- a/src/map/generator/shared/Morphology.h +++ b/src/map/generator/shared/Morphology.h @@ -64,4 +64,10 @@ int narrowestPassage(const Torus &, const std::vector &mask, /// up. std::vector slivers(const Torus &, const std::vector &mask, int minimumClearance); + +/// The least value of an integer field within `radius` Chebyshev steps (a square) of every tile, +/// across the wrap: a running minimum along rows then columns, so the cost does not grow with the +/// radius. A tile equal to its window's minimum is a local minimum with nothing lower within the +/// radius: pools at the bottoms of a field's troughs, spaced at least the radius apart. +std::vector windowMinimum(const Torus &, const std::vector &field, int radius); } // namespace MapGeneration diff --git a/src/map/generator/shared/Orbits.cpp b/src/map/generator/shared/Orbits.cpp index e88e2b714..e1c54c10b 100644 --- a/src/map/generator/shared/Orbits.cpp +++ b/src/map/generator/shared/Orbits.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace MapGeneration { namespace @@ -134,6 +135,18 @@ Symmetry translationSymmetry(int width, int height, int teams, long long *spacin return best; } +int latticePeriod(const Symmetry &s) +{ + int period = std::min(s.width, s.height); + for (const Isometry &g : s.elements) + { + if (g.a != 1 || g.b != 0 || g.c != 0 || g.d != 1) + return std::min(s.width, s.height); + period = std::gcd(period, std::gcd(std::abs(g.tx), std::abs(g.ty))); + } + return std::max(1, period); +} + LatticeSites latticeSites(int width, int height, int teams, double x0, double y0) { LatticeSites result; diff --git a/src/map/generator/shared/Orbits.h b/src/map/generator/shared/Orbits.h index 6ee56082b..0d86fb554 100644 --- a/src/map/generator/shared/Orbits.h +++ b/src/map/generator/shared/Orbits.h @@ -67,6 +67,13 @@ Symmetry pointSymmetry(int width, int height, int teams); /// receives the squared distance between the nearest two copies. Symmetry translationSymmetry(int width, int height, int teams, long long *spacing2 = nullptr); +/// The coarsest noise cell (in tiles) that every element of a translation group leaves unchanged: the +/// greatest common divisor of the map's sides and every element's move. Periodic noise on cells of this +/// size (LatticeNoise.h) is the same at every image of a tile, so a design that warps its borders with +/// it stays an exact copy of itself at every colony without summing anything over orbits (which would +/// average the noise away). For a point symmetry, or an empty group, the shorter side. +int latticePeriod(const Symmetry &); + /// Where `teams` colonies go on a lattice across the torus, one per site, starting from (x0, y0). /// `exact` when translationSymmetry serves the count, and then site k is element k's image of site 0; /// otherwise equal rows of evenly spaced sites, as many rows as brings the spacing between rows diff --git a/src/map/generator/shared/Patterns.cpp b/src/map/generator/shared/Patterns.cpp index b0af52f8e..4d3746293 100644 --- a/src/map/generator/shared/Patterns.cpp +++ b/src/map/generator/shared/Patterns.cpp @@ -126,6 +126,23 @@ std::vector stripePhase(const Torus &t, const StripeStyle &style, std::mt19 return phase; } +StripeStyle alongStripes(const Torus &t, const StripeStyle &across) +{ + StripeStyle along; + along.warpPercent = 0; + if (t.w >= t.h) + { + along.acrossX = across.acrossY * (t.w / t.h); + along.acrossY = -across.acrossX; + } + else + { + along.acrossX = across.acrossY; + along.acrossY = -across.acrossX * (t.h / t.w); + } + return along; +} + double stripeSpacing(const Torus &t, const StripeStyle &style) { const double kx = double(style.acrossX) / t.w, ky = double(style.acrossY) / t.h; diff --git a/src/map/generator/shared/Patterns.h b/src/map/generator/shared/Patterns.h index 89b780d62..e984b6353 100644 --- a/src/map/generator/shared/Patterns.h +++ b/src/map/generator/shared/Patterns.h @@ -41,6 +41,13 @@ struct StripeStyle }; std::vector stripePhase(const Torus &, const StripeStyle &, std::mt19937 &); +/// The phase along the stripes: a second stripe field whose gradient runs perpendicular to the +/// first's, also exactly periodic on the torus, so anything laid out along the stripes by it (gaps, +/// pools, dykes) repeats seamlessly. The first field's normal is (a / w, b / h); a vector along its +/// stripes is (b / h, -a / w), scaled by whichever of w and h is larger to keep the turn counts whole. +/// Unwarped: the warp belongs to the field it was drawn for. +StripeStyle alongStripes(const Torus &, const StripeStyle &across); + /// The distance between neighbouring stripes, in tiles. double stripeSpacing(const Torus &, const StripeStyle &); diff --git a/src/map/generator/shared/Points.cpp b/src/map/generator/shared/Points.cpp index b09c45b41..4ec088fcd 100644 --- a/src/map/generator/shared/Points.cpp +++ b/src/map/generator/shared/Points.cpp @@ -79,6 +79,13 @@ std::vector nearestSiteLabels(const Torus &t, const std::vector &site const int period = std::max(1, spacing * warpPeriodPercent / 100); const std::vector warpX = fractalNoise(t.w, t.h, period, 3, context.stream(stream)); const std::vector warpY = fractalNoise(t.w, t.h, period, 3, context.stream(stream)); + return nearestSiteLabels(t, sites, spacing, warpX, warpY, warpPercent); +} + +std::vector nearestSiteLabels(const Torus &t, const std::vector &sites, int spacing, + const std::vector &warpX, const std::vector &warpY, + int warpPercent) +{ const std::int64_t amplitude = std::int64_t(spacing) * 16 * warpPercent / 100; const int W = t.w * 16, H = t.h * 16; const int gx = std::max(1, t.w / spacing), gy = std::max(1, t.h / spacing); diff --git a/src/map/generator/shared/Points.h b/src/map/generator/shared/Points.h index 4607ab8c9..f57da805b 100644 --- a/src/map/generator/shared/Points.h +++ b/src/map/generator/shared/Points.h @@ -34,6 +34,13 @@ std::vector nearestSiteLabels(const Torus &, const std::vector &sites GenerationContext &, const std::string &stream, int warpPeriodPercent = 150, int warpPercent = 30); +/// The same labelling with the caller's own warp fields (two fields of 0..65535 per tile, as +/// periodicNoise or fractalNoise give them, or an orbitSum of one so a symmetric map gets symmetric +/// cells), each tile moved by up to `warpPercent` of the spacing. +std::vector nearestSiteLabels(const Torus &, const std::vector &sites, int spacing, + const std::vector &warpX, const std::vector &warpY, + int warpPercent); + /// Lloyd relaxation on the torus: each site moves to the centroid of the tiles nearest it (measured the /// short way round from the site), `iterations` times, so cells grow more even in size. Cheap enough for /// a few dozen sites; the search is exhaustive. diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index ad9637181..f5dd64092 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -192,3 +192,75 @@ macos-arm64 24 1 8 8 4 3 ok 285345205556791712 macos-arm64 24 1 8 8 8 1 ok 12174400163941988640 macos-arm64 24 1 9 8 4 1 ok 10026254367617799143 macos-arm64 24 1 9 9 4 1 ok 7691831584358967898 +macos-arm64 25 1 7 7 4 1 ok 8202260453455897702 +macos-arm64 25 1 8 8 2 1 ok 5251912485654861084 +macos-arm64 25 1 8 8 4 1 ok 520175664546379446 +macos-arm64 25 1 8 8 4 2 ok 4789748468574587495 +macos-arm64 25 1 8 8 4 3 ok 3449146876170199206 +macos-arm64 25 1 8 8 8 1 ok 1762371246384647436 +macos-arm64 25 1 9 8 4 1 ok 4711219588934138776 +macos-arm64 25 1 9 9 4 1 ok 10597102770546440263 +macos-arm64 26 1 7 7 4 1 ok 789646270326602504 +macos-arm64 26 1 8 8 2 1 ok 13763197687203102930 +macos-arm64 26 1 8 8 4 1 ok 4144666139421409147 +macos-arm64 26 1 8 8 4 2 ok 9339544561056119274 +macos-arm64 26 1 8 8 4 3 ok 3960702991416737426 +macos-arm64 26 1 8 8 8 1 ok 3818680882719787045 +macos-arm64 26 1 9 8 4 1 ok 8315516405593883041 +macos-arm64 26 1 9 9 4 1 ok 2082514701038793427 +macos-arm64 27 1 7 7 4 1 ok 10833365171923315257 +macos-arm64 27 1 8 8 2 1 ok 9247033084915382178 +macos-arm64 27 1 8 8 4 1 ok 1309570096415202470 +macos-arm64 27 1 8 8 4 2 ok 15608309425939438304 +macos-arm64 27 1 8 8 4 3 ok 15117378251408628476 +macos-arm64 27 1 8 8 8 1 ok 2092448838222506393 +macos-arm64 27 1 9 8 4 1 ok 1645009079489818947 +macos-arm64 27 1 9 9 4 1 ok 11877117867324334076 +macos-arm64 28 1 7 7 4 1 ok 10075553509688210940 +macos-arm64 28 1 8 8 2 1 ok 4856385324554745939 +macos-arm64 28 1 8 8 4 1 ok 10568332471141104940 +macos-arm64 28 1 8 8 4 2 ok 2824909142385618385 +macos-arm64 28 1 8 8 4 3 ok 8439701057196043041 +macos-arm64 28 1 8 8 8 1 ok 10981617522424806419 +macos-arm64 28 1 9 8 4 1 ok 11098418973656963763 +macos-arm64 28 1 9 9 4 1 ok 1123474046055604534 +macos-arm64 29 1 7 7 4 1 ok 4971400333522175054 +macos-arm64 29 1 8 8 2 1 ok 7612660082359254337 +macos-arm64 29 1 8 8 4 1 ok 2450863270395321431 +macos-arm64 29 1 8 8 4 2 ok 6736902494964553678 +macos-arm64 29 1 8 8 4 3 ok 18098183166195859585 +macos-arm64 29 1 8 8 8 1 ok 5930810053163922709 +macos-arm64 29 1 9 8 4 1 ok 3405819186314566499 +macos-arm64 29 1 9 9 4 1 ok 15824681712775267268 +macos-arm64 30 1 7 7 4 1 ok 10983756988564259189 +macos-arm64 30 1 8 8 2 1 ok 10864260481877802293 +macos-arm64 30 1 8 8 4 1 ok 8566090570972606806 +macos-arm64 30 1 8 8 4 2 ok 15454657613982169512 +macos-arm64 30 1 8 8 4 3 ok 17544008518965759461 +macos-arm64 30 1 8 8 8 1 ok 7267460019722476921 +macos-arm64 30 1 9 8 4 1 ok 1546476112144531604 +macos-arm64 30 1 9 9 4 1 ok 8553529852829638885 +macos-arm64 31 1 7 7 4 1 ok 12038632853119546767 +macos-arm64 31 1 8 8 2 1 ok 9039465495167751651 +macos-arm64 31 1 8 8 4 1 ok 5701764993649211609 +macos-arm64 31 1 8 8 4 2 ok 14643276734609589178 +macos-arm64 31 1 8 8 4 3 ok 11885043574201865905 +macos-arm64 31 1 8 8 8 1 ok 4867502529471531306 +macos-arm64 31 1 9 8 4 1 ok 12900263061264423478 +macos-arm64 31 1 9 9 4 1 ok 9240200539837525202 +macos-arm64 32 1 7 7 4 1 ok 10782923323802482804 +macos-arm64 32 1 8 8 2 1 ok 13146252309794736792 +macos-arm64 32 1 8 8 4 1 ok 11294612668479149179 +macos-arm64 32 1 8 8 4 2 ok 8787608960536469551 +macos-arm64 32 1 8 8 4 3 ok 14764557334209712283 +macos-arm64 32 1 8 8 8 1 ok 11568163353518237153 +macos-arm64 32 1 9 8 4 1 ok 5989731686133725586 +macos-arm64 32 1 9 9 4 1 ok 860619065683065594 +macos-arm64 33 1 7 7 4 1 ok 8691360005280724925 +macos-arm64 33 1 8 8 2 1 ok 6681693907801693688 +macos-arm64 33 1 8 8 4 1 ok 9400400174102501753 +macos-arm64 33 1 8 8 4 2 ok 12597503260781819139 +macos-arm64 33 1 8 8 4 3 ok 9614762065543089229 +macos-arm64 33 1 8 8 8 1 ok 2497376319729488020 +macos-arm64 33 1 9 8 4 1 ok 818754618973904098 +macos-arm64 33 1 9 9 4 1 ok 14036216783204707805 From 6536aa6f0c33d11295acc6729b1eb05c260dfdf7 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 19:17:30 -0400 Subject: [PATCH 116/136] Revise Old growth, Anthill and Old town from first play Feedback from the first play of each, quoted in the code as FEEDBACK 2026-09-13 comments so later changes can see what was asked: - Old growth (revision 2): "clearing the wood hurts workers ... so little water for food". Homes default to radius 20 (was 13) with a ring of five pools round the central pond (home-pools), wheat on every shore and no wood inside the clearing, and a two-tile sand ring round it so the forest never spreads back in; wood is cut from the forest edge just past the sand. - Anthill (revision 2): "doesn't provide enough water ... AIs are putting buildings into the channels and trapping themselves ... every chamber should have something". Every chamber is dug bigger (size 7, was 5) with a pond at its middle; a sand road runs down every tunnel to the pond's beach at each end (sand-roads); farm chambers ring their pond with wheat, other chambers alternate wheat and wood, dead ends keep fruit and wheat; queen rooms grow to 60 sites (was 40). - Old town (revision 2): "unfair accessibility for the outlying farm lands ... pools 2x bigger ... more fruit". The wall and gates are gone; farm-plots (3 per colony) are stamped through the fields with the new shared stampFarmPlot, extracted from layFarm (byte-identical there); the fields carry no outcrops and a third of the wood; every plaza's pool fills its block to a tile short of the streets, with a fruit grove beside each and the full orchard at the cathedral. Verified on macOS arm64: golden table regenerated at the new revisions (264 rows, 0 failures), --sweep 0 failing combinations, defaults test, strict translation check, client build. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 4 + data/texts.br.txt | 4 + data/texts.ca.txt | 4 + data/texts.cz.txt | 4 + data/texts.de.txt | 4 + data/texts.dk.txt | 4 + data/texts.en.txt | 4 + data/texts.eo.txt | 4 + data/texts.es.txt | 4 + data/texts.eu.txt | 4 + data/texts.fa.txt | 4 + data/texts.fi.txt | 4 + data/texts.fr.txt | 4 + data/texts.gr.txt | 4 + data/texts.hu.txt | 4 + data/texts.id.txt | 4 + data/texts.it.txt | 4 + data/texts.ja.txt | 4 + data/texts.keys.txt | 2 + data/texts.ko.txt | 4 + data/texts.nl.txt | 4 + data/texts.pl.txt | 4 + data/texts.pt.txt | 4 + data/texts.ro.txt | 4 + data/texts.ru.txt | 4 + data/texts.si.txt | 4 + data/texts.sk.txt | 4 + data/texts.sr.txt | 4 + data/texts.sv.txt | 4 + data/texts.tr.txt | 4 + data/texts.uk.txt | 4 + data/texts.vi.txt | 4 + data/texts.zh-cn.txt | 4 + data/texts.zh-tw.txt | 4 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 57 +++-- .../generator/generators/AnthillGenerator.cpp | 214 +++++++++++++----- .../generator/generators/AnthillGenerator.h | 1 + .../generators/OldGrowthGenerator.cpp | 117 ++++++++-- .../generator/generators/OldGrowthGenerator.h | 2 +- .../generator/generators/OldTownGenerator.cpp | 174 +++++++++----- .../generator/generators/OldTownGenerator.h | 2 +- src/map/generator/shared/Farmland.cpp | 55 +++-- src/map/generator/shared/Farmland.h | 7 + test/map-generator-golden.txt | 48 ++-- 44 files changed, 594 insertions(+), 217 deletions(-) diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 8d6889608..9c4902df7 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2164,3 +2164,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.br.txt b/data/texts.br.txt index a052c9c27..e35ad973d 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -2162,3 +2162,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.ca.txt b/data/texts.ca.txt index fdaca11f1..0d7a0a971 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2174,3 +2174,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 598891132..835bddef6 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.de.txt b/data/texts.de.txt index 23b57ff84..eb6e2ece1 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.dk.txt b/data/texts.dk.txt index 4ae3ba17d..f6a0f3cc6 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2242,3 +2242,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.en.txt b/data/texts.en.txt index 91c385bbe..5ebd2ce9c 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2156,3 +2156,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 38197a7b2..6c94aebc0 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2164,3 +2164,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.es.txt b/data/texts.es.txt index 700dc17a0..4613054ca 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.eu.txt b/data/texts.eu.txt index f383eeacf..cc2146b23 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2176,3 +2176,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 2e104258b..147cc3604 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2164,3 +2164,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 3fe78f836..690214ee8 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 16a447a63..78752de06 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2176,3 +2176,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 26e1f97f7..0a9a42b63 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2236,3 +2236,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 3ea571d50..bc33a29b4 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.id.txt b/data/texts.id.txt index 2cd1bdb31..a90dfb2bf 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2162,3 +2162,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.it.txt b/data/texts.it.txt index 8faa58912..0f6b732ad 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2228,3 +2228,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 4605735cb..bf10af336 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2162,3 +2162,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 88ad56537..dcbb2c528 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1076,3 +1076,5 @@ [Queen room] [Tunnel width] [Loops] +[Home pools] +[Farm plots] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index a3fc4e493..ad50ee35a 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2162,3 +2162,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 7d2affb7d..1fc2a1afd 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2190,3 +2190,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 954369de8..b9651c3b9 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.pt.txt b/data/texts.pt.txt index b029a939a..b33d5ee20 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2172,3 +2172,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 486a6b5c8..63721a53c 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.ru.txt b/data/texts.ru.txt index e9bbb69ea..e1cc32935 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2164,3 +2164,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.si.txt b/data/texts.si.txt index 678f38292..445d3dafe 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 414b7593e..17b89e321 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2174,3 +2174,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.sr.txt b/data/texts.sr.txt index de6cbb77d..e8866447e 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2166,3 +2166,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 8371ef65d..191303284 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2176,3 +2176,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.tr.txt b/data/texts.tr.txt index ce9df7111..420900900 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2174,3 +2174,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 365889a49..045624279 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2162,3 +2162,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.vi.txt b/data/texts.vi.txt index d637cbc65..643dcadbe 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2162,3 +2162,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 978622773..1f6700deb 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2162,3 +2162,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index dc56410e0..76c9a6ea0 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2238,3 +2238,7 @@ Queen room Tunnel width [Loops] Loops +[Home pools] +Home pools +[Farm plots] +Farm plots diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 2074fe733..311808b21 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -1002,11 +1002,18 @@ at intervals staggered from ridge to ridge, so moving along a valley is easy and ## Old growth -A dry continent under unbroken forest: every colony starts in a clearing with its own pond and kit, -and beyond the clearing stands wood in every direction. There is no other water but a few lakes far from +A dry continent under unbroken forest: every colony starts in a big clearing with a pond, a ring of +pools, wheat on every shore and a ring of sand round it, and beyond the sand stands wood in every +direction. There is no other water but a few lakes far from every home, so almost none of the forest ever grows back: what a colony cuts stays cut, the map opens as the game goes on, and contact happens only where someone has cut through. +- **Homes.** Radius `home-size` (12-24, 20) with `home-pools` (0-8, 5) pools of radius 2.5 on a ring + at 60% of it, so the whole clearing is within the growth probe's reach of water; two wheat patches + and a quarry by the central pond and 12 wheat on every pool's shore, and no wood inside (first play: + "clearing the wood hurts workers"; the forest edge is the woodlot, within the crop guarantee's 32 + steps). A two-tile ring of sand round the clearing keeps the forest from spreading in, since crops + spread only onto grass. - **Forest.** Wood on `forest-density` percent (60-100, 90) of the forest ground, the gaps drawn from a noise field so they are small openings; below the 8-connected site percolation threshold (about 41% open) the openings never join into a way through, so at any density offered the forest is a wall @@ -1076,30 +1083,36 @@ until someone can swim. ## Old town -A walled city of stone blocks and narrow grass streets, farmland outside the wall. Streets are buildable -and buildings block walking, so every building a player puts up closes a street: the players build the -city's fortifications themselves, and a tower on a street fires over the block into the next. +A city of stone blocks and narrow grass streets with farmland all round it, open on every side. +Streets are buildable and buildings block walking, so every building a player puts up closes a street: +the players build the city's fortifications themselves, and a tower on a street fires over the block +into the next. - **City.** A disc of `city-size` percent of the half side (40-90, 70), tiled into blocks of `block-size` (10-20, 14) warped by `warp` (0-100, 50); the band `street-width` (3-7, 4) along every - cell border is street, everything else in a city cell is a stone block. A wall two thick with `gates` - (2-8, 4) evenly spaced from a random start rings the city (`ringWithGates`), a ring road inside it. -- **Plazas.** Blocks left open, with a fountain pond. A home plaza is two adjoining blocks (a single block + cell border is street, everything else in a city cell is a stone block. There is no wall: the first + play found a walled city unfair to whoever started far from a gate. +- **Plazas.** Blocks left open, each filled by a fountain pond to a tile short of its streets (radius + 4.0 at the default block and street, two and a half times the first build's), with a grove of one + fruit in turn beside every plaza's pool. A home plaza is two adjoining blocks (a single block of 14 less its streets, with a fountain in it, has no room for a 4x4 swarm; measured: every settlement failed), the fountain in one and the swarm in the other, the pairs as far apart as the city allows; `plazas` (0-4, 2) more per colony farthest from those, given up first when the city is small; and the cathedral square at the cell holding the centre, with an orchard of the three fruits round its fountain. -- **Fields.** Outside the wall, farm rows along the map's axis (`layFarm` at `bestFarmRows`, bridged - every 16), half their fertile ground under wheat and a sixth under wood; the plazas inside get a light - share so they stay open. Every kit has no quarry: the blocks are stone. -- **Checked, not assumed.** Every fountain present, the wall unbroken but for its gates, every colony - walkable from the first through the streets. +- **Fields.** Outside the city, farm rows along the map's axis (`layFarm` at `bestFarmRows`, bridged + every 16) with `farm-plots` (0-6, 3) 10x4 building plots per colony spread through them + (`stampFarmPlot`, each the farthest a plot can stand from the city and the plots before it), half + their fertile ground under wheat and a twentieth under wood, no outcrops; the plazas inside get a + light share so they stay open. Every kit has no quarry: the blocks are stone. +- **Checked, not assumed.** Every fountain present, every colony walkable from the first through the + streets. ## Anthill -Solid stone carved into chambers joined by winding tunnels. Every colony starts in a queen chamber, a -cul-de-sac with one door and a pond; the other chambers are farm chambers with a pond and a field, or -dead-end treasure chambers with fruit and wheat. Stone is everywhere and never runs out, but it cannot be +Solid stone carved into chambers joined by winding tunnels, every chamber with a pond at its middle +and a sand road down every tunnel to the pond at each end. Every colony starts in a queen chamber, a +cul-de-sac with one door; the other chambers are farm chambers with a ring of wheat round the pond, +plain chambers with a wheat or wood patch in turn, or dead-end treasure chambers with fruit and wheat. Stone is everywhere and never runs out, but it cannot be built on or cleared, so room is the one scarce thing: a chamber holds a few buildings, and growing means taking the next chamber down the tunnel. @@ -1107,11 +1120,15 @@ taking the next chamber down the tunnel. cells the graph (`cellGraph` over `siteNeighbours`); a spanning tree through every chamber but the queens' (`carveSpanningTree`), one door into each queen chamber, and `loops` percent (0-60, 20) of the chambers' count in extra tunnels. Every open edge is a wandering tunnel `tunnel-width` wide (2-4, - 3) between its chambers' middles (`carveCorridor`); every chamber a rough disc of `chamber-size` - (4-8, 5), farm chambers two bigger, each turned by the golden angle so one outline reads as many. + 3) between its chambers' middles (`wanderingPath`), with a sand road traced down its middle that + stops on the pond's beach at each end (`sand-roads`, on: a sand corner spoils the tiles round it, so + nothing can be built across a tunnel; first play had AIs walling themselves in); every chamber a + rough disc of `chamber-size` (5-10, 7), farm chambers two bigger, queen chambers three, each turned + by the golden angle so one outline reads as many. Every chamber's pond is 40% of the chamber's + radius (a tile more for a farm, half a tile for a queen). - **Kinds.** Dead ends other than the queens' are treasure chambers; of the rest every other one is a - farm chamber with a pond and a field. A queen chamber's pond lies on the far side from its door and - its swarm towards the door, and it is grown until it holds `queen-room` building sites (20-120, 40: + farm chamber. A queen chamber's swarm stands between its pond and its door, and the chamber is grown + until it holds `queen-room` building sites (20-160, 60: overlapping 4x4 footprints, the start scorer's measure) with `growUntilSites`, within its own cell and never onto a pond's beach, so no colony starts with more room than another. - **Rock.** Stone on every uncarved tile the beaches left pure grass; `openColonyRoutes` clears crops diff --git a/src/map/generator/generators/AnthillGenerator.cpp b/src/map/generator/generators/AnthillGenerator.cpp index 0f0365c33..09f75c14a 100644 --- a/src/map/generator/generators/AnthillGenerator.cpp +++ b/src/map/generator/generators/AnthillGenerator.cpp @@ -41,25 +41,51 @@ using namespace MapGeneration; // farm chambers are the only water, so a colony that wants its fields to regrow holds a farm // chamber; and a tower scans square rings with no line of sight, so the rock between two tunnels // is no cover at all. +// +// FEEDBACK 2026-09-13 (first play): "visually striking ... but the map currently just doesn't provide +// enough water to make a playable game. the buildable area restriction feels real, maybe a bit too +// tight. ... each of the nodes just needs to dig out a larger hole with a larger pond of water in the +// center ... the default channels are so narrow that AIs are putting buildings into them and +// trapping themselves. maybe we add sand roads to all of the narrow ant tunnels to make sure they +// stay open? if the sand roads connect all the way into the water pool in the center, and we make +// sure that we stamp all of the area around the lake with resources (so there are no sections +// empty of growth), it could work really nicely. and let's make sure that every chamber actually +// has something - either wheat, wood, fruit." So: every chamber, whatever its kind, is dug bigger +// (default size 7, was 5) with a pond at its middle sized to it; a sand road runs down every tunnel +// and on to the shore of the pond at each end (a sand corner spoils the tiles round it for building, +// so nothing can be built across a tunnel); a farm chamber's pond is ringed with wheat; every other +// chamber alternates a wheat and a wood patch; queen chambers grow to 60 sites (was 40). namespace { -// The queen chamber's pond (radius 2, a dozen corners of water with a beach a unit walks round) and -// its kit, unscaled whatever the amounts say: wheat and wood either side of the pond. No quarry: the -// walls are stone. -constexpr double kPondRadius = 2.0; -constexpr int kHomeWheat = 12, kHomeWood = 10; +// Every chamber's pond sits at its middle (FEEDBACK 2026-09-13), its radius this share of the +// chamber's: 2.8 at the default size 7, about 25 corners of water, which the growth probe finds from +// the whole chamber. A farm chamber's pond is a tile bigger, a queen chamber's half a tile. +constexpr double kPondShare = 0.4, kFarmPondExtra = 1.0, kQueenPondExtra = 0.5; +double pondRadiusFor(int chamberSize) +{ + return std::max(2.0, kPondShare * chamberSize); +} +// The queen chamber's kit, unscaled whatever the amounts say: wheat and wood either side of the pond. +// No quarry: the walls are stone. +constexpr int kHomeWheat = 14, kHomeWood = 10; // A farm chamber is this much bigger than an ordinary chamber, to hold its pond and field; its field -// is this much wheat and wood. -constexpr int kFarmExtra = 2, kFarmWheat = 12, kFarmWood = 8; -// A treasure chamber holds a fruit grove of radius 1 (5 tiles) and this much wheat. -constexpr int kTreasureWheat = 8; +// is a ring of wheat round the whole pond (FEEDBACK 2026-09-13, "no sections empty of growth": the +// ring two to four tiles out from a pond of radius 4 is about 60 tiles) and a wood patch beyond. +constexpr int kFarmExtra = 2, kFarmWheat = 40, kFarmWood = 12; +// Every other chamber holds a wheat patch or a wood patch in turn (FEEDBACK 2026-09-13, "every +// chamber actually has something"); a treasure chamber holds a fruit grove of radius 1 (5 tiles) and +// this much wheat. +constexpr int kPlainWheat = 16, kPlainWood = 12, kTreasureWheat = 12; +// A tunnel's sand road stops this far past the pond's radius at each end: on the pond's beach, so it +// joins the water without cutting into it. +constexpr double kRoadStop = 1.5; // Tunnels wander sideways by this share of the chamber spacing, and swell and narrow by this share // of their width, so they read as dug rather than drawn. constexpr double kWanderShare = 0.18, kWidthJitter = 0.25; // The queen chamber starts this much bigger than an ordinary chamber and grows until it holds // `queenRoom` building sites, never more than this many tiles. -constexpr int kQueenExtra = 1, kQueenGrowthLimit = 300; +constexpr int kQueenExtra = 3, kQueenGrowthLimit = 400; struct Layout { @@ -67,7 +93,8 @@ struct Layout std::vector sites; std::vector label; // every tile's nearest site std::vector homeSite, farmSite, treasureSite; - std::vector open, water; // tunnels and chambers; ponds + std::vector open, water, road; // tunnels and chambers; ponds; sand roads + std::vector pondRadius; // every chamber's pond std::vector homeOf; std::vector homes, kits; std::vector axes; // each home's facing: away from its door @@ -119,28 +146,51 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (!isPocket[site] && !isEnd[site] && site % 2 == 0) L.farmSite.push_back(site); - // Carving: every open edge a wandering tunnel between its two chambers' middles, then every - // chamber a rough disc, queen and farm chambers bigger. + // Every chamber's kind sets its size and its pond: 0 plain, 1 farm, 2 queen. + std::vector kind(L.sites.size(), 0); + for (int site : L.farmSite) + kind[site] = 1; + for (int site : L.homeSite) + kind[site] = 2; + L.pondRadius.assign(L.sites.size(), pondRadiusFor(o.chamberSize)); + for (int site : L.farmSite) + L.pondRadius[site] += kFarmPondExtra; + for (int site : L.homeSite) + L.pondRadius[site] += kQueenPondExtra; + + // Carving: every open edge a wandering tunnel between its two chambers' middles, with a sand road + // down its middle that stops on the shore of the pond at each end (FEEDBACK 2026-09-13); then + // every chamber a rough disc, queen and farm chambers bigger. L.open.assign(n, 0); L.water.assign(n, 0); + L.road.assign(n, 0); std::mt19937 &tunnels = context.stream("anthill-tunnels"); for (int edge = 0; edge < int(openEdge.size()); ++edge) - if (openEdge[edge]) - { - const Site a = L.sites[graph.edgeCells[edge][0]], b = L.sites[graph.edgeCells[edge][1]]; - carveCorridor(L.open, t, {a.x + 0.5, a.y + 0.5}, {b.x + 0.5, b.y + 0.5}, - o.tunnelWidth / 2.0, kWanderShare * o.chamberSpacing, kWidthJitter, - tunnels); - } + { + if (!openEdge[edge]) + continue; + const int siteA = graph.edgeCells[edge][0], siteB = graph.edgeCells[edge][1]; + const Site a = L.sites[siteA], b = L.sites[siteB]; + const std::vector path = + wanderingPath(t, {a.x + 0.5, a.y + 0.5}, {b.x + 0.5, b.y + 0.5}, o.tunnelWidth / 2.0, + kWanderShare * o.chamberSpacing, kWidthJitter, tunnels); + strokePath(L.open, t, path); + if (!o.sandRoads) + continue; + // The road: the path's points beyond each end's pond and beach. + std::vector road; + for (const StrokePoint &p : path) + if (std::hypot(p.x - path.front().x, p.y - path.front().y) > + L.pondRadius[siteA] + kRoadStop && + std::hypot(p.x - path.back().x, p.y - path.back().y) > + L.pondRadius[siteB] + kRoadStop) + road.push_back(p); + if (road.size() >= 2) + tracePath(L.road, t, road); + } const RadialShape chamber(o.chamberSize, 0.3, context, "anthill-chambers"); const RadialShape farm(o.chamberSize + kFarmExtra, 0.3, context, "anthill-chambers"); const RadialShape queen(o.chamberSize + kQueenExtra, 0.25, context, "anthill-chambers"); - const RadialShape pond(kPondRadius, 0.3, context, "anthill-ponds"); - std::vector kind(L.sites.size(), 0); // 0 chamber, 1 farm, 2 queen - for (int site : L.farmSite) - kind[site] = 1; - for (int site : L.homeSite) - kind[site] = 2; for (int site = 0; site < int(L.sites.size()); ++site) { // Each chamber turned by a different angle, so one outline reads as many. @@ -148,25 +198,35 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const RadialShape &shape = kind[site] == 2 ? queen : kind[site] == 1 ? farm : chamber; fillShape(L.open, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, shape, turn); } - // Ponds: in every farm chamber at its middle; in every queen chamber on the far side from the - // door, with the swarm towards the door, so the kit lies between them and the way out is clear. - for (int site : L.farmSite) - fillShape(L.water, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, pond, 0.0); + // A pond at the middle of every chamber (FEEDBACK 2026-09-13). In a queen chamber the swarm + // stands between the pond and the door, so the kit round the pond lies behind it and the way out + // is clear. + const RadialShape pondPlain(pondRadiusFor(o.chamberSize), 0.3, context, "anthill-ponds"); + const RadialShape pondFarm(pondRadiusFor(o.chamberSize) + kFarmPondExtra, 0.3, context, + "anthill-ponds"); + const RadialShape pondQueen(pondRadiusFor(o.chamberSize) + kQueenPondExtra, 0.3, context, + "anthill-ponds"); + for (int site = 0; site < int(L.sites.size()); ++site) + fillShape(L.water, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, + kind[site] == 2 ? pondQueen + : kind[site] == 1 ? pondFarm + : pondPlain, + 0.0); + for (int i = 0; i < n; ++i) + if (L.water[i]) + L.road[i] = 0; L.homeOf.assign(n, -1); for (int k = 0; k < teams; ++k) { const int site = L.homeSite[k]; const Site s = L.sites[site]; const ShapePoint centre{s.x + 0.5, s.y + 0.5}; - // The door's direction: towards the chamber across the door edge. + // The door's direction: towards the chamber across the door edge; the axis points away from it. const int across = graph.other(doors[k], site); const double axis = std::atan2(-double(t.offsetY(s.y, L.sites[across].y)), -double(t.offsetX(s.x, L.sites[across].x))); - const double pondOut = o.chamberSize + kQueenExtra - kPondRadius - 2.5; - const ShapePoint pondAt = polarPoint(centre.x, centre.y, pondOut, axis); - fillShape(L.water, t, pondAt.x, pondAt.y, pond, 0.0); L.homes.push_back(centre); - L.kits.push_back(pondAt); + L.kits.push_back(centre); L.axes.push_back(axis); } // The queen chambers grow until each holds `queenRoom` building sites: the tiles nearest the @@ -220,8 +280,7 @@ bool generate(Game &game, GenerationContext &context) context.stage = "anthill terrain"; TerrainSketch terrain(n, GRASS); for (int i = 0; i < n; ++i) - if (L.water[i]) - terrain[i] = WATER; + terrain[i] = L.water[i] ? WATER : L.road[i] ? SAND : GRASS; layBeaches(terrain, t); writeUndermap(map, terrain); // The rock: stone on every tile that is not carved, wherever pure grass allows it. @@ -241,10 +300,12 @@ bool generate(Game &game, GenerationContext &context) ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w) && !rock[i]; return ground; }; - // The swarm stands between the chamber's middle and its door. + // The swarm stands between the pond and the door: past the pond's beach (a tile), a lane, and half + // its own footprint. const auto anchor = [&](int team) { - const ShapePoint p = polarPoint(L.homes[team].x, L.homes[team].y, 2.0, L.axes[team] + kPi); + const ShapePoint p = polarPoint(L.homes[team].x, L.homes[team].y, + L.pondRadius[L.homeSite[team]] + 4.5, L.axes[team] + kPi); return MapGeneratorPoint(int(std::lround(p.x)) - 2, int(std::lround(p.y)) - 2); }; if (!settleColonies(game, context, "anthill-starts", homeMask, anchor)) @@ -256,35 +317,60 @@ bool generate(Game &game, GenerationContext &context) { const auto eligible = [&](int i) { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; - // Wheat and wood either side of the pond, towards the swarm. + // Wheat and wood either side of the pond, on the flanks of the way to the door. + const double pr = L.pondRadius[L.homeSite[k]]; const KitFrame frame{int(std::lround(L.kits[k].x)), int(std::lround(L.kits[k].y)), L.axes[k]}; plantKit(map, t, context, - {frame.at(-2, -(kPondRadius + 3), 6), frame.at(-2, kPondRadius + 3, 6), - frame.at(0, 0, 0), kHomeWheat, kHomeWood, -1}, + {frame.at(-1, -(pr + 3), 6), frame.at(-1, pr + 3, 6), frame.at(0, 0, 0), + kHomeWheat, kHomeWood, -1}, eligible); } - // Farm chambers: a field round the pond; treasure chambers: a fruit grove and a wheat patch. + // Farm chambers: a ring of wheat round the whole pond (grown from its shore, breadth first, so it + // fills the ring before it reaches further out) and a wood patch beyond; every other chamber a + // wheat patch or a wood patch in turn; treasure chambers a fruit grove and a wheat patch + // (FEEDBACK 2026-09-13: every chamber has something, and the ground round a pond is never empty). + std::vector kind(L.sites.size(), 0); for (int site : L.farmSite) - { - const Site s = L.sites[site]; - const auto here = [&](int i) - { return L.open[i] && L.label[i] == site && clearGround(map, i % t.w, i / t.w); }; - if (const int seed = seedNear(t, s.x - 4, s.y, 6, here); seed >= 0) - growPatch(map, t, seed, CORN, int(scaledCount(kFarmWheat, o.wheat)), here); - if (const int seed = seedNear(t, s.x + 4, s.y, 6, here); seed >= 0) - growPatch(map, t, seed, WOOD, int(scaledCount(kFarmWood, o.wood)), here); - } + kind[site] = 1; + for (int site : L.homeSite) + kind[site] = 2; for (int site : L.treasureSite) + kind[site] = 3; + for (int site = 0; site < int(L.sites.size()); ++site) { const Site s = L.sites[site]; + const int reach = int(L.pondRadius[site]) + 2; const auto here = [&](int i) - { return L.open[i] && L.label[i] == site && clearGround(map, i % t.w, i / t.w); }; - if (scaledCount(1, o.fruit) > 0 && here(t.at(s.x, s.y))) - placeResourceClump(map, context, MapGeneratorPoint(s.x, s.y), - CHERRY + int(context.bounded("anthill-fruit", 3)), 1); - if (const int seed = seedNear(t, s.x - 3, s.y - 3, 5, here); seed >= 0) - growPatch(map, t, seed, CORN, int(scaledCount(kTreasureWheat, o.wheat)), here); + { + return L.open[i] && L.label[i] == site && !reserved[i] && + clearGround(map, i % t.w, i / t.w); + }; + if (kind[site] == 2) + continue; + if (kind[site] == 1) + { + if (const int seed = seedNear(t, s.x, s.y, reach, here); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kFarmWheat, o.wheat)), here); + if (const int seed = seedNear(t, s.x + reach + 3, s.y, 4, here); seed >= 0) + growPatch(map, t, seed, WOOD, int(scaledCount(kFarmWood, o.wood)), here); + } + else if (kind[site] == 3) + { + if (scaledCount(1, o.fruit) > 0) + if (const int seed = seedNear(t, s.x - reach - 1, s.y, 3, here); seed >= 0) + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), + CHERRY + int(context.bounded("anthill-fruit", 3)), 1); + if (const int seed = seedNear(t, s.x + reach, s.y, 3, here); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kTreasureWheat, o.wheat)), here); + } + else if ((site / 2) % 2 == 0) + { + if (const int seed = seedNear(t, s.x, s.y, reach, here); seed >= 0) + growPatch(map, t, seed, CORN, int(scaledCount(kPlainWheat, o.wheat)), here); + } + else if (const int seed = seedNear(t, s.x, s.y, reach, here); seed >= 0) + growPatch(map, t, seed, WOOD, int(scaledCount(kPlainWood, o.wood)), here); } seedAlgae(map, context, t, "anthill-algae", o.algae, AlgaeBand::anyWater(12)); secureStartingCrops(game, context, t, 24, 32, 0, &rock); @@ -321,7 +407,8 @@ std::string validateWorld(const Game &game, const GenerationContext &context) AnthillOptions::AnthillOptions(const GenerationRequest &r) : chamberSpacing(r.option("chamber-spacing")), chamberSize(r.option("chamber-size")), queenRoom(r.option("queen-room")), tunnelWidth(r.option("tunnel-width")), - loops(r.option("loops")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + loops(r.option("loops")), sandRoads(r.option("sand-roads") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) { } @@ -332,7 +419,7 @@ GeneratorDefinition anthillDefinition() "anthill", 32, "Anthill", - 1, + 2, false, // Chambers 28 apart give a 256 map about a hundred of them; a chamber of radius 5 holds // three or four buildings; a queen chamber grown to 40 building sites (overlapping 4x4 @@ -340,10 +427,13 @@ GeneratorDefinition anthillDefinition() // three wide take a column of units and are closed by one tower; a fifth of the chambers' // count in loops keeps a way round most blockades. {{"chamber-spacing", "Chamber spacing", 20, 40, 2, 28, ControlGroup::Layout}, - {"chamber-size", "Chamber size", 4, 8, 1, 5, ControlGroup::Layout}, - {"queen-room", "Queen room", 20, 120, 10, 40, ControlGroup::Layout}, + // FEEDBACK 2026-09-13: chambers of 7 (was 5) with a pond each, queen chambers grown to + // 60 sites (was 40); tunnels stay 3 wide but carry a sand road so nothing is built across. + {"chamber-size", "Chamber size", 5, 10, 1, 7, ControlGroup::Layout}, + {"queen-room", "Queen room", 20, 160, 10, 60, ControlGroup::Layout}, {"tunnel-width", "Tunnel width", 2, 4, 1, 3, ControlGroup::Terrain}, {"loops", "Loops", 0, 60, 10, 20, ControlGroup::Terrain}, + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Terrain), GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), GeneratorControl::percentage("algae-amount", "Algae amount"), diff --git a/src/map/generator/generators/AnthillGenerator.h b/src/map/generator/generators/AnthillGenerator.h index a92100bf1..23404f1c1 100644 --- a/src/map/generator/generators/AnthillGenerator.h +++ b/src/map/generator/generators/AnthillGenerator.h @@ -5,6 +5,7 @@ struct AnthillOptions { int chamberSpacing, chamberSize, queenRoom, tunnelWidth, loops; + bool sandRoads; int wheat, wood, algae, fruit; // percentages of the default amounts explicit AnthillOptions(const GenerationRequest &r); }; diff --git a/src/map/generator/generators/OldGrowthGenerator.cpp b/src/map/generator/generators/OldGrowthGenerator.cpp index 3c4ebaacf..c65b18fc0 100644 --- a/src/map/generator/generators/OldGrowthGenerator.cpp +++ b/src/map/generator/generators/OldGrowthGenerator.cpp @@ -48,15 +48,37 @@ using namespace MapGeneration; // from water never regrows, so every cut is permanent and the map's history is written in it; wood // near the home pond does regrow, so a colony never runs out. The lakes' orchards are the only // fruit not hidden. +// +// FEEDBACK 2026-09-13 (first play): "clearing the wood hurts workers. so to have so little water for +// food, and then on top of it have to battle through this old growth forest to reach your opponent, +// its a huge lift. Each player's home base needs to be larger by default and have a lot more pools +// of water. There should be no wood spawned in the player's home area and instead only wheat spawned +// around all the player's various pools and lakes. Second, the player's base should be surrounded +// by a ring of sand to protect the old growth from starting to grow inwards into the wheat. It's +// almost like how humans would clear a settlement within the woods." So: homes default to radius +// 20 (was 13), carry a ring of pools besides the central pond, grow wheat round every pool and no +// wood at all inside (wood is cut from the forest edge just past the sand, within the crop +// guarantee's 32 steps), and a two-tile sand ring round the clearing stops the forest spreading in +// (wheat and wood spread only onto grass, and the growth probe refuses beside sand). namespace { -// Every home's starting kit, unscaled whatever the amounts say: wheat and wood beside the pond -// (the wood regrows there, being beside water), and a quarry. -constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// Every home's starting kit, unscaled whatever the amounts say: two wheat patches beside the central +// pond and a quarry. No wood (FEEDBACK 2026-09-13): the forest edge is the woodlot. +constexpr int kHomeWheat = 14, kHomeQuarry = 2; +// The ring of pools round a home (FEEDBACK 2026-09-13, "a lot more pools of water"): `homePools` +// pools of this radius on a ring at this share of the home's radius, each with this much wheat on its +// shore. At the default radius 20 the ring is 12 tiles out, so every pool waters the ground between +// itself and the central pond and the whole clearing is fertile. +constexpr double kPoolRadius = 2.5, kPoolRingShare = 0.6; +constexpr int kPoolWheat = 12; // The forest starts this far beyond a home's rough disc, so the clearing's edge is open ground to -// build against and the pond's beach never meets a tree. +// build against and the pond's beach never meets a tree; then a ring of sand this wide keeps the +// forest from spreading into the clearing (FEEDBACK 2026-09-13). Two tiles: crops spread only onto +// an adjacent grass tile, so a single tile of sand corners already stops them, and the second tile +// keeps the beach arithmetic from ever leaving a grass corner between forest and clearing. constexpr double kClearingMargin = 3.0; +constexpr int kSandRing = 2; // Lakes keep this far from every clearing and from each other's shores, so a lake's regrowth never // reaches a home's forest edge (the growth probe reaches 15 tiles) and two lakes read as two. constexpr int kLakeGap = 20; @@ -76,7 +98,8 @@ struct Layout double homeRadius = 0, spacing = 0; std::vector homes, kits; std::vector homeOf; - std::vector water, clearing, forest, lake; + std::vector water, clearing, sand, forest, lake; + std::vector> pools; // each home's ring of pools std::vector lakeCentres; std::string failure; }; @@ -100,23 +123,48 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.spacing = std::min(L.spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); - L.homeRadius = std::min(o.homeSize, std::floor(L.spacing / 3 - kClearingMargin)); + L.homeRadius = + std::min(o.homeSize, std::floor(L.spacing / 3 - kClearingMargin - kSandRing)); if (!homeHasRoom(L.homeRadius)) { L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; return L; } L.clearing.assign(n, 0); + L.sand.assign(n, 0); for (const ShapePoint &home : L.homes) for (int i = 0; i < n; ++i) - if (std::hypot(t.offsetX(int(home.x), i % t.w), t.offsetY(int(home.y), i / t.w)) <= - L.homeRadius + kClearingMargin) + { + const double d = + std::hypot(t.offsetX(int(home.x), i % t.w), t.offsetY(int(home.y), i / t.w)); + if (d <= L.homeRadius + kClearingMargin) L.clearing[i] = 1; + else if (d <= L.homeRadius + kClearingMargin + kSandRing) + L.sand[i] = 1; + } + for (int i = 0; i < n; ++i) + if (L.clearing[i]) + L.sand[i] = 0; L.homeOf.assign(n, -1); L.water.assign(n, 0); const RadialShape home(L.homeRadius, 0.15, context, "growth-home"); const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "growth-pond"); L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); + // The ring of pools (FEEDBACK 2026-09-13), evenly spaced from a random phase per home. + const RadialShape pool(kPoolRadius, 0.3, context, "growth-pools"); + for (const ShapePoint &home : L.homes) + { + std::vector ring; + const double phase = context.bounded("growth-pools", 3600) / 3600.0 * 2 * kPi; + for (int p = 0; p < o.homePools; ++p) + { + const ShapePoint at = polarPoint(home.x, home.y, kPoolRingShare * L.homeRadius, + phase + 2 * kPi * p / o.homePools); + ring.push_back(at); + fillShape(L.water, t, at.x, at.y, pool, 0.0); + } + L.pools.push_back(ring); + } // Lakes: each at the tile farthest from every clearing and every lake so far (the exact // distance transform, Morphology.h), grown to `lakeSize` tiles by distance from its seed with a @@ -124,7 +172,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // 128x128 of map, so a 256 map gets four times as many as a 128. L.lake.assign(n, 0); const int lakes = int(std::lround(o.lakes * double(n) / (128.0 * 128.0))); - std::vector keepClear = dilate(t, L.clearing, kLakeGap); + std::vector settled(n, 0); + for (int i = 0; i < n; ++i) + settled[i] = L.clearing[i] || L.sand[i]; + std::vector keepClear = dilate(t, settled, kLakeGap); const std::vector ripple = periodicNoise(t.w, t.h, 6, context.stream("growth-lakes")); std::vector queued(n, 0); for (int lake = 0; lake < lakes; ++lake) @@ -151,7 +202,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (int i = 0; i < n; ++i) if (L.water[i] && !L.clearing[i]) L.lake[i] = 1; - keepClear = dilate(t, L.clearing, kLakeGap); + keepClear = dilate(t, settled, kLakeGap); const std::vector shores = dilate(t, L.lake, kLakeGap); for (int i = 0; i < n; ++i) keepClear[i] = keepClear[i] || shores[i]; @@ -159,7 +210,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // The forest: everything that is not a clearing or water. L.forest.assign(n, 0); for (int i = 0; i < n; ++i) - L.forest[i] = !L.clearing[i] && !L.water[i]; + L.forest[i] = !L.clearing[i] && !L.sand[i] && !L.water[i]; return L; } @@ -248,8 +299,7 @@ bool generate(Game &game, GenerationContext &context) context.stage = "old growth terrain"; TerrainSketch terrain(n, GRASS); for (int i = 0; i < n; ++i) - if (L.water[i]) - terrain[i] = WATER; + terrain[i] = L.water[i] ? WATER : L.sand[i] ? SAND : GRASS; layBeaches(terrain, t); writeUndermap(map, terrain); @@ -267,11 +317,30 @@ bool generate(Game &game, GenerationContext &context) context.stage = "old growth resources"; const std::vector reserved = swarmSurroundings(t, context); + // The kit (FEEDBACK 2026-09-13: no wood in the home): two wheat patches beside the central pond + // and a quarry beyond it, then wheat on the shore of every pool of the ring. for (int k = 0; k < teams; ++k) - plantOpenHomeKit( - map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, - [&](int i) - { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); + { + const auto eligible = [&](int i) + { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + const KitFrame frame{int(std::lround(L.kits[k].x)), int(std::lround(L.kits[k].y)), 0.0}; + const double reach = homePondRadius(L.homeRadius) * 1.2 + 3; + plantKit(map, t, context, + {frame.at(-0.4 * reach, -reach, 12), frame.at(-0.4 * reach, reach, 12), + frame.at(reach + 3, 0, 10), kHomeWheat, 0, kHomeQuarry}, + eligible); + for (const ShapePoint &pool : L.pools[k]) + { + const int px = int(std::lround(pool.x)), py = int(std::lround(pool.y)); + const auto shore = [&](int i) + { + return eligible(i) && + t.dist2(px, py, i % t.w, i / t.w) <= (kPoolRadius + 4) * (kPoolRadius + 4); + }; + if (const int seed = seedNear(t, px, py, int(kPoolRadius) + 4, shore); seed >= 0) + growPatch(map, t, seed, CORN, kPoolWheat, shore); + } + } // The lakes' prizes go down before the forest, so they stand in the open on the shore: wheat all // round the water and an orchard of the three fruits a few tiles out. for (int centre : L.lakeCentres) @@ -358,9 +427,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) OldGrowthOptions::OldGrowthOptions(const GenerationRequest &r) : forestDensity(r.option("forest-density")), lakes(r.option("lakes")), lakeSize(r.option("lake-size")), homeSize(r.option("home-size")), - hiddenGroves(r.option("hidden-groves") != 0), trails(r.option("trails") != 0), - wheat(r.option("wheat-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) + homePools(r.option("home-pools")), hiddenGroves(r.option("hidden-groves") != 0), + trails(r.option("trails") != 0), wheat(r.option("wheat-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) { } @@ -370,7 +440,7 @@ GeneratorDefinition oldGrowthDefinition() "old-growth", 28, "Old growth", - 1, + 2, false, // 90% cover reads as unbroken forest with the odd glade; one lake per 128x128 of 90 // tiles (four on a 256 map, each a few days' cutting from any home) keeps them rare enough @@ -378,7 +448,10 @@ GeneratorDefinition oldGrowthDefinition() {{"forest-density", "Forest density", 60, 100, 5, 90, ControlGroup::Terrain}, {"lakes", "Lakes", 0, 4, 1, 1, ControlGroup::Terrain}, {"lake-size", "Lake size", 40, 160, 10, 90, ControlGroup::Terrain}, - {"home-size", "Home size", 10, 20, 1, 13, ControlGroup::Layout}, + // FEEDBACK 2026-09-13: homes of radius 20 with five pools (was 13 with one pond); the + // whole clearing is then within the growth probe's reach of water. + {"home-size", "Home size", 12, 24, 1, 20, ControlGroup::Layout}, + {"home-pools", "Home pools", 0, 8, 1, 5, ControlGroup::Layout}, GeneratorControl::toggle("hidden-groves", "Hidden groves", true, ControlGroup::Layout), // On, a trail is cut from every colony to the first before the game starts. GeneratorControl::toggle("trails", "Starting trails", false, ControlGroup::Layout), diff --git a/src/map/generator/generators/OldGrowthGenerator.h b/src/map/generator/generators/OldGrowthGenerator.h index 5e7b3146d..b098ef20c 100644 --- a/src/map/generator/generators/OldGrowthGenerator.h +++ b/src/map/generator/generators/OldGrowthGenerator.h @@ -4,7 +4,7 @@ #include "GeneratorDefinition.h" struct OldGrowthOptions { - int forestDensity, lakes, lakeSize, homeSize; + int forestDensity, lakes, lakeSize, homeSize, homePools; bool hiddenGroves, trails; int wheat, stone, algae, fruit; // percentages of the default amounts explicit OldGrowthOptions(const GenerationRequest &r); diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index a981ad146..d8a7f6829 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -22,13 +22,13 @@ #include using namespace MapGeneration; -// Old town: a walled city of stone blocks and narrow grass streets, with farmland outside the wall. +// Old town: a city of stone blocks and narrow grass streets, with farmland all round it. // The city is a warped grid of irregular blocks (Tessellation.h); every block is solid stone, every // street between blocks is grass, so the streets form a dense grid with a flanking route round every // corner, unlike a maze's single ways. Some blocks are plazas instead: open grass with a fountain // pond, and every colony starts in a plaza of its own; the plaza at the city's centre is the -// cathedral square, with an orchard of all three fruits round its fountain. A wall of stone with a -// few gates rings the city, and outside it the land is laid out in farm rows. +// cathedral square, with an orchard of all three fruits round its fountain. Outside the city the land +// is laid out in farm rows, with building plots dotted through them. // // Streets are buildable and buildings block walking, so every building a player puts up closes a // street: the players build the city's fortifications and chokepoints themselves, and a tower on a @@ -38,21 +38,40 @@ using namespace MapGeneration; // WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Stone blocks are permanent // and unbuildable, so the city's shape is fixed and only the streets can change hands; crops regrow // near water, and the only water inside the wall is the fountains, so a colony that wants to eat -// must farm the plazas or go out through a gate. A wall stops walking but not shooting, so the +// must farm the plazas or go out to the fields. A block stops walking but not shooting, so the // blocks are also cover for towers. +// +// FEEDBACK 2026-09-13 (first play): "the biggest problem is the unfair accessibility for accessing +// the outlying farm lands. delete the stone ring that surrounds the entire main base so that there +// is just free access to farm lands no matter where you are located. and otherwise make sure that we +// stamp tons of those 10x4 little 'farm hubs' all over these farm lands, sporadically, maybe like +// 2.5x the number of players. and remove all the stone from the outer farm lands and reduce the +// amount of wood by like 2/3. The little 'pools' that occasionally replace the 'old city' grid in +// the center need to be like 2x bigger, and more of them need to contain fruits - not just the one +// at the center. the pools should really be big enough that they push up close against their +// surrounding grid cells' stone. same with the pools associated with each user's home base." So: +// no wall and no gates; farm plots (layFarm's 10x4 clearings, stampFarmPlot) spread over the fields +// at three per colony (the control is whole plots per colony; 2.5 was asked for); no outcrops in the +// fields and a third of the wood; every plaza's pool sized to its block, a tile short of the streets +// round it; a fruit grove beside every plaza's pool, the cathedral keeping the full orchard. namespace { // Every home's starting kit, unscaled whatever the amounts say: wheat and wood beside the fountain. // No quarry: the blocks are stone, the nearest of them a few steps from every door. constexpr int kHomeWheat = 14, kHomeWood = 12; -// The wall is this thick (stone, sealed against diagonal steps) and stands this far outside the -// outermost blocks, leaving a ring road inside it; gates are this wide. -constexpr int kWallThickness = 2, kRingRoad = 4, kGateHalfWidth = 3; -// Outside the wall the farm rows keep this margin of grass from it and run along the map's axis. -constexpr int kFarmMargin = 4, kFarmRim = 2, kFarmBridges = 16; -// A fountain's radius: a small pond with a beach, the width of a street to walk round. -constexpr double kFountainRadius = 2.5; +// Outside the city the farm rows keep this margin of grass from the outermost blocks and run along +// the map's axis; a farm plot's middle keeps this far inside the fields, so its ring of sand and the +// margin round it (10 wide, 4 high, a ring of 2 and a vertex more) never meet the city. +constexpr int kFarmMargin = 4, kFarmRim = 2, kFarmBridges = 16, kPlotMargin = 9; +// A fountain fills its plaza to this far short of the streets round it (FEEDBACK 2026-09-13: pools +// "big enough that they push up close against their surrounding grid cells' stone"): the block's +// half width less the street's, less a tile for the beach. 4.0 at the default block and street, an +// area two and a half times the old 2.5. +double fountainRadiusFor(int blockSize, int streetWidth) +{ + return std::max(2.5, blockSize / 2.0 - streetWidth / 2.0 - 1.0); +} struct Layout { @@ -65,7 +84,9 @@ struct Layout std::vector axes; // each home's facing: from the fountain to the swarm int cathedral = -1; std::vector homeOf; - std::vector city, block, street, wall, water, farmSand, farmRegion; + std::vector city, block, street, water, farmRegion; + Farm farm; // the fields' rows, sand and plots + double fountainRadius = 0; std::vector homes, kits; std::string failure; }; @@ -106,7 +127,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { const ShapePoint c = tilePoint(L.g.cells[cell].centre); const double d = std::hypot(t.offsetX(int(L.cx), int(c.x)), t.offsetY(int(L.cy), int(c.y))); - inCity[cell] = d + o.blockSize / 2.0 < L.cityRadius - kWallThickness - kRingRoad; + inCity[cell] = d + o.blockSize / 2.0 < L.cityRadius; cityCells += inCity[cell]; } // Plazas: the colonies' plazas as far apart among the city's cells as the city allows @@ -177,26 +198,19 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } // Streets: the band along every cell border inside the city; blocks: the rest of the city's - // cells, except the plazas; the wall: a ring with gates; the ring road between them. + // cells, except the plazas. No wall (FEEDBACK 2026-09-13): the fields are open on every side. L.city.assign(n, 0); - L.wall.assign(n, 0); std::vector border(n, 0); for (int i = 0; i < n; ++i) { const int x = i % t.w, y = i / t.w; const double d = std::hypot(t.offsetX(int(L.cx), x), t.offsetY(int(L.cy), y)); - L.city[i] = d < L.cityRadius - kWallThickness; + L.city[i] = d < L.cityRadius; for (const auto &s : kCardinalSteps) if (L.cell[t.at(x + s[0], y + s[1])] != L.cell[i]) border[i] = 1; } L.street = dilateRound(t, border, o.streetWidth / 2.0); - const double gateSpin = context.bounded("town-gates", 3600) / 3600.0 * 2 * kPi; - std::vector gates; - for (int gate = 0; gate < o.gates; ++gate) - gates.push_back(gateSpin + 2 * kPi * gate / o.gates); - ringWithGates(t, L.cx, L.cy, L.cityRadius - kWallThickness / 2.0, kWallThickness / 2.0, gates, - kGateHalfWidth, [&](int i, int gate) { L.wall[i] = gate < 0; }); std::vector plaza(L.g.cellCount(), 0); plaza[L.cathedral] = 1; for (int cell : L.homeCell) @@ -207,13 +221,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) plaza[cell] = 1; L.block.assign(n, 0); for (int i = 0; i < n; ++i) - L.block[i] = - L.city[i] && inCity[L.cell[i]] && !L.street[i] && !plaza[L.cell[i]] && !L.wall[i]; + L.block[i] = L.city[i] && inCity[L.cell[i]] && !L.street[i] && !plaza[L.cell[i]]; // Fountains: a pond at the middle of every plaza; the colonies' kits round theirs. L.water.assign(n, 0); L.homeOf.assign(n, -1); L.homeRadius = std::max(6.0, o.blockSize / 2.0 - o.streetWidth / 2.0 - 1); - const RadialShape fountain(kFountainRadius, 0.3, context, "town-fountain"); + L.fountainRadius = fountainRadiusFor(o.blockSize, o.streetWidth); + const RadialShape fountain(L.fountainRadius, 0.25, context, "town-fountain"); for (int k = 0; k < teams; ++k) { const ShapePoint fountainAt = tilePoint(L.g.cells[L.homeCell[k]].centre); @@ -243,8 +257,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const ShapePoint c = tilePoint(L.g.cells[L.cathedral].centre); fillShape(L.water, t, c.x, c.y, fountain, 0.0); } - // Outside the wall: farm rows over everything beyond the wall's margin, along the map's axis - // with the row through the centre a crop row, bridged every kFarmBridges tiles. + // Outside the city: farm rows over everything beyond the outermost blocks' margin, along the map's + // axis with the row through the centre a crop row, bridged every kFarmBridges tiles; then farm + // plots (FEEDBACK 2026-09-13) spread through the fields, each the farthest a plot can stand from + // the city and from every plot before it, so they are evenly dotted about. L.farmRegion.assign(n, 0); for (int i = 0; i < n; ++i) { @@ -252,11 +268,30 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.farmRegion[i] = d > L.cityRadius + kFarmMargin; } TerrainSketch rows(n, GRASS); - const Farm farm = layFarm(rows, t, L.farmRegion, 0.0, {L.cx, L.cy}, kFarmRim, bestFarmRows(0.0), - nullptr, kFarmBridges, true); - L.farmSand = farm.sand; + L.farm = layFarm(rows, t, L.farmRegion, 0.0, {L.cx, L.cy}, kFarmRim, bestFarmRows(0.0), nullptr, + kFarmBridges, true); + const FarmPlot plot; + const std::vector roomy = erode(t, L.farmRegion, kPlotMargin); + std::vector keepClear(n, 0); for (int i = 0; i < n; ++i) - if (farm.water[i]) + keepClear[i] = !L.farmRegion[i]; + for (int p = 0; p < o.farmPlots * teams; ++p) + { + const std::vector far = distanceSquaredTo(t, keepClear); + int site = -1; + for (int i = 0; i < n; ++i) + if (roomy[i] && (site < 0 || far[i] > far[site])) + site = i; + if (site < 0) + break; + const int x0 = site % t.w - plot.width / 2, y0 = site / t.w - plot.height / 2; + stampFarmPlot(rows, t, L.farm, x0, y0, plot); + for (int dy = -kPlotMargin; dy <= kPlotMargin; ++dy) + for (int dx = -kPlotMargin; dx <= kPlotMargin; ++dx) + keepClear[t.at(site % t.w + dx, site / t.w + dy)] = 1; + } + for (int i = 0; i < n; ++i) + if (L.farm.water[i]) L.water[i] = 1; return L; } @@ -280,12 +315,12 @@ bool generate(Game &game, GenerationContext &context) context.stage = "old town terrain"; TerrainSketch terrain(n, GRASS); for (int i = 0; i < n; ++i) - terrain[i] = L.water[i] ? WATER : L.farmSand[i] ? SAND : GRASS; + terrain[i] = L.water[i] ? WATER : L.farm.sand[i] ? SAND : GRASS; layBeaches(terrain, t); writeUndermap(map, terrain); - // The blocks and the wall are stone, wherever the beaches left pure grass. + // The blocks are stone, wherever the beaches left pure grass. for (int i = 0; i < n; ++i) - if ((L.block[i] || L.wall[i]) && map.isResourceAllowed(i % t.w, i / t.w, STONE)) + if (L.block[i] && map.isResourceAllowed(i % t.w, i / t.w, STONE)) map.setResource(i % t.w, i / t.w, STONE, 1); context.stage = "old town colonies"; @@ -310,26 +345,47 @@ bool generate(Game &game, GenerationContext &context) plantHomeKit( map, t, context, L.kits[k], L.axes[k], L.homeRadius, kHomeWheat, kHomeWood, [&](int i) { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); - // The cathedral square's orchard, round its fountain. + // The cathedral square's orchard round its fountain, and a grove of one fruit in turn beside every + // other plaza's pool (FEEDBACK 2026-09-13: "more of them need to contain fruits"). A pool fills its + // plaza to a tile short of the streets, so the fruit stands on the ground round it. + const auto roundPlaza = [&](int cell) + { + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + return [&, c](int i) + { + return !L.block[i] && L.homeOf[i] < 0 && + std::hypot(t.offsetX(int(c.x), i % t.w), t.offsetY(int(c.y), i / t.w)) <= + o.blockSize / 2.0 + 2 && + clearGround(map, i % t.w, i / t.w); + }; + }; if (scaledCount(1, o.fruit) > 0) { const ShapePoint c = tilePoint(L.g.cells[L.cathedral].centre); const double spin = context.bounded("town-fruit", 3600) / 3600.0 * 2 * kPi; - plantOrchard(map, t, context, c.x, c.y, kFountainRadius + 3.5, {spin}, 4.5, 4, 1, - [&](int i) - { - return L.cell[i] == L.cathedral && !L.street[i] && - clearGround(map, i % t.w, i / t.w); - }); + plantOrchard(map, t, context, c.x, c.y, L.fountainRadius + 2.5, {spin}, 4.5, 5, 1, + roundPlaza(L.cathedral)); + int fruit = 0; + for (int cell : L.plazaCell) + { + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + const double at = context.bounded("town-fruit", 3600) / 3600.0 * 2 * kPi; + plantRound(map, t, context, c.x, c.y, L.fountainRadius + 2.5, {at}, + CHERRY + fruit++ % 3, 1, 5, roundPlaza(cell)); + } } - // The fields outside: half the fertile row ground under wheat and a sixth under wood, in patches; + // The fields outside: half the fertile row ground under wheat and a twentieth under wood (a third + // of the sixth it was, FEEDBACK 2026-09-13), no outcrops, in patches, the farm plots kept clear; // inside the city only the plazas are fertile (their fountains), and get a light share so they // stay open to build on. const Fertility::Field fertility = Fertility::forMap(map, false); const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("town-patch")); const std::vector split = periodicNoise(t.w, t.h, 4, context.stream("town-split")); const auto fields = [&](int i) - { return L.farmRegion[i] && !L.farmSand[i] && clearGround(map, i % t.w, i / t.w); }; + { + return L.farmRegion[i] && !L.farm.sand[i] && !L.farm.plot[i] && + clearGround(map, i % t.w, i / t.w); + }; int fertile = 0; for (int i = 0; i < n; ++i) fertile += fields(i) && fertility.at(i % t.w, i / t.w) > 0; @@ -339,14 +395,13 @@ bool generate(Game &game, GenerationContext &context) [&](int area) { return GroundAmounts{int(scaledCount(fertile * 50 / 100, o.wheat)), - int(scaledCount(fertile * 16 / 100, o.wood)), - int(scaledCount(area / 2000, o.stone)), + int(scaledCount(fertile * 5 / 100, o.wood)), 0, int(scaledCount(area / 3000, o.fruit))}; }, "town-stone", "town-fruit"); const auto plazas = [&](int i) { - return L.city[i] && !L.block[i] && !L.wall[i] && !L.street[i] && L.homeOf[i] < 0 && + return L.city[i] && !L.block[i] && !L.street[i] && L.homeOf[i] < 0 && clearGround(map, i % t.w, i / t.w); }; int plazaFertile = 0; @@ -362,14 +417,13 @@ bool generate(Game &game, GenerationContext &context) }, "town-stone", "town-fruit"); seedAlgae(map, context, t, "town-algae", o.algae, AlgaeBand::anyWater(50)); - std::vector stone(n, 0); - for (int i = 0; i < n; ++i) - stone[i] = L.block[i] || L.wall[i]; + const std::vector &stone = L.block; secureStartingCrops(game, context, t, 24, 32, 0, &stone); reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, &stone); - // The streets join every plaza and the gates join the city to the fields; only crops could close - // a street, so only crops are cleared. + clearFarmPlots(map, t, {L.farm}); + // The streets join every plaza and open onto the fields on every side; only crops could close a + // street, so only crops are cleared. context.stage = "old town routes"; openColonyRoutes(map, context, t, StepCosts{1, 3, -1, -1, -1}); return true; @@ -392,11 +446,6 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (!pond) return "Colony " + std::to_string(k) + "'s plaza has lost its fountain."; } - for (int i = 0; i < t.size(); ++i) - if (L.wall[i] && map.isGrass(i % t.w, i / t.w) && - map.getResource(i % t.w, i / t.w).type != STONE) - return "The city wall has a gap at (" + std::to_string(i % t.w) + ", " + - std::to_string(i / t.w) + ")."; return walkFromFirstColony(map, context.request.nbTeams, "the city", "through the streets") .error; } @@ -405,9 +454,9 @@ std::string validateWorld(const Game &game, const GenerationContext &context) OldTownOptions::OldTownOptions(const GenerationRequest &r) : citySize(r.option("city-size")), blockSize(r.option("block-size")), streetWidth(r.option("street-width")), warp(r.option("warp")), plazas(r.option("plazas")), - gates(r.option("gates")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), - stone(r.option("stone-amount")), algae(r.option("algae-amount")), - fruit(r.option("fruit-amount")) + farmPlots(r.option("farm-plots")), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) { } @@ -416,17 +465,18 @@ GeneratorDefinition oldTownDefinition() return {"old-town", 31, "Old town", - 1, + 2, false, // A city of 70% of the half side leaves a belt of fields round it; blocks of 14 with // streets of 4 give a 256 map about a hundred blocks and streets a column of units wide - // that one building closes; four gates, one to a side. + // that one building closes; three farm plots per colony through the fields (FEEDBACK + // 2026-09-13 asked for about two and a half). {{"city-size", "City size", 40, 90, 5, 70, ControlGroup::Layout}, {"block-size", "Block size", 10, 20, 1, 14, ControlGroup::Layout}, {"street-width", "Street width", 3, 7, 1, 4, ControlGroup::Terrain}, {"warp", "Warp", 0, 100, 10, 50, ControlGroup::Terrain}, {"plazas", "Plazas", 0, 4, 1, 2, ControlGroup::Layout}, - {"gates", "Gates", 2, 8, 1, 4, ControlGroup::Layout}, + {"farm-plots", "Farm plots", 0, 6, 1, 3, ControlGroup::Layout}, GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), GeneratorControl::percentage("stone-amount", "Stone amount"), diff --git a/src/map/generator/generators/OldTownGenerator.h b/src/map/generator/generators/OldTownGenerator.h index 2a0ab0139..ee6ff123b 100644 --- a/src/map/generator/generators/OldTownGenerator.h +++ b/src/map/generator/generators/OldTownGenerator.h @@ -4,7 +4,7 @@ #include "GeneratorDefinition.h" struct OldTownOptions { - int citySize, blockSize, streetWidth, warp, plazas, gates; + int citySize, blockSize, streetWidth, warp, plazas, farmPlots; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit OldTownOptions(const GenerationRequest &r); }; diff --git a/src/map/generator/shared/Farmland.cpp b/src/map/generator/shared/Farmland.cpp index cb0c91f32..76a6dfb4d 100644 --- a/src/map/generator/shared/Farmland.cpp +++ b/src/map/generator/shared/Farmland.cpp @@ -40,6 +40,35 @@ double farmYield(double angle) return yields[below] + (yields[below + 1] - yields[below]) * along; } +void stampFarmPlot(TerrainSketch &sketch, const Torus &t, Farm &farm, int x0, int y0, + const FarmPlot &plot) +{ + const int margin = plot.ring + 1; + for (int dy = -margin; dy <= plot.height + margin; ++dy) + for (int dx = -margin; dx <= plot.width + margin; ++dx) + { + const int i = t.at(x0 + dx, y0 + dy); + const bool grass = dx >= 0 && dx <= plot.width && dy >= 0 && dy <= plot.height; + const bool ring = !grass && dx >= -plot.ring && dx <= plot.width + plot.ring && + dy >= -plot.ring && dy <= plot.height + plot.ring; + // The clearing trumps the rows and bridges alike: its grass is grass. + if (farm.water[i] || (grass && farm.sand[i])) + { + farm.water[i] = 0; + farm.sand[i] = 0; + sketch[i] = GRASS; + } + if (ring) + { + farm.sand[i] = 1; + sketch[i] = SAND; + } + } + for (int dy = 0; dy < plot.height; ++dy) + for (int dx = 0; dx < plot.width; ++dx) + farm.plot[t.at(x0 + dx, y0 + dy)] = 1; +} + Farm layFarm(TerrainSketch &sketch, const Torus &t, const std::vector ®ion, double angle, ShapePoint origin, int rim, const FarmRows &rows, const FarmPlot *plot, int bridgeSpacing, bool caps) @@ -139,31 +168,7 @@ Farm layFarm(TerrainSketch &sketch, const Torus &t, const std::vectorheight + margin; ++dy) - for (int dx = -margin; dx <= plot->width + margin; ++dx) - { - const int i = t.at(x0 + dx, y0 + dy); - const bool grass = - dx >= 0 && dx <= plot->width && dy >= 0 && dy <= plot->height; - const bool ring = !grass && dx >= -plot->ring && - dx <= plot->width + plot->ring && dy >= -plot->ring && - dy <= plot->height + plot->ring; - // The clearing trumps the rows and bridges alike: its grass is grass. - if (farm.water[i] || (grass && farm.sand[i])) - { - farm.water[i] = 0; - farm.sand[i] = 0; - sketch[i] = GRASS; - } - if (ring) - { - farm.sand[i] = 1; - sketch[i] = SAND; - } - } - for (int dy = 0; dy < plot->height; ++dy) - for (int dx = 0; dx < plot->width; ++dx) - farm.plot[t.at(x0 + dx, y0 + dy)] = 1; + stampFarmPlot(sketch, t, farm, x0, y0, *plot); seen.assign(seen.size(), 0); for (int i = 0; i < n; ++i) if (farm.water[i]) diff --git a/src/map/generator/shared/Farmland.h b/src/map/generator/shared/Farmland.h index f86a5954b..029b9b70b 100644 --- a/src/map/generator/shared/Farmland.h +++ b/src/map/generator/shared/Farmland.h @@ -88,6 +88,13 @@ Farm layFarm(TerrainSketch &sketch, const Torus &, const std::vector Date: Sun, 13 Sep 2026 19:48:50 -0400 Subject: [PATCH 117/136] Revise Old growth, Anthill and Old town from second play FEEDBACK 2026-09-13 (second play), quoted in the code: - Old growth (revision 3): "default base size still needs to be just a bit bigger, and preseed a ton more wheat around these pools". Homes default to radius 24 (was 20), the kit's wheat patches are 24 each (was 14) and every pool is ringed with 32 wheat (was 12) grown from four seeds spaced round its shore. - Anthill (revision 3): "wheat and wood are only ever seeded on one side of the chamber, and because of the sand roads it never expands to the other sides ... NO water in your home base". Crops are grown from eight seeds spaced round each pond so a road never cuts them off; the queen chamber has no pond, only a bigger kit that does not regrow, so growing means taking a farm chamber. The validator now checks the farm chambers' ponds instead of the queens'. - Old town (revision 3): "little 'tendril' sand roads extending inwards". From the fields' sand cap a sand tendril runs along the streets (cheapest walk, never through a block) to every home plaza and the cathedral square (tendrils, on). Verified on macOS arm64: golden table regenerated at the new revisions (264 rows, 0 failures), defaults test, strict translation check, client build; every lobby cell of the three generates on seed 1. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 2 + data/texts.br.txt | 2 + data/texts.ca.txt | 2 + data/texts.cz.txt | 2 + data/texts.de.txt | 2 + data/texts.dk.txt | 2 + data/texts.en.txt | 2 + data/texts.eo.txt | 2 + data/texts.es.txt | 2 + data/texts.eu.txt | 2 + data/texts.fa.txt | 2 + data/texts.fi.txt | 2 + data/texts.fr.txt | 2 + data/texts.gr.txt | 2 + data/texts.hu.txt | 2 + data/texts.id.txt | 2 + data/texts.it.txt | 2 + data/texts.ja.txt | 2 + data/texts.keys.txt | 1 + data/texts.ko.txt | 2 + data/texts.nl.txt | 2 + data/texts.pl.txt | 2 + data/texts.pt.txt | 2 + data/texts.ro.txt | 2 + data/texts.ru.txt | 2 + data/texts.si.txt | 2 + data/texts.sk.txt | 2 + data/texts.sr.txt | 2 + data/texts.sv.txt | 2 + data/texts.tr.txt | 2 + data/texts.uk.txt | 2 + data/texts.vi.txt | 2 + data/texts.zh-cn.txt | 2 + data/texts.zh-tw.txt | 2 + .../generator/generators/AnthillGenerator.cpp | 144 ++++++++++-------- .../generators/OldGrowthGenerator.cpp | 33 ++-- .../generator/generators/OldTownGenerator.cpp | 60 +++++++- .../generator/generators/OldTownGenerator.h | 1 + test/map-generator-golden.txt | 48 +++--- 39 files changed, 252 insertions(+), 101 deletions(-) diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 9c4902df7..481d10e00 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2168,3 +2168,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.br.txt b/data/texts.br.txt index e35ad973d..67886e3e5 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -2166,3 +2166,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 0d7a0a971..addc2a32c 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2178,3 +2178,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 835bddef6..778b744ae 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.de.txt b/data/texts.de.txt index eb6e2ece1..e1ef0d108 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.dk.txt b/data/texts.dk.txt index f6a0f3cc6..e34704222 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2246,3 +2246,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.en.txt b/data/texts.en.txt index 5ebd2ce9c..165c0476a 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2160,3 +2160,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 6c94aebc0..55bf7cd2b 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2168,3 +2168,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.es.txt b/data/texts.es.txt index 4613054ca..db07eb28b 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.eu.txt b/data/texts.eu.txt index cc2146b23..31825342f 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2180,3 +2180,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 147cc3604..98bcfbba5 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2168,3 +2168,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 690214ee8..62529347a 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 78752de06..666c5a68f 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2180,3 +2180,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 0a9a42b63..bc1debf89 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2240,3 +2240,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.hu.txt b/data/texts.hu.txt index bc33a29b4..f53023402 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.id.txt b/data/texts.id.txt index a90dfb2bf..904238f6b 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2166,3 +2166,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.it.txt b/data/texts.it.txt index 0f6b732ad..a0ff2a1ac 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2232,3 +2232,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.ja.txt b/data/texts.ja.txt index bf10af336..6f7537714 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2166,3 +2166,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.keys.txt b/data/texts.keys.txt index dcbb2c528..f4fb4f019 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1078,3 +1078,4 @@ [Loops] [Home pools] [Farm plots] +[Tendril roads] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index ad50ee35a..5e6d21eaf 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2166,3 +2166,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 1fc2a1afd..34e540ca5 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2194,3 +2194,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.pl.txt b/data/texts.pl.txt index b9651c3b9..8bf7b857e 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.pt.txt b/data/texts.pt.txt index b33d5ee20..98caf47fd 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2176,3 +2176,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 63721a53c..64f5f742b 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.ru.txt b/data/texts.ru.txt index e1cc32935..2f0615e51 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2168,3 +2168,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.si.txt b/data/texts.si.txt index 445d3dafe..0bda7295c 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 17b89e321..5ab66f03e 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2178,3 +2178,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.sr.txt b/data/texts.sr.txt index e8866447e..2985efaa8 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2170,3 +2170,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 191303284..c6a5880cd 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2180,3 +2180,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 420900900..bcd88f8e0 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2178,3 +2178,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 045624279..600d85e76 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2166,3 +2166,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 643dcadbe..03b3fc89b 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2166,3 +2166,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 1f6700deb..abca31b2e 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2166,3 +2166,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 76c9a6ea0..1579eae20 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2242,3 +2242,5 @@ Loops Home pools [Farm plots] Farm plots +[Tendril roads] +Tendril roads diff --git a/src/map/generator/generators/AnthillGenerator.cpp b/src/map/generator/generators/AnthillGenerator.cpp index 09f75c14a..cc4dfc21e 100644 --- a/src/map/generator/generators/AnthillGenerator.cpp +++ b/src/map/generator/generators/AnthillGenerator.cpp @@ -55,28 +55,46 @@ using namespace MapGeneration; // and on to the shore of the pond at each end (a sand corner spoils the tiles round it for building, // so nothing can be built across a tunnel); a farm chamber's pond is ringed with wheat; every other // chamber alternates a wheat and a wood patch; queen chambers grow to 60 sites (was 40). +// +// FEEDBACK 2026-09-13 (second play): "wheat and wood are only ever seeded on one side of the chamber, +// and because of the sand roads it never expands to the other sides. We want to make sure all the +// chambers that have wheat or wood have it seeded on all sides of their pond. also I think anthill +// will play nicer if there is NO water in your home base. you can still have some starting resources +// but if you're forced to expand down the ant zones, that makes it play a lot closer to the intended +// design: one 'queen ant' chamber that's special and then farming chambers that the ants expand +// into." So: a chamber's crops are grown from eight seeds spaced round its pond, so a road through +// the chamber never cuts them off from the rest of it; and the queen chamber has no pond at all, just +// a bigger starting kit that never regrows, so growing means taking a farm chamber. namespace { // Every chamber's pond sits at its middle (FEEDBACK 2026-09-13), its radius this share of the // chamber's: 2.8 at the default size 7, about 25 corners of water, which the growth probe finds from -// the whole chamber. A farm chamber's pond is a tile bigger, a queen chamber's half a tile. -constexpr double kPondShare = 0.4, kFarmPondExtra = 1.0, kQueenPondExtra = 0.5; +// the whole chamber. A farm chamber's pond is a tile bigger. A queen chamber has none (FEEDBACK +// 2026-09-13, second play): its middle is kept clear to kQueenCore for the swarm, and the roads stop +// there. +constexpr double kPondShare = 0.4, kFarmPondExtra = 1.0, kQueenCore = 3.5; double pondRadiusFor(int chamberSize) { return std::max(2.0, kPondShare * chamberSize); } -// The queen chamber's kit, unscaled whatever the amounts say: wheat and wood either side of the pond. -// No quarry: the walls are stone. -constexpr int kHomeWheat = 14, kHomeWood = 10; +// The queen chamber's kit, unscaled whatever the amounts say: wheat and wood on the flanks behind the +// swarm and a second wheat patch straight behind it. Bigger than a watered home's, since none of it +// regrows (second play: no water in the home, "you can still have some starting resources"). No +// quarry: the walls are stone. +constexpr int kHomeWheat = 24, kHomeWood = 16, kHomeWheatBehind = 16; +// Crops in a chamber are grown from this many seeds spaced evenly round its pond, each given its +// share of the count (FEEDBACK 2026-09-13, second play: crops on one side never spread past the sand +// road to the other). +constexpr int kRingSeeds = 8; // A farm chamber is this much bigger than an ordinary chamber, to hold its pond and field; its field // is a ring of wheat round the whole pond (FEEDBACK 2026-09-13, "no sections empty of growth": the -// ring two to four tiles out from a pond of radius 4 is about 60 tiles) and a wood patch beyond. -constexpr int kFarmExtra = 2, kFarmWheat = 40, kFarmWood = 12; -// Every other chamber holds a wheat patch or a wood patch in turn (FEEDBACK 2026-09-13, "every +// ring two to four tiles out from a pond of radius 4 is about 60 tiles) and a ring of wood beyond. +constexpr int kFarmExtra = 2, kFarmWheat = 48, kFarmWood = 16; +// Every other chamber holds a wheat ring or a wood ring in turn (FEEDBACK 2026-09-13, "every // chamber actually has something"); a treasure chamber holds a fruit grove of radius 1 (5 tiles) and // this much wheat. -constexpr int kPlainWheat = 16, kPlainWood = 12, kTreasureWheat = 12; +constexpr int kPlainWheat = 24, kPlainWood = 16, kTreasureWheat = 16; // A tunnel's sand road stops this far past the pond's radius at each end: on the pond's beach, so it // joins the water without cutting into it. constexpr double kRoadStop = 1.5; @@ -156,7 +174,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (int site : L.farmSite) L.pondRadius[site] += kFarmPondExtra; for (int site : L.homeSite) - L.pondRadius[site] += kQueenPondExtra; + L.pondRadius[site] = 0; // no water in a queen chamber (second play) + // Where a road stops at each end: on a pond's beach, or short of a queen chamber's clear middle. + const auto roadStop = [&](int site) + { return (kind[site] == 2 ? kQueenCore : L.pondRadius[site]) + kRoadStop; }; // Carving: every open edge a wandering tunnel between its two chambers' middles, with a sand road // down its middle that stops on the shore of the pond at each end (FEEDBACK 2026-09-13); then @@ -180,10 +201,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // The road: the path's points beyond each end's pond and beach. std::vector road; for (const StrokePoint &p : path) - if (std::hypot(p.x - path.front().x, p.y - path.front().y) > - L.pondRadius[siteA] + kRoadStop && - std::hypot(p.x - path.back().x, p.y - path.back().y) > - L.pondRadius[siteB] + kRoadStop) + if (std::hypot(p.x - path.front().x, p.y - path.front().y) > roadStop(siteA) && + std::hypot(p.x - path.back().x, p.y - path.back().y) > roadStop(siteB)) road.push_back(p); if (road.size() >= 2) tracePath(L.road, t, road); @@ -198,20 +217,16 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const RadialShape &shape = kind[site] == 2 ? queen : kind[site] == 1 ? farm : chamber; fillShape(L.open, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, shape, turn); } - // A pond at the middle of every chamber (FEEDBACK 2026-09-13). In a queen chamber the swarm - // stands between the pond and the door, so the kit round the pond lies behind it and the way out - // is clear. + // A pond at the middle of every chamber but the queens' (FEEDBACK 2026-09-13, both plays): a + // queen chamber is dry, its swarm at its middle and its kit behind, so the colony must take a farm + // chamber for anything to regrow. const RadialShape pondPlain(pondRadiusFor(o.chamberSize), 0.3, context, "anthill-ponds"); const RadialShape pondFarm(pondRadiusFor(o.chamberSize) + kFarmPondExtra, 0.3, context, "anthill-ponds"); - const RadialShape pondQueen(pondRadiusFor(o.chamberSize) + kQueenPondExtra, 0.3, context, - "anthill-ponds"); for (int site = 0; site < int(L.sites.size()); ++site) - fillShape(L.water, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, - kind[site] == 2 ? pondQueen - : kind[site] == 1 ? pondFarm - : pondPlain, - 0.0); + if (kind[site] != 2) + fillShape(L.water, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, + kind[site] == 1 ? pondFarm : pondPlain, 0.0); for (int i = 0; i < n; ++i) if (L.water[i]) L.road[i] = 0; @@ -300,12 +315,10 @@ bool generate(Game &game, GenerationContext &context) ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w) && !rock[i]; return ground; }; - // The swarm stands between the pond and the door: past the pond's beach (a tile), a lane, and half - // its own footprint. + // The swarm stands at the chamber's middle, a tile towards the door. const auto anchor = [&](int team) { - const ShapePoint p = polarPoint(L.homes[team].x, L.homes[team].y, - L.pondRadius[L.homeSite[team]] + 4.5, L.axes[team] + kPi); + const ShapePoint p = polarPoint(L.homes[team].x, L.homes[team].y, 1.0, L.axes[team] + kPi); return MapGeneratorPoint(int(std::lround(p.x)) - 2, int(std::lround(p.y)) - 2); }; if (!settleColonies(game, context, "anthill-starts", homeMask, anchor)) @@ -317,19 +330,24 @@ bool generate(Game &game, GenerationContext &context) { const auto eligible = [&](int i) { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; - // Wheat and wood either side of the pond, on the flanks of the way to the door. - const double pr = L.pondRadius[L.homeSite[k]]; + // Wheat and wood on the flanks behind the swarm (the axis points away from the door), and a + // second wheat patch straight behind it. const KitFrame frame{int(std::lround(L.kits[k].x)), int(std::lround(L.kits[k].y)), L.axes[k]}; - plantKit(map, t, context, - {frame.at(-1, -(pr + 3), 6), frame.at(-1, pr + 3, 6), frame.at(0, 0, 0), - kHomeWheat, kHomeWood, -1}, - eligible); + plantKit( + map, t, context, + {frame.at(3, -5, 6), frame.at(3, 5, 6), frame.at(0, 0, 0), kHomeWheat, kHomeWood, -1}, + eligible); + const KitSeed behind = frame.at(7, 0, 6); + if (const int seed = seedNear(t, behind.x, behind.y, behind.within, eligible); seed >= 0) + growPatch(map, t, seed, CORN, kHomeWheatBehind, eligible); } - // Farm chambers: a ring of wheat round the whole pond (grown from its shore, breadth first, so it - // fills the ring before it reaches further out) and a wood patch beyond; every other chamber a - // wheat patch or a wood patch in turn; treasure chambers a fruit grove and a wheat patch - // (FEEDBACK 2026-09-13: every chamber has something, and the ground round a pond is never empty). + // Every chamber with crops has them on every side of its pond (FEEDBACK 2026-09-13, second play): + // a patch is grown from kRingSeeds seeds spaced evenly round the pond's shore, each given its + // share of the count, so no road through the chamber can cut its crops off from the rest of it. + // Farm chambers ring the pond with wheat and a wider ring of wood beyond ("no sections empty of + // growth"); every other chamber a wheat or a wood ring in turn; treasure chambers a fruit grove + // and a wheat ring. std::vector kind(L.sites.size(), 0); for (int site : L.farmSite) kind[site] = 1; @@ -337,10 +355,22 @@ bool generate(Game &game, GenerationContext &context) kind[site] = 2; for (int site : L.treasureSite) kind[site] = 3; + const auto ringPlant = [&](int site, double radius, int type, int count, const auto &here) + { + const Site s = L.sites[site]; + for (int k = 0; k < kRingSeeds; ++k) + { + const double a = 2 * kPi * k / kRingSeeds; + const int seed = seedNear(t, int(std::lround(s.x + 0.5 + radius * std::cos(a))), + int(std::lround(s.y + 0.5 + radius * std::sin(a))), 2, here); + if (seed >= 0) + growPatch(map, t, seed, type, (count + kRingSeeds - 1 - k) / kRingSeeds, here); + } + }; for (int site = 0; site < int(L.sites.size()); ++site) { const Site s = L.sites[site]; - const int reach = int(L.pondRadius[site]) + 2; + const double shore = L.pondRadius[site] + 2; const auto here = [&](int i) { return L.open[i] && L.label[i] == site && !reserved[i] && @@ -350,27 +380,21 @@ bool generate(Game &game, GenerationContext &context) continue; if (kind[site] == 1) { - if (const int seed = seedNear(t, s.x, s.y, reach, here); seed >= 0) - growPatch(map, t, seed, CORN, int(scaledCount(kFarmWheat, o.wheat)), here); - if (const int seed = seedNear(t, s.x + reach + 3, s.y, 4, here); seed >= 0) - growPatch(map, t, seed, WOOD, int(scaledCount(kFarmWood, o.wood)), here); + ringPlant(site, shore, CORN, int(scaledCount(kFarmWheat, o.wheat)), here); + ringPlant(site, shore + 3, WOOD, int(scaledCount(kFarmWood, o.wood)), here); } else if (kind[site] == 3) { if (scaledCount(1, o.fruit) > 0) - if (const int seed = seedNear(t, s.x - reach - 1, s.y, 3, here); seed >= 0) + if (const int seed = seedNear(t, s.x - int(shore) - 2, s.y, 3, here); seed >= 0) placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), CHERRY + int(context.bounded("anthill-fruit", 3)), 1); - if (const int seed = seedNear(t, s.x + reach, s.y, 3, here); seed >= 0) - growPatch(map, t, seed, CORN, int(scaledCount(kTreasureWheat, o.wheat)), here); + ringPlant(site, shore, CORN, int(scaledCount(kTreasureWheat, o.wheat)), here); } else if ((site / 2) % 2 == 0) - { - if (const int seed = seedNear(t, s.x, s.y, reach, here); seed >= 0) - growPatch(map, t, seed, CORN, int(scaledCount(kPlainWheat, o.wheat)), here); - } - else if (const int seed = seedNear(t, s.x, s.y, reach, here); seed >= 0) - growPatch(map, t, seed, WOOD, int(scaledCount(kPlainWood, o.wood)), here); + ringPlant(site, shore, CORN, int(scaledCount(kPlainWheat, o.wheat)), here); + else + ringPlant(site, shore, WOOD, int(scaledCount(kPlainWood, o.wood)), here); } seedAlgae(map, context, t, "anthill-algae", o.algae, AlgaeBand::anyWater(12)); secureStartingCrops(game, context, t, 24, 32, 0, &rock); @@ -390,15 +414,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (const std::string mismatch = designMismatch(L, map, "anthill"); !mismatch.empty()) return mismatch; const Torus &t = L.t; - for (int k = 0; k < context.request.nbTeams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s queen chamber has lost its pond."; - } + // A queen chamber is dry by design (second play); the farm chambers keep the water. + for (int site : L.farmSite) + if (!map.isWater(L.sites[site].x, L.sites[site].y)) + return "A farm chamber has lost its pond at (" + std::to_string(L.sites[site].x) + + ", " + std::to_string(L.sites[site].y) + ")."; return walkFromFirstColony(map, context.request.nbTeams, "the anthill", "through the tunnels") .error; } @@ -419,7 +439,7 @@ GeneratorDefinition anthillDefinition() "anthill", 32, "Anthill", - 2, + 3, false, // Chambers 28 apart give a 256 map about a hundred of them; a chamber of radius 5 holds // three or four buildings; a queen chamber grown to 40 building sites (overlapping 4x4 diff --git a/src/map/generator/generators/OldGrowthGenerator.cpp b/src/map/generator/generators/OldGrowthGenerator.cpp index c65b18fc0..b5b956d09 100644 --- a/src/map/generator/generators/OldGrowthGenerator.cpp +++ b/src/map/generator/generators/OldGrowthGenerator.cpp @@ -60,18 +60,23 @@ using namespace MapGeneration; // wood at all inside (wood is cut from the forest edge just past the sand, within the crop // guarantee's 32 steps), and a two-tile sand ring round the clearing stops the forest spreading in // (wheat and wood spread only onto grass, and the growth probe refuses beside sand). +// +// FEEDBACK 2026-09-13 (second play): "default base size still needs to be just a bit bigger than +// this, and we also need to preseed a ton more wheat around these pools in the player's home base." +// So: radius 24 by default (was 20), the kit's two patches 24 each (was 14), and every pool ringed +// with 32 wheat (was 12) grown from four seeds spaced round its shore so the ring is whole. namespace { // Every home's starting kit, unscaled whatever the amounts say: two wheat patches beside the central // pond and a quarry. No wood (FEEDBACK 2026-09-13): the forest edge is the woodlot. -constexpr int kHomeWheat = 14, kHomeQuarry = 2; +constexpr int kHomeWheat = 24, kHomeQuarry = 2; // The ring of pools round a home (FEEDBACK 2026-09-13, "a lot more pools of water"): `homePools` // pools of this radius on a ring at this share of the home's radius, each with this much wheat on its // shore. At the default radius 20 the ring is 12 tiles out, so every pool waters the ground between // itself and the central pond and the whole clearing is fertile. constexpr double kPoolRadius = 2.5, kPoolRingShare = 0.6; -constexpr int kPoolWheat = 12; +constexpr int kPoolWheat = 32, kPoolSeeds = 4; // The forest starts this far beyond a home's rough disc, so the clearing's edge is open ground to // build against and the pond's beach never meets a tree; then a ring of sand this wide keeps the // forest from spreading into the clearing (FEEDBACK 2026-09-13). Two tiles: crops spread only onto @@ -329,16 +334,25 @@ bool generate(Game &game, GenerationContext &context) {frame.at(-0.4 * reach, -reach, 12), frame.at(-0.4 * reach, reach, 12), frame.at(reach + 3, 0, 10), kHomeWheat, 0, kHomeQuarry}, eligible); + // Wheat all round every pool (second play: "a ton more wheat around these pools"): grown + // from kPoolSeeds seeds spaced round the shore, each with its share, so the ring is whole. for (const ShapePoint &pool : L.pools[k]) { const int px = int(std::lround(pool.x)), py = int(std::lround(pool.y)); const auto shore = [&](int i) { return eligible(i) && - t.dist2(px, py, i % t.w, i / t.w) <= (kPoolRadius + 4) * (kPoolRadius + 4); + t.dist2(px, py, i % t.w, i / t.w) <= (kPoolRadius + 5) * (kPoolRadius + 5); }; - if (const int seed = seedNear(t, px, py, int(kPoolRadius) + 4, shore); seed >= 0) - growPatch(map, t, seed, CORN, kPoolWheat, shore); + for (int q = 0; q < kPoolSeeds; ++q) + { + const double a = 2 * kPi * q / kPoolSeeds; + const int seed = + seedNear(t, int(std::lround(pool.x + (kPoolRadius + 2) * std::cos(a))), + int(std::lround(pool.y + (kPoolRadius + 2) * std::sin(a))), 2, shore); + if (seed >= 0) + growPatch(map, t, seed, CORN, kPoolWheat / kPoolSeeds, shore); + } } } // The lakes' prizes go down before the forest, so they stand in the open on the shore: wheat all @@ -440,7 +454,7 @@ GeneratorDefinition oldGrowthDefinition() "old-growth", 28, "Old growth", - 2, + 3, false, // 90% cover reads as unbroken forest with the odd glade; one lake per 128x128 of 90 // tiles (four on a 256 map, each a few days' cutting from any home) keeps them rare enough @@ -448,9 +462,10 @@ GeneratorDefinition oldGrowthDefinition() {{"forest-density", "Forest density", 60, 100, 5, 90, ControlGroup::Terrain}, {"lakes", "Lakes", 0, 4, 1, 1, ControlGroup::Terrain}, {"lake-size", "Lake size", 40, 160, 10, 90, ControlGroup::Terrain}, - // FEEDBACK 2026-09-13: homes of radius 20 with five pools (was 13 with one pond); the - // whole clearing is then within the growth probe's reach of water. - {"home-size", "Home size", 12, 24, 1, 20, ControlGroup::Layout}, + // FEEDBACK 2026-09-13: homes of radius 24 with five pools (first 13 with one pond, then + // 20; the second play wanted "just a bit bigger"); the whole clearing is within the growth + // probe's reach of water. + {"home-size", "Home size", 12, 30, 1, 24, ControlGroup::Layout}, {"home-pools", "Home pools", 0, 8, 1, 5, ControlGroup::Layout}, GeneratorControl::toggle("hidden-groves", "Hidden groves", true, ControlGroup::Layout), // On, a trail is cut from every colony to the first before the game starts. diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index d8a7f6829..906e1b934 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -54,6 +54,13 @@ using namespace MapGeneration; // at three per colony (the control is whole plots per colony; 2.5 was asked for); no outcrops in the // fields and a third of the wood; every plaza's pool sized to its block, a tile short of the streets // round it; a fruit grove beside every plaza's pool, the cathedral keeping the full orchard. +// +// FEEDBACK 2026-09-13 (second play): "close to good, but it might play nicer if the outer ring had +// little 'tendril' sand roads extending inwards towards the cities and settlements." So: from the +// fields' sand cap a tendril of sand runs in to every home plaza and to the cathedral square, along +// the streets (the cheapest walk over street ground, never through a block), so those streets stay +// open however the city is built up (a sand corner spoils the tiles round it for building) and every +// colony has a marked way to its fields (`tendrils`, on). namespace { @@ -84,7 +91,7 @@ struct Layout std::vector axes; // each home's facing: from the fountain to the swarm int cathedral = -1; std::vector homeOf; - std::vector city, block, street, water, farmRegion; + std::vector city, block, street, water, road, farmRegion; Farm farm; // the fields' rows, sand and plots double fountainRadius = 0; std::vector homes, kits; @@ -293,6 +300,45 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (int i = 0; i < n; ++i) if (L.farm.water[i]) L.water[i] = 1; + // Tendrils (FEEDBACK 2026-09-13, second play): from every home plaza and the cathedral square, the + // cheapest walk over ground that is not a block, to the band just outside the city's margin where + // the fields' sand cap runs, traced as a line of sand corners. + L.road.assign(n, 0); + if (o.tendrils) + { + std::vector goal(n, 0), blocked(n, 0), none(n, 0); + for (int i = 0; i < n; ++i) + { + const double d = + std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); + goal[i] = d >= L.cityRadius + kFarmMargin + 0.5 && d < L.cityRadius + kFarmMargin + 2.5; + blocked[i] = L.block[i] || L.water[i]; + } + std::vector starts(L.homeCell); + starts.push_back(L.cathedral); + for (int cell : starts) + { + std::vector sources; + for (int i = 0; i < n; ++i) + if (L.cell[i] == cell && !blocked[i]) + sources.push_back(i); + const std::vector route = cheapestRoute(t, sources, goal, blocked, none); + std::vector line; + for (int i : route) + line.push_back({double(i % t.w), double(i / t.w), 0.5}); + // The route is a chain of neighbouring tiles, which may step across the seam; tracePath + // joins consecutive points the short way, so it is traced a segment at a time. + for (size_t p = 0; p + 1 < line.size(); ++p) + { + const int dx = t.offsetX(int(line[p].x), int(line[p + 1].x)); + const int dy = t.offsetY(int(line[p].y), int(line[p + 1].y)); + tracePath(L.road, t, {line[p], {line[p].x + dx, line[p].y + dy, 0.5}}); + } + } + for (int i = 0; i < n; ++i) + if (L.water[i]) + L.road[i] = 0; + } return L; } @@ -315,7 +361,7 @@ bool generate(Game &game, GenerationContext &context) context.stage = "old town terrain"; TerrainSketch terrain(n, GRASS); for (int i = 0; i < n; ++i) - terrain[i] = L.water[i] ? WATER : L.farm.sand[i] ? SAND : GRASS; + terrain[i] = L.water[i] ? WATER : (L.farm.sand[i] || L.road[i]) ? SAND : GRASS; layBeaches(terrain, t); writeUndermap(map, terrain); // The blocks are stone, wherever the beaches left pure grass. @@ -454,9 +500,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) OldTownOptions::OldTownOptions(const GenerationRequest &r) : citySize(r.option("city-size")), blockSize(r.option("block-size")), streetWidth(r.option("street-width")), warp(r.option("warp")), plazas(r.option("plazas")), - farmPlots(r.option("farm-plots")), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) + farmPlots(r.option("farm-plots")), tendrils(r.option("tendrils") != 0), + wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")) { } @@ -465,7 +512,7 @@ GeneratorDefinition oldTownDefinition() return {"old-town", 31, "Old town", - 2, + 3, false, // A city of 70% of the half side leaves a belt of fields round it; blocks of 14 with // streets of 4 give a 256 map about a hundred blocks and streets a column of units wide @@ -477,6 +524,7 @@ GeneratorDefinition oldTownDefinition() {"warp", "Warp", 0, 100, 10, 50, ControlGroup::Terrain}, {"plazas", "Plazas", 0, 4, 1, 2, ControlGroup::Layout}, {"farm-plots", "Farm plots", 0, 6, 1, 3, ControlGroup::Layout}, + GeneratorControl::toggle("tendrils", "Tendril roads", true, ControlGroup::Terrain), GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), GeneratorControl::percentage("stone-amount", "Stone amount"), diff --git a/src/map/generator/generators/OldTownGenerator.h b/src/map/generator/generators/OldTownGenerator.h index ee6ff123b..2744ca975 100644 --- a/src/map/generator/generators/OldTownGenerator.h +++ b/src/map/generator/generators/OldTownGenerator.h @@ -5,6 +5,7 @@ struct OldTownOptions { int citySize, blockSize, streetWidth, warp, plazas, farmPlots; + bool tendrils; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit OldTownOptions(const GenerationRequest &r); }; diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index f1b4fe612..91c6bea00 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -216,14 +216,14 @@ macos-arm64 27 1 8 8 4 3 ok 15117378251408628476 macos-arm64 27 1 8 8 8 1 ok 2092448838222506393 macos-arm64 27 1 9 8 4 1 ok 1645009079489818947 macos-arm64 27 1 9 9 4 1 ok 11877117867324334076 -macos-arm64 28 2 7 7 4 1 ok 12828393615859966248 -macos-arm64 28 2 8 8 2 1 ok 599465256471295286 -macos-arm64 28 2 8 8 4 1 ok 17502920827962285745 -macos-arm64 28 2 8 8 4 2 ok 15639686272591918501 -macos-arm64 28 2 8 8 4 3 ok 16688021067582874395 -macos-arm64 28 2 8 8 8 1 ok 14384250993784145721 -macos-arm64 28 2 9 8 4 1 ok 172782605289400047 -macos-arm64 28 2 9 9 4 1 ok 3886822729879572227 +macos-arm64 28 3 7 7 4 1 ok 10826945051381357680 +macos-arm64 28 3 8 8 2 1 ok 13829565247877238314 +macos-arm64 28 3 8 8 4 1 ok 4975978679616822016 +macos-arm64 28 3 8 8 4 2 ok 1491883777312121906 +macos-arm64 28 3 8 8 4 3 ok 13102395041856171672 +macos-arm64 28 3 8 8 8 1 ok 11416359873756795221 +macos-arm64 28 3 9 8 4 1 ok 5369678402303790376 +macos-arm64 28 3 9 9 4 1 ok 7404275210351283322 macos-arm64 29 1 7 7 4 1 ok 4971400333522175054 macos-arm64 29 1 8 8 2 1 ok 7612660082359254337 macos-arm64 29 1 8 8 4 1 ok 2450863270395321431 @@ -240,22 +240,22 @@ macos-arm64 30 1 8 8 4 3 ok 17544008518965759461 macos-arm64 30 1 8 8 8 1 ok 7267460019722476921 macos-arm64 30 1 9 8 4 1 ok 1546476112144531604 macos-arm64 30 1 9 9 4 1 ok 8553529852829638885 -macos-arm64 31 2 7 7 4 1 ok 17522295745450980172 -macos-arm64 31 2 8 8 2 1 ok 3060687545961273628 -macos-arm64 31 2 8 8 4 1 ok 17182767664541181617 -macos-arm64 31 2 8 8 4 2 ok 16267097138348319701 -macos-arm64 31 2 8 8 4 3 ok 17025794210660588829 -macos-arm64 31 2 8 8 8 1 ok 2732986575812595196 -macos-arm64 31 2 9 8 4 1 ok 5119741422102883260 -macos-arm64 31 2 9 9 4 1 ok 16536774409556665085 -macos-arm64 32 2 7 7 4 1 ok 15003951044065992680 -macos-arm64 32 2 8 8 2 1 ok 529453986809017478 -macos-arm64 32 2 8 8 4 1 ok 10670349862726652006 -macos-arm64 32 2 8 8 4 2 ok 18369750067703358534 -macos-arm64 32 2 8 8 4 3 ok 5402358506440399801 -macos-arm64 32 2 8 8 8 1 ok 13740435838411464033 -macos-arm64 32 2 9 8 4 1 ok 5139230782542717947 -macos-arm64 32 2 9 9 4 1 ok 7904570940048982311 +macos-arm64 31 3 7 7 4 1 ok 6085651638957483935 +macos-arm64 31 3 8 8 2 1 ok 18428696912702139381 +macos-arm64 31 3 8 8 4 1 ok 4300530431076339358 +macos-arm64 31 3 8 8 4 2 ok 4848582803774937852 +macos-arm64 31 3 8 8 4 3 ok 9408582954644297693 +macos-arm64 31 3 8 8 8 1 ok 4514528244961590411 +macos-arm64 31 3 9 8 4 1 ok 3693429133197469880 +macos-arm64 31 3 9 9 4 1 ok 3520152432439000952 +macos-arm64 32 3 7 7 4 1 ok 5172582828228199195 +macos-arm64 32 3 8 8 2 1 ok 8633803889491191707 +macos-arm64 32 3 8 8 4 1 ok 8410000769200599553 +macos-arm64 32 3 8 8 4 2 ok 18324534515658063123 +macos-arm64 32 3 8 8 4 3 ok 14927243951147519255 +macos-arm64 32 3 8 8 8 1 ok 16589278676902375793 +macos-arm64 32 3 9 8 4 1 ok 3458795526725179421 +macos-arm64 32 3 9 9 4 1 ok 5226362590732428076 macos-arm64 33 1 7 7 4 1 ok 8691360005280724925 macos-arm64 33 1 8 8 2 1 ok 6681693907801693688 macos-arm64 33 1 8 8 4 1 ok 9400400174102501753 From 8f5be47815e2ebcfd69351f1fc86cbae1c2ed1c7 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 19:54:48 -0400 Subject: [PATCH 118/136] Make Old town's tendrils short, frequent and two tiles wide FEEDBACK 2026-09-13 (third play): the tendrils were meant as "small roads but way more frequent, extending from the outer perimeter of the circle, inwards like 10 or 12 squares, maybe with a bit of meandering ... dozens of them, all the way around the perimeter, roughly evenly spaced ... a default width of 2 cells of sand". Old town (revision 4) now lays one wandering sand road every 8 tiles round the fields' cap ring (some seventy on a 256 map), 10 to 13 tiles long and two tiles wide, running in across the margin into the outer streets and notching the outer blocks; the earlier plaza-to-ring routes are gone. Verified on macOS arm64: golden table regenerated at revision 4 (264 rows, 0 failures), every Old town lobby cell on seeds 1 and 2, defaults test, strict translation check, client build. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 4 ++ .../generator/generators/OldTownGenerator.cpp | 67 +++++++++---------- test/map-generator-golden.txt | 16 ++--- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 311808b21..fa1eed8a9 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -1099,6 +1099,10 @@ into the next. failed), the fountain in one and the swarm in the other, the pairs as far apart as the city allows; `plazas` (0-4, 2) more per colony farthest from those, given up first when the city is small; and the cathedral square at the cell holding the centre, with an orchard of the three fruits round its fountain. +- **Tendrils.** From the fields' sand cap, one every 8 tiles round it (some seventy on a 256 map), a + wavy road of sand two tiles wide and 10-13 tiles long runs in towards the city (`tendrils`, on): across the margin and into the outer streets, notching + the outer blocks where it meets them (the second and third plays asked for "little tendril sand + roads extending inwards", short and frequent). - **Fields.** Outside the city, farm rows along the map's axis (`layFarm` at `bestFarmRows`, bridged every 16) with `farm-plots` (0-6, 3) 10x4 building plots per colony spread through them (`stampFarmPlot`, each the farthest a plot can stand from the city and the plots before it), half diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index 906e1b934..c12ecd87c 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -56,11 +56,13 @@ using namespace MapGeneration; // round it; a fruit grove beside every plaza's pool, the cathedral keeping the full orchard. // // FEEDBACK 2026-09-13 (second play): "close to good, but it might play nicer if the outer ring had -// little 'tendril' sand roads extending inwards towards the cities and settlements." So: from the -// fields' sand cap a tendril of sand runs in to every home plaza and to the cathedral square, along -// the streets (the cheapest walk over street ground, never through a block), so those streets stay -// open however the city is built up (a sand corner spoils the tiles round it for building) and every -// colony has a marked way to its fields (`tendrils`, on). +// little 'tendril' sand roads extending inwards towards the cities and settlements." A first reading +// ran a road from the ring along the streets to every plaza; the user meant something else (third +// play): "small roads but way more frequent, extending from the outer perimeter of the circle, +// inwards like 10 or 12 squares, maybe with a bit of meandering or waviness." So: from the fields' +// sand cap, every kTendrilSpacing tiles round it, a wandering line of sand ten to twelve tiles long +// runs straight in towards the city's middle (`tendrils`, on). Where one meets an outer block it +// carves a notch of sand into it (stone stands only on pure grass), so the city frays into the fields. namespace { @@ -71,6 +73,13 @@ constexpr int kHomeWheat = 14, kHomeWood = 12; // the map's axis; a farm plot's middle keeps this far inside the fields, so its ring of sand and the // margin round it (10 wide, 4 high, a ring of 2 and a vertex more) never meet the city. constexpr int kFarmMargin = 4, kFarmRim = 2, kFarmBridges = 16, kPlotMargin = 9; +// Tendrils (third play: "dozens of them, all the way around the perimeter, roughly evenly spaced ... +// a default width of 2 cells of sand"): one every this many tiles round the fields' sand cap, this +// long (plus up to kTendrilExtra), wandering sideways by up to this many tiles, stroked +// kTendrilHalfWidth either side of its line: three corners across, which is two whole tiles of sand. +// Eight tiles apart on a ring of about 600 tiles is some seventy tendrils on a 256 map. +constexpr int kTendrilSpacing = 8, kTendrilLength = 10, kTendrilExtra = 3; +constexpr double kTendrilWander = 2.0, kTendrilHalfWidth = 1.5; // A fountain fills its plaza to this far short of the streets round it (FEEDBACK 2026-09-13: pools // "big enough that they push up close against their surrounding grid cells' stone"): the block's // half width less the street's, less a tile for the beach. 4.0 at the default block and street, an @@ -300,40 +309,26 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (int i = 0; i < n; ++i) if (L.farm.water[i]) L.water[i] = 1; - // Tendrils (FEEDBACK 2026-09-13, second play): from every home plaza and the cathedral square, the - // cheapest walk over ground that is not a block, to the band just outside the city's margin where - // the fields' sand cap runs, traced as a line of sand corners. + // Tendrils (FEEDBACK 2026-09-13, third play): short wavy sand roads from the fields' cap ring in + // towards the city, one every kTendrilSpacing tiles round the ring, kTendrilLength to + // kTendrilLength + kTendrilExtra tiles long, each a wanderingPath stroked two tiles of sand wide. + // They cross the margin and run into the outer streets and blocks; the pools are left alone. L.road.assign(n, 0); if (o.tendrils) { - std::vector goal(n, 0), blocked(n, 0), none(n, 0); - for (int i = 0; i < n; ++i) - { - const double d = - std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); - goal[i] = d >= L.cityRadius + kFarmMargin + 0.5 && d < L.cityRadius + kFarmMargin + 2.5; - blocked[i] = L.block[i] || L.water[i]; - } - std::vector starts(L.homeCell); - starts.push_back(L.cathedral); - for (int cell : starts) + const double ring = L.cityRadius + kFarmMargin + 1; + const int count = std::max(1, int(std::lround(2 * kPi * ring / kTendrilSpacing))); + const double phase = context.bounded("town-tendrils", 3600) / 3600.0 * 2 * kPi; + std::mt19937 &wobble = context.stream("town-tendrils"); + for (int k = 0; k < count; ++k) { - std::vector sources; - for (int i = 0; i < n; ++i) - if (L.cell[i] == cell && !blocked[i]) - sources.push_back(i); - const std::vector route = cheapestRoute(t, sources, goal, blocked, none); - std::vector line; - for (int i : route) - line.push_back({double(i % t.w), double(i / t.w), 0.5}); - // The route is a chain of neighbouring tiles, which may step across the seam; tracePath - // joins consecutive points the short way, so it is traced a segment at a time. - for (size_t p = 0; p + 1 < line.size(); ++p) - { - const int dx = t.offsetX(int(line[p].x), int(line[p + 1].x)); - const int dy = t.offsetY(int(line[p].y), int(line[p + 1].y)); - tracePath(L.road, t, {line[p], {line[p].x + dx, line[p].y + dy, 0.5}}); - } + const double a = phase + 2 * kPi * k / count; + const double length = + kTendrilLength + context.bounded("town-tendrils", kTendrilExtra + 1); + const ShapePoint from = polarPoint(L.cx, L.cy, ring, a); + const ShapePoint to = polarPoint(L.cx, L.cy, ring - length, a); + strokePath(L.road, t, + wanderingPath(t, from, to, kTendrilHalfWidth, kTendrilWander, 0.0, wobble)); } for (int i = 0; i < n; ++i) if (L.water[i]) @@ -512,7 +507,7 @@ GeneratorDefinition oldTownDefinition() return {"old-town", 31, "Old town", - 3, + 4, false, // A city of 70% of the half side leaves a belt of fields round it; blocks of 14 with // streets of 4 give a 256 map about a hundred blocks and streets a column of units wide diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 91c6bea00..d8ef48f9a 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -240,14 +240,14 @@ macos-arm64 30 1 8 8 4 3 ok 17544008518965759461 macos-arm64 30 1 8 8 8 1 ok 7267460019722476921 macos-arm64 30 1 9 8 4 1 ok 1546476112144531604 macos-arm64 30 1 9 9 4 1 ok 8553529852829638885 -macos-arm64 31 3 7 7 4 1 ok 6085651638957483935 -macos-arm64 31 3 8 8 2 1 ok 18428696912702139381 -macos-arm64 31 3 8 8 4 1 ok 4300530431076339358 -macos-arm64 31 3 8 8 4 2 ok 4848582803774937852 -macos-arm64 31 3 8 8 4 3 ok 9408582954644297693 -macos-arm64 31 3 8 8 8 1 ok 4514528244961590411 -macos-arm64 31 3 9 8 4 1 ok 3693429133197469880 -macos-arm64 31 3 9 9 4 1 ok 3520152432439000952 +macos-arm64 31 4 7 7 4 1 ok 7521704668368614625 +macos-arm64 31 4 8 8 2 1 ok 8316691494804024369 +macos-arm64 31 4 8 8 4 1 ok 15465797287389910629 +macos-arm64 31 4 8 8 4 2 ok 2919888090512930921 +macos-arm64 31 4 8 8 4 3 ok 8166632277809661422 +macos-arm64 31 4 8 8 8 1 ok 4835778594405494983 +macos-arm64 31 4 9 8 4 1 ok 4428905252989944732 +macos-arm64 31 4 9 9 4 1 ok 5968096499992773644 macos-arm64 32 3 7 7 4 1 ok 5172582828228199195 macos-arm64 32 3 8 8 2 1 ok 8633803889491191707 macos-arm64 32 3 8 8 4 1 ok 8410000769200599553 From 4414a17f99c09ddf1e308ee60aa73d1011551991 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 20:15:41 -0400 Subject: [PATCH 119/136] Halve Old town's tendrils FEEDBACK 2026-09-13 (third play): "right idea on old town but let's do 1/2 the number of those inwards roads". Old town (revision 5) lays a tendril every 16 tiles round the fields' cap ring instead of every 8: some thirty-five on a 256 map. Verified on macOS arm64: golden table regenerated at revision 5 (264 rows, 0 failures), every Old town lobby cell on seed 1, defaults test, client build. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/map-generators/MAP_GENERATOR_FRAMEWORK.md | 2 +- .../generator/generators/OldTownGenerator.cpp | 13 +++++++------ test/map-generator-golden.txt | 16 ++++++++-------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index fa1eed8a9..73885cde5 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -1099,7 +1099,7 @@ into the next. failed), the fountain in one and the swarm in the other, the pairs as far apart as the city allows; `plazas` (0-4, 2) more per colony farthest from those, given up first when the city is small; and the cathedral square at the cell holding the centre, with an orchard of the three fruits round its fountain. -- **Tendrils.** From the fields' sand cap, one every 8 tiles round it (some seventy on a 256 map), a +- **Tendrils.** From the fields' sand cap, one every 16 tiles round it (some thirty-five on a 256 map), a wavy road of sand two tiles wide and 10-13 tiles long runs in towards the city (`tendrils`, on): across the margin and into the outer streets, notching the outer blocks where it meets them (the second and third plays asked for "little tendril sand roads extending inwards", short and frequent). diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index c12ecd87c..b95619810 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -74,11 +74,12 @@ constexpr int kHomeWheat = 14, kHomeWood = 12; // margin round it (10 wide, 4 high, a ring of 2 and a vertex more) never meet the city. constexpr int kFarmMargin = 4, kFarmRim = 2, kFarmBridges = 16, kPlotMargin = 9; // Tendrils (third play: "dozens of them, all the way around the perimeter, roughly evenly spaced ... -// a default width of 2 cells of sand"): one every this many tiles round the fields' sand cap, this -// long (plus up to kTendrilExtra), wandering sideways by up to this many tiles, stroked -// kTendrilHalfWidth either side of its line: three corners across, which is two whole tiles of sand. -// Eight tiles apart on a ring of about 600 tiles is some seventy tendrils on a 256 map. -constexpr int kTendrilSpacing = 8, kTendrilLength = 10, kTendrilExtra = 3; +// a default width of 2 cells of sand", then "1/2 the number of those inwards roads"): one every this +// many tiles round the fields' sand cap, this long (plus up to kTendrilExtra), wandering sideways by +// up to this many tiles, stroked kTendrilHalfWidth either side of its line: three corners across, +// which is two whole tiles of sand. Sixteen tiles apart on a ring of about 600 tiles is some +// thirty-five tendrils on a 256 map (eight apart, seventy, was too many). +constexpr int kTendrilSpacing = 16, kTendrilLength = 10, kTendrilExtra = 3; constexpr double kTendrilWander = 2.0, kTendrilHalfWidth = 1.5; // A fountain fills its plaza to this far short of the streets round it (FEEDBACK 2026-09-13: pools // "big enough that they push up close against their surrounding grid cells' stone"): the block's @@ -507,7 +508,7 @@ GeneratorDefinition oldTownDefinition() return {"old-town", 31, "Old town", - 4, + 5, false, // A city of 70% of the half side leaves a belt of fields round it; blocks of 14 with // streets of 4 give a 256 map about a hundred blocks and streets a column of units wide diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index d8ef48f9a..c29e0c9e4 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -240,14 +240,14 @@ macos-arm64 30 1 8 8 4 3 ok 17544008518965759461 macos-arm64 30 1 8 8 8 1 ok 7267460019722476921 macos-arm64 30 1 9 8 4 1 ok 1546476112144531604 macos-arm64 30 1 9 9 4 1 ok 8553529852829638885 -macos-arm64 31 4 7 7 4 1 ok 7521704668368614625 -macos-arm64 31 4 8 8 2 1 ok 8316691494804024369 -macos-arm64 31 4 8 8 4 1 ok 15465797287389910629 -macos-arm64 31 4 8 8 4 2 ok 2919888090512930921 -macos-arm64 31 4 8 8 4 3 ok 8166632277809661422 -macos-arm64 31 4 8 8 8 1 ok 4835778594405494983 -macos-arm64 31 4 9 8 4 1 ok 4428905252989944732 -macos-arm64 31 4 9 9 4 1 ok 5968096499992773644 +macos-arm64 31 5 7 7 4 1 ok 3076683908112672630 +macos-arm64 31 5 8 8 2 1 ok 11763462524905175676 +macos-arm64 31 5 8 8 4 1 ok 8413900832615966718 +macos-arm64 31 5 8 8 4 2 ok 9908515123101461592 +macos-arm64 31 5 8 8 4 3 ok 13708832934710116111 +macos-arm64 31 5 8 8 8 1 ok 7865455393007647241 +macos-arm64 31 5 9 8 4 1 ok 5703755596576899428 +macos-arm64 31 5 9 9 4 1 ok 3878540907432692789 macos-arm64 32 3 7 7 4 1 ok 5172582828228199195 macos-arm64 32 3 8 8 2 1 ok 8633803889491191707 macos-arm64 32 3 8 8 4 1 ok 8410000769200599553 From 452224bcac0935b46210f1b4f03deb1e1cd08c30 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 21:31:52 -0400 Subject: [PATCH 120/136] Drop Marches and Patchwork; revise Fingerprint, Rain shadow, Polder, Canals from play FEEDBACK 2026-09-13 (first play of each), quoted in the code: - Marches (25) and Patchwork (33) are removed: "just not working conceptually at all" and "the concept is failing". Their ids are retired, never reused; shared/Biomes stays in the toolkit. - Fingerprint (revision 2): "looks very similar to everglades ... a higher wavelength has a nicer feel ... feels very empty ... home size needs to be larger". Wavelength 30 (was 20), homes of 18 (was 12), ambient layer more than doubled. - Rain shadow (revision 2): "not enough water; ponds deeper and more connected; sand roadways through the passes; valleys feel empty; rivers to inland lakes; sand patches". Streams 5 deep and 20 long every 24; a sand road through every pass and 12 tiles into the valley; inland lakes (inland-lakes) with wandering rivers to the nearest stream; sand-patches over 6% of the valleys' grass. - Polder (revision 2): "randomize the angle ... bases way too small, growth crowds them out ... 10x4 sand plots at 2.5x the players". row-angle is Random / Vertical / Horizontal / Diagonal (Random draws any angle and rounds to whole turn counts); villages of 14 in a two-tile sand ring; two and a half plots per colony (stampFarmPlot) spread through the rows away from the villages. - Canals (revision 2): "remove the home pond; 1.5x bridges; 2x warp; every cell has its own little surprise". Dry home blocks, extra bridges 30, warp 80, and every other block dealt one of nine kinds: plain, lake, orchard, a 4x4 pad in a sand ring, two pads, quarry, woodlot, wheatfield, dune. Verified on macOS arm64: golden table regenerated (248 rows, 0 failures), --sweep 0 failing combinations, defaults test, strict translation check, client build. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 8 + data/texts.br.txt | 8 + data/texts.ca.txt | 8 + data/texts.cz.txt | 8 + data/texts.de.txt | 8 + data/texts.dk.txt | 8 + data/texts.en.txt | 8 + data/texts.eo.txt | 8 + data/texts.es.txt | 8 + data/texts.eu.txt | 8 + data/texts.fa.txt | 8 + data/texts.fi.txt | 8 + data/texts.fr.txt | 8 + data/texts.gr.txt | 8 + data/texts.hu.txt | 8 + data/texts.id.txt | 8 + data/texts.it.txt | 8 + data/texts.ja.txt | 8 + data/texts.keys.txt | 4 + data/texts.ko.txt | 8 + data/texts.nl.txt | 8 + data/texts.pl.txt | 8 + data/texts.pt.txt | 8 + data/texts.ro.txt | 8 + data/texts.ru.txt | 8 + data/texts.si.txt | 8 + data/texts.sk.txt | 8 + data/texts.sr.txt | 8 + data/texts.sv.txt | 8 + data/texts.tr.txt | 8 + data/texts.uk.txt | 8 + data/texts.vi.txt | 8 + data/texts.zh-cn.txt | 8 + data/texts.zh-tw.txt | 8 + .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 118 ++--- src/SConscript | 2 - src/map/generator/core/GeneratorRegistry.cpp | 4 - .../generator/generators/CanalsGenerator.cpp | 227 +++++++--- .../generators/FingerprintGenerator.cpp | 23 +- .../generator/generators/MarchesGenerator.cpp | 428 ------------------ .../generator/generators/MarchesGenerator.h | 12 - .../generators/PatchworkGenerator.cpp | 348 -------------- .../generator/generators/PatchworkGenerator.h | 11 - .../generator/generators/PolderGenerator.cpp | 123 ++++- .../generators/RainShadowGenerator.cpp | 166 +++++-- .../generators/RainShadowGenerator.h | 3 +- test/map-generator-golden.txt | 80 ++-- 47 files changed, 756 insertions(+), 1057 deletions(-) delete mode 100644 src/map/generator/generators/MarchesGenerator.cpp delete mode 100644 src/map/generator/generators/MarchesGenerator.h delete mode 100644 src/map/generator/generators/PatchworkGenerator.cpp delete mode 100644 src/map/generator/generators/PatchworkGenerator.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 481d10e00..e419dcf57 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2170,3 +2170,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.br.txt b/data/texts.br.txt index 67886e3e5..f1fa2eb5c 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -2168,3 +2168,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.ca.txt b/data/texts.ca.txt index addc2a32c..c95779823 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2180,3 +2180,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 778b744ae..1e16b9d7a 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.de.txt b/data/texts.de.txt index e1ef0d108..cc75a33f5 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.dk.txt b/data/texts.dk.txt index e34704222..907895df8 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2248,3 +2248,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.en.txt b/data/texts.en.txt index 165c0476a..640825dcb 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2162,3 +2162,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.eo.txt b/data/texts.eo.txt index 55bf7cd2b..b593d4b05 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2170,3 +2170,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.es.txt b/data/texts.es.txt index db07eb28b..ec4d91ff0 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 31825342f..59e9cbecb 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2182,3 +2182,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 98bcfbba5..ee8412d81 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2170,3 +2170,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 62529347a..71c17f8e5 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 666c5a68f..72d30df54 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2182,3 +2182,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.gr.txt b/data/texts.gr.txt index bc1debf89..61953bffd 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2242,3 +2242,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.hu.txt b/data/texts.hu.txt index f53023402..68f8ed932 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.id.txt b/data/texts.id.txt index 904238f6b..58096a959 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2168,3 +2168,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.it.txt b/data/texts.it.txt index a0ff2a1ac..5cd714349 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2234,3 +2234,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 6f7537714..d082088d7 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2168,3 +2168,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.keys.txt b/data/texts.keys.txt index f4fb4f019..da6383868 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1079,3 +1079,7 @@ [Home pools] [Farm plots] [Tendril roads] +[Random] +[Vertical] +[Horizontal] +[Inland lakes] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 5e6d21eaf..556531a9b 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2168,3 +2168,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 34e540ca5..ad1e797e5 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2196,3 +2196,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 8bf7b857e..402c315ee 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 98caf47fd..f3bbf3bd7 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2178,3 +2178,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 64f5f742b..ddac9dae4 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 2f0615e51..2aea148fa 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2170,3 +2170,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.si.txt b/data/texts.si.txt index 0bda7295c..813b81e03 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 5ab66f03e..8913d5011 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2180,3 +2180,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 2985efaa8..1499b421e 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2172,3 +2172,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.sv.txt b/data/texts.sv.txt index c6a5880cd..a3db9758f 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2182,3 +2182,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.tr.txt b/data/texts.tr.txt index bcd88f8e0..f8827dfd3 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2180,3 +2180,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 600d85e76..dd79c3e00 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2168,3 +2168,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 03b3fc89b..aed0b3428 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2168,3 +2168,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index abca31b2e..e5a050b2e 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2168,3 +2168,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 1579eae20..c894e0be7 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2244,3 +2244,11 @@ Home pools Farm plots [Tendril roads] Tendril roads +[Random] +Random +[Vertical] +Vertical +[Horizontal] +Horizontal +[Inland lakes] +Inland lakes diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 73885cde5..7a1b8824d 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -63,10 +63,10 @@ them so the ranking rewards what it is meant to be. ## The designed generator -Twenty-three generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, -City states, Tidal flats, Everglades, Spider web, Coral, Carousel, Amphitheatre, Switchbacks, and the nine -landscape generators Marches, Fingerprint, Rain shadow, Old growth, Canals, Polder, Old town, Anthill and -Patchwork) follow one shape, and the newest of them are little more than a sequence of shared stages: +Twenty-one generators (Maze, Fjord continent, Watershed, Stone highlands, Symmetric arena, Ring world, +City states, Tidal flats, Everglades, Spider web, Coral, Carousel, Amphitheatre, Switchbacks, and the +landscape generators Fingerprint, Rain shadow, Old growth, Canals, Polder, Old town and Anthill) follow one +shape, and the newest of them are little more than a sequence of shared stages: 1. `design(request, context)` computes the whole layout from the request and the context's named streams without touching the map, and returns it with a `failure` string when the @@ -113,7 +113,6 @@ reused after a generator is retired. | `carousel` | 22 | Carousel | Its own — see below | | `amphitheatre` | 23 | Amphitheatre | Its own — see below | | `switchbacks` | 24 | Switchbacks | Its own — see below | -| `marches` | 25 | Marches | Its own — see below | | `fingerprint` | 26 | Fingerprint | Its own — see below | | `rain-shadow` | 27 | Rain shadow | Its own — see below | | `old-growth` | 28 | Old growth | Its own — see below | @@ -121,7 +120,6 @@ reused after a generator is retired. | `polder` | 30 | Polder | Its own — see below | | `old-town` | 31 | Old town | Its own — see below | | `anthill` | 32 | Anthill | Its own — see below | -| `patchwork` | 33 | Patchwork | Its own — see below | | `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | | `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | @@ -130,6 +128,11 @@ reused after a generator is retired. | `river` | 2 | River | Height-field noise; a winding river through connected land | | `uniform` | 0 | uniform terrain | Editor-only; one terrain type, unstructured | +Retired ids, never reused: 25 (Marches, a lattice of homelands with wooded border bands, dropped +2026-09-13 after play: "not working conceptually") and 33 (Patchwork, a different biome kit per colony, +dropped the same day: "the concept is failing"). `shared/Biomes` stays in the toolkit for any map that +wants a kind of land as data. + Swamp, River, Islands and Crater Lakes ("the height-field generators") shape their terrain and paint their resource bands from the same Perlin noise field via `generateHeightField`'s stages (`shared/Terrain`), and use `chooseBalancedStarts` (below) for colony placement. Contested @@ -920,45 +923,14 @@ next leg up. Every home reaches a walled wheat farm on either flank. trails shut and with only the middle legs shut (so no leg can be skipped), a level-1 tower at home reaching the first leg and one on the plateau reaching the last, and even walks to the plateau. -## Marches - -Homelands on a lattice across the whole torus, with no centre and no prize in the middle. Every colony's -home lies in its own territory, and between every two neighbouring territories runs a band of wild -border country, the marches: wooded, with a watering hole on every stretch of border and an orchard of -all three fruits wherever three or four territories meet. A colony has a frontier with every neighbour, -and its choice is which border to push. - -- **Lattice.** Homes on the roomiest translation lattice the torus holds (`latticeSites`, `Orbits`): - exact copies of one another for 2, 4, 8 or 16 colonies, evenly staggered rows otherwise. Nothing is - designed round a middle, so a rectangular map is served as well as a square one. -- **Homelands.** Every tile's nearest home after a warp (`nearestSiteLabels`). On an exact lattice the - warp noise is laid on cells the lattice's moves are whole multiples of (`latticePeriod`), so the - homelands are exact copies too; summing plain noise over the orbits instead averaged the roughness - away and left the borders ruled straight. `border-roughness` (0-100, 40) bends a border by up to 30% - of the home spacing at full; `march-width` (6-24, 12) is the band either side of the border. -- **Fit.** A home disc must stay inside its homeland however far the warp bends the border: on a crowded - map the homes shrink (`home-size`, 10-24, 14) to what is left, then the marches narrow (never below 4), - and only then is the map refused. -- **Prizes.** One watering hole (a pool of radius 3 and 24 tiles of wheat) at the middle of every - separate stretch of border between two homelands, found as the 8-connected groups of border tiles per - pair of neighbours; an orchard of the three fruits with a quarry at every junction of three or more - homelands, junctions within three tiles merged (`orchards`, on). The marches carry wood over 55% of - their ground in noise patches (the gaps are the natural ways through) and never regrow it, being far - from water. -- **Kits and fields.** A wheat and wood kit with a quarry at every home's pond (`plantOpenHomeKit`); - ambient farmland as a share of each homeland's *fertile* ground, the ring round its pond, not of its - area (4% of the area buried the home under six hundred tiles of crops). Where the woods close a - colony in, `openColonyRoutes` cuts the cheapest way, never a ford. -- **Checked, not assumed.** The design rebuilt, every home's pond present, every colony walkable from - the first. - ## Fingerprint An organic labyrinth: a Turing pattern grown over the whole torus (`turingPattern`, `Patterns`) draws long curving bands that fork, merge and dead-end the way a fingerprint's ridges do, and the bands become the barriers. -- **Pattern.** `wavelength` (12-40, 20) is the crest-to-crest width, so about half of it is corridor; +- **Pattern.** `wavelength` (12-48, 30; the first play found 20 read like Everglades) is the + crest-to-crest width, so about half of it is corridor; `grain` (0-100, 0) stretches the blur along one axis so the bands run rather than wander. `pattern` chooses the barrier's share of the map: Labyrinth 42% (bands and corridors read alike once a water band's beach has taken a tile of each side), Islands 60%, Channels 28%. @@ -967,9 +939,11 @@ become the barriers. only fertile ground. The pools lie at the bottoms of the field's troughs: every local minimum with nothing lower within two wavelengths (`windowMinimum`), grown to 40 tiles along its trough; a percentile of the field instead gave a hundred tiny pools whose beaches cut the stone into blobs. -- **Homes.** Clearings on a lattice (`latticeSites`), each a rough disc with a pond (`stampRoundHomes`) - and a margin of three tiles of pattern cleared beyond it, shrunk so a band's width of pattern always - runs between neighbours. Fairness is statistical, like Everglades'. +- **Homes.** Clearings on a lattice (`latticeSites`), each a rough disc of `home-size` (10-24, 18) + with a pond (`stampRoundHomes`) and a margin of three tiles of pattern cleared beyond it, shrunk so + a band's width of pattern always runs between neighbours. Fairness is statistical, like Everglades'. + The ambient layer is 22% of the fertile ground under wheat and 12% under wood, an outcrop per 600 + tiles and a grove per 1000 (the first play found half that "very empty"). - **Routes.** The pattern owes nobody a way through: `openColonyRoutes` opens the cheapest way from the first colony to any it cannot walk to, a sand ford across water or a gap cut in the stone. @@ -989,11 +963,18 @@ at intervals staggered from ridge to ridge, so moving along a valley is easy and reach across (`towerReach`). - **Passes.** Every `pass-spacing` tiles (24-96, 48) along the ridge, `pass-width` (3-9, 5) wide, laid out by the phase along the ridges (`alongStripes`), so they repeat seamlessly. -- **Wind.** Pools run along the windward foot, 12 tiles long every 24, four tiles out from the stone +- **Wind.** Streams run along the windward foot, 20 tiles long every 24 and 5 deep (the first play + wanted the ponds "deeper and more connected"), four tiles out from the stone (three took the ridge's windward row with them: a pool's beach spoils the tiles round it and stone stands only on pure grass); the lee sand starts two tiles behind the ridge for the same reason and runs `lee-width` (2-12, 6) tiles, measured downwind from the stone itself (`upwindSteps`) so it stops where a pass lets the rain through. +- **Pass roads, lakes and rivers.** A line of sand runs through every pass and 12 tiles into the + valley on either side, so a pass never grows shut; no stream lies within 4 tiles of a pass along the + ridge. `inland-lakes` (on) throws lake sites 56 apart, keeps those within 18% of a valley's middle + phase, grows a rough lake of radius 5 at each and joins it to the nearest stream by a wandering river + one tile wide (`wanderingPath`); `sand-patches` (0-20, 6) percent of the valleys' grass is sprinkled + into sand patches (`sprinkleSand`). All from the first play: "the valleys feel too empty". - **Homes.** On a lattice, each slid along the wind to the middle of its valley, so every home has the same ridge behind it and the same foot before it. Crops go on the fertile ground, which the pools make the windward side of every valley: a third of it under wheat and a fifth under wood. @@ -1040,18 +1021,21 @@ a unit and just narrow enough for a tower on one bank to shoot the other, and on bridges. Towers reach across from the first minute and armies cannot, so where the first towers go is the opening; once swimming pools are built every canal is a road. -- **Blocks.** A square tiling of `block-size` (16-40, 24) warped by `warp` (0-100, 40), every edge an +- **Blocks.** A square tiling of `block-size` (16-40, 24) warped by `warp` (0-100, 80), every edge an obstacle the warp keeps apart (`warpCorners`), every edge stroked `canal-width` corners of water (3-5, 3). A canal's water is a corner narrower than the stroke and a diagonal canal is sealed against a diagonal step only when its water is two tiles thick, which is why the narrowest offered is 3; a straight canal w corners wide puts the banks' grass w + 4 apart (`Channels`), so 3 is reached by a level-2 tower and not a level-1. -- **Homes and markets.** The colonies' blocks are the ones farthest apart on the block graph - (`spreadPockets`), each with a pond and a kit; a market block per colony, farthest from every home, - carries an orchard of the three fruits. Fairness is statistical. +- **Homes and block kinds.** The colonies' blocks are the ones farthest apart on the block graph + (`spreadPockets`), each with a kit and no pond (the first play asked for the pond gone; the canal + waters the block). Every other block is dealt one of nine kinds from a weighted draw (24/12/9/15/8/ + 8/8/8/8 in 100): plain fields; a lake; an orchard round a small pond; a 4x4 pad of grass in a ring + of sand (a building spot nothing grows onto, `stampFarmPlot`); two such pads; a quarry; a woodlot; + a wheatfield; a dune of bare sand. "Each cell has its own little surprise." Fairness is statistical. - **Bridges.** A tree of shortest block-to-block paths from the first colony's block to every other - colony's, so every colony can be walked to, then `extra-bridges` percent of the blocks' count more at - random (`openLoops`), so most blocks stay islands until someone swims. A bridge is a line of sand + colony's, so every colony can be walked to, then `extra-bridges` percent (0-100, 30) of the blocks' + count more at random (`openLoops`), so most blocks stay islands until someone swims. A bridge is a line of sand corners across the canal (a tile with a sand corner is no longer pure water) reaching onto both banks. - **Towers.** `tower-count` towers at `starting-towers` level (default 2 of level 2) and two pads per colony, on its own bank against the beach (`chooseTowerSites` with `against`), covering the most of @@ -1068,13 +1052,19 @@ two, sand dykes across the ditches at intervals, small grass villages for the co between them. Food is effectively unlimited; the game is logistics, on the dykes and the ditches' beaches, until someone can swim. -- **Rows.** Stripes that wrap the torus exactly (`stripePhase`): straight rows 10 of crops and 6 of water - (a period of 16 divides every map side; the yield fit's 10 and 8 would give 18, which does not, for - under 3% of yield), or diagonal rows (`row-angle`) 11 and 6 on a spacing the map sets. Dykes every +- **Rows.** Stripes that wrap the torus exactly (`stripePhase`), 10 of crops and 6 of water along an + axis (a period of 16 divides every map side; the yield fit's 10 and 8 would give 18, which does not, + for under 3% of yield) and 11 and 5 on the diagonal. `row-angle` is Random by default (the first + play asked for any angle each round): an angle drawn per map and rounded to whole turn counts on a + circle of `s / 16`, so the rows stay 16 apart and the angle is one of some fifty on a 256 map; + Vertical, Horizontal and Diagonal are fixed. Dykes every `dyke-spacing` tiles (12-48, 24) along the rows, two corners of sand wide, laid by the phase along the rows (`alongStripes`). -- **Villages.** Grass discs of `village-size` (8-16, 11) on a lattice, shrunk so a whole row and ditch - lie between two; each holds a swarm, its kit and a few buildings and no more. `hamlets` (on) puts a +- **Villages.** Grass discs of `village-size` (8-20, 14) on a lattice, shrunk so a whole row and ditch + lie between two, each in a two-tile ring of sand the crops cannot cross (first play: growth "quickly + crowds out the base"); each holds a swarm, its kit and a few buildings. Two and a half 10x4 farm + plots per colony (`stampFarmPlot`) are spread through the rows, each the farthest a plot can stand + from every village, hamlet and earlier plot, and at least 18 tiles from any village. `hamlets` (on) puts a grass disc of radius 5 half way between neighbouring villages, room for a forward inn and a tower, with a fruit grove. - **Fields.** 55% of the fertile row ground under wheat and 15% under wood, in patches; every row tile is @@ -1138,26 +1128,6 @@ taking the next chamber down the tunnel. - **Rock.** Stone on every uncarved tile the beaches left pure grass; `openColonyRoutes` clears crops in a tunnel and cuts stone only as a last resort, at a cost that keeps it to a tile or two. -## Patchwork - -Every colony starts on a different kind of land: the map is shared out into one territory per colony -and each is dealt a kit from `Biomes` in turn, a fertile plain, a stone fortress, an orchard island or a -forest, so every player plays a different opening. The one map in the catalog that gives up fairness by -construction. - -- **Territories.** Homes on a lattice; `growTerritories` by worth, so a territory dealt a poorer kit gets - proportionally more tiles (`biomeWorth`, an estimate from the start scorer's weights); noise in the - cost bends the borders by `border-roughness` (0-100, 40) and `smoothing` passes (0-4, 2) at radius 3 - straighten them into curves. Whatever was left unclaimed goes to the nearest home. -- **Kits.** Each kit's terrain over its whole territory (`sketchBiome`: ponds, an orchard island, a - fortress ring with a gate towards every neighbouring territory at the rim tile nearest the line between - the two homes), the home's own clearing and pond kept; then its deposits (`furnishBiome`), the amounts - scaling each kit's own shares. Every home gets the same unscaled kit with a quarry, so every opening is - at least viable. -- **Tuning.** `biomeWorth` is not a measurement: run the fairness tournament - ([FAIRNESS_TOURNAMENT.md](FAIRNESS_TOURNAMENT.md)) before trusting the deal. `openColonyRoutes` opens - the cheapest way through crops, and through a ring only where a gate failed. - ## Compatibility notes - A `GeneratorDefinition::legacyId` is a stable compatibility identifier, not a display or sort diff --git a/src/SConscript b/src/SConscript index 5d0768c3e..89b99e345 100644 --- a/src/SConscript +++ b/src/SConscript @@ -203,7 +203,6 @@ map/generator/generators/SymmetricArenaGenerator.cpp map/generator/generators/TidalFlatsGenerator.cpp map/generator/generators/UniformGenerator.cpp map/generator/generators/WatershedGenerator.cpp -map/generator/generators/PatchworkGenerator.cpp map/generator/generators/AnthillGenerator.cpp map/generator/generators/OldTownGenerator.cpp map/generator/generators/PolderGenerator.cpp @@ -211,7 +210,6 @@ map/generator/generators/CanalsGenerator.cpp map/generator/generators/OldGrowthGenerator.cpp map/generator/generators/RainShadowGenerator.cpp map/generator/generators/FingerprintGenerator.cpp -map/generator/generators/MarchesGenerator.cpp map/generator/shared/legacy/Distances.cpp map/generator/shared/HeightMap.cpp map/generator/shared/Noise.cpp diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 6806cfb2b..6dd0cea08 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -24,7 +24,6 @@ #include "TidalFlatsGenerator.h" #include "UniformGenerator.h" #include "WatershedGenerator.h" -#include "MarchesGenerator.h" #include "FingerprintGenerator.h" #include "RainShadowGenerator.h" #include "OldGrowthGenerator.h" @@ -32,7 +31,6 @@ #include "PolderGenerator.h" #include "OldTownGenerator.h" #include "AnthillGenerator.h" -#include "PatchworkGenerator.h" #include #include #include @@ -136,7 +134,6 @@ const GeneratorRegistry &GeneratorRegistry::builtins() carouselDefinition(), amphitheatreDefinition(), switchbacksDefinition(), - marchesDefinition(), fingerprintDefinition(), rainShadowDefinition(), oldGrowthDefinition(), @@ -144,7 +141,6 @@ const GeneratorRegistry &GeneratorRegistry::builtins() polderDefinition(), oldTownDefinition(), anthillDefinition(), - patchworkDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), diff --git a/src/map/generator/generators/CanalsGenerator.cpp b/src/map/generator/generators/CanalsGenerator.cpp index 4e2d60d14..98063db09 100644 --- a/src/map/generator/generators/CanalsGenerator.cpp +++ b/src/map/generator/generators/CanalsGenerator.cpp @@ -44,6 +44,18 @@ using namespace MapGeneration; // so the default canal of 3 is reached by a level-2 tower (range 7) and not by a level-1 (range 5). // Every bank has sand beside water within reach of every tile, so algae grows everywhere and crops // regrow everywhere; the fight is over bridges and banks, not fields. +// +// FEEDBACK 2026-09-13 (first play): "remove the pond from the user's starting base. increase the +// default rate of bridges across the squares 1.5x, and the default amount of warping 2x. it's also a +// little boring right now: can every grid cell have one of N types? I see some of them occasionally +// have a lake in the center; let's have a few different types - the fruit type, the lake type. I +// want a couple of types that have a sand patch protecting a 4x4 spot on the square, with no +// resources, which can be used as a building spot. maybe a few of them have a sand patch. come up +// with a few more cool ideas; each cell has its own little surprise." So: home blocks have no pond +// (their canal waters them), extra bridges default to 30% (was 20) and warp to 80 (was 40), and +// every other block is dealt one of nine kinds (BlockKind) from a weighted draw: plain fields, a +// lake, an orchard round a pond, a 4x4 building pad in a ring of sand, two such pads, a quarry, a +// woodlot, a wheatfield, or a dune of bare sand. namespace { @@ -53,8 +65,28 @@ constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; // carries units two tiles wide, since a tile with a sand corner is no longer pure water), reaching // this far past the canal's edge onto each bank so it lands on grass, not beach. constexpr double kBridgeLanding = 2.5; -// Market blocks, per home block: blocks farthest from every home carry an orchard of all three fruits. -constexpr int kMarketsPerHome = 1; +// What a block holds (first play: "each cell has its own little surprise"), and the weight each kind +// is dealt with, out of 100. Plain blocks carry the ambient fields; the rest carry one thing each. +enum BlockKind +{ + Home = -1, + Plain = 0, + Lake, // a pond at the middle, the fields round it + Orchard, // a small pond with the three fruits round it + Pad, // a 4x4 pad of grass in a ring of sand: a building spot nothing grows onto + TwoPads, // two such pads + Quarry, // a stone clump of radius 3 + Woodlot, // wood over most of the block + Wheatfield, // wheat over most of the block + Dune // a disc of bare sand: walkable, unbuildable, and nothing grows on it +}; +constexpr int kKinds = 9; +constexpr int kKindWeight[kKinds] = {24, 12, 9, 15, 8, 8, 8, 8, 8}; +// The kinds' features: the lake's and the orchard pond's radii, the pad (4 tiles square in a ring +// one corner wide; stampFarmPlot), how far apart two pads stand, the quarry's radius, the dune's +// radius, and the share of a woodlot or wheatfield under its crop. +constexpr double kLakeRadius = 3.5, kOrchardPond = 2.5, kDuneRadius = 4.0; +constexpr int kPadSize = 4, kPadRing = 1, kPadsApart = 10, kQuarryRadius = 3, kCoverPercent = 60; // Tower pads beside each colony's starting towers, and the spacing between a colony's sites. constexpr int kTowerPads = 2, kTowerSpacing = 6; @@ -63,9 +95,12 @@ struct Layout Torus t{1, 1}; Tessellation g; std::vector cell; // every tile's block - std::vector homeCell, marketCell; + std::vector homeCell; + std::vector kind; // every block's BlockKind std::vector homeOf; // the home block a tile is in, or -1 - std::vector canal, bridge, water, land; + std::vector canal, bridge, water, sand, land; + Farm pads; // the building pads, stamped as farm plots + TerrainSketch sketch; std::vector homes, kits; double homeRadius = 0; std::string failure; @@ -116,33 +151,19 @@ Layout design(const GenerationRequest &request, GenerationContext &context) "fewer colonies."; return L; } - std::vector taken(L.g.cellCount(), 0); + // Every other block's kind, dealt from a weighted draw in block order (first play: "each cell + // has its own little surprise"). + L.kind.assign(L.g.cellCount(), Plain); for (int cell : L.homeCell) - taken[cell] = 1; - for (int market = 0; market < kMarketsPerHome * teams; ++market) + L.kind[cell] = Home; + for (int cell = 0; cell < L.g.cellCount(); ++cell) { - int best = -1; - long long bestGap = -1; - for (int cell = 0; cell < L.g.cellCount(); ++cell) - { - if (taken[cell]) - continue; - long long gap = -1; - for (int other : L.homeCell) - gap = gap < 0 ? L.g.distance2(cell, other) - : std::min(gap, L.g.distance2(cell, other)); - for (int other : L.marketCell) - gap = std::min(gap, L.g.distance2(cell, other)); - if (gap > bestGap) - { - bestGap = gap; - best = cell; - } - } - if (best < 0) - break; - taken[best] = 1; - L.marketCell.push_back(best); + if (L.kind[cell] == Home) + continue; + int roll = int(context.bounded("canals-kinds", 100)), kind = 0; + while (kind + 1 < kKinds && roll >= kKindWeight[kind]) + roll -= kKindWeight[kind++]; + L.kind[cell] = kind; } // The canals: every edge stroked canalWidth corners wide. A canal's water is canalWidth - 1 tiles @@ -205,25 +226,64 @@ Layout design(const GenerationRequest &request, GenerationContext &context) {middle.x + dx / length * reach, middle.y + dy / length * reach, kBridgeHalfWidth}}); } - // Homes: a pond at the middle of every home block, and a kit round it. The block's "radius" for - // the kit is what is left between its centre and its canal. + // Homes: the kit round the middle of every home block, with no pond (first play; the block's canal + // is within the growth probe's reach of all of it). The block's "radius" for the kit is what is + // left between its centre and its canal. L.homeRadius = std::max(6.0, o.blockSize / 2.0 - o.canalWidth / 2.0 - 3); L.water = L.canal; + L.sand.assign(n, 0); L.homeOf.assign(n, -1); L.land.assign(n, 0); for (int i = 0; i < n; ++i) L.land[i] = !L.canal[i]; - for (int k = 0; k < teams; ++k) - L.homes.push_back(tilePoint(L.g.cells[L.homeCell[k]].centre)); - const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "canals-pond"); for (int k = 0; k < teams; ++k) { - L.kits.push_back( - stampRoundHome(t, L.homes[k], 0.0, pond, L.homeRadius, 1, &pond, L.water, [](int) {})); + L.homes.push_back(tilePoint(L.g.cells[L.homeCell[k]].centre)); + L.kits.push_back(L.homes[k]); for (int i = 0; i < n; ++i) if (L.cell[i] == L.homeCell[k] && L.land[i]) L.homeOf[i] = k; } + // Every block's terrain feature, at its middle: a lake, an orchard's pond, a pad or two of grass + // in a ring of sand (stamped as farm plots into the sketch), or a dune. + const RadialShape lake(kLakeRadius, 0.3, context, "canals-lakes"); + const RadialShape orchardPond(kOrchardPond, 0.3, context, "canals-lakes"); + const RadialShape dune(kDuneRadius, 0.3, context, "canals-dunes"); + const FarmPlot pad{kPadSize, kPadSize, kPadRing}; + for (int cell = 0; cell < L.g.cellCount(); ++cell) + { + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + if (L.kind[cell] == Lake) + fillShape(L.water, t, c.x, c.y, lake, 0.0); + else if (L.kind[cell] == Orchard) + fillShape(L.water, t, c.x, c.y, orchardPond, 0.0); + else if (L.kind[cell] == Dune) + fillShape(L.sand, t, c.x, c.y, dune, 0.0); + } + L.sketch.assign(n, GRASS); + for (int i = 0; i < n; ++i) + L.sketch[i] = L.water[i] ? WATER + : (L.sand[i] || (L.bridge[i] && L.canal[i])) ? SAND + : GRASS; + L.pads.water = L.water; + L.pads.sand.assign(n, 0); + L.pads.plot.assign(n, 0); + L.pads.row.assign(n, -1); + for (int cell = 0; cell < L.g.cellCount(); ++cell) + { + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + const int cx = int(std::lround(c.x)), cy = int(std::lround(c.y)); + if (L.kind[cell] == Pad) + stampFarmPlot(L.sketch, t, L.pads, cx - kPadSize / 2, cy - kPadSize / 2, pad); + else if (L.kind[cell] == TwoPads) + { + stampFarmPlot(L.sketch, t, L.pads, cx - kPadsApart / 2 - kPadSize / 2, + cy - kPadSize / 2, pad); + stampFarmPlot(L.sketch, t, L.pads, cx + kPadsApart / 2 - kPadSize / 2, + cy - kPadSize / 2, pad); + } + } + L.water = L.pads.water; return L; } @@ -274,10 +334,7 @@ bool generate(Game &game, GenerationContext &context) game.addTeam(); context.stage = "canals terrain"; - TerrainSketch terrain(n, GRASS); - for (int i = 0; i < n; ++i) - if (L.water[i]) - terrain[i] = WATER; + TerrainSketch terrain = L.sketch; layBeaches(terrain, t); for (int i = 0; i < n; ++i) if (L.bridge[i] && L.canal[i]) @@ -322,17 +379,23 @@ bool generate(Game &game, GenerationContext &context) clearGround(map, i % t.w, i / t.w); }); // Every block is fertile (its canal is within the growth probe's reach of all of it), so the - // ambient farmland is a modest share of every block, in patches, the home blocks included; the - // bridges' landings stay clear so no field closes a bridge. + // ambient farmland is a modest share of the plain, lake and home blocks, in patches; the other + // kinds carry their own thing instead, and the bridges' landings stay clear so no field closes + // a bridge. const std::vector landings = dilate(t, bridges, 2); const Fertility::Field fertility = Fertility::forMap(map, false); const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("canals-patch")); const std::vector split = periodicNoise(t.w, t.h, 5, context.stream("canals-split")); - const auto eligible = [&](int i) + const auto open = [&](int i) { - return L.land[i] && !reserved[i] && !pads[i] && !landings[i] && + return L.land[i] && !reserved[i] && !pads[i] && !landings[i] && !L.pads.plot[i] && clearGround(map, i % t.w, i / t.w); }; + const auto eligible = [&](int i) + { + const int kind = L.kind[L.cell[i]]; + return open(i) && (kind == Plain || kind == Lake || kind == Home); + }; int fertile = 0; for (int i = 0; i < n; ++i) fertile += eligible(i) && fertility.at(i % t.w, i / t.w) > 0; @@ -346,16 +409,53 @@ bool generate(Game &game, GenerationContext &context) int(scaledCount(area / 900, o.stone)), 0}; }, "canals-stone", "canals-fruit"); - // The markets: an orchard of all three fruits round each market block's middle. - if (scaledCount(1, o.fruit) > 0) - for (int cell : L.marketCell) + // Every block's own thing (first play): the three fruits round an orchard's pond, a quarry's + // stone, a woodlot's or wheatfield's cover on the block's most-noise share of ground. + const std::vector cover = periodicNoise(t.w, t.h, 6, context.stream("canals-cover")); + for (int cell = 0; cell < L.g.cellCount(); ++cell) + { + const ShapePoint centre = tilePoint(L.g.cells[cell].centre); + const auto inBlock = [&](int i) { return L.cell[i] == cell && open(i); }; + switch (L.kind[cell]) { - const ShapePoint centre = tilePoint(L.g.cells[cell].centre); - const double spin = context.bounded("canals-fruit", 3600) / 3600.0 * 2 * kPi; - plantOrchard(map, t, context, centre.x, centre.y, 3.5, {spin}, 4.5, 6, 1, - [&](int i) { return L.cell[i] == cell && eligible(i); }); + case Orchard: + if (scaledCount(1, o.fruit) > 0) + { + const double spin = context.bounded("canals-fruit", 3600) / 3600.0 * 2 * kPi; + plantOrchard(map, t, context, centre.x, centre.y, kOrchardPond + 2.5, {spin}, 4.5, + 6, 1, inBlock); + } + break; + case Quarry: + if (scaledCount(1, o.stone) > 0) + if (const int seed = seedNear(t, int(centre.x), int(centre.y), 3, inBlock); + seed >= 0) + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), + STONE, kQuarryRadius); + break; + case Woodlot: + case Wheatfield: + { + std::vector levels; + for (int i = 0; i < n; ++i) + if (inBlock(i)) + levels.push_back(cover[i]); + if (levels.empty()) + break; + const int share = std::clamp( + int(scaledCount(kCoverPercent, L.kind[cell] == Woodlot ? o.wood : o.wheat)), 0, + 100); + const int level = percentile(levels, 100 - share); + plantCover(map, t, L.land, L.kind[cell] == Woodlot ? WOOD : CORN, + [&](int i) { return inBlock(i) && cover[i] >= level; }); + break; } + default: + break; + } + } seedAlgae(map, context, t, "canals-algae", o.algae, AlgaeBand::anyWater(25)); + clearFarmPlots(map, t, {L.pads}); secureStartingCrops(game, context, t); reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}); // The bridges join every colony to the first; only deposits could close the way, so only those @@ -373,15 +473,15 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (const std::string mismatch = designMismatch(L, map, "canals"); !mismatch.empty()) return mismatch; const Torus &t = L.t; - for (int k = 0; k < context.request.nbTeams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s block has lost its pond."; - } + // Home blocks are dry by design (first play); every lake block keeps its lake. + for (int cell = 0; cell < L.g.cellCount(); ++cell) + if (L.kind[cell] == Lake) + { + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + if (!map.isWater(t.x(int(c.x)), t.y(int(c.y)))) + return "A lake block has lost its lake at (" + std::to_string(int(c.x)) + ", " + + std::to_string(int(c.y)) + ")."; + } return walkFromFirstColony(map, context.request.nbTeams, "the city", "over the bridges").error; } } // namespace @@ -402,15 +502,16 @@ GeneratorDefinition canalsDefinition() "canals", 29, "Canals", - 1, + 2, false, // Blocks of 24 give a 256 map about a hundred blocks; a canal of 3 corners (two tiles of // water) is sealed against diagonal steps and is reached by a level-2 tower, which is what // the colonies start with; a fifth again in extra bridges keeps most blocks islands. {{"block-size", "Block size", 16, 40, 2, 24, ControlGroup::Layout}, {"canal-width", "Canal width", 3, 5, 1, 3, ControlGroup::Terrain}, - {"warp", "Warp", 0, 100, 10, 40, ControlGroup::Terrain}, - {"extra-bridges", "Extra bridges", 0, 100, 10, 20, ControlGroup::Layout}, + // FEEDBACK 2026-09-13: warp 80 and extra bridges 30 (were 40 and 20). + {"warp", "Warp", 0, 100, 10, 80, ControlGroup::Terrain}, + {"extra-bridges", "Extra bridges", 0, 100, 10, 30, ControlGroup::Layout}, {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, {"tower-count", "Towers per colony", 1, 4, 1, 2, ControlGroup::Layout}, GeneratorControl::percentage("wheat-amount", "Wheat amount"), diff --git a/src/map/generator/generators/FingerprintGenerator.cpp b/src/map/generator/generators/FingerprintGenerator.cpp index ce9b44d54..de88beb23 100644 --- a/src/map/generator/generators/FingerprintGenerator.cpp +++ b/src/map/generator/generators/FingerprintGenerator.cpp @@ -41,6 +41,14 @@ using namespace MapGeneration; // the fields; a stone fingerprint is the opposite, its pools the only fertile ground. Corridors that // fork and dead-end give ambushes and cul-de-sacs no grid has, and reads as country rather than as // a maze. +// +// FEEDBACK 2026-09-13 (first play): "looks very similar to everglades right now, but if we raise the +// default wavelength it has a nicer feel. but also needs a lot more resources scattered throughout by +// default; right now it feels very empty. default home size needs to be larger." So: wavelength 30 +// by default (was 20: corridors twice as wide read as country rather than as a swamp of pools), home +// clearings of radius 18 (was 12), and the ambient layer more than doubled (22% of the fertile ground +// under wheat and 12% under wood, was 9% and 6%; an outcrop per 600 tiles and a grove per 1000, were +// 1000 and 1800). namespace { @@ -218,10 +226,10 @@ bool generate(Game &game, GenerationContext &context) [&](int i) { return split[i]; }, [&](int area) { - return GroundAmounts{int(scaledCount(fertile * 9 / 100, o.wheat)), - int(scaledCount(fertile * 6 / 100, o.wood)), - int(scaledCount(area / 1000, o.stone)), - int(scaledCount(area / 1800, o.fruit))}; + return GroundAmounts{int(scaledCount(fertile * 22 / 100, o.wheat)), + int(scaledCount(fertile * 12 / 100, o.wood)), + int(scaledCount(area / 600, o.stone)), + int(scaledCount(area / 1000, o.fruit))}; }, "fingerprint-stone", "fingerprint-fruit"); seedAlgae(map, context, t, "fingerprint-algae", o.algae, AlgaeBand::shallows(1, 6, 60)); @@ -272,15 +280,16 @@ GeneratorDefinition fingerprintDefinition() return {"fingerprint", 26, "Fingerprint", - 1, + 2, false, - {{"wavelength", "Wavelength", 12, 40, 2, 20, ControlGroup::Terrain}, + {// FEEDBACK 2026-09-13: wavelength 30 and homes of 18 (were 20 and 12). + {"wavelength", "Wavelength", 12, 48, 2, 30, ControlGroup::Terrain}, GeneratorControl::choice("pattern", "Pattern", {"Labyrinth", "Islands", "Channels"}, 0, ControlGroup::Terrain), GeneratorControl::choice("barrier", "Barrier", {"Water", "Stone"}, 0, ControlGroup::Terrain), {"grain", "Grain", 0, 100, 10, 0, ControlGroup::Terrain}, - {"home-size", "Home size", 10, 20, 1, 12, ControlGroup::Layout}, + {"home-size", "Home size", 10, 24, 1, 18, ControlGroup::Layout}, GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), GeneratorControl::percentage("stone-amount", "Stone amount"), diff --git a/src/map/generator/generators/MarchesGenerator.cpp b/src/map/generator/generators/MarchesGenerator.cpp deleted file mode 100644 index b0e9d4afe..000000000 --- a/src/map/generator/generators/MarchesGenerator.cpp +++ /dev/null @@ -1,428 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#include "MarchesGenerator.h" -#include "FertilityField.h" -#include "Game.h" -#include "GenerationContext.h" -#include "Geometry.h" -#include "Grid.h" -#include "Homes.h" -#include "LatticeNoise.h" -#include "Morphology.h" -#include "Orbits.h" -#include "Pipeline.h" -#include "Planting.h" -#include "Points.h" -#include "Resources.h" -#include "Roads.h" -#include "Settlements.h" -#include "Sketch.h" -#include "Topology.h" -#include "Walls.h" -#include -#include -#include -#include -#include -#include -using namespace MapGeneration; - -// Marches: homelands on a lattice across the whole torus, with no centre and no prize in the middle. -// Every colony's home lies in its own territory, and between every two neighbouring territories runs -// a band of wild border country, the marches: wooded, with a watering hole on every stretch of -// border and an orchard of all three fruits wherever three (or four) territories meet. A colony has -// a frontier with every neighbour, and its choice is which border to push. -// -// Fairness comes from sliding the map onto itself rather than turning it: the homes sit on the -// roomiest translation lattice the torus holds (Orbits.h), so with 2, 4, 8 or 16 colonies every -// colony's neighbourhood is an exact copy of every other's (the border warp is summed over the -// lattice's orbits so the territories match too); other counts get evenly staggered rows and -// statistical fairness. A rectangular map is served as well as a square one, since nothing is -// designed round a middle. -// -// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Every home has its own pond, -// so its fields regrow without leaving home; the marches' woods block walking until cut, so the -// border is crossed where someone chose to cut it; a watering hole is the only water on a border and -// so the only place a forward field grows; and an orchard of all three fruits at a junction is the -// strongest prize the game has, shared by three rivals at once. -namespace -{ - -// Every home's starting kit, unscaled whatever the amounts say, so every start is viable: wheat and -// wood patches beside the pond (12 tiles each is a few visits' worth, and they regrow beside the -// water) and a quarry of radius 2 (13 tiles of stone, which never runs out). -constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; -// The land a home disc keeps from the edge of the marches: room for the disc's own rough outline -// (15% wobble of a 14-tile radius is 2 tiles) and a watering hole's pool and beach (4 tiles) never to -// meet the home's pond and beach. -constexpr int kHomeMargin = 5; -// A crowded map narrows the marches before it is refused, but never below this: two tiles of woods -// either side of the border still reads as a border. -constexpr int kNarrowestMarch = 4; -// A watering hole: a pool of radius 3 (about 25 corners of water, a beach round it) and 24 tiles of -// wheat, a forward field the size of a home kit, so holding a border stretch is worth a field. -constexpr double kPoolRadius = 3.0; -constexpr int kPoolWheat = 24; -// How far the border warp can move a border, as a share of the home spacing at full roughness: at -// 30% two neighbouring borders can never cross (they start half a spacing apart), and a home disc -// sized by geometryFor's rule always stays inside its homeland. -constexpr int kWarpPercent = 30; -// The share of the marches under wood at the default wood amount. Wood is planted in patches from a -// noise field (the gaps between patches are the natural ways through); 55% leaves the band clearly -// wooded but always crossable somewhere, and the route backstop cuts a way where it isn't. -constexpr int kWoodedPercent = 55; - -struct Layout -{ - Torus t{1, 1}; - bool exact = false; - double spacing = 0, homeRadius = 0; - int marchWidth = 0; - std::vector homes, kits; - std::vector territory; // every tile's homeland, including the marches - std::vector land; // the homeland a tile is in, or -1 in the marches - std::vector homeOf; // the home disc a tile is in, or -1 - std::vector march, water; - std::vector holes, junctions; // the watering holes' and orchards' tiles - std::string failure; -}; - -// The tiles of the marches where several homelands meet, grouped: each group's tile nearest its -// middle is where its prize goes. -std::vector prizeSites(const Torus &t, const std::vector &mask) -{ - const std::vector label = connectedRegions(mask, t.w, t.h, true, GridNeighbors::Eight); - std::vector> groups; - for (int i = 0; i < t.size(); ++i) - if (label[i] >= 0) - { - if (label[i] >= int(groups.size())) - groups.resize(label[i] + 1); - groups[label[i]].push_back(i); - } - std::vector sites; - for (const std::vector &tiles : groups) - { - const int first = tiles.front(); - double sx = 0, sy = 0; - for (int i : tiles) - { - sx += t.offsetX(first % t.w, i % t.w); - sy += t.offsetY(first / t.w, i / t.w); - } - const int mx = t.x(first % t.w + int(std::lround(sx / tiles.size()))); - const int my = t.y(first / t.w + int(std::lround(sy / tiles.size()))); - int best = first; - for (int i : tiles) - if (t.dist2(mx, my, i % t.w, i / t.w) < t.dist2(mx, my, best % t.w, best / t.w)) - best = i; - sites.push_back(best); - } - return sites; -} - -Layout design(const GenerationRequest &request, GenerationContext &context) -{ - const MarchesOptions o(request); - Layout L; - L.t = {1 << request.wDec, 1 << request.hDec}; - const Torus &t = L.t; - const int n = t.size(), teams = std::max(1, request.nbTeams); - L.homeRadius = o.homeSize; - - // The homes, on the roomiest lattice the torus holds. - const LatticeSites lattice = - latticeSites(t.w, t.h, teams, context.bounded("marches-layout", std::uint32_t(t.w)), - context.bounded("marches-layout", std::uint32_t(t.h))); - L.exact = lattice.exact; - L.homes = lattice.sites; - L.spacing = std::min(t.w, t.h); - for (size_t a = 0; a < L.homes.size(); ++a) - for (size_t b = a + 1; b < L.homes.size(); ++b) - L.spacing = - std::min(L.spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), - t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); - // A home disc must stay inside its homeland however far the warp bends the border towards it: on - // a crowded map the homes shrink to fit, then the marches narrow, before the map is refused. - const int warpPercent = o.borderRoughness * kWarpPercent / 100; - const double border = L.spacing * (0.5 - warpPercent / 100.0); - L.marchWidth = o.marchWidth; - L.homeRadius = std::min(L.homeRadius, std::floor(border - L.marchWidth / 2.0 - kHomeMargin)); - while (!homeHasRoom(L.homeRadius) && L.marchWidth > kNarrowestMarch) - { - L.marchWidth -= 2; - L.homeRadius = - std::min(o.homeSize, std::floor(border - L.marchWidth / 2.0 - kHomeMargin)); - } - if (!homeHasRoom(L.homeRadius)) - { - L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; - return L; - } - - // Homelands: every tile's nearest home after a warp that bends the borders. On an exact lattice - // the warp noise is laid on cells the lattice's moves are whole multiples of (latticePeriod), so - // it is the same at every colony's image of a tile and the homelands are exact copies of one - // another; summing plain noise over the orbits instead would average the roughness away and leave - // the borders ruled straight. Otherwise the noise's cells are one and a half spacings across, big - // enough to bend a border as a whole rather than fray it. - std::vector sites; - for (const ShapePoint &p : L.homes) - sites.push_back({int(std::lround(p.x)), int(std::lround(p.y))}); - const int period = L.exact ? latticePeriod(translationSymmetry(t.w, t.h, teams)) - : std::max(4, int(L.spacing) * 3 / 2); - const std::vector warpX = - fractalNoise(t.w, t.h, period, 3, context.stream("marches-warp")); - const std::vector warpY = - fractalNoise(t.w, t.h, period, 3, context.stream("marches-warp")); - L.territory = - nearestSiteLabels(t, sites, std::max(4, int(L.spacing)), warpX, warpY, warpPercent); - - // The marches: the band along every border, both sides alike. - std::vector border2(n, 0); - for (int i = 0; i < n; ++i) - for (const auto &s : kCardinalSteps) - if (L.territory[t.at(i % t.w + s[0], i / t.w + s[1])] != L.territory[i]) - border2[i] = 1; - L.march = dilateRound(t, border2, L.marchWidth / 2.0); - L.land.assign(n, -1); - for (int i = 0; i < n; ++i) - if (!L.march[i]) - L.land[i] = L.territory[i]; - - // The homes: a round clearing (the whole map is grass, so only its pond shows) at every site. - L.homeOf.assign(n, -1); - L.water.assign(n, 0); - const RadialShape home(L.homeRadius, 0.15, context, "marches-home"); - const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "marches-pond"); - L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); - - // Watering holes: one on every stretch of border between two homelands, at its middle. On the - // torus two homelands may share several separate stretches; each gets its own. - std::map, std::vector> stretches; - for (int i = 0; i < n; ++i) - for (const auto &s : kCardinalSteps) - { - const int other = L.territory[t.at(i % t.w + s[0], i / t.w + s[1])]; - if (other > L.territory[i]) - { - auto &mask = stretches[{L.territory[i], other}]; - if (mask.empty()) - mask.assign(n, 0); - mask[i] = 1; - } - } - for (const auto &entry : stretches) - for (int site : prizeSites(t, entry.second)) - L.holes.push_back(site); - // Junctions: wherever three or more homelands meet within a tile; junctions within a few tiles - // of each other (a four-way corner the warp split in two) are one orchard. - std::vector junction(n, 0); - for (int y = 0; y < t.h; ++y) - for (int x = 0; x < t.w; ++x) - { - int seen[9], count = 0; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) - { - const int label = L.territory[t.at(x + dx, y + dy)]; - if (std::find(seen, seen + count, label) == seen + count) - seen[count++] = label; - } - junction[size_t(y) * t.w + x] = count >= 3; - } - if (o.orchards) - L.junctions = prizeSites(t, dilate(t, junction, 3)); - // A pool at every watering hole. - const RadialShape pool(kPoolRadius, 0.3, context, "marches-pool"); - for (int hole : L.holes) - fillShape(L.water, t, hole % t.w + 0.5, hole / t.w + 0.5, pool, 0.0); - return L; -} - -// The deposits: every home's kit, farmland on each homeland's fertile ground, woods through the -// marches, wheat round every watering hole, and an orchard with a quarry at every junction. -void furnish(Map &map, const Layout &L, GenerationContext &context, const MarchesOptions &o, - int teams) -{ - const Torus &t = L.t; - const int n = t.size(); - const std::vector reserved = swarmSurroundings(t, context); - for (int k = 0; k < teams; ++k) - plantOpenHomeKit( - map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, - [&](int i) - { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); - const Fertility::Field fertility = Fertility::forMap(map, false); - const std::vector patch = periodicNoise(t.w, t.h, 12, context.stream("marches-patch")); - const std::vector split = periodicNoise(t.w, t.h, 6, context.stream("marches-split")); - // A homeland's only water is its pond, so its fertile ground (where the engine's growth probe - // finds water, within 15 tiles) is the ring round the home; the ambient farmland is a share of - // that ring, not of the homeland, or it would bury the home: 12% wheat and 8% wood of about a - // thousand fertile tiles is a few patches, in the top half of a noise field so they clump. - for (int k = 0; k < teams; ++k) - { - const auto eligible = [&](int i) - { return L.land[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; - int fertile = 0; - for (int i = 0; i < n; ++i) - fertile += eligible(i) && fertility.at(i % t.w, i / t.w) > 0; - furnishGround( - map, t, context, fertility, eligible, [&](int i) { return float(patch[i]); }, - [&](int i) { return split[i]; }, - [&](int area) - { - return GroundAmounts{int(scaledCount(fertile * 12 / 100, o.wheat)), - int(scaledCount(fertile * 8 / 100, o.wood)), - int(scaledCount(area / 900, o.stone)), - int(scaledCount(area / 2500, o.fruit))}; - }, - "marches-stone", "marches-fruit"); - } - // The marches' woods: wood over the wooded share of the band, in patches from a noise field - // (the gaps between patches are the natural ways through), and the odd outcrop. Far from water, - // none of it grows back: a way cut through the marches stays cut. - std::vector band, levels; - const std::vector woods = periodicNoise(t.w, t.h, 9, context.stream("marches-woods")); - const auto wild = [&](int i) - { return L.march[i] && !L.water[i] && clearGround(map, i % t.w, i / t.w); }; - for (int i = 0; i < n; ++i) - if (L.march[i]) - { - band.push_back(i); - levels.push_back(woods[i]); - } - const int wooded = std::clamp(int(scaledCount(kWoodedPercent, o.wood)), 0, 100); - const int level = percentile(levels, 100 - wooded); - plantCover(map, t, L.march, WOOD, [&](int i) { return wild(i) && woods[i] >= level; }); - scatterClumps(context, t, band, int(scaledCount(int(band.size()) / 1200, o.stone)), - "marches-outcrops", wild, - [&](MapGeneratorPoint p) { placeResourceClump(map, context, p, STONE, 1); }); - // Wheat round every watering hole, on the first grass past the pool's beach. - for (int hole : L.holes) - { - const int hx = hole % t.w, hy = hole / t.w; - const auto near = [&](int i) { return wild(i) && t.dist2(hx, hy, i % t.w, i / t.w) <= 64; }; - if (const int seed = seedNear(t, hx, hy, 8, near); seed >= 0) - growPatch(map, t, seed, CORN, int(scaledCount(kPoolWheat, o.wheat)), near); - } - // An orchard of all three fruits round every junction, with a quarry in the middle. - for (int junction : L.junctions) - { - const int jx = junction % t.w, jy = junction / t.w; - const auto near = [&](int i) - { return wild(i) && t.dist2(jx, jy, i % t.w, i / t.w) <= 100; }; - if (scaledCount(1, o.stone) > 0 && near(junction)) - placeResourceClump(map, context, MapGeneratorPoint(jx, jy), STONE, 1); - if (scaledCount(1, o.fruit) > 0) - { - const double spin = context.bounded("marches-fruit", 3600) / 3600.0 * 2 * kPi; - plantOrchard(map, t, context, jx, jy, 4.5, {spin}, 4.5, 4, 1, near); - } - } -} - -bool generate(Game &game, GenerationContext &context) -{ - context.stage = "marches layout"; - const MarchesOptions o(context.request); - const Layout L = design(context.request, context); - if (!L.failure.empty()) - { - context.detail = L.failure; - return false; - } - Map &map = game.map; - const Torus &t = L.t; - const int teams = context.request.nbTeams; - for (int k = 0; k < teams; ++k) - game.addTeam(); - - context.stage = "marches terrain"; - TerrainSketch terrain(t.size(), GRASS); - for (int i = 0; i < t.size(); ++i) - if (L.water[i]) - terrain[i] = WATER; - layBeaches(terrain, t); - writeUndermap(map, terrain); - - context.stage = "marches colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(t.size()), 0); - for (int i = 0; i < t.size(); ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; - const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; - if (!settleColonies(game, context, "marches-starts", homeMask, anchor)) - return false; - - context.stage = "marches resources"; - furnish(map, L, context, o, teams); - seedAlgae(map, context, t, "marches-algae", o.algae, AlgaeBand::anyWater(30)); - secureStartingCrops(game, context, t); - reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}); - // The marches are wooded and the fields grow; where they close a colony in, the cheapest cut - // through the woods is opened, never a ford. - context.stage = "marches routes"; - openColonyRoutes(map, context, t, StepCosts{1, 3, -1, -1, -1}); - return true; -} - -std::string validateWorld(const Game &game, const GenerationContext &context) -{ - GenerationContext replay(context.request); - const Layout L = design(context.request, replay); - const Map &map = game.map; - if (const std::string mismatch = designMismatch(L, map, "marches"); !mismatch.empty()) - return mismatch; - const Torus &t = L.t; - for (int k = 0; k < context.request.nbTeams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s home has lost its pond."; - } - return walkFromFirstColony(map, context.request.nbTeams, "the marches", "through the marches") - .error; -} -} // namespace - -MarchesOptions::MarchesOptions(const GenerationRequest &r) - : homeSize(r.option("home-size")), marchWidth(r.option("march-width")), - borderRoughness(r.option("border-roughness")), orchards(r.option("orchards") != 0), - wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), - stone(r.option("stone-amount")), algae(r.option("algae-amount")), - fruit(r.option("fruit-amount")) -{ -} - -GeneratorDefinition marchesDefinition() -{ - return {"marches", - 25, - "Marches", - 1, - false, - {{"home-size", "Home size", 10, 24, 1, 14, ControlGroup::Layout}, - {"march-width", "March width", 6, 24, 2, 12, ControlGroup::Terrain}, - {"border-roughness", "Border roughness", 0, 100, 10, 40, ControlGroup::Terrain}, - // Off, the junctions carry no orchard and the watering holes are the only prizes. - GeneratorControl::toggle("orchards", "Orchards", true, ControlGroup::Layout), - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, - validateWorld}; -} diff --git a/src/map/generator/generators/MarchesGenerator.h b/src/map/generator/generators/MarchesGenerator.h deleted file mode 100644 index 6a8f11dfa..000000000 --- a/src/map/generator/generators/MarchesGenerator.h +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#pragma once -#include "GenerationRequest.h" -#include "GeneratorDefinition.h" -struct MarchesOptions -{ - int homeSize, marchWidth, borderRoughness; - bool orchards; - int wheat, wood, stone, algae, fruit; // percentages of the default amounts - explicit MarchesOptions(const GenerationRequest &r); -}; -GeneratorDefinition marchesDefinition(); diff --git a/src/map/generator/generators/PatchworkGenerator.cpp b/src/map/generator/generators/PatchworkGenerator.cpp deleted file mode 100644 index 3f6882572..000000000 --- a/src/map/generator/generators/PatchworkGenerator.cpp +++ /dev/null @@ -1,348 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#include "PatchworkGenerator.h" -#include "Biomes.h" -#include "FertilityField.h" -#include "Game.h" -#include "GenerationContext.h" -#include "Geometry.h" -#include "Grid.h" -#include "Homes.h" -#include "LatticeNoise.h" -#include "Morphology.h" -#include "Orbits.h" -#include "Pipeline.h" -#include "Planting.h" -#include "Resources.h" -#include "Roads.h" -#include "Settlements.h" -#include "Sketch.h" -#include "Territories.h" -#include -#include -#include -#include -using namespace MapGeneration; - -// Patchwork: every colony starts on a different kind of land. The map is shared out into one -// territory per colony, and each territory is dealt a kit from Biomes.h in turn: a fertile plain -// (big ponds, wide fields, open ground), a stone fortress (a ring of stone with gates, outcrops, -// poor water), an orchard island (a lake with an island of all three fruits, little farmland) or a -// forest (most of the ground under wood, ponds to keep it growing). Every player plays a different -// opening: the plain builds wide, the fortress holds, the island pulls hungry enemies across with -// its fruit, the forest cuts. -// -// This is the one map in the catalog that gives up fairness by construction. What it does instead is -// share the ground out by worth: a territory dealt a poorer kit gets proportionally more tiles -// (growTerritories' worth, from biomeWorth's estimate), and the start scorer ranks the seeds the -// lobby rolls. biomeWorth is an estimate from the scorer's weights, not a measurement: the kits are -// to be tuned with the fairness tournament (docs/map-generators/FAIRNESS_TOURNAMENT.md). -// -// WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Crops regrow near water, so -// the plain out-produces everyone and must be attacked before it does; stone is a wall no one -// clears, so the fortress's gates are its whole defence; an inn stocked with all three fruits pulls -// hungry enemy units across, so the island's orchard is a weapon; wood is a wall until cut, so the -// forest colony chooses where its front is. -namespace -{ - -// Every home's starting kit, unscaled whatever the amounts say, so every opening is at least -// viable: wheat and wood beside the home's own pond, and a quarry. -constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; -// The home's clearing keeps this much beyond its rough disc free of the kit's cover, ponds and ring. -constexpr double kClearingMargin = 3.0; -// A fortress's gates: one towards every neighbouring territory, this wide. -constexpr int kGateRadius = 3; -// Territory borders wander with noise cells half a home spacing across, weighted so a border can -// bend by a few tiles but never wraps round a home. -constexpr int kBorderCostScale = 700; - -struct Layout -{ - Torus t{1, 1}; - double homeRadius = 0; - std::vector homes, kits; - std::vector kitOf; // which BiomeKit each colony was dealt - std::vector territory, homeOf; - std::vector> regions, doors; - std::vector biomes; - std::vector water, clearing; - TerrainSketch sketch; - std::string failure; -}; - -const std::vector &kits() -{ - static const std::vector all{fertilePlain(), stoneFortress(), orchardIsland(), - forest()}; - return all; -} - -Layout design(const GenerationRequest &request, GenerationContext &context) -{ - const PatchworkOptions o(request); - Layout L; - L.t = {1 << request.wDec, 1 << request.hDec}; - const Torus &t = L.t; - const int n = t.size(), teams = std::max(1, request.nbTeams); - - // Homes on a lattice, and the kits dealt round from a random first. - L.homes = latticeSites(t.w, t.h, teams, context.bounded("patch-layout", std::uint32_t(t.w)), - context.bounded("patch-layout", std::uint32_t(t.h))) - .sites; - double spacing = std::min(t.w, t.h); - for (size_t a = 0; a < L.homes.size(); ++a) - for (size_t b = a + 1; b < L.homes.size(); ++b) - spacing = - std::min(spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), - t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); - L.homeRadius = std::min(o.homeSize, std::floor(spacing / 3 - kClearingMargin)); - if (!homeHasRoom(L.homeRadius)) - { - L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; - return L; - } - const int first = int(context.bounded("patch-deal", std::uint32_t(kits().size()))); - std::vector worth; - for (int k = 0; k < teams; ++k) - { - L.kitOf.push_back((first + k) % int(kits().size())); - worth.push_back(biomeWorth(kits()[L.kitOf[k]])); - } - - // Territories by worth (growTerritories): the colony whose ground is worth least per tile takes - // the next tile, so it ends with more of them; noise in the cost bends the borders, and a few - // smoothing passes straighten them into curves. - std::vector> seeds; - for (const ShapePoint &home : L.homes) - seeds.push_back({t.at(int(home.x), int(home.y))}); - const std::vector noise = - fractalNoise(t.w, t.h, std::max(4, int(spacing) / 2), 3, context.stream("patch-borders")); - const int roughness = o.borderRoughness * kBorderCostScale / 100; - L.territory = growTerritories( - t, std::vector(n, 1), seeds, [&](int i) - { return int(std::int64_t(noise[i]) * roughness / 65536); }, &worth) - .labels; - std::vector keep(n, 0); - for (const auto &seed : seeds) - keep[seed[0]] = 1; - if (o.smoothing > 0) - smoothLabels(t, L.territory, o.smoothing, keep, 3); - // Every tile must belong to someone: whatever smoothing or growth left unclaimed goes to the - // nearest home. - for (int i = 0; i < n; ++i) - if (L.territory[i] < 0) - { - int best = 0; - for (int k = 1; k < teams; ++k) - if (t.dist2(i % t.w, i / t.w, int(L.homes[k].x), int(L.homes[k].y)) < - t.dist2(i % t.w, i / t.w, int(L.homes[best].x), int(L.homes[best].y))) - best = k; - L.territory[i] = best; - } - L.regions.assign(teams, std::vector(n, 0)); - for (int i = 0; i < n; ++i) - L.regions[L.territory[i]][i] = 1; - - // Gates: for a fortress, a door in its ring towards every neighbouring territory, at the rim tile - // nearest the line between the two homes. Computed for every kit, harmlessly. - L.doors.assign(teams, std::vector(n, 0)); - for (int k = 0; k < teams; ++k) - { - std::vector neighbour(teams, 0); - for (int i = 0; i < n; ++i) - if (L.territory[i] == k) - for (const auto &s : kCardinalSteps) - { - const int other = L.territory[t.at(i % t.w + s[0], i / t.w + s[1])]; - if (other != k) - neighbour[other] = 1; - } - std::vector gates; - for (int j = 0; j < teams; ++j) - { - if (!neighbour[j]) - continue; - const double mx = L.homes[k].x + t.offsetX(int(L.homes[k].x), int(L.homes[j].x)) / 2.0; - const double my = L.homes[k].y + t.offsetY(int(L.homes[k].y), int(L.homes[j].y)) / 2.0; - int best = -1; - for (int i = 0; i < n; ++i) - { - if (L.territory[i] != k) - continue; - bool rim = false; - for (const auto &s : kCardinalSteps) - rim = rim || L.territory[t.at(i % t.w + s[0], i / t.w + s[1])] != k; - if (rim && (best < 0 || t.dist2(int(mx), int(my), i % t.w, i / t.w) < - t.dist2(int(mx), int(my), best % t.w, best / t.w))) - best = i; - } - if (best >= 0) - gates.push_back(best); - } - L.doors[k] = dilate(t, tileMask(t, gates), kGateRadius); - } - - // The homes' clearings and ponds, then each kit's own terrain in its territory: a kit's ponds - // and ring keep out of the clearing, whose pond is the home's. - L.clearing.assign(n, 0); - for (const ShapePoint &home : L.homes) - for (int i = 0; i < n; ++i) - if (std::hypot(t.offsetX(int(home.x), i % t.w), t.offsetY(int(home.y), i / t.w)) <= - L.homeRadius + kClearingMargin) - L.clearing[i] = 1; - L.homeOf.assign(n, -1); - L.water.assign(n, 0); - const RadialShape home(L.homeRadius, 0.15, context, "patch-home"); - const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "patch-pond"); - L.kits = stampRoundHomes(t, L.homes, 0.0, home, L.homeRadius, 1, &pond, L.water, L.homeOf); - // Each kit's terrain over its whole territory (so a fortress's ring runs round the territory, - // not round the home clearing); a kit's pond that landed in the clearing is drained, leaving the - // home its own pond. - L.sketch.assign(n, GRASS); - const std::vector homeWater = L.water; - for (int k = 0; k < teams; ++k) - { - L.biomes.push_back(sketchBiome(L.sketch, t, L.regions[k], L.doors[k], kits()[L.kitOf[k]], - context, "patch-biome-" + std::to_string(k))); - for (int i = 0; i < n; ++i) - { - if (L.clearing[i] && !homeWater[i]) - { - L.biomes[k].water[i] = 0; - L.sketch[i] = GRASS; - } - if (L.biomes[k].water[i]) - L.water[i] = 1; - } - } - for (int i = 0; i < n; ++i) - if (L.water[i]) - L.sketch[i] = WATER; - return L; -} - -bool generate(Game &game, GenerationContext &context) -{ - context.stage = "patchwork layout"; - const PatchworkOptions o(context.request); - const Layout L = design(context.request, context); - if (!L.failure.empty()) - { - context.detail = L.failure; - return false; - } - Map &map = game.map; - const Torus &t = L.t; - const int n = t.size(), teams = context.request.nbTeams; - for (int k = 0; k < teams; ++k) - game.addTeam(); - - context.stage = "patchwork terrain"; - TerrainSketch terrain = L.sketch; - layBeaches(terrain, t); - writeUndermap(map, terrain); - - context.stage = "patchwork colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(n), 0); - for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; - const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; - if (!settleColonies(game, context, "patch-starts", homeMask, anchor)) - return false; - - context.stage = "patchwork resources"; - const std::vector reserved = swarmSurroundings(t, context); - for (int k = 0; k < teams; ++k) - plantOpenHomeKit( - map, t, context, L.kits[k], 0.0, L.homeRadius, kHomeWheat, kHomeWood, kHomeQuarry, - [&](int i) - { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); - // Each kit's deposits over its territory, the home's clearing kept open. The amounts scale each - // kit's own shares. - std::vector stone(n, 0); - for (int k = 0; k < teams; ++k) - { - BiomeKit kit = kits()[L.kitOf[k]]; - kit.farmPerMille = int(scaledCount(kit.farmPerMille, (o.wheat + o.wood) / 2)); - kit.outcropsPer1000 = int(scaledCount(kit.outcropsPer1000, o.stone)); - kit.grovesPer1000 = int(scaledCount(kit.grovesPer1000, o.fruit)); - kit.coverPercent = std::min(100, int(scaledCount(kit.coverPercent, o.wood))); - std::vector keepClear(n, 0), ground(n, 0); - for (int i = 0; i < n; ++i) - { - keepClear[i] = L.clearing[i] || reserved[i]; - ground[i] = L.regions[k][i]; - stone[i] = stone[i] || L.biomes[k].wall[i]; - } - furnishBiome(map, t, context, ground, L.biomes[k], kit, keepClear, - "patch-" + std::to_string(k)); - } - seedAlgae(map, context, t, "patch-algae", o.algae, AlgaeBand::anyWater(30)); - secureStartingCrops(game, context, t, 24, 32, 0, &stone); - reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}, 24, 32, 0, - &stone); - // The kits' cover and rings can close a colony in; the cheapest way through is opened, through - // crops for preference, through a ring only when a gate failed. - context.stage = "patchwork routes"; - openColonyRoutes(map, context, t, StepCosts{1, 3, 12, -1, -1}); - return true; -} - -std::string validateWorld(const Game &game, const GenerationContext &context) -{ - GenerationContext replay(context.request); - const Layout L = design(context.request, replay); - const Map &map = game.map; - if (const std::string mismatch = designMismatch(L, map, "patchwork"); !mismatch.empty()) - return mismatch; - const Torus &t = L.t; - for (int k = 0; k < context.request.nbTeams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s home has lost its pond."; - } - return walkFromFirstColony(map, context.request.nbTeams, "the patchwork", "across the borders") - .error; -} -} // namespace - -PatchworkOptions::PatchworkOptions(const GenerationRequest &r) - : homeSize(r.option("home-size")), borderRoughness(r.option("border-roughness")), - smoothing(r.option("smoothing")), wheat(r.option("wheat-amount")), - wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) -{ -} - -GeneratorDefinition patchworkDefinition() -{ - return {"patchwork", - 33, - "Patchwork", - 1, - false, - {{"home-size", "Home size", 10, 20, 1, 13, ControlGroup::Layout}, - {"border-roughness", "Border roughness", 0, 100, 10, 40, ControlGroup::Terrain}, - {"smoothing", "Smoothing", 0, 4, 1, 2, ControlGroup::Terrain}, - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, - validateWorld}; -} diff --git a/src/map/generator/generators/PatchworkGenerator.h b/src/map/generator/generators/PatchworkGenerator.h deleted file mode 100644 index 56fc54097..000000000 --- a/src/map/generator/generators/PatchworkGenerator.h +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#pragma once -#include "GenerationRequest.h" -#include "GeneratorDefinition.h" -struct PatchworkOptions -{ - int homeSize, borderRoughness, smoothing; - int wheat, wood, stone, algae, fruit; // percentages of the default amounts - explicit PatchworkOptions(const GenerationRequest &r); -}; -GeneratorDefinition patchworkDefinition(); diff --git a/src/map/generator/generators/PolderGenerator.cpp b/src/map/generator/generators/PolderGenerator.cpp index 0a24b37bb..d9b35f84a 100644 --- a/src/map/generator/generators/PolderGenerator.cpp +++ b/src/map/generator/generators/PolderGenerator.cpp @@ -3,6 +3,7 @@ #include "FertilityField.h" #include "Game.h" #include "GenerationContext.h" +#include "Farmland.h" #include "Geometry.h" #include "Grid.h" #include "Homes.h" @@ -18,6 +19,7 @@ #include "Sketch.h" #include #include +#include #include #include using namespace MapGeneration; @@ -40,6 +42,19 @@ using namespace MapGeneration; // water within a few tiles of every crop keeps every field regrowing, so the map never runs short; // sand beside a ditch is where everyone walks. Swimming turns every ditch into a road: the second // half of the game is a different map. +// +// FEEDBACK 2026-09-13 (first play): "can we randomize the angle that the farms run at each round? +// also, the bases are wayyy so small and the aggressive growth quickly crowds out the base, so the +// bases need to be surrounded by a sand ring to hold back the growth. change the Row Angle parameter +// to have options Random, Vertical, Horizontal, Diagonal; Random ideally chooses any random angle, +// not just from a couple of options. I also want the farm lands stamped with the 10x4 sand plots +// similar to other map gens using farm lands: 2.5x the number of players, spread out evenly across +// the map (except some distance away from player bases), so the player is really encouraged to +// spread into these zones to extract food rapidly." So: `row-angle` defaults to Random, an angle +// drawn per map and rounded to the whole turn counts a stripe field needs (about a tenth of a turn's +// steps on a 256 map, so it is any angle to the eye); villages are radius 14 (was 11) inside a +// two-tile ring of sand the crops cannot cross; and two and a half farm plots per colony are stamped +// through the rows, each the farthest a plot can stand from every village and every plot before it. namespace { @@ -59,6 +74,13 @@ constexpr int kDykeCorners = 2; // Hamlets are grass discs of this radius half way between neighbouring villages: room for an inn and // a tower, not for a swarm. constexpr double kHamletRadius = 5.0; +// A ring of sand this wide round every village (first play: growth "quickly crowds out the base"): +// crops spread only onto grass, so one tile of sand corners stops them and the second keeps a grass +// corner from ever bridging the ring. +constexpr int kVillageSand = 2; +// Farm plots (first play): two and a half per colony (rounded up), each at least this far from every +// village and hamlet and from the plots before it, so they are dotted evenly through the rows. +constexpr int kPlotsPerTwoColonies = 5, kPlotClear = 18, kPlotMargin = 9; struct Layout { @@ -67,7 +89,9 @@ struct Layout double spacing = 0, crops = 0, villageRadius = 0; std::vector homes, kits, hamlets; std::vector homeOf; - std::vector water, dyke, village, hamlet; + std::vector water, dyke, village, ring, hamlet; + Farm farm; // the rows' water and sand as a farm, so plots can be stamped into them + TerrainSketch sketch; // the terrain as designed, plots and all std::string failure; }; @@ -79,20 +103,32 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const Torus &t = L.t; const int n = t.size(), teams = std::max(1, request.nbTeams); - // The rows: straight rows run across the map with `h / 16` of them from top to bottom; diagonal - // rows climb as many times across the width as down the height. Both wrap exactly. + // The rows: stripes that wrap exactly, so their direction is a pair of whole turn counts across the + // width and down the height. Horizontal rows cross the map `h / 16` times from top to bottom, + // vertical ones `w / 16` times across; diagonal rows climb as many times across as down; and a + // random angle (first play; the default) is drawn as any angle and rounded to the nearest whole + // counts on a circle of `s / 16` (s the shorter side: 16 on a 256 map), so the rows stay about 16 + // apart at any angle and the angle is one of some fifty on a 256 map. Crops take 10 of every 16 + // along an axis and 11 on the diagonal (Farmland.h's fit), by the sine of twice the angle. L.rows.warpPercent = 0; - if (o.rowAngle == 0) - { - L.rows.acrossX = 0; - L.rows.acrossY = t.h / kStraightPeriod; - L.crops = kStraightCrops; - } - else { - L.rows.acrossX = std::max(1, t.w / kDiagonalStep); - L.rows.acrossY = std::max(1, t.h / kDiagonalStep); - L.crops = kDiagonalCrops; + const int s = std::min(t.w, t.h), turns = std::max(1, s / kStraightPeriod); + const double draw = context.bounded("polder-angle", 3600) / 3600.0 * kPi; + const double theta = o.rowAngle == 0 ? draw + : o.rowAngle == 1 ? 0.0 + : o.rowAngle == 2 ? kPi / 2 + : kPi / 4; + L.rows.acrossX = int(std::lround(turns * std::cos(theta) * t.w / s)); + L.rows.acrossY = int(std::lround(turns * std::sin(theta) * t.h / s)); + if (o.rowAngle == 3) + { + L.rows.acrossX = std::max(1, t.w / kDiagonalStep); + L.rows.acrossY = std::max(1, t.h / kDiagonalStep); + } + if (L.rows.acrossX == 0 && L.rows.acrossY == 0) + L.rows.acrossY = turns; + const double angle = std::atan2(double(L.rows.acrossY) / t.h, double(L.rows.acrossX) / t.w); + L.crops = kStraightCrops + std::lround(std::fabs(std::sin(2 * angle))); } L.spacing = stripeSpacing(t, L.rows); L.along = alongStripes(t, L.rows); @@ -109,7 +145,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) nearest = std::min(nearest, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); - L.villageRadius = std::min(o.villageSize, std::floor((nearest - L.spacing) / 2)); + L.villageRadius = + std::min(o.villageSize, std::floor((nearest - L.spacing) / 2 - kVillageSand)); if (L.villageRadius < 8) { L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; @@ -123,6 +160,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) stampRoundHomes(t, L.homes, 0.0, village, L.villageRadius, 0, nullptr, L.water, L.homeOf); for (int i = 0; i < n; ++i) L.village[i] = L.homeOf[i] >= 0; + // The ring of sand round every village (first play), outside its rough edge. + L.ring = dilate(t, L.village, kVillageSand); + for (int i = 0; i < n; ++i) + L.ring[i] = L.ring[i] && !L.village[i]; // Hamlets: a small grass disc half way between every two villages that are nearest neighbours. L.hamlet.assign(n, 0); if (o.hamlets) @@ -151,13 +192,47 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.dyke.assign(n, 0); for (int i = 0; i < n; ++i) { - if (L.village[i] || L.hamlet[i]) + if (L.village[i] || L.ring[i] || L.hamlet[i]) continue; const int offset = along[i] % dykePeriod; const bool onDyke = offset < dykeHalf || dykePeriod - offset < dykeHalf; if (phase[i] >= cropUnits) (onDyke ? L.dyke[i] : L.water[i]) = 1; } + // The terrain as designed, and the rows as a farm so plots can be stamped into them + // (stampFarmPlot): two and a half plots per colony (first play), each the farthest a plot can + // stand from every village, hamlet and earlier plot, with the whole plot and its ring at least + // kPlotMargin inside that room. + L.sketch.assign(n, GRASS); + for (int i = 0; i < n; ++i) + L.sketch[i] = L.water[i] ? WATER : (L.dyke[i] || L.ring[i]) ? SAND : GRASS; + L.farm.water = L.water; + L.farm.sand = L.dyke; + L.farm.plot.assign(n, 0); + L.farm.row.assign(n, -1); + const FarmPlot plot; + std::vector keepClear(n, 0); + for (int i = 0; i < n; ++i) + keepClear[i] = L.village[i] || L.ring[i] || L.hamlet[i]; + keepClear = dilate(t, keepClear, kPlotClear); + const int plots = (teams * kPlotsPerTwoColonies + 1) / 2; + for (int p = 0; p < plots; ++p) + { + const std::vector far = distanceSquaredTo(t, keepClear); + int site = -1; + for (int i = 0; i < n; ++i) + if (!keepClear[i] && (site < 0 || far[i] > far[site])) + site = i; + if (site < 0 || far[site] < std::int64_t(kPlotMargin) * kPlotMargin) + break; + const int x0 = site % t.w - plot.width / 2, y0 = site / t.w - plot.height / 2; + stampFarmPlot(L.sketch, t, L.farm, x0, y0, plot); + for (int dy = -kPlotMargin; dy <= kPlotMargin; ++dy) + for (int dx = -kPlotMargin; dx <= kPlotMargin; ++dx) + keepClear[t.at(site % t.w + dx, site / t.w + dy)] = 1; + } + L.water = L.farm.water; + L.dyke = L.farm.sand; return L; } @@ -178,9 +253,7 @@ bool generate(Game &game, GenerationContext &context) game.addTeam(); context.stage = "polder terrain"; - TerrainSketch terrain(n, GRASS); - for (int i = 0; i < n; ++i) - terrain[i] = L.water[i] ? WATER : L.dyke[i] ? SAND : GRASS; + TerrainSketch terrain = L.sketch; layBeaches(terrain, t); writeUndermap(map, terrain); @@ -212,7 +285,10 @@ bool generate(Game &game, GenerationContext &context) const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("polder-patch")); const std::vector split = periodicNoise(t.w, t.h, 4, context.stream("polder-split")); const auto rowGround = [&](int i) - { return !L.village[i] && !L.hamlet[i] && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; + { + return !L.village[i] && !L.hamlet[i] && !L.farm.plot[i] && !reserved[i] && + clearGround(map, i % t.w, i / t.w); + }; int fertile = 0; for (int i = 0; i < n; ++i) fertile += rowGround(i) && fertility.at(i % t.w, i / t.w) > 0; @@ -236,6 +312,7 @@ bool generate(Game &game, GenerationContext &context) placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), CHERRY + int(context.bounded("polder-fruit", 3)), 1); seedAlgae(map, context, t, "polder-algae", o.algae, AlgaeBand::anyWater(60)); + clearFarmPlots(map, t, {L.farm}); secureStartingCrops(game, context, t); reopenCrampedStarts(game, context, {o.wheat, o.wood, o.stone, o.algae, o.fruit}); // The beaches along the ditches and the dykes across them join everything; the crops could still @@ -271,14 +348,16 @@ GeneratorDefinition polderDefinition() "polder", 30, "Polder", - 1, + 2, false, // A dyke every 24 tiles is a lane every one and a half rows' walk; villages of radius 11 // hold a swarm, its kit and a few more buildings and no more. - {GeneratorControl::choice("row-angle", "Row angle", {"Straight", "Diagonal"}, 0, + // FEEDBACK 2026-09-13: a random angle by default; villages of 14 (was 11) in a sand ring. + {GeneratorControl::choice("row-angle", "Row angle", + {"Random", "Vertical", "Horizontal", "Diagonal"}, 0, ControlGroup::Terrain), {"dyke-spacing", "Dyke spacing", 12, 48, 4, 24, ControlGroup::Terrain}, - {"village-size", "Village size", 8, 16, 1, 11, ControlGroup::Layout}, + {"village-size", "Village size", 8, 20, 1, 14, ControlGroup::Layout}, GeneratorControl::toggle("hamlets", "Hamlets", true, ControlGroup::Layout), GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), diff --git a/src/map/generator/generators/RainShadowGenerator.cpp b/src/map/generator/generators/RainShadowGenerator.cpp index c6c0f8208..9eff215e4 100644 --- a/src/map/generator/generators/RainShadowGenerator.cpp +++ b/src/map/generator/generators/RainShadowGenerator.cpp @@ -11,6 +11,7 @@ #include "Orbits.h" #include "Patterns.h" #include "Pipeline.h" +#include "Points.h" #include "Planting.h" #include "Resources.h" #include "Roads.h" @@ -43,6 +44,18 @@ using namespace MapGeneration; // pools decide where the fields are and the lee sand is a desert even though it is walkable: an // army on it is far from any inn. Sand also carries no building, so a forward base has to wait for // the next valley. +// +// FEEDBACK 2026-09-13 (first play): "default amount of water is not enough. the ponds need to be +// deeper and more connected to each other. also, for the passes that connect between the ridges, we +// need sand roadways leading inland to prevent those chasms from being overgrown because they get +// clogged up too easily. also in the valleys between the mountains it just feels too empty; toss in +// a few rivers connected to some inland lakes connecting from the rain shadows, and some inland +// desert or sporadic sand patches to break up the texture." So: the windward foot is a chain of +// streams 5 deep and 20 long every 24 (was 3 deep, 12 long: nearly continuous water now, with a +// four-tile beach gap between streams); a sand road runs through every pass and kPassRoad tiles into +// the valley on both sides; inland lakes lie scattered along the middle of every valley, each joined +// by a wandering river to the nearest windward stream; and sand patches are sprinkled over the +// valleys' grass (`sand-patches` percent). namespace { @@ -64,12 +77,23 @@ constexpr int kRidgeWarpPeriod = 40; // one out from its water, and a corner spoils the four tiles round it, so a pool three tiles out // would take the ridge's windward row with it (measured: the ridge came out one row thick); four // tiles leaves all three rows standing. -constexpr int kFootGap = 4, kFootDepth = 3; +constexpr int kFootGap = 4, kFootDepth = 5; // Pools along the foot: each this long along the ridge, every this many tiles. A pool 12 long and 3 // deep is about 36 corners of water, enough for the engine's growth probe to find from a good part // of the valley; the 12-tile gaps between pools are where the valley's beach lets units walk along // the foot. -constexpr int kPoolLength = 12, kPoolSpacing = 24; +constexpr int kPoolLength = 20, kPoolSpacing = 24; +// A sand road through every pass, running this far into the valley on either side of the stone +// (first play: the passes "get clogged up too easily"): a line of single sand corners, which nothing +// grows onto and nothing is built across. No stream lies within kPassClear tiles along the ridge of a +// pass, so the road never runs into water. +constexpr int kPassRoad = 12, kPassClear = 4; +// Inland lakes (first play: "a few rivers connected to some inland lakes"): sites at least this far +// apart along the middle of each valley (the band within kLakeBand of the valley's middle phase), each +// a rough disc of this radius, joined to the nearest windward stream by a river this many corners +// wide (one tile of water, with the fords a wandering line leaves) wandering by up to kRiverWander. +constexpr int kLakeSpacing = 56; +constexpr double kLakeBand = 0.18, kLakeRadius = 5.0, kRiverHalfWidth = 1.0, kRiverWander = 4.0; struct Layout { @@ -81,7 +105,8 @@ struct Layout int windX = 0, windY = 0; // the wind's direction in whole steps std::vector homes, kits; std::vector homeOf; - std::vector stone, water, sand, clearing; + std::vector stone, water, sand, road, clearing; + std::vector lakes; std::string failure; }; @@ -168,24 +193,73 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // sand stops where a pass lets the rain through. L.water.assign(n, 0); L.sand.assign(n, 0); + L.road.assign(n, 0); const int footNear = ridgeHalf + acrossUnits(kFootGap), footFar = footNear + acrossUnits(kFootDepth); const int poolPeriod = alongUnits(kPoolSpacing), poolHalf = alongUnits(kPoolLength / 2.0); + const int passClear = passHalf + alongUnits(kPassClear); + // Where a pass's line runs along the ridge, given the along phase and the row's pass shift (the + // same shift the stone uses). + const auto passOffset = [&](int i) + { + const int y = i / t.w; + const int shift = o.slant == 0 ? ((L.ridges * y / t.h) % 2) * passPeriod / 2 : 0; + const int offset = ((along[i] + shift) % passPeriod + passPeriod) % passPeriod; + return std::min(offset, passPeriod - offset); + }; for (int i = 0; i < n; ++i) { const int below = 65536 - phase[i]; // how far below the next ridge's crest - if (below >= footNear && below < footFar) + if (below >= footNear && below < footFar && passOffset(i) > passClear) { const int offset = along[i] % std::max(1, poolPeriod); if (offset < poolHalf || poolPeriod - offset < poolHalf) L.water[i] = 1; } + // The pass road: the pass's centre line, from kPassRoad tiles before the ridge to as far past. + if (passOffset(i) <= alongUnits(0.5) && + stripeDistance(phase[i]) < ridgeHalf + acrossUnits(kPassRoad)) + L.road[i] = 1; } // The sand starts two tiles behind the stone, not one: a sand corner spoils the tiles round it // and stone stands only on pure grass, so sand touching the ridge would strip its lee row. const std::vector shadow = upwindSteps(t, L.stone, L.windX, L.windY, o.leeWidth + 1); for (int i = 0; i < n; ++i) L.sand[i] = shadow[i] >= 2; + // Inland lakes along the middle of every valley (first play: the valleys "feel too empty"): sites + // thrown at least kLakeSpacing apart over the map and kept where the across phase is within + // kLakeBand of the valley's middle, each a rough disc, joined to the nearest stream tile by a + // wandering river. Rivers leave the lee sand alone but cross the valley's grass. + if (o.inlandLakes) + { + const RadialShape lake(kLakeRadius, 0.35, context, "rain-lakes"); + std::mt19937 &rivers = context.stream("rain-rivers"); + std::vector streams = L.water; + for (const Site &site : spreadPoints(t, kLakeSpacing, context, "rain-lakes")) + { + const int at = t.at(site.x, site.y); + if (std::abs(phase[at] - 32768) > kLakeBand * 65536) + continue; + L.lakes.push_back(site); + fillShape(L.water, t, site.x + 0.5, site.y + 0.5, lake, 0.0); + int nearest = -1; + for (int i = 0; i < n; ++i) + if (streams[i] && + (nearest < 0 || t.dist2(site.x, site.y, i % t.w, i / t.w) < + t.dist2(site.x, site.y, nearest % t.w, nearest / t.w))) + nearest = i; + if (nearest < 0) + continue; + const ShapePoint mouth{site.x + 0.5 + t.offsetX(site.x, nearest % t.w), + site.y + 0.5 + t.offsetY(site.y, nearest / t.w)}; + strokePath(L.water, t, + wanderingPath(t, {site.x + 0.5, site.y + 0.5}, mouth, kRiverHalfWidth, + kRiverWander, 0.0, rivers)); + } + } + for (int i = 0; i < n; ++i) + if (L.water[i]) + L.road[i] = L.sand[i] = 0; // Homes: on a lattice, each slid across its valley to the middle of it (phase half a turn), so // every home has the same ridge behind it and the same foot before it. The clearing round a home @@ -222,7 +296,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } for (int i = 0; i < n; ++i) if (L.clearing[i]) - L.water[i] = L.sand[i] = L.stone[i] = 0; + L.water[i] = L.sand[i] = L.stone[i] = L.road[i] = 0; L.homeOf.assign(n, -1); const RadialShape home(L.homeRadius, 0.15, context, "rain-home"); const RadialShape pond(homePondRadius(L.homeRadius), 0.3, context, "rain-pond"); @@ -249,7 +323,19 @@ bool generate(Game &game, GenerationContext &context) context.stage = "rain shadow terrain"; TerrainSketch terrain(n, GRASS); for (int i = 0; i < n; ++i) - terrain[i] = L.water[i] ? WATER : L.sand[i] ? SAND : GRASS; + terrain[i] = L.water[i] ? WATER : (L.sand[i] || L.road[i]) ? SAND : GRASS; + // Sand patches over the valleys' grass (first play: "some inland desert or sporadic sand patches + // to break up the texture"): `sand-patches` percent of the grass at least three tiles from any + // water, in patches from a noise field, kept off the homes, the stone and the roads. + { + std::vector valley(n, 0); + for (int i = 0; i < n; ++i) + valley[i] = !L.stone[i] && !L.road[i] && !L.sand[i] && L.homeOf[i] < 0; + const std::vector patches = + periodicNoise(t.w, t.h, 14, context.stream("rain-patches")); + sprinkleSand(terrain, t, valley, o.sandPatches / 100.0, 3, + [&](int i) { return patches[i]; }); + } layBeaches(terrain, t); writeUndermap(map, terrain); // Stone stands only on pure grass; a ridge tile the beaches or the lee sand spoiled is left out, @@ -346,7 +432,8 @@ RainShadowOptions::RainShadowOptions(const GenerationRequest &r) : ridges(r.option("ridges")), slant(r.option("slant")), ridgeThickness(r.option("ridge-thickness")), passSpacing(r.option("pass-spacing")), passWidth(r.option("pass-width")), leeWidth(r.option("lee-width")), - homeSize(r.option("home-size")), wheat(r.option("wheat-amount")), + homeSize(r.option("home-size")), sandPatches(r.option("sand-patches")), + inlandLakes(r.option("inland-lakes") != 0), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), stone(r.option("stone-amount")), algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) { @@ -354,34 +441,39 @@ RainShadowOptions::RainShadowOptions(const GenerationRequest &r) GeneratorDefinition rainShadowDefinition() { - return {"rain-shadow", - 27, - "Rain shadow", - 1, - false, - // Four ridges on a 256 map give 64-tile valleys: a 12-tile home, a pass every 48 tiles - // and a lee band of 6 leave a valley wide enough to farm and to fight in. Ridges three - // thick are sealed against diagonal steps and thin enough for a level-1 tower to shoot - // across (Walls.h's towerReach); passes five wide take a column of units and can be - // walled shut by whoever holds them. - {{"ridges", "Ridges", 2, 8, 1, 4, ControlGroup::Terrain}, - {"slant", "Slant", 0, 3, 1, 1, ControlGroup::Terrain}, - {"ridge-thickness", "Ridge thickness", 2, 6, 1, 3, ControlGroup::Terrain}, - {"pass-spacing", "Pass spacing", 24, 96, 8, 48, ControlGroup::Terrain}, - {"pass-width", "Pass width", 3, 9, 1, 5, ControlGroup::Terrain}, - {"lee-width", "Lee width", 2, 12, 1, 6, ControlGroup::Terrain}, - {"home-size", "Home size", 10, 20, 1, 12, ControlGroup::Layout}, - GeneratorControl::percentage("wheat-amount", "Wheat amount"), - GeneratorControl::percentage("wood-amount", "Wood amount"), - GeneratorControl::percentage("stone-amount", "Stone amount"), - GeneratorControl::percentage("algae-amount", "Algae amount"), - GeneratorControl::percentage("fruit-amount", "Fruit amount")}, - generate, - true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, - validateWorld}; + return { + "rain-shadow", + 27, + "Rain shadow", + 2, + false, + // Four ridges on a 256 map give 64-tile valleys: a 12-tile home, a pass every 48 tiles + // and a lee band of 6 leave a valley wide enough to farm and to fight in. Ridges three + // thick are sealed against diagonal steps and thin enough for a level-1 tower to shoot + // across (Walls.h's towerReach); passes five wide take a column of units and can be + // walled shut by whoever holds them. + {{"ridges", "Ridges", 2, 8, 1, 4, ControlGroup::Terrain}, + {"slant", "Slant", 0, 3, 1, 1, ControlGroup::Terrain}, + {"ridge-thickness", "Ridge thickness", 2, 6, 1, 3, ControlGroup::Terrain}, + {"pass-spacing", "Pass spacing", 24, 96, 8, 48, ControlGroup::Terrain}, + {"pass-width", "Pass width", 3, 9, 1, 5, ControlGroup::Terrain}, + {"lee-width", "Lee width", 2, 12, 1, 6, ControlGroup::Terrain}, + {"home-size", "Home size", 10, 20, 1, 12, ControlGroup::Layout}, + // FEEDBACK 2026-09-13: lakes with rivers in every valley, and 6% of the valleys' grass in + // sand patches. + GeneratorControl::toggle("inland-lakes", "Inland lakes", true, ControlGroup::Terrain), + {"sand-patches", "Sand patches", 0, 20, 2, 6, ControlGroup::Terrain}, + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount")}, + generate, + true, + [](const GenerationRequest &r) -> std::string + { + GenerationContext probe(r); + return design(r, probe).failure; + }, + validateWorld}; } diff --git a/src/map/generator/generators/RainShadowGenerator.h b/src/map/generator/generators/RainShadowGenerator.h index 8dc01fa0c..1af42f79f 100644 --- a/src/map/generator/generators/RainShadowGenerator.h +++ b/src/map/generator/generators/RainShadowGenerator.h @@ -4,7 +4,8 @@ #include "GeneratorDefinition.h" struct RainShadowOptions { - int ridges, slant, ridgeThickness, passSpacing, passWidth, leeWidth, homeSize; + int ridges, slant, ridgeThickness, passSpacing, passWidth, leeWidth, homeSize, sandPatches; + bool inlandLakes; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit RainShadowOptions(const GenerationRequest &r); }; diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index c29e0c9e4..c6db6225d 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -192,30 +192,22 @@ macos-arm64 24 1 8 8 4 3 ok 285345205556791712 macos-arm64 24 1 8 8 8 1 ok 12174400163941988640 macos-arm64 24 1 9 8 4 1 ok 10026254367617799143 macos-arm64 24 1 9 9 4 1 ok 7691831584358967898 -macos-arm64 25 1 7 7 4 1 ok 8202260453455897702 -macos-arm64 25 1 8 8 2 1 ok 5251912485654861084 -macos-arm64 25 1 8 8 4 1 ok 520175664546379446 -macos-arm64 25 1 8 8 4 2 ok 4789748468574587495 -macos-arm64 25 1 8 8 4 3 ok 3449146876170199206 -macos-arm64 25 1 8 8 8 1 ok 1762371246384647436 -macos-arm64 25 1 9 8 4 1 ok 4711219588934138776 -macos-arm64 25 1 9 9 4 1 ok 10597102770546440263 -macos-arm64 26 1 7 7 4 1 ok 789646270326602504 -macos-arm64 26 1 8 8 2 1 ok 13763197687203102930 -macos-arm64 26 1 8 8 4 1 ok 4144666139421409147 -macos-arm64 26 1 8 8 4 2 ok 9339544561056119274 -macos-arm64 26 1 8 8 4 3 ok 3960702991416737426 -macos-arm64 26 1 8 8 8 1 ok 3818680882719787045 -macos-arm64 26 1 9 8 4 1 ok 8315516405593883041 -macos-arm64 26 1 9 9 4 1 ok 2082514701038793427 -macos-arm64 27 1 7 7 4 1 ok 10833365171923315257 -macos-arm64 27 1 8 8 2 1 ok 9247033084915382178 -macos-arm64 27 1 8 8 4 1 ok 1309570096415202470 -macos-arm64 27 1 8 8 4 2 ok 15608309425939438304 -macos-arm64 27 1 8 8 4 3 ok 15117378251408628476 -macos-arm64 27 1 8 8 8 1 ok 2092448838222506393 -macos-arm64 27 1 9 8 4 1 ok 1645009079489818947 -macos-arm64 27 1 9 9 4 1 ok 11877117867324334076 +macos-arm64 26 2 7 7 4 1 ok 2023176864508665095 +macos-arm64 26 2 8 8 2 1 ok 10412697564682542969 +macos-arm64 26 2 8 8 4 1 ok 15513776986458079690 +macos-arm64 26 2 8 8 4 2 ok 15016749003048531273 +macos-arm64 26 2 8 8 4 3 ok 12664969190182808656 +macos-arm64 26 2 8 8 8 1 ok 11702788174617585795 +macos-arm64 26 2 9 8 4 1 ok 11816300156118143275 +macos-arm64 26 2 9 9 4 1 ok 9989525276080186334 +macos-arm64 27 2 7 7 4 1 ok 429138953340905733 +macos-arm64 27 2 8 8 2 1 ok 12106426376107521520 +macos-arm64 27 2 8 8 4 1 ok 5974881896777234373 +macos-arm64 27 2 8 8 4 2 ok 2561641735748172305 +macos-arm64 27 2 8 8 4 3 ok 1999890532377512024 +macos-arm64 27 2 8 8 8 1 ok 12588833595632048376 +macos-arm64 27 2 9 8 4 1 ok 6175662201951227478 +macos-arm64 27 2 9 9 4 1 ok 4522824076839458332 macos-arm64 28 3 7 7 4 1 ok 10826945051381357680 macos-arm64 28 3 8 8 2 1 ok 13829565247877238314 macos-arm64 28 3 8 8 4 1 ok 4975978679616822016 @@ -224,22 +216,22 @@ macos-arm64 28 3 8 8 4 3 ok 13102395041856171672 macos-arm64 28 3 8 8 8 1 ok 11416359873756795221 macos-arm64 28 3 9 8 4 1 ok 5369678402303790376 macos-arm64 28 3 9 9 4 1 ok 7404275210351283322 -macos-arm64 29 1 7 7 4 1 ok 4971400333522175054 -macos-arm64 29 1 8 8 2 1 ok 7612660082359254337 -macos-arm64 29 1 8 8 4 1 ok 2450863270395321431 -macos-arm64 29 1 8 8 4 2 ok 6736902494964553678 -macos-arm64 29 1 8 8 4 3 ok 18098183166195859585 -macos-arm64 29 1 8 8 8 1 ok 5930810053163922709 -macos-arm64 29 1 9 8 4 1 ok 3405819186314566499 -macos-arm64 29 1 9 9 4 1 ok 15824681712775267268 -macos-arm64 30 1 7 7 4 1 ok 10983756988564259189 -macos-arm64 30 1 8 8 2 1 ok 10864260481877802293 -macos-arm64 30 1 8 8 4 1 ok 8566090570972606806 -macos-arm64 30 1 8 8 4 2 ok 15454657613982169512 -macos-arm64 30 1 8 8 4 3 ok 17544008518965759461 -macos-arm64 30 1 8 8 8 1 ok 7267460019722476921 -macos-arm64 30 1 9 8 4 1 ok 1546476112144531604 -macos-arm64 30 1 9 9 4 1 ok 8553529852829638885 +macos-arm64 29 2 7 7 4 1 ok 1248853318282972913 +macos-arm64 29 2 8 8 2 1 ok 11827857063654687224 +macos-arm64 29 2 8 8 4 1 ok 15125053460343388513 +macos-arm64 29 2 8 8 4 2 ok 6382575433522100820 +macos-arm64 29 2 8 8 4 3 ok 4867298262581307149 +macos-arm64 29 2 8 8 8 1 ok 7252091979753729218 +macos-arm64 29 2 9 8 4 1 ok 8862399760805201561 +macos-arm64 29 2 9 9 4 1 ok 16820382624897441879 +macos-arm64 30 2 7 7 4 1 ok 16256156098464641688 +macos-arm64 30 2 8 8 2 1 ok 18262511519283505011 +macos-arm64 30 2 8 8 4 1 ok 11525836950436894646 +macos-arm64 30 2 8 8 4 2 ok 11474847624102155044 +macos-arm64 30 2 8 8 4 3 ok 7877433232038019115 +macos-arm64 30 2 8 8 8 1 ok 1972023696793533848 +macos-arm64 30 2 9 8 4 1 ok 6758751765979608801 +macos-arm64 30 2 9 9 4 1 ok 2931095272636382767 macos-arm64 31 5 7 7 4 1 ok 3076683908112672630 macos-arm64 31 5 8 8 2 1 ok 11763462524905175676 macos-arm64 31 5 8 8 4 1 ok 8413900832615966718 @@ -256,11 +248,3 @@ macos-arm64 32 3 8 8 4 3 ok 14927243951147519255 macos-arm64 32 3 8 8 8 1 ok 16589278676902375793 macos-arm64 32 3 9 8 4 1 ok 3458795526725179421 macos-arm64 32 3 9 9 4 1 ok 5226362590732428076 -macos-arm64 33 1 7 7 4 1 ok 8691360005280724925 -macos-arm64 33 1 8 8 2 1 ok 6681693907801693688 -macos-arm64 33 1 8 8 4 1 ok 9400400174102501753 -macos-arm64 33 1 8 8 4 2 ok 12597503260781819139 -macos-arm64 33 1 8 8 4 3 ok 9614762065543089229 -macos-arm64 33 1 8 8 8 1 ok 2497376319729488020 -macos-arm64 33 1 9 8 4 1 ok 818754618973904098 -macos-arm64 33 1 9 9 4 1 ok 14036216783204707805 From 45b528bd36226aa4851c70e1b7e8dbc7e3fc26ed Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 21:39:21 -0400 Subject: [PATCH 121/136] Deal start sites to colonies at random (dealStarts) A design's home sites come in a fixed order (farthest-point spreading starts from cell 0, a lattice from its first row), so team 0 always started on the same ground: top right of Old town, for one. Pipeline.h gains dealStarts, a shuffle of a design's sites on its own stream, and every designed generator calls it before anything is keyed by colony index: Fingerprint, Old growth, Polder, Rain shadow, Canals, Anthill, Old town (homes and annexes together) and Maze. Validators rebuilding the design from a fresh context get the same deal. Revisions bumped and the golden table regenerated; a landscape check covers the deal (a permutation, repeatable, not the identity for most seeds). Also clears two warnings: an unused Torus alias in Anthill's validator and Polder's diagonal row width constant, now used for the same value. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/map-generators/ADDING_A_GENERATOR.md | 2 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 6 +- .../generator/generators/AnthillGenerator.cpp | 5 +- .../generator/generators/CanalsGenerator.cpp | 4 +- .../generators/FingerprintGenerator.cpp | 3 +- .../generator/generators/MazeGenerator.cpp | 129 +++++++++--------- .../generators/OldGrowthGenerator.cpp | 3 +- .../generator/generators/OldTownGenerator.cpp | 17 ++- .../generator/generators/PolderGenerator.cpp | 6 +- .../generators/RainShadowGenerator.cpp | 3 +- src/map/generator/shared/Pipeline.h | 14 ++ test/MapGeneratorLandscapeChecks.h | 30 ++++ test/map-generator-golden.txt | 128 ++++++++--------- 13 files changed, 213 insertions(+), 137 deletions(-) diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index d215daeb5..800c10995 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -81,7 +81,7 @@ Everything named here is in `shared/`; `homeMaskFor`, `anchorFor`, `kitFor` and ## Generation, randomness and failure -The stages are shared functions; call them in the order the map needs. A designed generator usually runs: `design()`; stamp the layout into a `TerrainSketch`, `layBeaches` and `writeUndermap` (`Sketch.h`); `settleColonies` (`Pipeline.h`); the kits and ambient layers (`growPatch`, `seedNear`, `scatterResources`, `seedAlgae`, `stockIslands`); `clearAroundSwarms`; `guaranteeStartingResources`; `openRoad` for any walk the deposits closed; then `reopenCrampedStarts` at non-default amounts. The height-field modes run `generateHeightField`'s stages (`Terrain.h`) then `placeStarts`; the legacy point-dispersion modes place colonies before their final resource pass and interleave region division, resources and starts. Nothing forces one order, but a generator that needs a different one should say why in its header comment. +The stages are shared functions; call them in the order the map needs. A designed generator usually runs: `design()`, which deals its home sites to the colonies with `dealStarts` before anything is keyed by colony index (a fixed order would give the same team the same ground every map); stamp the layout into a `TerrainSketch`, `layBeaches` and `writeUndermap` (`Sketch.h`); `settleColonies` (`Pipeline.h`); the kits and ambient layers (`growPatch`, `seedNear`, `scatterResources`, `seedAlgae`, `stockIslands`); `clearAroundSwarms`; `guaranteeStartingResources`; `openRoad` for any walk the deposits closed; then `reopenCrampedStarts` at non-default amounts. The height-field modes run `generateHeightField`'s stages (`Terrain.h`) then `placeStarts`; the legacy point-dispersion modes place colonies before their final resource pass and interleave region division, resources and starts. Nothing forces one order, but a generator that needs a different one should say why in its header comment. Use `context.stream("name")` for a per-attempt `std::mt19937` stream, or `context.bounded("name", count)` for a rejection-sampled bounded choice. Seed derivation is defined in `GenerationContext.cpp`: unsigned FNV-1a mixed with the root seed, followed by a fixed integer avalanche. Do not use `std::hash`, wall-clock reseeding, `rand()`, `srand()` or the gameplay RNG in generator algorithms. Noise tables and stamp caches belong to each height-map instance. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 7a1b8824d..2274bc46b 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -48,7 +48,7 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Room` | Building room as a design decision: `buildableTiles`, `buildAnchors` (every 4x4 footprint's top-left, across the wrap), `buildSites` (footprints in a region) and `growUntilSites` (a chamber grown until it holds exactly what it promised) | | `Biomes` | Kinds of land as data: `BiomeKit` (ponds, stone ring, farmland, wood share, outcrops, groves, cover, orchard island) with `fertilePlain`, `stoneFortress`, `orchardIsland` and `forest`; `biomeWorth` (an estimate from the start scorer's weights to share ground by, to be tuned with the fairness tournament); `sketchBiome` (its ponds, island and ring) and `furnishBiome` (its deposits) | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | -| `Pipeline` | The stages round the others: `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | +| `Pipeline` | The stages round the others: `dealStarts` (the design's start sites dealt to the colonies at random, so a team number never gets the same ground map after map), `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | | `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | | `StartQuality` | `scoreStarts`, the finished map's colony quality and fairness the service ranks candidates by | | `GenerationContext` | Named `std::mt19937` streams, `bounded` draws and `shuffle` | @@ -72,7 +72,9 @@ shape, and the newest of them are little more than a sequence of shared stages: named streams without touching the map, and returns it with a `failure` string when the request leaves no room. 2. `generate` stamps the layout into a `TerrainSketch`, calls `layBeaches` and `writeUndermap`, - then `settleColonies` with a home mask and an anchor per colony. + then `settleColonies` with a home mask and an anchor per colony. The design has already dealt its + start sites to the colonies at random (`dealStarts`), so which team gets which home is a draw: + without it, farthest-point spreading and lattices hand team 0 the same ground on every map. 3. The kits go down with `plantKit` (three seeds, each grown from the nearest eligible tile), then the ambient layers (`scatterResources`, or the generator's own ranking fed to `plantFields` and `scatterClumps`, diff --git a/src/map/generator/generators/AnthillGenerator.cpp b/src/map/generator/generators/AnthillGenerator.cpp index cc4dfc21e..0a47c0d1d 100644 --- a/src/map/generator/generators/AnthillGenerator.cpp +++ b/src/map/generator/generators/AnthillGenerator.cpp @@ -138,6 +138,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) "colonies."; return L; } + // spreadPockets always starts from site 0, so the deal decides which colony gets which chamber. + dealStarts(context, L.homeSite); // The tunnels: a spanning tree through every chamber but the queens', a door into each queen // chamber, and `loops` percent of the chambers' count in extra tunnels. std::vector isPocket(L.sites.size(), 0); @@ -413,7 +415,6 @@ std::string validateWorld(const Game &game, const GenerationContext &context) const Map &map = game.map; if (const std::string mismatch = designMismatch(L, map, "anthill"); !mismatch.empty()) return mismatch; - const Torus &t = L.t; // A queen chamber is dry by design (second play); the farm chambers keep the water. for (int site : L.farmSite) if (!map.isWater(L.sites[site].x, L.sites[site].y)) @@ -439,7 +440,7 @@ GeneratorDefinition anthillDefinition() "anthill", 32, "Anthill", - 3, + 4, false, // Chambers 28 apart give a 256 map about a hundred of them; a chamber of radius 5 holds // three or four buildings; a queen chamber grown to 40 building sites (overlapping 4x4 diff --git a/src/map/generator/generators/CanalsGenerator.cpp b/src/map/generator/generators/CanalsGenerator.cpp index 98063db09..1dd149281 100644 --- a/src/map/generator/generators/CanalsGenerator.cpp +++ b/src/map/generator/generators/CanalsGenerator.cpp @@ -151,6 +151,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) "fewer colonies."; return L; } + // spreadPockets always starts from block 0, so the deal decides which colony gets which block. + dealStarts(context, L.homeCell); // Every other block's kind, dealt from a weighted draw in block order (first play: "each cell // has its own little surprise"). L.kind.assign(L.g.cellCount(), Plain); @@ -502,7 +504,7 @@ GeneratorDefinition canalsDefinition() "canals", 29, "Canals", - 2, + 3, false, // Blocks of 24 give a 256 map about a hundred blocks; a canal of 3 corners (two tiles of // water) is sealed against diagonal steps and is reached by a level-2 tower, which is what diff --git a/src/map/generator/generators/FingerprintGenerator.cpp b/src/map/generator/generators/FingerprintGenerator.cpp index de88beb23..fbcd27d07 100644 --- a/src/map/generator/generators/FingerprintGenerator.cpp +++ b/src/map/generator/generators/FingerprintGenerator.cpp @@ -95,6 +95,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) latticeSites(t.w, t.h, teams, context.bounded("fingerprint-layout", std::uint32_t(t.w)), context.bounded("fingerprint-layout", std::uint32_t(t.h))) .sites; + dealStarts(context, L.homes); // which colony gets which site is a draw, not the order double spacing = std::min(t.w, t.h); for (size_t a = 0; a < L.homes.size(); ++a) for (size_t b = a + 1; b < L.homes.size(); ++b) @@ -280,7 +281,7 @@ GeneratorDefinition fingerprintDefinition() return {"fingerprint", 26, "Fingerprint", - 2, + 3, false, {// FEEDBACK 2026-09-13: wavelength 30 and homes of 18 (were 20 and 12). {"wavelength", "Wavelength", 12, 48, 2, 30, ControlGroup::Terrain}, diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index 9b39e4f13..a0383d8f3 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -113,8 +113,9 @@ MazeLayout mazeLayout(const MazeOptions &o, int width, int height, int teams) L.g = latticeFor(o, width, height); L.half = passageHalf(L.g, o.channelWidth); if (L.g.columns < 2 || L.g.rows < 2) - L.failure = "The maze needs at least two cells across and down; use a bigger map or smaller " - "cells."; + L.failure = + "The maze needs at least two cells across and down; use a bigger map or smaller " + "cells."; else if (L.half < kMinimumPassageHalf) L.failure = "Channels this wide leave too little grass in each cell; use narrower channels " "or bigger cells."; @@ -147,6 +148,11 @@ MazeDesign designMaze(const GenerationRequest &request, GenerationContext &conte d.layout = mazeLayout(o, 1 << request.wDec, 1 << request.hDec, request.nbTeams); MazeLayout &L = d.layout; Tessellation &g = L.g; + // The pockets come in the fixed order farthest-point spreading finds them (from cell 0), so the + // deal decides which colony gets which cul-de-sac (dealStarts, Pipeline.h; FEEDBACK 2026-09-13: + // the same player kept starting in the same spot). + if (L.failure.empty()) + dealStarts(context, L.homes); if (!L.failure.empty()) { d.failure = L.failure; @@ -297,7 +303,8 @@ void furnishHome(Map &map, const Torus &t, const MazeDesign &d, const std::vecto { return a.u != b.u ? a.u < b.u : std::abs(a.v) > std::abs(b.v); }; const auto nearBackCentre = [back](const CellTile &a, const CellTile &b) { - const int da = std::max(a.u + back, std::abs(a.v)), db = std::max(b.u + back, std::abs(b.v)); + const int da = std::max(a.u + back, std::abs(a.v)), + db = std::max(b.u + back, std::abs(b.v)); if (da != db) return da < db; if (std::abs(a.v) != std::abs(b.v)) @@ -462,8 +469,8 @@ std::vector openCorners(const Torus &t, const MazeDesign &d) walled[g.edges[edge].corners[0]] = walled[g.edges[edge].corners[1]] = 1; for (size_t corner = 0; corner < g.corners.size(); ++corner) if (!walled[corner]) - ponds[t.at(int(subtileTile(g.corners[corner].x)), int(subtileTile(g.corners[corner].y)))] = - 1; + ponds[t.at(int(subtileTile(g.corners[corner].x)), + int(subtileTile(g.corners[corner].y)))] = 1; return ponds; } @@ -579,8 +586,7 @@ bool generate(Game &game, GenerationContext &context) for (size_t i = 0; i < deadEnds.size(); ++i) placeTreasure(map, t, d, deadEnds[i], o.fruit, CHERRY + int((firstType + i) % 3)); } - scatterThroughMaze(map, context, d, onRoad, o, - o.treasure ? 0 : int(deadEnds.size()) * o.fruit); + scatterThroughMaze(map, context, d, onRoad, o, o.treasure ? 0 : int(deadEnds.size()) * o.fruit); seedAlgae(map, context, o.algae); return true; } @@ -661,58 +667,59 @@ MazeOptions::MazeOptions(const GenerationRequest &r) GeneratorDefinition mazeDefinition() { - return {"maze", - 11, - "Maze", - 12, - false, - {GeneratorControl::choice("cell-shape", "Cell shape", {"Squares", "Hexagons"}, 0), - {"cell-size", - "Cell size", - 24, - 48, - 1, - 32, - ControlGroup::Layout, - false, - false, - {24, 32, 40, 48}}, - // Cells snap to 24, 32, 40 or 48 tiles; the default 32 gives 64 square cells on a 256 - // map, enough corridors to feel like a maze without passages narrower than a small base. - // Hexagons sit half as far apart again (kHexPitchPercent). - // Warp moves the cells' corners by up to this share of what keeps every cell whole, so - // squares and hexagons become irregular polygons; doorways never narrow past the - // smallest a passage may be, so at small cells warp is gentler. - {"warp", "Warp", 0, 100, 25, 0, ControlGroup::Layout}, - // Open water on each side of a wall's stone line; passages widen to fill the rest. - {"channel-width", "Channel width", 1, 6, 1, 2, ControlGroup::Layout}, - {"loopiness", - "Loopiness", - 0, - 50, - 1, - 5, - ControlGroup::Layout, - false, - false, - {0, 5, 10, 20, 35, 50}}, - // Loopiness is the share of non-home cells that get one extra opening: the default 5 - // opens about 3 walls on a 256 map, a few flanking routes without dissolving the maze. - // Densities for the deposits scattered along the passages (per 256 shore tiles); homes - // always get the same fixed amounts. Fruit is the size, in tiles, of the treasure at - // every dead end that isn't a home. - {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, - {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, - {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, - {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}, - // Off, passages are grass from shore to shore, with no sand road down the middle. - GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), - // Off, the treasure's fruit is scattered along the passages' shores instead. - GeneratorControl::toggle("dead-end-treasure", "Treasure in dead ends", true, - ControlGroup::Resources)}, - generate, - true, - validate, - validateWorld}; + return { + "maze", + 11, + "Maze", + 13, + false, + {GeneratorControl::choice("cell-shape", "Cell shape", {"Squares", "Hexagons"}, 0), + {"cell-size", + "Cell size", + 24, + 48, + 1, + 32, + ControlGroup::Layout, + false, + false, + {24, 32, 40, 48}}, + // Cells snap to 24, 32, 40 or 48 tiles; the default 32 gives 64 square cells on a 256 + // map, enough corridors to feel like a maze without passages narrower than a small base. + // Hexagons sit half as far apart again (kHexPitchPercent). + // Warp moves the cells' corners by up to this share of what keeps every cell whole, so + // squares and hexagons become irregular polygons; doorways never narrow past the + // smallest a passage may be, so at small cells warp is gentler. + {"warp", "Warp", 0, 100, 25, 0, ControlGroup::Layout}, + // Open water on each side of a wall's stone line; passages widen to fill the rest. + {"channel-width", "Channel width", 1, 6, 1, 2, ControlGroup::Layout}, + {"loopiness", + "Loopiness", + 0, + 50, + 1, + 5, + ControlGroup::Layout, + false, + false, + {0, 5, 10, 20, 35, 50}}, + // Loopiness is the share of non-home cells that get one extra opening: the default 5 + // opens about 3 walls on a 256 map, a few flanking routes without dissolving the maze. + // Densities for the deposits scattered along the passages (per 256 shore tiles); homes + // always get the same fixed amounts. Fruit is the size, in tiles, of the treasure at + // every dead end that isn't a home. + {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, + {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, + {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, + {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, + {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}, + // Off, passages are grass from shore to shore, with no sand road down the middle. + GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), + // Off, the treasure's fruit is scattered along the passages' shores instead. + GeneratorControl::toggle("dead-end-treasure", "Treasure in dead ends", true, + ControlGroup::Resources)}, + generate, + true, + validate, + validateWorld}; } diff --git a/src/map/generator/generators/OldGrowthGenerator.cpp b/src/map/generator/generators/OldGrowthGenerator.cpp index b5b956d09..1dbb94c05 100644 --- a/src/map/generator/generators/OldGrowthGenerator.cpp +++ b/src/map/generator/generators/OldGrowthGenerator.cpp @@ -122,6 +122,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.homes = latticeSites(t.w, t.h, teams, context.bounded("growth-layout", std::uint32_t(t.w)), context.bounded("growth-layout", std::uint32_t(t.h))) .sites; + dealStarts(context, L.homes); // which colony gets which site is a draw, not the order L.spacing = std::min(t.w, t.h); for (size_t a = 0; a < L.homes.size(); ++a) for (size_t b = a + 1; b < L.homes.size(); ++b) @@ -454,7 +455,7 @@ GeneratorDefinition oldGrowthDefinition() "old-growth", 28, "Old growth", - 3, + 4, false, // 90% cover reads as unbroken forest with the odd glade; one lake per 128x128 of 90 // tiles (four on a 256 map, each a few days' cutting from any home) keeps them rare enough diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index b95619810..74bec9e1c 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -18,6 +18,7 @@ #include "Tessellation.h" #include #include +#include #include #include using namespace MapGeneration; @@ -212,6 +213,20 @@ Layout design(const GenerationRequest &request, GenerationContext &context) "smaller blocks or fewer colonies."; return L; } + // The spreading picks the plazas in a fixed order (the first is always the same for a + // given tiling), so the deal decides which colony gets which pair (FEEDBACK 2026-09-13: + // "player 1 always top right"). The annexes travel with their plazas. + std::vector order(teams); + std::iota(order.begin(), order.end(), 0); + dealStarts(context, order); + std::vector dealtHomes, dealtAnnexes; + for (int k : order) + { + dealtHomes.push_back(L.homeCell[k]); + dealtAnnexes.push_back(L.annexCell[k]); + } + L.homeCell = dealtHomes; + L.annexCell = dealtAnnexes; } // Streets: the band along every cell border inside the city; blocks: the rest of the city's @@ -508,7 +523,7 @@ GeneratorDefinition oldTownDefinition() return {"old-town", 31, "Old town", - 5, + 6, false, // A city of 70% of the half side leaves a belt of fields round it; blocks of 14 with // streets of 4 give a 256 map about a hundred blocks and streets a column of units wide diff --git a/src/map/generator/generators/PolderGenerator.cpp b/src/map/generator/generators/PolderGenerator.cpp index d9b35f84a..bb96a9371 100644 --- a/src/map/generator/generators/PolderGenerator.cpp +++ b/src/map/generator/generators/PolderGenerator.cpp @@ -128,7 +128,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (L.rows.acrossX == 0 && L.rows.acrossY == 0) L.rows.acrossY = turns; const double angle = std::atan2(double(L.rows.acrossY) / t.h, double(L.rows.acrossX) / t.w); - L.crops = kStraightCrops + std::lround(std::fabs(std::sin(2 * angle))); + L.crops = kStraightCrops + + std::lround((kDiagonalCrops - kStraightCrops) * std::fabs(std::sin(2 * angle))); } L.spacing = stripeSpacing(t, L.rows); L.along = alongStripes(t, L.rows); @@ -139,6 +140,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.homes = latticeSites(t.w, t.h, teams, context.bounded("polder-layout", std::uint32_t(t.w)), context.bounded("polder-layout", std::uint32_t(t.h))) .sites; + dealStarts(context, L.homes); // which colony gets which site is a draw, not the order double nearest = std::min(t.w, t.h); for (size_t a = 0; a < L.homes.size(); ++a) for (size_t b = a + 1; b < L.homes.size(); ++b) @@ -348,7 +350,7 @@ GeneratorDefinition polderDefinition() "polder", 30, "Polder", - 2, + 3, false, // A dyke every 24 tiles is a lane every one and a half rows' walk; villages of radius 11 // hold a swarm, its kit and a few more buildings and no more. diff --git a/src/map/generator/generators/RainShadowGenerator.cpp b/src/map/generator/generators/RainShadowGenerator.cpp index 9eff215e4..12a459a22 100644 --- a/src/map/generator/generators/RainShadowGenerator.cpp +++ b/src/map/generator/generators/RainShadowGenerator.cpp @@ -283,6 +283,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.homeRadius + kHomeMargin) L.clearing[i] = 1; } + dealStarts(context, L.homes); // which colony gets which valley site is a draw, not the order double nearest = std::min(t.w, t.h); for (size_t a = 0; a < L.homes.size(); ++a) for (size_t b = a + 1; b < L.homes.size(); ++b) @@ -445,7 +446,7 @@ GeneratorDefinition rainShadowDefinition() "rain-shadow", 27, "Rain shadow", - 2, + 3, false, // Four ridges on a 256 map give 64-tile valleys: a 12-tile home, a pass every 48 tiles // and a lee band of 6 leave a valley wide enough to farm and to fight in. Ridges three diff --git a/src/map/generator/shared/Pipeline.h b/src/map/generator/shared/Pipeline.h index 582889d28..5eafb0feb 100644 --- a/src/map/generator/shared/Pipeline.h +++ b/src/map/generator/shared/Pipeline.h @@ -20,6 +20,20 @@ namespace MapGeneration // its crops; open any route the deposits closed (Roads.h); and have validateWorld rebuild the // design and check the finished world against it. +/// Deals a design's start sites to the colonies at random: the design decides where the homes are, +/// and which colony gets which is a draw from `stream`, so a team number never lands on the same +/// ground map after map. A design's sites often come in a fixed order (farthest-point spreading +/// starts from cell 0, a lattice from its first row), and without this team 0 always started top +/// right of Old town (FEEDBACK 2026-09-13). Every designed generator calls it on its list of home +/// sites, cells or slots before anything is keyed by colony index, so kits, towers and validators +/// follow the deal without knowing about it; a validator rebuilding the design from a fresh context +/// gets the same deal. +template +void dealStarts(GenerationContext &context, Sites &sites, const char *stream = "starts-deal") +{ + context.shuffle(sites.begin(), sites.end(), stream); +} + /// A swarm and its workers for every colony, each inside its own home mask and as near as the /// mask allows to its anchor. Fails the candidate on the first colony that does not fit. template diff --git a/test/MapGeneratorLandscapeChecks.h b/test/MapGeneratorLandscapeChecks.h index 0a7c2fe2f..1afe2d922 100644 --- a/test/MapGeneratorLandscapeChecks.h +++ b/test/MapGeneratorLandscapeChecks.h @@ -16,6 +16,7 @@ #include "Morphology.h" #include "Orbits.h" #include "Patterns.h" +#include "Pipeline.h" #include "Points.h" #include "Roads.h" #include "Room.h" @@ -521,8 +522,37 @@ inline void biomeChecks() } } +// dealStarts: a permutation, the same again from the same request, and not the identity for every +// seed (so a design's first site is not always colony 0's). +inline void dealChecks() +{ + GenerationRequest request; + request.seed = 21; + std::vector sites{0, 1, 2, 3, 4, 5, 6, 7}; + GenerationContext context(request), again(request); + dealStarts(context, sites); + std::vector repeat{0, 1, 2, 3, 4, 5, 6, 7}; + dealStarts(again, repeat); + assert(sites == repeat); + std::vector sorted(sites); + std::sort(sorted.begin(), sorted.end()); + assert(sorted == std::vector({0, 1, 2, 3, 4, 5, 6, 7})); + int moved = 0; + for (std::uint32_t seed = 1; seed <= 12; ++seed) + { + GenerationRequest other; + other.seed = seed; + GenerationContext c(other); + std::vector dealt{0, 1, 2, 3}; + dealStarts(c, dealt); + moved += dealt[0] != 0; + } + assert(moved >= 6); +} + inline void landscapeChecks() { + dealChecks(); orbitChecks(); morphologyChecks(); growthChecks(); diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index c6db6225d..2672cde42 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -80,14 +80,14 @@ macos-arm64 9 2 8 8 4 3 ok 5877899263698877100 macos-arm64 9 2 8 8 8 1 ok 4204618854271989380 macos-arm64 9 2 9 8 4 1 ok 13364805290811868144 macos-arm64 9 2 9 9 4 1 ok 17370525382688177735 -macos-arm64 11 12 7 7 4 1 ok 13998387927847893421 -macos-arm64 11 12 8 8 2 1 ok 1580814293182404478 -macos-arm64 11 12 8 8 4 1 ok 9436694036498979113 -macos-arm64 11 12 8 8 4 2 ok 6039395870650123087 -macos-arm64 11 12 8 8 4 3 ok 17321560833037622047 -macos-arm64 11 12 8 8 8 1 ok 13476391256386289913 -macos-arm64 11 12 9 8 4 1 ok 11214128330290450603 -macos-arm64 11 12 9 9 4 1 ok 2809958562561781155 +macos-arm64 11 13 7 7 4 1 ok 7573201930155389176 +macos-arm64 11 13 8 8 2 1 ok 1580814293182404478 +macos-arm64 11 13 8 8 4 1 ok 6132248727033081699 +macos-arm64 11 13 8 8 4 2 ok 17571897192944793353 +macos-arm64 11 13 8 8 4 3 ok 17321560833037622047 +macos-arm64 11 13 8 8 8 1 ok 9101533112797905055 +macos-arm64 11 13 9 8 4 1 ok 8608092470811997215 +macos-arm64 11 13 9 9 4 1 ok 5365458970422917083 macos-arm64 12 11 7 7 4 1 ok 7388571519173228055 macos-arm64 12 11 8 8 2 1 ok 15699276765293966482 macos-arm64 12 11 8 8 4 1 ok 11621882014804934120 @@ -192,59 +192,59 @@ macos-arm64 24 1 8 8 4 3 ok 285345205556791712 macos-arm64 24 1 8 8 8 1 ok 12174400163941988640 macos-arm64 24 1 9 8 4 1 ok 10026254367617799143 macos-arm64 24 1 9 9 4 1 ok 7691831584358967898 -macos-arm64 26 2 7 7 4 1 ok 2023176864508665095 -macos-arm64 26 2 8 8 2 1 ok 10412697564682542969 -macos-arm64 26 2 8 8 4 1 ok 15513776986458079690 -macos-arm64 26 2 8 8 4 2 ok 15016749003048531273 -macos-arm64 26 2 8 8 4 3 ok 12664969190182808656 -macos-arm64 26 2 8 8 8 1 ok 11702788174617585795 -macos-arm64 26 2 9 8 4 1 ok 11816300156118143275 -macos-arm64 26 2 9 9 4 1 ok 9989525276080186334 -macos-arm64 27 2 7 7 4 1 ok 429138953340905733 -macos-arm64 27 2 8 8 2 1 ok 12106426376107521520 -macos-arm64 27 2 8 8 4 1 ok 5974881896777234373 -macos-arm64 27 2 8 8 4 2 ok 2561641735748172305 -macos-arm64 27 2 8 8 4 3 ok 1999890532377512024 -macos-arm64 27 2 8 8 8 1 ok 12588833595632048376 -macos-arm64 27 2 9 8 4 1 ok 6175662201951227478 -macos-arm64 27 2 9 9 4 1 ok 4522824076839458332 -macos-arm64 28 3 7 7 4 1 ok 10826945051381357680 -macos-arm64 28 3 8 8 2 1 ok 13829565247877238314 -macos-arm64 28 3 8 8 4 1 ok 4975978679616822016 -macos-arm64 28 3 8 8 4 2 ok 1491883777312121906 -macos-arm64 28 3 8 8 4 3 ok 13102395041856171672 -macos-arm64 28 3 8 8 8 1 ok 11416359873756795221 -macos-arm64 28 3 9 8 4 1 ok 5369678402303790376 -macos-arm64 28 3 9 9 4 1 ok 7404275210351283322 -macos-arm64 29 2 7 7 4 1 ok 1248853318282972913 -macos-arm64 29 2 8 8 2 1 ok 11827857063654687224 -macos-arm64 29 2 8 8 4 1 ok 15125053460343388513 -macos-arm64 29 2 8 8 4 2 ok 6382575433522100820 -macos-arm64 29 2 8 8 4 3 ok 4867298262581307149 -macos-arm64 29 2 8 8 8 1 ok 7252091979753729218 -macos-arm64 29 2 9 8 4 1 ok 8862399760805201561 -macos-arm64 29 2 9 9 4 1 ok 16820382624897441879 -macos-arm64 30 2 7 7 4 1 ok 16256156098464641688 -macos-arm64 30 2 8 8 2 1 ok 18262511519283505011 -macos-arm64 30 2 8 8 4 1 ok 11525836950436894646 -macos-arm64 30 2 8 8 4 2 ok 11474847624102155044 -macos-arm64 30 2 8 8 4 3 ok 7877433232038019115 -macos-arm64 30 2 8 8 8 1 ok 1972023696793533848 -macos-arm64 30 2 9 8 4 1 ok 6758751765979608801 -macos-arm64 30 2 9 9 4 1 ok 2931095272636382767 -macos-arm64 31 5 7 7 4 1 ok 3076683908112672630 -macos-arm64 31 5 8 8 2 1 ok 11763462524905175676 -macos-arm64 31 5 8 8 4 1 ok 8413900832615966718 -macos-arm64 31 5 8 8 4 2 ok 9908515123101461592 -macos-arm64 31 5 8 8 4 3 ok 13708832934710116111 -macos-arm64 31 5 8 8 8 1 ok 7865455393007647241 -macos-arm64 31 5 9 8 4 1 ok 5703755596576899428 -macos-arm64 31 5 9 9 4 1 ok 3878540907432692789 -macos-arm64 32 3 7 7 4 1 ok 5172582828228199195 -macos-arm64 32 3 8 8 2 1 ok 8633803889491191707 -macos-arm64 32 3 8 8 4 1 ok 8410000769200599553 -macos-arm64 32 3 8 8 4 2 ok 18324534515658063123 -macos-arm64 32 3 8 8 4 3 ok 14927243951147519255 -macos-arm64 32 3 8 8 8 1 ok 16589278676902375793 -macos-arm64 32 3 9 8 4 1 ok 3458795526725179421 -macos-arm64 32 3 9 9 4 1 ok 5226362590732428076 +macos-arm64 26 3 7 7 4 1 ok 7596233670176169203 +macos-arm64 26 3 8 8 2 1 ok 10412697564682542969 +macos-arm64 26 3 8 8 4 1 ok 15850274609968439542 +macos-arm64 26 3 8 8 4 2 ok 1451418342198471041 +macos-arm64 26 3 8 8 4 3 ok 12664969190182808656 +macos-arm64 26 3 8 8 8 1 ok 696076906107120864 +macos-arm64 26 3 9 8 4 1 ok 6547976804033402803 +macos-arm64 26 3 9 9 4 1 ok 11162413902762714846 +macos-arm64 27 3 7 7 4 1 ok 3004732659855785781 +macos-arm64 27 3 8 8 2 1 ok 12106426376107521520 +macos-arm64 27 3 8 8 4 1 ok 241424920386510085 +macos-arm64 27 3 8 8 4 2 ok 13721009075482023062 +macos-arm64 27 3 8 8 4 3 ok 1999890532377512024 +macos-arm64 27 3 8 8 8 1 ok 1936688929129444838 +macos-arm64 27 3 9 8 4 1 ok 18057613840189663773 +macos-arm64 27 3 9 9 4 1 ok 17117909255655369195 +macos-arm64 28 4 7 7 4 1 ok 10897682792262896502 +macos-arm64 28 4 8 8 2 1 ok 13829565247877238314 +macos-arm64 28 4 8 8 4 1 ok 4320506443997572230 +macos-arm64 28 4 8 8 4 2 ok 2058307586040359342 +macos-arm64 28 4 8 8 4 3 ok 13102395041856171672 +macos-arm64 28 4 8 8 8 1 ok 14091742024861043395 +macos-arm64 28 4 9 8 4 1 ok 8120863798265800623 +macos-arm64 28 4 9 9 4 1 ok 4370189412371078302 +macos-arm64 29 3 7 7 4 1 ok 1513765199421365488 +macos-arm64 29 3 8 8 2 1 ok 11827857063654687224 +macos-arm64 29 3 8 8 4 1 ok 10145298994600649149 +macos-arm64 29 3 8 8 4 2 ok 2534114081112037916 +macos-arm64 29 3 8 8 4 3 ok 4867298262581307149 +macos-arm64 29 3 8 8 8 1 ok 7706014024180394011 +macos-arm64 29 3 9 8 4 1 ok 17327182427130708652 +macos-arm64 29 3 9 9 4 1 ok 16102946102284687512 +macos-arm64 30 3 7 7 4 1 ok 6823664725837608399 +macos-arm64 30 3 8 8 2 1 ok 18262511519283505011 +macos-arm64 30 3 8 8 4 1 ok 18435202348722167438 +macos-arm64 30 3 8 8 4 2 ok 15449658255269471781 +macos-arm64 30 3 8 8 4 3 ok 7877433232038019115 +macos-arm64 30 3 8 8 8 1 ok 10268677231018964684 +macos-arm64 30 3 9 8 4 1 ok 15407699937027208274 +macos-arm64 30 3 9 9 4 1 ok 1129976864654011214 +macos-arm64 31 6 7 7 4 1 ok 7688111426140491122 +macos-arm64 31 6 8 8 2 1 ok 11763462524905175676 +macos-arm64 31 6 8 8 4 1 ok 10294467211899142010 +macos-arm64 31 6 8 8 4 2 ok 2468014486729326948 +macos-arm64 31 6 8 8 4 3 ok 13708832934710116111 +macos-arm64 31 6 8 8 8 1 ok 6715384199869579049 +macos-arm64 31 6 9 8 4 1 ok 10414113953351534244 +macos-arm64 31 6 9 9 4 1 ok 10377664767817836693 +macos-arm64 32 4 7 7 4 1 ok 7540126588246018722 +macos-arm64 32 4 8 8 2 1 ok 8633803889491191707 +macos-arm64 32 4 8 8 4 1 ok 17633894656724945162 +macos-arm64 32 4 8 8 4 2 ok 17969857465891887079 +macos-arm64 32 4 8 8 4 3 ok 14927243951147519255 +macos-arm64 32 4 8 8 8 1 ok 14387082642563060217 +macos-arm64 32 4 9 8 4 1 ok 15882581644053452132 +macos-arm64 32 4 9 9 4 1 ok 5175145437434078792 From 3e709fc3ea002b511cbf5f26161fcafa1fd5a74a Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 22:10:30 -0400 Subject: [PATCH 122/136] Switchbacks: farms on every map size 128 maps had no farms (two home ponds instead) because a farm squeezed between the mountains kept losing a strip to its own coast walls: at 128x256 a strip joined to its field through an isthmus three tiles wide was sealed off when the walls of the coasts either side met across it, so the farm-reach check failed and the lobby retried a quarter of seeds. growFarmFields now keeps only the ground whose eroded core is joined to the home's core (two cores that do not touch can still overlap once grown back, through a waist the walls then close); the opening radius is a parameter with the old default, so Carousel is byte-identical. The size floor and the pond fallback go; 12 seeds pass at every 128-based cell. Revision 2, golden rows regenerated. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 7 +- .../generators/SwitchbacksGenerator.cpp | 128 +++++++++--------- src/map/generator/shared/Farmland.cpp | 55 ++++---- src/map/generator/shared/Farmland.h | 28 ++-- test/map-generator-golden.txt | 16 +-- 5 files changed, 123 insertions(+), 111 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 2274bc46b..0a933b3d3 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -35,7 +35,7 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Tessellation` | Polygon tilings of the torus with no terrain attached: `squareTessellation` and `hexTessellation` (cells, the corners they share and edges as identities, exact across the wrap even two cells wide), `edgeEnds`, `centreAcross`, `outline`, `transform` (the lattice's translations and mirrors), `labelTiles` (every tile's cell), `shortestEdgeSteps` and `centreClearance`; `warpLimit` and `warpCorners`, corners moved at random into irregular polygons that still tile, never closing the gap between walls that share no corner below a minimum (Maze) | | `GraphMaze` | A maze carved through any `CellGraph` (a tessellation's with `cellGraph(tiling)`, or scattered sites' with `cellGraph(torus, sites, siteNeighbours)`): `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | | `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic, or shared by value when each claimant's ground has a worth per tile; `smoothLabels`, a majority filter over any radius that trims spurs or, at a radius of 3 or 4, straightens borders into curves; `separateTerritories`, a gap opened between neighbouring territories; `fillToNearest`, the reverse: unclaimed ground near labelled regions given to the nearest, so water between pieces becomes land; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `growLakeBeside`, one on either flank of a site, kept wholly to its side of the line from the way in; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | -| `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges across the water rows every so many tiles along them, so workers need not walk round a long row (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives, and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | +| `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges across the water rows every so many tiles along them, so workers need not walk round a long row (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives (and only ground whose eroded core joins the home's core stays, since two cores that do not touch can overlap once grown back through a waist the coast walls then close), and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | | `Towers` | Tower sites chosen for the ground they cover over the walls, other colonies' (offence) or their own (defence), each weighted: `startingTowerRequest` (a request from a map's level and count controls), `chooseTowerSites` (best first, a colony at a time in turn, optionally every site directly against a wall, no stocked tower in range of another colony's tower or swarm, plus open 2x2 pads for players to build more), `settleStartingTowers` (the sequence every arena map runs: `dropBlockingSites` so no site closes a colony's walk to a goal, `evenTowerPlan` so every colony has as many sites as the fewest got, then `raiseTowers`), `towerFootprints` and `roomyGround` (no tower on a strip it could close); a tower in range of another colony's tower starts empty | | `Homes` | Homes built alike: `stampRoundHome`, `homeSwarmSite` and `plantHomeKit` (a round home facing out from the middle, its swarm towards its door, optional ponds kept clear of its edge, and a wheat and wood kit near the swarm with no stone, since these homes are walled in stone; `homeHasRoom` is the size floor; Carousel, Switchbacks; the axis is any heading, out from the middle on a ring), `regionHome` (a home in ground of any shape: the swarm the same walk in from the door as every other colony's, on the roomiest such tile, facing away from the door), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | | `Orbits` | Fairness by symmetry groups rather than wedges: `Symmetry` (signed permutation matrices on doubled centred coordinates plus a whole-tile translation, mapping tiles and corners exactly), `pointSymmetry` (the half turn, quarter turns, mirrors and all eight square symmetries; Symmetric arena), `translationSymmetry` (the roomiest lattice of 2, 4, 8... colonies on the torus, square, staggered or sheared, with no centre and served as well on a rectangular map), `latticeSites` (colonies on that lattice, or evenly staggered rows when the count has no exact group), `stampOrbits`, `orbitSum`, `orbitNoise` and `topShare` (features and fields every symmetry leaves unchanged), `equaliseDeposits` (the gameplay RNG's amounts made equal across each orbit) and `orbitMismatch` (the finished world checked exactly symmetric, colonies permuted one to one) | @@ -911,8 +911,9 @@ next leg up. Every home reaches a walled wheat farm on either flank. two sealed coasts, within a range-7 tower's reach. - **Walls.** Every coast sealed, farms included; mountains keep water between each other beyond their innermost legs. -- **Homes and plateau.** Round homes (`stampRoundHome`) whose farms are their water, with two ponds - only on maps too small for farms; a wheat and wood kit with no stone. The plateau has a pond and only +- **Homes and plateau.** Round homes (`stampRoundHome`) whose farms are their water on every size of + map (128 maps had two home ponds instead until the farms' core-joined opening, 2026-09-13); a wheat + and wood kit with no stone. The plateau has a pond and only fruit, an orchard of the three fruits between every two summits, so nothing overgrows it. - **Starting towers.** `tower-count` towers (default 3) and four open pads per colony, all on its trail beside the walkway down its middle, each directly against stone on the inner side of a wall - the side diff --git a/src/map/generator/generators/SwitchbacksGenerator.cpp b/src/map/generator/generators/SwitchbacksGenerator.cpp index 326fff40f..04f7ce5e6 100644 --- a/src/map/generator/generators/SwitchbacksGenerator.cpp +++ b/src/map/generator/generators/SwitchbacksGenerator.cpp @@ -41,9 +41,9 @@ using namespace MapGeneration; // // THE WATER. Every coast is sealed: stone stands on every grass tile touching the sea's margin, so a // swimmer can land on a beach but never get inside, and the trails are the only way anywhere for the -// whole game. On maps big enough, each home reaches two walled wheat farms, one on either flank, laid in -// rows that regrow best and shared out by equal yield (Farmland); they are the homes' water. A map too -// small for farms gives every home two ponds instead. The plateau has a pond and only fruit, the prize +// whole game. Each home reaches two walled wheat farms, one on either flank, laid in rows that regrow +// best and shared out by equal yield (Farmland); they are the homes' water, on every size of map (128 +// maps went without, and had home ponds, until 2026-09-13). The plateau has a pond and only fruit, the prize // every trail climbs to, with no wheat or wood to smother it. // // HOW IT IS BUILT. Everything is designed once in the wedge's frame, along the colony's axis and across @@ -56,7 +56,7 @@ using namespace MapGeneration; // so a mountain's walls are permanent and its trail is the only door; a tower scans round its footprint // with no line of sight (BuildingUtils::turretScanTile), which is what makes a thin wall between two legs // a firing line; grass may never touch water, so a coast wall always stands a tile in from its sand lane; -// wheat and wood regrow only near water, which is why every home has farms or ponds; and fruit lets an +// wheat and wood regrow only near water, which is why every home has farms; and fruit lets an // inn pull hungry enemy units across, which makes the plateau's orchard worth the climb. // // THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a plateau 28 tiles in radius; mountains about 40 @@ -107,9 +107,6 @@ constexpr int kWalkSpread = 12; // The farms: water kept between a farm and any other land, and the neck joining it to its home. constexpr int kFarmGap = 3; constexpr double kNeckHalf = 8; -// The smallest half side that has farms: below it the sea between the mountains is too narrow for farms -// that stay joined to their homes, and every home gets ponds instead. -constexpr int kFarmMinimumHalf = 100; // The least share of a farm's crop land its colony must be able to walk to. constexpr double kFarmReach = 0.95; // How far apart, along a farm's rows, the sand bridges across its water rows stand; the land a farm keeps @@ -135,7 +132,7 @@ constexpr int kCoverTower = 1; struct Geometry { - int teams = 0, half = 0, legs = 0, ponds = 0; + int teams = 0, half = 0, legs = 0; double wedge = 0, outer = 0; double trailHalf = 0, narrowestHalf = 0, legWall = 0, pitch = 0, span = 0, blockHalf = 0; double plateauR = 0, plateauPondR = 0; @@ -206,8 +203,6 @@ Geometry geometryFor(const GenerationRequest &r) g.homeRadius = g.outer - g.homeReach; layLegs(g, g.homeRadius - g.homeR); g.blockOut = g.homeRadius; - // A home's farms are its water; a map too small for farms gives every home two ponds instead. - g.ponds = g.half >= kFarmMinimumHalf ? 0 : 2; g.roads = o.sandRoads; if (!homeHasRoom(g.homeR)) g.failure = "Too many colonies for this map: the homes are too small."; @@ -342,7 +337,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.homes.push_back(polarPoint(L.cx, L.cy, g.homeRadius, L.axis[k])); } for (int k = 0; k < teams; ++k) - L.kitCentre.push_back(stampRoundHome(t, L.homes[k], L.axis[k], homeShape, g.homeR, g.ponds, + // No pond in a home: its farms are its water, on every size of map. + L.kitCentre.push_back(stampRoundHome(t, L.homes[k], L.axis[k], homeShape, g.homeR, 0, &pondShape, L.pond, [&](int i) { L.homeOf[i] = k; })); fillShape(L.plateau, t, L.cx, L.cy, plateauShape); fillShape(L.pond, t, L.cx, L.cy, plateauPondShape); @@ -390,65 +386,69 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // building plot (layFarm), and every farm's coast is sealed like the rest. L.farmOf.assign(n, -1); L.farmSand.assign(n, 0); - // A map under kFarmMinimumHalf across its shorter half has no room between its mountains for farms - // that stay joined to their homes; it has none. - if (g.half >= kFarmMinimumHalf) + // Farms on every size of map. They were off below a half side of 100 (128 maps got two home ponds + // instead) because a farm squeezed between the mountains kept losing a strip to its own coast walls; + // FEEDBACK 2026-09-13: "on 128x128 on switchback, for some reason the farms lands are not + // generating" - the fix is in growFarmFields (see the call below), not in going without. + std::vector seeds, anchors; + for (int k = 0; k < teams; ++k) + for (const double side : {-1.0, 1.0}) + { + seeds.push_back(L.frames[k].at(g.homeRadius, side * (g.homeReach + 2))); + anchors.push_back(L.homes[k]); + } + const std::vector everywhere(n, 1); + const FarmPlot plot; + // Rows run across every colony's axis; fields whose rows fall on the diagonal get more ground, + // so every farm yields alike (farmYield). + std::vector owners; + std::vector angles; + for (size_t f = 0; f < seeds.size(); ++f) { - std::vector seeds, anchors; - for (int k = 0; k < teams; ++k) - for (const double side : {-1.0, 1.0}) + owners.push_back(int(f) / 2); + angles.push_back(L.axis[f / 2] + kPi / 2); + } + // A field's strips narrower than a beach and a wall each side go, and only ground whose core joins + // the home's core stays (growFarmFields): at 128x256 a strip joined to its field through an isthmus + // three tiles wide was sealed off when the walls of the coasts either side met across it, a fifth + // of the farm, so the farm-reach check failed and the lobby retried a quarter of its seeds. Opening + // by the whole rim (kFarmRim) instead would drop every strip with no crop row in it, but on a 128 map + // that is most of a farm squeezed between the mountains, and the size check below then fails. + const std::vector fields = growFarmFields(t, L.land, L.homeOf, everywhere, seeds, owners, + anchors, kFarmGap, kNeckHalf, angles); + const double homeArea = kPi * g.homeR * g.homeR; + for (int f = 0; f < 2 * teams; ++f) + { + const int k = f / 2; + std::vector region(n, 0); + int size = 0; + for (int i = 0; i < n; ++i) + if (fields[i] == f) { - seeds.push_back(L.frames[k].at(g.homeRadius, side * (g.homeReach + 2))); - anchors.push_back(L.homes[k]); + region[i] = 1; + ++size; } - const std::vector everywhere(n, 1); - const FarmPlot plot; - // Rows run across every colony's axis; fields whose rows fall on the diagonal get more ground, - // so every farm yields alike (farmYield). - std::vector owners; - std::vector angles; - for (size_t f = 0; f < seeds.size(); ++f) + if (size < kMinimumFarmShare * homeArea) { - owners.push_back(int(f) / 2); - angles.push_back(L.axis[f / 2] + kPi / 2); + L.failure = + "Too many colonies for this map: there is no room for every colony's farms."; + return L; } - const std::vector fields = growFarmFields( - t, L.land, L.homeOf, everywhere, seeds, owners, anchors, kFarmGap, kNeckHalf, angles); - const double homeArea = kPi * g.homeR * g.homeR; - for (int f = 0; f < 2 * teams; ++f) - { - const int k = f / 2; - std::vector region(n, 0); - int size = 0; - for (int i = 0; i < n; ++i) - if (fields[i] == f) - { - region[i] = 1; - ++size; - } - if (size < kMinimumFarmShare * homeArea) + const double across = L.axis[k] + kPi / 2; + TerrainSketch rows(n, GRASS); + L.farms.push_back(layFarm(rows, t, region, across, seeds[f], kFarmRim, bestFarmRows(across), + o.farmPlots ? &plot : nullptr, kFarmBridgeSpacing)); + L.farmColony.push_back(k); + for (int i = 0; i < n; ++i) + if (region[i] && L.homeOf[i] < 0) { - L.failure = - "Too many colonies for this map: there is no room for every colony's farms."; - return L; + L.farmOf[i] = k; + L.land[i] = 1; + if (L.farms.back().water[i]) + L.pond[i] = 1; + if (L.farms.back().sand[i]) + L.farmSand[i] = 1; } - const double across = L.axis[k] + kPi / 2; - TerrainSketch rows(n, GRASS); - L.farms.push_back(layFarm(rows, t, region, across, seeds[f], kFarmRim, - bestFarmRows(across), o.farmPlots ? &plot : nullptr, - kFarmBridgeSpacing)); - L.farmColony.push_back(k); - for (int i = 0; i < n; ++i) - if (region[i] && L.homeOf[i] < 0) - { - L.farmOf[i] = k; - L.land[i] = 1; - if (L.farms.back().water[i]) - L.pond[i] = 1; - if (L.farms.back().sand[i]) - L.farmSand[i] = 1; - } - } } // A road keeps well inside the land, so its sand never meets a beach. std::vector water(n, 0); @@ -787,7 +787,7 @@ GeneratorDefinition switchbacksDefinition() "switchbacks", 24, "Switchbacks", - 1, + 2, false, // The trail's width and the stone between its legs in tiles; the plateau's radius as a // share of the half side (the mountains fill the rest with as many legs as fit); each home's diff --git a/src/map/generator/shared/Farmland.cpp b/src/map/generator/shared/Farmland.cpp index 76a6dfb4d..96f81e126 100644 --- a/src/map/generator/shared/Farmland.cpp +++ b/src/map/generator/shared/Farmland.cpp @@ -14,8 +14,6 @@ namespace { // How far from its point a field's seed may move to find open ground. constexpr int kSeedSearch = 16; -// A field loses any strip narrower than twice this: the beach and wall each side would fill it. -constexpr int kOpening = 3; } // namespace FarmRows bestFarmRows(double angle) @@ -179,13 +177,12 @@ Farm layFarm(TerrainSketch &sketch, const Torus &t, const std::vector growFarmFields(const Torus &t, const std::vector &occupied, - const std::vector &homeOf, - const std::vector &area, - const std::vector &seeds, - const std::vector &owners, - const std::vector &anchors, int gap, - double neckHalfWidth, const std::vector &rowAngles) +std::vector +growFarmFields(const Torus &t, const std::vector &occupied, + const std::vector &homeOf, const std::vector &area, + const std::vector &seeds, const std::vector &owners, + const std::vector &anchors, int gap, double neckHalfWidth, + const std::vector &rowAngles, int opening) { const int n = t.size(); // Open ground: not designed, allowed, and clear of all land that is no home by the gap. Homes' @@ -257,32 +254,36 @@ std::vector growFarmFields(const Torus &t, const std::vector labels[i] = -1; } separateTerritories(t, labels, gap); - // A strip of field narrower than a beach and a wall on each side would be all sand and stone, and - // cut the ground beyond it off: every field is opened (shrunk by kOpening, then grown back as far - // within itself), which removes such strips and keeps the rest. + // A strip of field narrower than its rim on each side would be all beach, wall and cap with no crop + // row in it, and where such a strip joined the field the walls of the coasts either side would meet + // across it and seal it off. So a field keeps only the ground whose core (the field and its home + // shrunk by the opening) is joined to the home's core, grown back out over the field; that is the + // field opened, with the joining judged on the cores rather than the outlines, since two cores that + // do not touch can still overlap once grown back, through a waist their walls would close. for (size_t s = 0; s < seeds.size(); ++s) { - std::vector inside(n, 0); + std::vector inside(n, 0), home(n, 0); for (int i = 0; i < n; ++i) + { inside[i] = labels[i] == int(s) || homeOf[i] == owners[s]; - const std::vector opened = openMask(t, inside, kOpening); - for (int i = 0; i < n; ++i) - if (labels[i] == int(s) && !opened[i]) - labels[i] = -1; - } - // Trimming a field to its gaps can leave slivers cut off from the rest; a field keeps only the - // ground joined to its own home. - for (size_t s = 0; s < seeds.size(); ++s) - { - std::vector mine(n, 0), home(n, 0); + home[i] = homeOf[i] == owners[s]; + } + const std::vector core = erode(t, inside, opening); + std::vector homeCore(n, 0); + bool any = false; for (int i = 0; i < n; ++i) { - mine[i] = labels[i] == int(s) || homeOf[i] == owners[s]; - home[i] = homeOf[i] == owners[s]; + homeCore[i] = core[i] && home[i]; + any = any || homeCore[i]; } - const std::vector joined = stepsFrom(t, home, mine); + // A home too small to have a core of its own (none of the maps' homes are) joins from its whole. + const std::vector joined = stepsFrom(t, any ? homeCore : home, core); + std::vector kept(n, 0); + for (int i = 0; i < n; ++i) + kept[i] = joined[i] >= 0; + kept = dilate(t, kept, opening); for (int i = 0; i < n; ++i) - if (labels[i] == int(s) && joined[i] < 0) + if (labels[i] == int(s) && !kept[i]) labels[i] = -1; } // However the trimming left it, every field is joined to its home by a neck the full width from the diff --git a/src/map/generator/shared/Farmland.h b/src/map/generator/shared/Farmland.h index 029b9b70b..852ce4852 100644 --- a/src/map/generator/shared/Farmland.h +++ b/src/map/generator/shared/Farmland.h @@ -71,6 +71,10 @@ struct FarmPlot /// stays unbroken. constexpr double kBridgeHalfWidth = 0.75; +/// The least a field is opened by (growFarmFields): a beach and a wall each side fill any strip narrower +/// than twice this. A map with a wider rim round its rows passes that rim instead. +constexpr int kFarmOpening = 3; + /// Lays rows over `region` running at `angle` (radians), with the row at `origin` a crop row centred on /// it, and no water within `rim` steps of the region's edge, so the region's own coast (or wall) keeps a /// margin of land and every crop row joins the rim at both ends. Then, in order: @@ -93,7 +97,8 @@ Farm layFarm(TerrainSketch &sketch, const Torus &, const std::vector growFarmFields(const Torus &, const std::vector &occupied, - const std::vector &homeOf, - const std::vector &area, - const std::vector &seeds, - const std::vector &owners, - const std::vector &anchors, int gap, - double neckHalfWidth, const std::vector &rowAngles = {}); +std::vector +growFarmFields(const Torus &, const std::vector &occupied, + const std::vector &homeOf, const std::vector &area, + const std::vector &seeds, const std::vector &owners, + const std::vector &anchors, int gap, double neckHalfWidth, + const std::vector &rowAngles = {}, int opening = kFarmOpening); /// How much of a farm a colony can work: the share of the farm's crop-row grass (and the whole of its /// plot, if any, or 0 is returned) that a unit from `sources` can walk to once crops are cleared - diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 2672cde42..42d728aa0 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -184,14 +184,14 @@ macos-arm64 23 1 8 8 4 3 ok 13401851896821371429 macos-arm64 23 1 8 8 8 1 ok 5249400730010945541 macos-arm64 23 1 9 8 4 1 ok 10936074571144773422 macos-arm64 23 1 9 9 4 1 ok 16289602417876527911 -macos-arm64 24 1 7 7 4 1 ok 6532907721709105135 -macos-arm64 24 1 8 8 2 1 ok 9467112104662893497 -macos-arm64 24 1 8 8 4 1 ok 10198941300671204125 -macos-arm64 24 1 8 8 4 2 ok 384900143150519072 -macos-arm64 24 1 8 8 4 3 ok 285345205556791712 -macos-arm64 24 1 8 8 8 1 ok 12174400163941988640 -macos-arm64 24 1 9 8 4 1 ok 10026254367617799143 -macos-arm64 24 1 9 9 4 1 ok 7691831584358967898 +macos-arm64 24 2 7 7 4 1 ok 12334073435199074534 +macos-arm64 24 2 8 8 2 1 ok 9467112104662893497 +macos-arm64 24 2 8 8 4 1 ok 10730227179384458050 +macos-arm64 24 2 8 8 4 2 ok 1694352259125214876 +macos-arm64 24 2 8 8 4 3 ok 7400067849160728249 +macos-arm64 24 2 8 8 8 1 ok 12174400163941988640 +macos-arm64 24 2 9 8 4 1 ok 10026254367617799143 +macos-arm64 24 2 9 9 4 1 ok 9363167097043043462 macos-arm64 26 3 7 7 4 1 ok 7596233670176169203 macos-arm64 26 3 8 8 2 1 ok 10412697564682542969 macos-arm64 26 3 8 8 4 1 ok 15850274609968439542 From 1bbc06efc92643eb61632957721ee82d9d7362e7 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 22:29:57 -0400 Subject: [PATCH 123/136] Switchbacks: farms fill all the sea, stone lines between colonies Third play: "the farms on switchback still don't seem to expand fully into the available area ... just ensure that the farms always grow to fill the available space" and "get rid of the water channels in between bases, similar to how we had to modify carousel". The fields kept three tiles of water from each other and from the mountains; on a 256x128 map that gap, cut through fields grown into fingers, took two fifths of the open sea and the opening most of the rest. Now the fields grow with no gap, every tile of sea is filled to its nearest field (fillToNearest with the whole map as reach), and a single line of stone stands on every border between colonies (labelBorders), as on Carousel; the farms' rim drops to 3 since no coast lies beside them. Sea within a level-3 tower's range of the plateau becomes rock first, so the mountains' inner ends join round the plateau and no farm reaches it. Revision 3, 12 seeds pass at every cell from 128x128/2 to 256x256/8, golden rows regenerated. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 26 +-- .../generators/SwitchbacksGenerator.cpp | 157 +++++++++++++----- test/map-generator-golden.txt | 16 +- 3 files changed, 136 insertions(+), 63 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 0a933b3d3..ceda2cfc1 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -902,18 +902,22 @@ next leg up. Every home reaches a walled wheat farm on either flank. trail and walls with the square root of its size. - **Trail.** `zigzagPath` gives the trail and each leg's straight run; everything else in the mountain is stone. A sand road runs down the trail's middle. -- **Farms.** On maps at least 200 tiles across their shorter side (a 128-tile side has no room - between the mountains), two fields per colony, growing out of either flank of the home, share all the - open sea between the colonies by equal yield for their row angles (`growFarmFields`), joined to the - home with no coast between. Rows run across the axis at `bestFarmRows` widths (`layFarm`), with a sand - cap, a sand bridge across the water every 16 tiles and a 10x4 building plot under `farm-plots` (on); - wheat with one woodlot (`plantFarm`). Neighbouring colonies' farms meet across three tiles of water and - two sealed coasts, within a range-7 tower's reach. -- **Walls.** Every coast sealed, farms included; mountains keep water between each other beyond - their innermost legs. +- **Farms.** Every home reaches out across its axis to a field on either flank, and the fields share all + the open sea between the mountains and round the rim by equal yield for their row angles + (`growFarmFields` with no gap), joined to the home with no coast between. Then every tile of sea left + is filled to its nearest field (`fillToNearest`, the whole map as its reach), so the farms always + grow to fill the available space, and a single line of stone stands on every border between one + colony's ground and another's (`labelBorders`): nothing but stone parts two colonies, as on Carousel + (third play, 2026-09-13; before this the fields kept three tiles of water from each other and the + mountains, which on a 256x128 map took over half the sea). Rows run across the axis at `bestFarmRows` + widths (`layFarm`, rim 3), with a sand cap, a sand bridge across the water every 16 tiles and a 10x4 + building plot under `farm-plots` (on); wheat with one woodlot (`plantFarm`). +- **Walls.** The mountains' rock, the border lines between colonies, and a sealed coast on whatever sea a + design leaves (none at the defaults). Sea within a level-3 tower's range of the plateau becomes rock + before the fill, so the mountains' inner ends join round the plateau and nothing outside the trails + comes near it. - **Homes and plateau.** Round homes (`stampRoundHome`) whose farms are their water on every size of - map (128 maps had two home ponds instead until the farms' core-joined opening, 2026-09-13); a wheat - and wood kit with no stone. The plateau has a pond and only + map (128 maps had two home ponds instead until 2026-09-13); a wheat and wood kit with no stone. The plateau has a pond and only fruit, an orchard of the three fruits between every two summits, so nothing overgrows it. - **Starting towers.** `tower-count` towers (default 3) and four open pads per colony, all on its trail beside the walkway down its middle, each directly against stone on the inner side of a wall - the side diff --git a/src/map/generator/generators/SwitchbacksGenerator.cpp b/src/map/generator/generators/SwitchbacksGenerator.cpp index 04f7ce5e6..01b2cfe2a 100644 --- a/src/map/generator/generators/SwitchbacksGenerator.cpp +++ b/src/map/generator/generators/SwitchbacksGenerator.cpp @@ -39,12 +39,15 @@ using namespace MapGeneration; // attackers coming down from the plateau must pass. Every attack walks the whole zigzag under fire from // towers that ignore it. // -// THE WATER. Every coast is sealed: stone stands on every grass tile touching the sea's margin, so a -// swimmer can land on a beach but never get inside, and the trails are the only way anywhere for the -// whole game. Each home reaches two walled wheat farms, one on either flank, laid in rows that regrow -// best and shared out by equal yield (Farmland); they are the homes' water, on every size of map (128 -// maps went without, and had home ponds, until 2026-09-13). The plateau has a pond and only fruit, the prize -// every trail climbs to, with no wheat or wood to smother it. +// THE WATER. Nothing but stone parts two colonies' ground (third play, as on Carousel): each home +// reaches two wheat farms, one on either flank, laid in rows that regrow best and shared out by equal +// yield (Farmland), and the fields grow to fill every tile of sea between the mountains and round the +// rim, with a single line of stone on every border between colonies, so a defence tower behind it +// shoots across into the neighbour's rows and no unit walks through. The farms are the homes' water, on +// every size of map. Whatever sea a design leaves (none at the defaults) keeps a sealed coast, stone on +// every grass tile touching its margin, so a swimmer can land on a beach but never get inside; the +// trails are the only way anywhere for the whole game. The plateau has a pond and only fruit, the +// prize every trail climbs to, with no wheat or wood to smother it. // // HOW IT IS BUILT. Everything is designed once in the wedge's frame, along the colony's axis and across // it, and turned round the centre for every colony, so every colony's ground and trail are the same and @@ -104,17 +107,33 @@ constexpr int kTrailClearance = 6; constexpr int kRoadSeaGap = 5; // The most the colonies' walks to the plateau may differ. constexpr int kWalkSpread = 12; -// The farms: water kept between a farm and any other land, and the neck joining it to its home. -constexpr int kFarmGap = 3; +// The farms: the neck joining a farm to its home; the least share of a farm's crop land its colony must +// be able to walk to; how far apart, along a farm's rows, the sand bridges across its water rows stand; +// the land a farm keeps between its water rows and its edge (a wall or a beach, and the sand cap), and +// the extra it keeps from any open sea (a beach and its sealed coast); and the smallest farm worth +// having, as a share of the home's area. +// FEEDBACK 2026-09-13, third play: "the farms on switchback still don't seem to expand fully into the +// available area ... get rid of whatever is causing the map to keep all of this inaccessible water and +// just ensure that the farms always grow to fill the available space", and "in order to make more space +// available for the farms on switchback, can we get rid of the water channels in between bases? similar +// to how we had to modify carousel?" Before this the fields kept three tiles of water from each other +// and from the mountains, and on a 256x128 map that gap, cut through fields grown into fingers, took two +// fifths of the open sea and the fields' opening most of the rest, so the farms held under half of it. +// Now the fields grow with no gap, every tile of sea is filled to its nearest field, and a single line +// of stone stands on every border between colonies (fillAndWall); with no coast beside a farm, its rim +// need only hold the cap and the wall, as Carousel's does. constexpr double kNeckHalf = 8; -// The least share of a farm's crop land its colony must be able to walk to. constexpr double kFarmReach = 0.95; -// How far apart, along a farm's rows, the sand bridges across its water rows stand; the land a farm keeps -// between its water rows and its coast (a beach, a coast wall and the sand cap); and the smallest farm -// worth having, as a share of the home's area. constexpr int kFarmBridgeSpacing = 16; -constexpr int kFarmRim = 6; +constexpr int kFarmRim = 3; +constexpr int kFarmSeaRim = 3; constexpr double kMinimumFarmShare = 0.5; +// Sea this close to the plateau's ground becomes rock before the farms fill the rest: the channels +// between neighbouring mountains used to be sea with sealed coasts, reaching from the outside right to +// the plateau's edge, and a fill would carry the farms up them onto the plateau. With the mountains' +// inner ends joined round it, no farm comes nearer the plateau than a level-3 tower's range, so the +// plateau is still won only by the trails (its towers cover their last legs, nothing outside shoots in). +constexpr int kPlateauGuard = kTowerRange[2] + 1; // A farm is a wheat farm: wheat on this share of its crop rows at 100, and a small woodlot of this // share along one row. constexpr int kFarmWheatShare = 45; @@ -231,6 +250,7 @@ struct Layout std::vector farms; std::vector farmSand; // all the farms' sand: plot rings, bridges and caps std::vector plateau, land, pond, stone, road, roadTile; + std::vector wall; // the stone lines on the borders between colonies' ground std::vector axis; std::vector frames; // every colony's frame: along its axis from the centre, and across @@ -386,10 +406,25 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // building plot (layFarm), and every farm's coast is sealed like the rest. L.farmOf.assign(n, -1); L.farmSand.assign(n, 0); - // Farms on every size of map. They were off below a half side of 100 (128 maps got two home ponds - // instead) because a farm squeezed between the mountains kept losing a strip to its own coast walls; - // FEEDBACK 2026-09-13: "on 128x128 on switchback, for some reason the farms lands are not - // generating" - the fix is in growFarmFields (see the call below), not in going without. + // THE FARMS (growFarmFields, then the fill and the walls). Every home reaches out sideways, across + // its axis, to a field on either flank, and the fields share all the open sea between the mountains + // and round the rim by equal yield, with no gap between them or from the mountains (a mountain's + // face is rock). Then every tile of sea left over is filled to its nearest field, so the farms + // always grow to fill the available space, and a single line of stone stands on every border between + // one colony's ground and another's (labelBorders): nothing but stone parts two colonies, as on + // Carousel (FEEDBACK 2026-09-13, third play; see kFarmRim). Farms on every size of map: they were + // off below a half side of 100 (128 maps got two home ponds instead) because a farm squeezed between + // the mountains kept losing a strip to its own coast walls (FEEDBACK 2026-09-13: "on 128x128 on + // switchback, for some reason the farms lands are not generating"); with no coast beside a farm, + // that cannot happen. + // The plateau's guard (kPlateauGuard): the sea between the mountains' inner ends becomes rock. + const std::vector fromPlateau = stepsFrom(t, L.plateau); + for (int i = 0; i < n; ++i) + if (!L.land[i] && fromPlateau[i] >= 0 && fromPlateau[i] <= kPlateauGuard) + { + L.stone[i] = 1; + L.land[i] = 1; + } std::vector seeds, anchors; for (int k = 0; k < teams; ++k) for (const double side : {-1.0, 1.0}) @@ -399,8 +434,8 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } const std::vector everywhere(n, 1); const FarmPlot plot; - // Rows run across every colony's axis; fields whose rows fall on the diagonal get more ground, - // so every farm yields alike (farmYield). + // Rows run across every colony's axis; fields whose rows fall on the diagonal get more ground, so + // every farm yields alike (farmYield). std::vector owners; std::vector angles; for (size_t f = 0; f < seeds.size(); ++f) @@ -408,42 +443,66 @@ Layout design(const GenerationRequest &request, GenerationContext &context) owners.push_back(int(f) / 2); angles.push_back(L.axis[f / 2] + kPi / 2); } - // A field's strips narrower than a beach and a wall each side go, and only ground whose core joins - // the home's core stays (growFarmFields): at 128x256 a strip joined to its field through an isthmus - // three tiles wide was sealed off when the walls of the coasts either side met across it, a fifth - // of the farm, so the farm-reach check failed and the lobby retried a quarter of its seeds. Opening - // by the whole rim (kFarmRim) instead would drop every strip with no crop row in it, but on a 128 map - // that is most of a farm squeezed between the mountains, and the size check below then fails. - const std::vector fields = growFarmFields(t, L.land, L.homeOf, everywhere, seeds, owners, - anchors, kFarmGap, kNeckHalf, angles); + std::vector fields = growFarmFields(t, L.land, L.homeOf, everywhere, seeds, owners, + anchors, 0, kNeckHalf, angles); + // Every field must have grown to a worthwhile size on its own before the fill tops it up. const double homeArea = kPi * g.homeR * g.homeR; for (int f = 0; f < 2 * teams; ++f) - { - const int k = f / 2; - std::vector region(n, 0); - int size = 0; - for (int i = 0; i < n; ++i) - if (fields[i] == f) - { - region[i] = 1; - ++size; - } - if (size < kMinimumFarmShare * homeArea) + if (std::count(fields.begin(), fields.end(), f) < kMinimumFarmShare * homeArea) { L.failure = "Too many colonies for this map: there is no room for every colony's farms."; return L; } + // The fill: every tile of sea becomes part of the nearest field, however far (the reach is the whole + // map), so no water is left but the farms' own rows and the plateau's pond. Homes take no part, so + // the ground behind a home becomes rows too, not more home. A field's opening (growFarmFields) can + // have cut a finger off it; the fill hands the finger back to whichever field is nearest, which + // leaves a stub rather than a waist for the walls to close. + std::vector sea(n, 0); + for (int i = 0; i < n; ++i) + sea[i] = !L.land[i] && fields[i] < 0; + fillToNearest(t, fields, sea, t.w + t.h); + // The sides: a colony's home and both its fields are one side, and the plateau the highest of all, + // so should any farm reach it (the guard's rock keeps them off) the wall stands on the plateau's + // edge; the mountains and trails are none, since their rock parts them from everything (a trail + // meets its home through the home's own ground, so no wall stands in the door). The wall stands on + // the higher side. + std::vector side(n, -1); + for (int i = 0; i < n; ++i) + side[i] = L.homeOf[i] >= 0 ? L.homeOf[i] + : fields[i] >= 0 ? owners[fields[i]] + : L.plateau[i] ? teams + : -1; + L.wall = labelBorders(t, side); + for (int i = 0; i < n; ++i) + if (fields[i] >= 0 && L.homeOf[i] < 0) + { + L.farmOf[i] = owners[fields[i]]; + L.land[i] = 1; + } + // The rows over every field inside its walls (layFarm), across the axis at the widths that yield + // most (bestFarmRows), with sand caps, bridges and a building plot; kFarmSeaRim more from any sea + // the fill could not reach. + std::vector openSea(n, 0); + for (int i = 0; i < n; ++i) + openSea[i] = !L.land[i]; + const std::vector fromSea = stepsFrom(t, openSea); + for (int f = 0; f < 2 * teams; ++f) + { + const int k = f / 2; + std::vector region(n, 0); + for (int i = 0; i < n; ++i) + region[i] = fields[i] == f && L.homeOf[i] < 0 && !L.wall[i] && + (fromSea[i] < 0 || fromSea[i] > kFarmSeaRim); const double across = L.axis[k] + kPi / 2; TerrainSketch rows(n, GRASS); L.farms.push_back(layFarm(rows, t, region, across, seeds[f], kFarmRim, bestFarmRows(across), o.farmPlots ? &plot : nullptr, kFarmBridgeSpacing)); L.farmColony.push_back(k); for (int i = 0; i < n; ++i) - if (region[i] && L.homeOf[i] < 0) + if (region[i]) { - L.farmOf[i] = k; - L.land[i] = 1; if (L.farms.back().water[i]) L.pond[i] = 1; if (L.farms.back().sand[i]) @@ -455,6 +514,12 @@ Layout design(const GenerationRequest &request, GenerationContext &context) for (int i = 0; i < n; ++i) water[i] = !L.land[i] || L.pond[i]; keepRoadInland(L.road, t, water, kRoadSeaGap); + // ...and no road vertex touches a wall tile, since stone stands only on pure grass. + for (int i = 0; i < n; ++i) + for (int dy = -1; dy <= 0 && L.road[i]; ++dy) + for (int dx = -1; dx <= 0; ++dx) + if (L.wall[t.at(i % t.w + dx, i / t.w + dy)]) + L.road[i] = 0; L.roadTile = roadTiles(t, L.road); return L; } @@ -465,10 +530,14 @@ std::vector seaMargin(const Map &map, const Layout &L) return islandSeaMargin(map, L.t, L.pond, L.roadTile, L.farmSand); } -// The design's stone once the terrain is laid: every coast sealed, and every mountain's rock. +// The design's stone once the terrain is laid: every coast sealed, every mountain's rock and every wall +// between two colonies' ground. std::vector stoneTiles(const Map &map, const Layout &L) { - return sealedIslandStone(map, L.t, seaMargin(map, L), L.land, L.stone); + std::vector designed(L.stone); + for (int i = 0; i < L.t.size(); ++i) + designed[i] = designed[i] || L.wall[i]; + return sealedIslandStone(map, L.t, seaMargin(map, L), L.land, designed); } void furnish(Map &map, const Layout &L, GenerationContext &context, const SwitchbacksOptions &o, @@ -787,7 +856,7 @@ GeneratorDefinition switchbacksDefinition() "switchbacks", 24, "Switchbacks", - 2, + 3, false, // The trail's width and the stone between its legs in tiles; the plateau's radius as a // share of the half side (the mountains fill the rest with as many legs as fit); each home's diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 42d728aa0..0047fa56e 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -184,14 +184,14 @@ macos-arm64 23 1 8 8 4 3 ok 13401851896821371429 macos-arm64 23 1 8 8 8 1 ok 5249400730010945541 macos-arm64 23 1 9 8 4 1 ok 10936074571144773422 macos-arm64 23 1 9 9 4 1 ok 16289602417876527911 -macos-arm64 24 2 7 7 4 1 ok 12334073435199074534 -macos-arm64 24 2 8 8 2 1 ok 9467112104662893497 -macos-arm64 24 2 8 8 4 1 ok 10730227179384458050 -macos-arm64 24 2 8 8 4 2 ok 1694352259125214876 -macos-arm64 24 2 8 8 4 3 ok 7400067849160728249 -macos-arm64 24 2 8 8 8 1 ok 12174400163941988640 -macos-arm64 24 2 9 8 4 1 ok 10026254367617799143 -macos-arm64 24 2 9 9 4 1 ok 9363167097043043462 +macos-arm64 24 3 7 7 4 1 ok 8766269131759353289 +macos-arm64 24 3 8 8 2 1 ok 11694144927456181775 +macos-arm64 24 3 8 8 4 1 ok 12461145917554129793 +macos-arm64 24 3 8 8 4 2 ok 14956981799443403869 +macos-arm64 24 3 8 8 4 3 ok 16634610244346170796 +macos-arm64 24 3 8 8 8 1 ok 14595537376914576811 +macos-arm64 24 3 9 8 4 1 ok 16587253345581904688 +macos-arm64 24 3 9 9 4 1 ok 12750052710567335197 macos-arm64 26 3 7 7 4 1 ok 7596233670176169203 macos-arm64 26 3 8 8 2 1 ok 10412697564682542969 macos-arm64 26 3 8 8 4 1 ok 15850274609968439542 From da8ba4c28a10280ed9db168362f6d803f0bcc01b Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 22:42:55 -0400 Subject: [PATCH 124/136] Amphitheatre: territories by balanced power diagram, straight borders Third play: "we need to find a more reliable mechanism to divide the territory between players for amphitheatre ... wildly unsmooth borders. lets come up with something fair but also simple and reliable." The territories were grown a tile at a time by the smallest first with noise in the cost for a border-roughness control, then smoothed by a majority filter; the turn-taking grows fingers along every border and smoothing left them ragged. Territories.h gains balancedTerritories: every tile outside the wall goes to the colony whose ramp mouth is nearest by squared distance less a weight, a power diagram whose borders are straight lines, and the weights are tuned round by round (a step sized from the shortfall over the border, undone and halved when it made things worse) until the areas are within 1%. Each colony keeps a disc round its mouth whatever the balance, so a shifted border never crosses the way in. Steps and distance-from-arcs were tried first and swing instead of settling (documented in the header). The roughness control goes, with its key in all 34 tables. The validator now targets the pit tile each ramp can reach rather than the very middle, which the orchard ringed with fruit on some seeds (128x128/4 seed 1 was a golden "failed"). Revision 2; 12 seeds pass at every cell from 128/2 to 256/8. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 2 - data/texts.br.txt | 2 - data/texts.ca.txt | 2 - data/texts.cz.txt | 2 - data/texts.de.txt | 2 - data/texts.dk.txt | 2 - data/texts.en.txt | 2 - data/texts.eo.txt | 2 - data/texts.es.txt | 2 - data/texts.eu.txt | 2 - data/texts.fa.txt | 2 - data/texts.fi.txt | 2 - data/texts.fr.txt | 2 - data/texts.gr.txt | 2 - data/texts.hu.txt | 2 - data/texts.id.txt | 2 - data/texts.it.txt | 2 - data/texts.ja.txt | 2 - data/texts.keys.txt | 1 - data/texts.ko.txt | 2 - data/texts.nl.txt | 2 - data/texts.pl.txt | 2 - data/texts.pt.txt | 2 - data/texts.ro.txt | 2 - data/texts.ru.txt | 2 - data/texts.si.txt | 2 - data/texts.sk.txt | 2 - data/texts.sr.txt | 2 - data/texts.sv.txt | 2 - data/texts.tr.txt | 2 - data/texts.uk.txt | 2 - data/texts.vi.txt | 2 - data/texts.zh-cn.txt | 2 - data/texts.zh-tw.txt | 2 - .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 12 +- .../generators/AmphitheatreGenerator.cpp | 100 +++++++------ .../generators/AmphitheatreGenerator.h | 2 +- src/map/generator/shared/Territories.h | 136 +++++++++++++++++- test/map-generator-golden.txt | 16 +-- 39 files changed, 202 insertions(+), 131 deletions(-) diff --git a/data/texts.ar.txt b/data/texts.ar.txt index e419dcf57..5799c9775 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1958,8 +1958,6 @@ OpenGL غير متوفر في هذا الإصدار. حجم الحفرة [Terrace width] عرض المدرجات -[Border roughness] -تعرّج الحدود [Bay size] حجم الخلجان [Border wall] diff --git a/data/texts.br.txt b/data/texts.br.txt index f1fa2eb5c..906ade05a 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1956,8 +1956,6 @@ Ledander ar pantennoù Ment ar poull [Terrace width] Ledander ar bazennoù -[Border roughness] -Direizhder an harzoù [Bay size] Ment ar baeoù [Border wall] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index c95779823..b033d38a0 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1968,8 +1968,6 @@ Amplada de les rampes Mida del fossat [Terrace width] Amplada de les graderies -[Border roughness] -Irregularitat de les fronteres [Bay size] Mida de les badies [Border wall] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 1e16b9d7a..88b34a886 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1960,8 +1960,6 @@ Prstence Velikost jámy [Terrace width] Šířka teras -[Border roughness] -Členitost hranic [Bay size] Velikost zátok [Border wall] diff --git a/data/texts.de.txt b/data/texts.de.txt index cc75a33f5..5978cd151 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1960,8 +1960,6 @@ Rampenbreite Arenagröße [Terrace width] Terrassenbreite -[Border roughness] -Grenzverlauf [Bay size] Buchtgröße [Border wall] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index 907895df8..e6c47d99d 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2036,8 +2036,6 @@ Rampebredde Arenaens størrelse [Terrace width] Terrassebredde -[Border roughness] -Grænsernes ujævnhed [Bay size] Bugternes størrelse [Border wall] diff --git a/data/texts.en.txt b/data/texts.en.txt index 640825dcb..2e85272b9 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -1950,8 +1950,6 @@ Ramp width Pit size [Terrace width] Terrace width -[Border roughness] -Border roughness [Bay size] Bay size [Border wall] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index b593d4b05..caa5e9d95 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1958,8 +1958,6 @@ Larĝo de deklivirejoj Grando de la areno [Terrace width] Larĝo de terasoj -[Border roughness] -Malglateco de limoj [Bay size] Grando de golfetoj [Border wall] diff --git a/data/texts.es.txt b/data/texts.es.txt index ec4d91ff0..4972c19cd 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1960,8 +1960,6 @@ Ancho de las rampas Tamaño del foso [Terrace width] Ancho de las gradas -[Border roughness] -Irregularidad de las fronteras [Bay size] Tamaño de las bahías [Border wall] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 59e9cbecb..199b5069b 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1970,8 +1970,6 @@ Arrapalen zabalera Zuloaren tamaina [Terrace width] Harmailen zabalera -[Border roughness] -Mugen zimurtasuna [Bay size] Badien tamaina [Border wall] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index ee8412d81..713d6c21d 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1958,8 +1958,6 @@ OpenGL در این بیلد موجود نیست. اندازه گودال [Terrace width] پهنای سکوها -[Border roughness] -ناهمواری مرزها [Bay size] اندازه خلیج‌ها [Border wall] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 71c17f8e5..d3d25e9eb 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1960,8 +1960,6 @@ Ramppien leveys Areenan koko [Terrace width] Terassien leveys -[Border roughness] -Rajojen rosoisuus [Bay size] Lahtien koko [Border wall] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 72d30df54..62f3c5c0e 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1970,8 +1970,6 @@ Largeur des rampes Taille de l'arène [Terrace width] Largeur des gradins -[Border roughness] -Irrégularité des frontières [Bay size] Taille des baies [Border wall] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 61953bffd..e7d9d2de3 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2030,8 +2030,6 @@ Cortex Μέγεθος αρένας [Terrace width] Πλάτος κερκίδων -[Border roughness] -Ανωμαλία συνόρων [Bay size] Μέγεθος κόλπων [Border wall] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 68f8ed932..e621affd6 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1960,8 +1960,6 @@ Rámpák szélessége Aréna mérete [Terrace width] Teraszok szélessége -[Border roughness] -Határok szabálytalansága [Bay size] Öblök mérete [Border wall] diff --git a/data/texts.id.txt b/data/texts.id.txt index 58096a959..79aee3b12 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1956,8 +1956,6 @@ Lebar tanjakan Ukuran arena [Terrace width] Lebar teras -[Border roughness] -Kekasaran batas [Bay size] Ukuran teluk [Border wall] diff --git a/data/texts.it.txt b/data/texts.it.txt index 5cd714349..00b8a5e94 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2022,8 +2022,6 @@ Larghezza delle rampe Dimensione della fossa [Terrace width] Larghezza delle gradinate -[Border roughness] -Irregolarità dei confini [Bay size] Dimensione delle baie [Border wall] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index d082088d7..75fa48c48 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1956,8 +1956,6 @@ Globulation 2 のカーソルを表示します。 闘技場の大きさ [Terrace width] 段の幅 -[Border roughness] -境界の起伏 [Bay size] 入り江の大きさ [Border wall] diff --git a/data/texts.keys.txt b/data/texts.keys.txt index da6383868..6a4ec152d 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -973,7 +973,6 @@ [Ramp width] [Pit size] [Terrace width] -[Border roughness] [Bay size] [Border wall] [Plateau size] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 556531a9b..27649d9cd 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1956,8 +1956,6 @@ Globulation 2 커서를 표시합니다. 경기장 크기 [Terrace width] 계단식 단 너비 -[Border roughness] -경계의 굴곡 [Bay size] 만 크기 [Border wall] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index ad1e797e5..6a095dd41 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1984,8 +1984,6 @@ Hellingbreedte Grootte van de arena [Terrace width] Terrasbreedte -[Border roughness] -Grillige grenzen [Bay size] Grootte van de baaien [Border wall] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 402c315ee..69226f2d7 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1960,8 +1960,6 @@ Szerokość ramp Rozmiar areny [Terrace width] Szerokość tarasów -[Border roughness] -Nieregularność granic [Bay size] Rozmiar zatok [Border wall] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index f3bbf3bd7..73c27b5d1 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1966,8 +1966,6 @@ Largura das rampas Tamanho da arena [Terrace width] Largura das bancadas -[Border roughness] -Irregularidade das fronteiras [Bay size] Tamanho das baías [Border wall] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index ddac9dae4..36456f693 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1960,8 +1960,6 @@ Lățimea rampelor Mărimea arenei [Terrace width] Lățimea teraselor -[Border roughness] -Neregularitatea granițelor [Bay size] Mărimea golfurilor [Border wall] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 2aea148fa..cd8618af7 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1958,8 +1958,6 @@ OpenGL недоступен в этой сборке. Размер арены [Terrace width] Ширина террас -[Border roughness] -Изрезанность границ [Bay size] Размер заливов [Border wall] diff --git a/data/texts.si.txt b/data/texts.si.txt index 813b81e03..1bccab6e7 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1960,8 +1960,6 @@ Obroči Velikost jame [Terrace width] Širina teras -[Border roughness] -Razgibanost meja [Bay size] Velikost zalivov [Border wall] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 8913d5011..299b31434 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1968,8 +1968,6 @@ Prstence Veľkosť jamy [Terrace width] Šírka terás -[Border roughness] -Členitosť hraníc [Bay size] Veľkosť zálivov [Border wall] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 1499b421e..40eda4153 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1960,8 +1960,6 @@ OpenGL није доступан у овој верзији. Величина јаме [Terrace width] Ширина тераса -[Border roughness] -Разуђеност граница [Bay size] Величина залива [Border wall] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index a3db9758f..1e17df9ea 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1970,8 +1970,6 @@ Rampbredd Arenans storlek [Terrace width] Terrassbredd -[Border roughness] -Gränsernas ojämnhet [Bay size] Vikarnas storlek [Border wall] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index f8827dfd3..c090d65ec 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1968,8 +1968,6 @@ Rampa genişliği Arena boyutu [Terrace width] Teras genişliği -[Border roughness] -Sınır düzensizliği [Bay size] Koy boyutu [Border wall] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index dd79c3e00..e92fa4285 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1956,8 +1956,6 @@ OpenGL недоступний у цій збірці. Розмір арени [Terrace width] Ширина терас -[Border roughness] -Порізаність кордонів [Bay size] Розмір заток [Border wall] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index aed0b3428..b4c9e45d3 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1956,8 +1956,6 @@ Số vòng Kích thước hố [Terrace width] Độ rộng bậc -[Border roughness] -Độ gồ ghề của biên giới [Bay size] Kích thước vịnh [Border wall] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index e5a050b2e..6f218ad70 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1956,8 +1956,6 @@ OpenGL 在此版本中不可用。 竞技场大小 [Terrace width] 台阶宽度 -[Border roughness] -边界起伏 [Bay size] 海湾大小 [Border wall] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index c894e0be7..a4923edb1 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2032,8 +2032,6 @@ OpenGL 在此版本中不可用。 競技場大小 [Terrace width] 台階寬度 -[Border roughness] -邊界起伏 [Bay size] 海灣大小 [Border wall] diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index ceda2cfc1..e6f62ae0a 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -861,12 +861,12 @@ is a meeting at a known place. - **Arena.** `rings` walls `terrace-width` apart round a pit of `pit-size`, drawn with `ringWithGates`; ramps `ramp-width` wide alternate between each colony's axis and the middle of its wedge. -- **Territories.** The ground outside the outer wall is shared out by `growTerritories` from every - colony's frontage (its wedge's arc just outside the wall), with `territory-roughness` noise in the - cost, then smoothed by four passes of `smoothLabels` over a radius of four, so every border runs in - a clean curve. This handles a square map's corners, the wrap and odd colony counts, which wedges - cannot. Borders become stone (`labelBorders`, `border-wall` thick), and any unclaimed tile becomes - stone. The areas come out within a few percent of each other. +- **Territories.** The ground outside the outer wall is shared out by `balancedTerritories` from every + colony's frontage (its wedge's arc just outside the wall): every tile goes to the colony whose frontage + is nearest by steps less a weight per colony, and the weights are tuned until the areas are equal, so + every border is a smooth curve of equal weighted distance between two frontages. (Until 2026-09-13 + the territories were grown a tile at a time with `territory-roughness` noise and smoothed by a + majority filter, which left every border ragged; the control is gone.) - **Homes.** Every swarm stands the same number of steps from its ramp (`siteAtDepth`), four tenths of the way to the shallowest territory's far end; the wheat and wood kit (no stone clump: the territory is walled in stone) faces the nearest sea; scattered fields by `furnishGround`. diff --git a/src/map/generator/generators/AmphitheatreGenerator.cpp b/src/map/generator/generators/AmphitheatreGenerator.cpp index 767082039..0d4bfc4cc 100644 --- a/src/map/generator/generators/AmphitheatreGenerator.cpp +++ b/src/map/generator/generators/AmphitheatreGenerator.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -46,13 +47,20 @@ using namespace MapGeneration; // // The arena is designed round the centre and turned for every colony, like a wedge design. The // territories cannot be: a square map's corners and the ground across its wrap belong to no wedge, -// and with an odd number of colonies the corners fall unevenly. So the territories are grown instead -// (growTerritories): every colony's frontage on the outer wall is its own arc of equal length, and -// from there the smallest territory always takes the next tile, until the whole of the ground outside -// the walls is shared out to the tile. Every colony's seas are then grown to the same number of tiles, -// and every swarm stands the same number of steps from its ramp, so what the growth cannot make identical is -// measured and held equal instead. The design is a pure function of the request, so validateWorld -// rebuilds it and checks the finished world. +// and with an odd number of colonies the corners fall unevenly. So the territories are shared out by +// distance instead (balancedTerritories): every tile outside belongs to the colony whose ramp mouth is +// nearest by squared distance less a weight per colony, and the weights are tuned until the areas are +// equal, so every border is a straight line, perpendicular to the line between two ramps and shifted +// towards the colony with the more ground to spare. FEEDBACK 2026-09-13 (third play): "we need to +// find a more reliable mechanism to divide the territory between players for amphitheatre. because +// whatever our current mechanism is, it is causing wildly unsmooth borders. lets come up with something +// fair but also simple and reliable." Until then the territories were grown a tile at a time by the +// smallest first (growTerritories) with noise in the cost for a "border roughness" control, then +// smoothed by a majority filter: the turn-taking grows fingers along every border, the noise more, and +// four passes of smoothing left them ragged. Every colony's seas are then grown to the same number of +// tiles, and every swarm stands the same number of steps from its ramp, so what the division cannot +// make identical is measured and held equal instead. The design is a pure function of the request, so +// validateWorld rebuilds it and checks the finished world. // // GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be // cleared, so the walls and the ramps are permanent; a unit may step diagonally, which is why every @@ -75,12 +83,15 @@ constexpr double kRingHalf = 1.2; constexpr double kMinimumTerrace = 8; // Ground kept outside the outer wall before the map's edge, per colony at least this many tiles. constexpr int kMinimumTerritory = 2000; -// The majority filter that smooths the territories' borders into curves: passes, and the radius of the -// square each tile looks at. -constexpr int kSmoothingPasses = 4; -constexpr int kSmoothingRadius = 4; -// The territory control's noise: a border wanders by up to this many steps at 100. -constexpr double kRoughSteps = 24; +// The territories' balance (balancedTerritories): rounds of weight tuning, and how near an equal share +// every area must come, a fifth of what the validator allows (kAreaTolerance). +constexpr int kBalanceRounds = 80; +constexpr double kBalanceTolerance = 0.01; +// The ground every colony keeps round its ramp mouth whatever the balance says: on a 2:1 map the +// colonies on the short sides start with half the ground and their borders shift far, and without this +// a border crossed a neighbour's mouth (colony 0 could not walk to the pit on a third of the seeds). +// As deep as a swarm stands from its mouth (kHomeRampDepth), so the way in is always the colony's own. +constexpr double kSiteKeep = 14; // An inland sea keeps this many steps from any wall, so its beach never reaches stone and its // neighbours' seas are always parted by land. constexpr int kBayWallGap = 7; @@ -236,29 +247,22 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } } - // The territories (growTerritories): every colony's frontage is the ground just outside the outer - // wall across its own wedge, and from there the ground outside is shared out to the tile, then - // smoothed (smoothLabels) so the stone along every border runs in a clean curve. - const WedgeFrame wedges(t, L.phase - g.wedge / 2, teams); - std::vector outside(n, 0), frontage(n, 0); - std::vector> seeds(teams); + // The territories (balancedTerritories): the ground outside the outer wall is shared out by weighted + // squared distance from every colony's ramp mouth, the weights tuned until the areas are equal, so + // every border is a straight line of stone (third play; see the header). + std::vector outside(n, 0); for (int i = 0; i < n; ++i) - { outside[i] = L.zone[i] == g.rings; - const WedgeFrame::Cell cell = wedges.cell(i % t.w, i / t.w); - if (outside[i] && cell.d <= g.outer + kRingHalf + 2) - { - seeds[cell.k].push_back(i); - frontage[i] = 1; - } + // Every colony's site is the middle of its outer ramp's mouth, just outside the wall, so the border + // between two neighbours starts on the wall half way between their ramps and runs straight out. + std::vector sites; + for (int k = 0; k < teams; ++k) + { + const ShapePoint p = polarPoint(L.cx, L.cy, g.outer + kRingHalf + 2, L.phase + g.wedge * k); + sites.push_back(t.at(int(std::lround(p.x)), int(std::lround(p.y)))); } - const PeriodicNoise wander(t.w, t.h, std::max(16, g.half / 4), - context.stream("amphitheatre-borders")); - const double swing = kRoughSteps * 1000 * o.roughness / 100.0; - L.territory = growTerritories(t, outside, seeds, - [&](int i) { return long(wander.at(i % t.w, i / t.w) * swing); }) - .labels; - smoothLabels(t, L.territory, kSmoothingPasses, frontage, kSmoothingRadius); + L.territory = + balancedTerritories(t, outside, sites, kSiteKeep, kBalanceRounds, kBalanceTolerance).labels; L.areas.assign(teams, 0); for (int i = 0; i < n; ++i) if (L.territory[i] >= 0) @@ -665,11 +669,25 @@ std::string validateWorld(const Game &game, const GenerationContext &context) const int s = seedNear(t, tile % t.w, tile / t.w, 4, [&](int i) { return open[i] != 0; }); return s >= 0 ? s : tile; }; + // Every colony's target in the pit is the open tile of the pit's middle disc (the disc the roads + // stage clears a way to, 0.4 of the pit's radius) that its ramp reaches in the fewest steps. The + // very middle will not do: the orchard's fruit can ring it until eaten, and targeting it failed + // every colony's walk on some seeds (128x128 with four colonies, seed 1) whatever the territories. + std::vector pitDisc(n, 0); + for (int i = 0; i < n; ++i) + pitDisc[i] = + L.zone[i] == 0 && open[i] && + std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) < 0.4 * g.pitR; std::vector ramps, pits; for (int k = 0; k < teams; ++k) { ramps.push_back(nearestOpen(L.rampMiddle[k])); - pits.push_back(nearestOpen(L.pitMiddle)); + const std::vector fromRamp = stepsFrom(t, tileMask(t, {ramps.back()}), open); + int best = -1; + for (int i = 0; i < n; ++i) + if (pitDisc[i] && fromRamp[i] >= 0 && (best < 0 || fromRamp[i] < fromRamp[best])) + best = i; + pits.push_back(best >= 0 ? best : nearestOpen(L.pitMiddle)); } for (const auto &[targets, what] : {std::pair{ramps, "ramps"}, std::pair{pits, "the pit"}}) { @@ -686,12 +704,11 @@ std::string validateWorld(const Game &game, const GenerationContext &context) AmphitheatreOptions::AmphitheatreOptions(const GenerationRequest &r) : rings(r.option("rings")), rampWidth(r.option("ramp-width")), pitSize(r.option("pit-size")), - terraceWidth(r.option("terrace-width")), roughness(r.option("territory-roughness")), - baySize(r.option("bay-size")), borderWall(r.option("border-wall")), - towers(r.option("starting-towers")), towerCount(r.option("tower-count")), - wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), - stone(r.option("stone-amount")), algae(r.option("algae-amount")), - fruit(r.option("fruit-amount")) + terraceWidth(r.option("terrace-width")), baySize(r.option("bay-size")), + borderWall(r.option("border-wall")), towers(r.option("starting-towers")), + towerCount(r.option("tower-count")), wheat(r.option("wheat-amount")), + wood(r.option("wood-amount")), stone(r.option("stone-amount")), + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) { } @@ -701,7 +718,7 @@ GeneratorDefinition amphitheatreDefinition() "amphitheatre", 23, "Amphitheatre", - 1, + 2, false, // Rings of wall; each ramp's width in tiles; the pit's radius and each terrace's width as // shares of the half side; how far the territories' borders wander; every colony's inland seas @@ -710,7 +727,6 @@ GeneratorDefinition amphitheatreDefinition() {"ramp-width", "Ramp width", 5, 11, 2, 7, ControlGroup::Terrain}, {"pit-size", "Pit size", 8, 30, 2, 16, ControlGroup::Layout}, {"terrace-width", "Terrace width", 6, 20, 1, 14, ControlGroup::Layout}, - {"territory-roughness", "Border roughness", 0, 100, 10, 50, ControlGroup::Terrain}, {"bay-size", "Bay size", 6, 24, 2, 14, ControlGroup::Terrain}, {"border-wall", "Border wall", 1, 3, 1, 2, ControlGroup::Terrain}, // The towers every colony starts with, all against its walls: their level (0 for none, just diff --git a/src/map/generator/generators/AmphitheatreGenerator.h b/src/map/generator/generators/AmphitheatreGenerator.h index e5d9f0726..480bc24da 100644 --- a/src/map/generator/generators/AmphitheatreGenerator.h +++ b/src/map/generator/generators/AmphitheatreGenerator.h @@ -4,7 +4,7 @@ #include "GeneratorDefinition.h" struct AmphitheatreOptions { - int rings, rampWidth, pitSize, terraceWidth, roughness, baySize, borderWall, towers, towerCount; + int rings, rampWidth, pitSize, terraceWidth, baySize, borderWall, towers, towerCount; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit AmphitheatreOptions(const GenerationRequest &r); }; diff --git a/src/map/generator/shared/Territories.h b/src/map/generator/shared/Territories.h index 0676635b4..d466cca5b 100644 --- a/src/map/generator/shared/Territories.h +++ b/src/map/generator/shared/Territories.h @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -77,7 +79,8 @@ Territories growTerritories(const Torus &t, const std::vector &el for (const auto &s : kCardinalSteps) { const int next = t.at(tile % t.w + s[0], tile / t.w + s[1]); - if (eligible[next] && result.labels[next] < 0 && !queued[k][next] && steps[k][next] >= 0) + if (eligible[next] && result.labels[next] < 0 && !queued[k][next] && + steps[k][next] >= 0) { queued[k][next] = 1; frontier[k].push({std::int64_t(steps[k][next]) * 1000 + cost(next), next}); @@ -130,6 +133,123 @@ Territories growTerritories(const Torus &t, const std::vector &el } return result; } +/// Territories shared out from one site per claimant with every border a straight line: a tile belongs +/// to the claimant whose squared straight-line distance from its site (the short way round the wrap) +/// less that claimant's weight is least, ties to the lowest claimant - a power diagram, whose borders +/// are lines perpendicular to the segment between two sites, shifted by the weights. The weights are +/// tuned round by round until every area is within `tolerance` of an equal share, or `rounds` are up: +/// a claimant with too little ground gains weight and its borders move out, one with too much loses +/// it, each by the step that would mend the shortfall if its borders moved alone (the shortfall over +/// the border's length, times twice the distance to the nearest other site, since a border moves one +/// tile for that much weight), halved, and a round that made the areas less equal is undone and the +/// step halved. Nothing is random and nothing takes turns, which is what tells it from growTerritories: +/// there the smallest claimant takes the next tile wherever its frontier is cheapest, so a border grows +/// fingers that a wall then follows (FEEDBACK 2026-09-13, Amphitheatre: "wildly unsmooth borders ... +/// something fair but also simple and reliable"). Two other forms were tried first and could not be +/// balanced: distance in steps, whose Chebyshev "bisector" is a whole band of equidistant tiles, and +/// distance from a claimant's whole frontage arc, where every tile beyond two touching arcs is nearly +/// equidistant from both, so either flips thousands of tiles for a weight one tile different. The +/// distance is straight, not walked: it is for claimants whose sites face their ground with nothing in +/// between (Amphitheatre's ramp mouths round the arena). Every claimant keeps the eligible ground within +/// `keep` tiles of its site whatever the weights say, so a border shifted far by the balance (a 2:1 +/// map gives the colonies on its short sides much less ground to start with) never crosses the way in +/// from a site; the wall then rounds that disc and carries on straight. Eligible ground is all claimed. +inline Territories balancedTerritories(const Torus &t, const std::vector &eligible, + const std::vector &sites, double keep = 0, + int rounds = 80, double tolerance = 0.01) +{ + const int n = t.w * t.h, claimants = int(sites.size()); + Territories result; + result.labels.assign(n, -1); + result.areas.assign(claimants, 0); + if (claimants == 0) + return result; + std::vector> d2(claimants); + std::vector own(n, -1); // the ground kept round each site, the lowest claimant's on a tie + for (int k = 0; k < claimants; ++k) + { + d2[k] = distanceSquaredTo(t, tileMask(t, {sites[k]})); + for (int i = 0; i < n; ++i) + if (own[i] < 0 && double(d2[k][i]) <= keep * keep) + own[i] = k; + } + // Twice the distance from each site to the nearest other: the weight that moves a border a tile. + std::vector reach(claimants, 1.0); + for (int k = 0; k < claimants; ++k) + { + double nearest = -1; + for (int j = 0; j < claimants; ++j) + if (j != k && (nearest < 0 || d2[j][sites[k]] < nearest)) + nearest = double(d2[j][sites[k]]); + reach[k] = nearest > 0 ? 2 * std::sqrt(nearest) : 1.0; + } + int total = 0; + for (int i = 0; i < n; ++i) + total += eligible[i] != 0; + const double target = double(total) / claimants; + std::vector weight(claimants, 0.0); + const auto assign = [&](const std::vector &w) + { + std::fill(result.areas.begin(), result.areas.end(), 0); + for (int i = 0; i < n; ++i) + { + result.labels[i] = -1; + if (!eligible[i]) + continue; + double best = 0; + if (own[i] >= 0) + result.labels[i] = own[i]; + else + for (int k = 0; k < claimants; ++k) + { + const double value = double(d2[k][i]) - w[k]; + if (result.labels[i] < 0 || value < best) + { + result.labels[i] = k; + best = value; + } + } + ++result.areas[result.labels[i]]; + } + double error = 0; + for (int k = 0; k < claimants; ++k) + error = std::max(error, std::abs(result.areas[k] - target)); + return error; + }; + double error = assign(weight), gain = 0.5; + for (int round = 0; round < rounds && error > tolerance * target; ++round) + { + std::vector border(claimants, 0); + for (int y = 0; y < t.h; ++y) + for (int x = 0; x < t.w; ++x) + { + const int k = result.labels[y * t.w + x]; + if (k < 0) + continue; + bool edge = false; + for (const auto &s : kCardinalSteps) + { + const int other = result.labels[t.at(x + s[0], y + s[1])]; + edge = edge || (other >= 0 && other != k); + } + border[k] += edge; + } + std::vector tried(weight); + for (int k = 0; k < claimants; ++k) + tried[k] += gain * (target - result.areas[k]) / std::max(1, border[k]) * reach[k]; + const double after = assign(tried); + if (after < error) + { + weight = tried; + error = after; + } + else + gain *= 0.5; + } + assign(weight); + return result; +} + /// Smooths territories' borders, which a race for the cheapest tile leaves ragged: for `passes` /// passes, every labelled tile whose neighbourhood - the square `radius` tiles round it - is more than /// `share` another territory's takes that label, all at once per pass. A radius of 1 trims single-tile @@ -209,7 +329,8 @@ inline void separateTerritories(const Torus &t, std::vector &labels, int ga /// fill that water in and part them with a wall on the border instead (labelBorders). Returns the /// tiles filled. inline std::vector fillToNearest(const Torus &t, std::vector &labels, - const std::vector &eligible, int reach) + const std::vector &eligible, + int reach) { std::vector filled(t.size(), 0); std::vector steps(t.size(), -1), frontier, next; @@ -294,7 +415,8 @@ int growFarLake(const Torus &t, std::vector &water, const std::ve [&](int i) { const double d = std::sqrt(double(t.dist2(seed % t.w, seed / t.w, i % t.w, i / t.w))); - return std::int64_t(d * 1000) + (far - depth[i]) * 250LL + std::int64_t(noiseAt(i) * 2500); + return std::int64_t(d * 1000) + (far - depth[i]) * 250LL + + std::int64_t(noiseAt(i) * 2500); }, queued, stamp); } @@ -311,12 +433,12 @@ int growFarLake(const Torus &t, std::vector &water, const std::ve /// 0 when that side has no room. template int growLakeBeside(const Torus &t, std::vector &water, const std::vector &depth, - const std::vector &room, int gap, int target, int site, double heading, int side, - int siteGap, int reach, NoiseAt noiseAt, std::vector &queued, int stamp) + const std::vector &room, int gap, int target, int site, double heading, + int side, int siteGap, int reach, NoiseAt noiseAt, std::vector &queued, + int stamp) { const int n = t.size(), sx = site % t.w, sy = site / t.w; - const auto away = [&](int i) - { return std::sqrt(double(t.dist2(sx, sy, i % t.w, i / t.w))); }; + const auto away = [&](int i) { return std::sqrt(double(t.dist2(sx, sy, i % t.w, i / t.w))); }; const double hx = std::cos(heading), hy = std::sin(heading); // How far a tile lies to the left of the line through the site along the heading (right is // negative). The whole lake keeps to its side, clear of the line by half the site gap, so it never diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 0047fa56e..d40e81f2f 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -176,14 +176,14 @@ macos-arm64 22 1 8 8 4 3 ok 16156515469674000849 macos-arm64 22 1 8 8 8 1 ok 12707724408115629147 macos-arm64 22 1 9 8 4 1 ok 16181991639738054527 macos-arm64 22 1 9 9 4 1 ok 18170981972027515569 -macos-arm64 23 1 7 7 4 1 failed 0 -macos-arm64 23 1 8 8 2 1 ok 11310442957433887598 -macos-arm64 23 1 8 8 4 1 ok 7375768657791499996 -macos-arm64 23 1 8 8 4 2 ok 1481389391399968114 -macos-arm64 23 1 8 8 4 3 ok 13401851896821371429 -macos-arm64 23 1 8 8 8 1 ok 5249400730010945541 -macos-arm64 23 1 9 8 4 1 ok 10936074571144773422 -macos-arm64 23 1 9 9 4 1 ok 16289602417876527911 +macos-arm64 23 2 7 7 4 1 ok 549496627108919023 +macos-arm64 23 2 8 8 2 1 ok 10411914600145665080 +macos-arm64 23 2 8 8 4 1 ok 10630205222293629395 +macos-arm64 23 2 8 8 4 2 ok 3769887846353985102 +macos-arm64 23 2 8 8 4 3 ok 14438238323646079445 +macos-arm64 23 2 8 8 8 1 ok 7486873941404335262 +macos-arm64 23 2 9 8 4 1 ok 9501696698924728828 +macos-arm64 23 2 9 9 4 1 ok 8928207243182979823 macos-arm64 24 3 7 7 4 1 ok 8766269131759353289 macos-arm64 24 3 8 8 2 1 ok 11694144927456181775 macos-arm64 24 3 8 8 4 1 ok 12461145917554129793 From 987d25d820aa360c44007afd78cb4462b725a70c Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 22:49:31 -0400 Subject: [PATCH 125/136] Canals: fourteen block kinds, forts and fields beside every pad Second play: "too many of them are empty of resources. like with the 4x4 squares - those squares are to hold farming buildings. they only matter if there are resources elsewhere on that island outside the sand square. can we also add some types that are more like forts? with some stone that guides enemies into a kill zone or something?" and "a few of them should be intended for building buildings on of course. but too many of them are boring right now." The pad kinds are now homesteads and hamlets with a wheat and a wood clump beside every pad; plain blocks fall from 24 in 100 to 6; and five kinds built of stone or water join, each dealt a facing: a fort (a 13-tile square of wall round a pad with one three-tile gate), a bastion (four corner walls, an opening in every side), a funnel (two walls in a V onto a three-tile gap, the pad behind it), a chicane (two staggered walls with a corridor between) and a moat (a ring of water round an islet with a pad and one sand causeway). Walls are drawn in the block's frame only where the warp left pure grass clear of water, and a block whose walls would cut its land or a bridge off gets none (blockWalls). Every pad and built kind carries the ambient fields too (15/10 in 100 of fertile tiles), a tile clear of the walls. Validator checks every wall's stone and every moat. Revision 4; 12 seeds pass at 128/2 to 512/4. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 19 +- .../generator/generators/CanalsGenerator.cpp | 383 ++++++++++++++++-- test/map-generator-golden.txt | 16 +- 3 files changed, 370 insertions(+), 48 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index e6f62ae0a..238f1decd 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -1035,11 +1035,20 @@ opening; once swimming pools are built every canal is a road. straight canal w corners wide puts the banks' grass w + 4 apart (`Channels`), so 3 is reached by a level-2 tower and not a level-1. - **Homes and block kinds.** The colonies' blocks are the ones farthest apart on the block graph - (`spreadPockets`), each with a kit and no pond (the first play asked for the pond gone; the canal - waters the block). Every other block is dealt one of nine kinds from a weighted draw (24/12/9/15/8/ - 8/8/8/8 in 100): plain fields; a lake; an orchard round a small pond; a 4x4 pad of grass in a ring - of sand (a building spot nothing grows onto, `stampFarmPlot`); two such pads; a quarry; a woodlot; - a wheatfield; a dune of bare sand. "Each cell has its own little surprise." Fairness is statistical. + (`spreadPockets`), dealt to the colonies at random, each with a kit and no pond (the first play asked + for the pond gone; the canal waters the block). Every other block is dealt one of fourteen kinds and a + facing from a weighted draw (6/9/8/10/6/5/7/7/4/9/8/7/6/8 in 100): plain fields; a lake; an orchard + round a small pond; a homestead (a 4x4 pad of grass in a ring of sand, `stampFarmPlot`, with a wheat + and a wood clump beside it); a hamlet of two pads; a quarry; a woodlot; a wheatfield; a dune of bare + sand; and five kinds built of stone or water (second play, 2026-09-13: "too many of them are empty of + resources ... add some types that are more like forts, with some stone that guides enemies into a + kill zone"): a fort (a 13-tile square of wall round a pad with one three-tile gate), a bastion (four + corner walls, an opening in every side), a funnel (two walls in a V onto a three-tile gap with the pad + behind it), a chicane (two staggered walls with a corridor between) and a moat (a ring of water round + an islet with a pad and one sand causeway). Walls are drawn in the block's frame, only where the warp + left pure grass clear of water, and a block whose walls would cut its land or a bridge off gets none + (`blockWalls`). Every pad kind and built kind carries the ambient fields too, a tile clear of its + walls. "Each cell has its own little surprise." Fairness is statistical. - **Bridges.** A tree of shortest block-to-block paths from the first colony's block to every other colony's, so every colony can be walked to, then `extra-bridges` percent (0-100, 30) of the blocks' count more at random (`openLoops`), so most blocks stay islands until someone swims. A bridge is a line of sand diff --git a/src/map/generator/generators/CanalsGenerator.cpp b/src/map/generator/generators/CanalsGenerator.cpp index 1dd149281..3e3c74a28 100644 --- a/src/map/generator/generators/CanalsGenerator.cpp +++ b/src/map/generator/generators/CanalsGenerator.cpp @@ -18,9 +18,11 @@ #include "Settlements.h" #include "Sketch.h" #include "Tessellation.h" +#include "Topology.h" #include "Towers.h" #include #include +#include #include #include using namespace MapGeneration; @@ -53,9 +55,10 @@ using namespace MapGeneration; // resources, which can be used as a building spot. maybe a few of them have a sand patch. come up // with a few more cool ideas; each cell has its own little surprise." So: home blocks have no pond // (their canal waters them), extra bridges default to 30% (was 20) and warp to 80 (was 40), and -// every other block is dealt one of nine kinds (BlockKind) from a weighted draw: plain fields, a -// lake, an orchard round a pond, a 4x4 building pad in a ring of sand, two such pads, a quarry, a -// woodlot, a wheatfield, or a dune of bare sand. +// every other block is dealt one of fourteen kinds (BlockKind) from a weighted draw, and a facing: +// plain fields, a lake, an orchard round a pond, a homestead or a hamlet (building pads with wheat and +// wood beside them), a quarry, a woodlot, a wheatfield, a dune of bare sand, and five kinds built of +// stone or water (second play): a fort, a bastion, a funnel, a chicane and a moat. namespace { @@ -66,27 +69,62 @@ constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; // this far past the canal's edge onto each bank so it lands on grass, not beach. constexpr double kBridgeLanding = 2.5; // What a block holds (first play: "each cell has its own little surprise"), and the weight each kind -// is dealt with, out of 100. Plain blocks carry the ambient fields; the rest carry one thing each. +// is dealt with, out of 100. Second play, FEEDBACK 2026-09-13: "i like the new square types you +// added, but too many of them are empty of resources. like with the 4x4 squares - those squares are to +// hold farming buildings. they only matter if there are resources elsewhere on that island outside the +// sand square. can we also add some types that are more like forts? with some stone that guides +// enemies into a kill zone or something? just add some cool ideas like this to make the cells more +// interesting", and "a few of them should be intended for building buildings on of course. but too +// many of them are boring right now". So the pad kinds became homesteads and hamlets (a wheat and a +// wood clump beside every pad, and the ambient fields over the rest of the block), plain blocks went +// from 24 in 100 to 6, and five built kinds joined, each with a clump of wheat and one of wood outside +// its stone or water and the ambient fields too, so what the stone guards is worth guarding. enum BlockKind { Home = -1, Plain = 0, Lake, // a pond at the middle, the fields round it Orchard, // a small pond with the three fruits round it - Pad, // a 4x4 pad of grass in a ring of sand: a building spot nothing grows onto - TwoPads, // two such pads + Homestead, // a 4x4 pad of grass in a ring of sand, a wheat and a wood clump either side of it + Hamlet, // two such pads side by side, the clumps before and behind them Quarry, // a stone clump of radius 3 Woodlot, // wood over most of the block Wheatfield, // wheat over most of the block - Dune // a disc of bare sand: walkable, unbuildable, and nothing grows on it + Dune, // a disc of bare sand: walkable, unbuildable, and nothing grows on it + Fort, // a square of stone wall round a pad with one gate at the front, the kill zone + Bastion, // four corner walls round a pad, an opening in the middle of every side + Funnel, // two walls in a V that guide a walk onto a three-tile gap, the pad just behind it + Chicane, // two staggered walls with a corridor between them, the way across the block + Moat // a ring of water round an islet with a pad, one causeway of sand onto it }; -constexpr int kKinds = 9; -constexpr int kKindWeight[kKinds] = {24, 12, 9, 15, 8, 8, 8, 8, 8}; -// The kinds' features: the lake's and the orchard pond's radii, the pad (4 tiles square in a ring -// one corner wide; stampFarmPlot), how far apart two pads stand, the quarry's radius, the dune's +constexpr int kKinds = 14; +constexpr int kKindWeight[kKinds] = {6, 9, 8, 10, 6, 5, 7, 7, 4, 9, 8, 7, 6, 8}; +// The kinds' features: the lake's and the orchard pond's radii, the pad (4 tiles square in a ring one +// corner wide; stampFarmPlot), how far apart a hamlet's two pads stand, the quarry's radius, the dune's // radius, and the share of a woodlot or wheatfield under its crop. constexpr double kLakeRadius = 3.5, kOrchardPond = 2.5, kDuneRadius = 4.0; constexpr int kPadSize = 4, kPadRing = 1, kPadsApart = 10, kQuarryRadius = 3, kCoverPercent = 60; +// The built kinds, in tiles from the block's middle in the block's own frame (every block faces one of +// four ways, dealt with its kind; localTile). A wall's reach is the half block less kWallMargin (a +// beach, a bridge's landing and a lane round the wall), at most kWallReach: 6 on the default block of +// 24, so a fort is 13 tiles square, its wall a tile beyond the pad's sand ring. A fort's gate and the +// funnel's gap are kGateWidth tiles; a bastion's corner arms are two short of the half side, leaving +// kGateWidth + 2 open in the middle of every side; the funnel's walls run diagonally in from the back +// corners, two tiles thick because a unit steps diagonally between two stones on a diagonal; the +// chicane's walls stand kChicaneOffset either side of the middle, each reaching kChicaneOverlap past it +// from its own side, so the corridor between them is entered at one end and left at the other. Any wall +// tile the warp has put within two of water, on sand, or off its block is left out, and a block whose +// walls would cut its land or a bridge off (blockWalls) gets no walls and the pad kind instead. A moat +// is kMoatWidth corners of water (two tiles of water, which only a swimmer crosses) round an islet of +// kMoatOuter - kMoatWidth in radius, exactly a pad, its ring and its beach; it needs a block of +// kMoatMinimumBlock with every corner within kMoatOuter + 1.5 of the middle on the block, else it is +// dealt as a lake. Beside every pad kind stand a wheat clump of kClumpRadius (a homestead's; the others +// one smaller) and a wood clump one smaller, kClumpOut from the middle or just outside a built kind's +// stone or water, so the pad has something to farm. +constexpr int kWallReach = 6, kWallMargin = 4, kGateWidth = 3, kChicaneOffset = 3, + kChicaneOverlap = 2; +constexpr double kMoatOuter = 7.5, kMoatWidth = 3.0; +constexpr int kMoatMinimumBlock = 22, kClumpRadius = 3, kClumpOut = 6; // Tower pads beside each colony's starting towers, and the spacing between a colony's sites. constexpr int kTowerPads = 2, kTowerSpacing = 6; @@ -96,8 +134,10 @@ struct Layout Tessellation g; std::vector cell; // every tile's block std::vector homeCell; - std::vector kind; // every block's BlockKind - std::vector homeOf; // the home block a tile is in, or -1 + std::vector kind; // every block's BlockKind + std::vector facing; // every block's quarter turn (0-3) for its built kind + std::vector stone; // the built kinds' walls, as tiles + std::vector homeOf; // the home block a tile is in, or -1 std::vector canal, bridge, water, sand, land; Farm pads; // the building pads, stamped as farm plots TerrainSketch sketch; @@ -112,6 +152,148 @@ ShapePoint tilePoint(SubtilePoint p) return {p.x / 16.0, p.y / 16.0}; } +// A tile at (u, v) in a block's frame: from its middle, turned a quarter turn per facing. +int localTile(const Layout &L, int cell, int u, int v) +{ + const ShapePoint c = tilePoint(L.g.cells[cell].centre); + const int cx = int(std::lround(c.x)), cy = int(std::lround(c.y)); + int dx = u, dy = v; + switch (L.facing[cell]) + { + case 1: + dx = -v; + dy = u; + break; + case 2: + dx = -u; + dy = -v; + break; + case 3: + dx = v; + dy = -u; + break; + default: + break; + } + return L.t.at(cx + dx, cy + dy); +} + +// How far a built kind's walls reach from the block's middle. +int wallReach(const CanalsOptions &o) +{ + return std::min(kWallReach, o.blockSize / 2 - kWallMargin); +} + +// THE WALLS of a built kind, as the tiles of the block they may stand on: on the block, clear of water +// by two corners all round (so the beaches leave them pure grass and nothing stands on a bridge's +// landing), and off any pad. The block must stay one piece with them in - its land and its bridges, +// eight ways, as a unit walks - else the block gets no walls and `fallback` instead. +void blockWalls(Layout &L, int cell, int reach) +{ + const Torus &t = L.t; + const int n = t.size(); + const auto wallable = [&](int i) + { + if (L.cell[i] != cell || L.canal[i] || L.pads.plot[i]) + return false; + const int x = i % t.w, y = i / t.w; + for (int dy = -1; dy <= 2; ++dy) + for (int dx = -1; dx <= 2; ++dx) + { + const int j = t.at(x + dx, y + dy); + if (L.sketch[j] == WATER) + return false; + if (dx >= 0 && dx <= 1 && dy >= 0 && dy <= 1 && L.sketch[j] != GRASS) + return false; + } + return true; + }; + std::set tiles; + const auto wall = [&](int u, int v) + { + const int i = localTile(L, cell, u, v); + if (wallable(i)) + tiles.insert(i); + }; + const int half = kGateWidth / 2; + switch (L.kind[cell]) + { + case Fort: + for (int k = -reach; k <= reach; ++k) + { + wall(k, -reach); + wall(-reach, k); + wall(reach, k); + if (std::abs(k) > half) + wall(k, reach); + } + break; + case Bastion: + for (int k = 0; k < reach - 2; ++k) + for (const int su : {-1, 1}) + for (const int sv : {-1, 1}) + { + wall(su * reach, sv * (reach - k)); + wall(su * (reach - k), sv * reach); + } + break; + case Funnel: + for (int k = 0; k <= reach - 3; ++k) + { + wall(-reach + k, -reach + k); + wall(-reach + k + 1, -reach + k); + wall(reach - k, -reach + k); + wall(reach - k - 1, -reach + k); + } + break; + case Chicane: + for (int u = -reach; u <= kChicaneOverlap; ++u) + wall(u, -kChicaneOffset); + for (int u = -kChicaneOverlap; u <= reach; ++u) + wall(u, kChicaneOffset); + break; + default: + return; + } + if (tiles.empty()) + return; + std::vector walk(n, 0); + for (int i = 0; i < n; ++i) + { + const int x = i % t.w, y = i / t.w; + const bool water = L.sketch[i] == WATER && L.sketch[t.at(x + 1, y)] == WATER && + L.sketch[t.at(x, y + 1)] == WATER && + L.sketch[t.at(x + 1, y + 1)] == WATER; + walk[i] = (L.cell[i] == cell && !water) || (L.bridge[i] && L.canal[i]); + } + for (int i : tiles) + walk[i] = 0; + const std::vector region = connectedRegions(walk, t.w, t.h, true, GridNeighbors::Eight); + std::vector size; + for (int i = 0; i < n; ++i) + if (walk[i] && region[i] >= 0) + { + if (region[i] >= int(size.size())) + size.resize(region[i] + 1, 0); + ++size[region[i]]; + } + // Bridges of other blocks make their own small regions; only pieces of this block's land count. + int pieces = 0; + for (int i = 0; i < n; ++i) + if (walk[i] && L.cell[i] == cell && region[i] >= 0 && size[region[i]] > 0) + { + ++pieces; + size[region[i]] = 0; + } + if (pieces > 1) + { + L.kind[cell] = L.kind[cell] == Chicane ? Plain : Homestead; + return; + } + for (int i : tiles) + L.stone[i] = 1; +} + Layout design(const GenerationRequest &request, GenerationContext &context) { const CanalsOptions o(request); @@ -156,6 +338,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // Every other block's kind, dealt from a weighted draw in block order (first play: "each cell // has its own little surprise"). L.kind.assign(L.g.cellCount(), Plain); + L.facing.assign(L.g.cellCount(), 0); for (int cell : L.homeCell) L.kind[cell] = Home; for (int cell = 0; cell < L.g.cellCount(); ++cell) @@ -165,7 +348,11 @@ Layout design(const GenerationRequest &request, GenerationContext &context) int roll = int(context.bounded("canals-kinds", 100)), kind = 0; while (kind + 1 < kKinds && roll >= kKindWeight[kind]) roll -= kKindWeight[kind++]; + // A moat needs a block big enough for its ring; a smaller block's moat is a lake. + if (kind == Moat && o.blockSize < kMoatMinimumBlock) + kind = Lake; L.kind[cell] = kind; + L.facing[cell] = int(context.bounded("canals-facing", 4)); } // The canals: every edge stroked canalWidth corners wide. A canal's water is canalWidth - 1 tiles @@ -246,46 +433,92 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (L.cell[i] == L.homeCell[k] && L.land[i]) L.homeOf[i] = k; } - // Every block's terrain feature, at its middle: a lake, an orchard's pond, a pad or two of grass - // in a ring of sand (stamped as farm plots into the sketch), or a dune. + // Every block's water and sand feature, at its middle: a lake, an orchard's pond, a dune, or a + // moat (a ring of water with one causeway of sand corners across it, at the block's front; a moat + // whose ring would not lie wholly on its block is a lake). const RadialShape lake(kLakeRadius, 0.3, context, "canals-lakes"); const RadialShape orchardPond(kOrchardPond, 0.3, context, "canals-lakes"); const RadialShape dune(kDuneRadius, 0.3, context, "canals-dunes"); - const FarmPlot pad{kPadSize, kPadSize, kPadRing}; + const RadialShape moatRing(kMoatOuter, 0.0, context, "canals-moats"); + const RadialShape islet(kMoatOuter - kMoatWidth, 0.0, context, "canals-moats"); for (int cell = 0; cell < L.g.cellCount(); ++cell) { const ShapePoint c = tilePoint(L.g.cells[cell].centre); + const int cx = int(std::lround(c.x)), cy = int(std::lround(c.y)); + if (L.kind[cell] == Moat) + { + bool fits = true; + const int span = int(std::ceil(kMoatOuter + 1.5)); + for (int dy = -span; dy <= span && fits; ++dy) + for (int dx = -span; dx <= span && fits; ++dx) + if (std::hypot(dx, dy) < kMoatOuter + 1.5) + { + const int i = t.at(cx + dx, cy + dy); + fits = L.cell[i] == cell && !L.canal[i]; + } + if (!fits) + L.kind[cell] = Lake; + } if (L.kind[cell] == Lake) fillShape(L.water, t, c.x, c.y, lake, 0.0); else if (L.kind[cell] == Orchard) fillShape(L.water, t, c.x, c.y, orchardPond, 0.0); else if (L.kind[cell] == Dune) fillShape(L.sand, t, c.x, c.y, dune, 0.0); + else if (L.kind[cell] == Moat) + { + // Round the rounded middle, as the pad will be, so the islet holds it exactly. + fillShape(L.water, t, cx, cy, moatRing, 0.0); + fillShape(L.water, t, cx, cy, islet, 0.0, 0); + for (int v = int(kMoatOuter - kMoatWidth) - 1; v <= int(kMoatOuter) + 1; ++v) + { + const int i = localTile(L, cell, 0, v); + L.water[i] = 0; + L.sand[i] = 1; + } + } } L.sketch.assign(n, GRASS); for (int i = 0; i < n; ++i) L.sketch[i] = L.water[i] ? WATER : (L.sand[i] || (L.bridge[i] && L.canal[i])) ? SAND : GRASS; + // The pads (stampFarmPlot): at the middle of a homestead, fort, bastion or moat, two side by side + // on a hamlet, and just behind the gap of a funnel. + const FarmPlot pad{kPadSize, kPadSize, kPadRing}; L.pads.water = L.water; L.pads.sand.assign(n, 0); L.pads.plot.assign(n, 0); L.pads.row.assign(n, -1); - for (int cell = 0; cell < L.g.cellCount(); ++cell) + const auto stampPad = [&](int cell, int u, int v) { - const ShapePoint c = tilePoint(L.g.cells[cell].centre); - const int cx = int(std::lround(c.x)), cy = int(std::lround(c.y)); - if (L.kind[cell] == Pad) - stampFarmPlot(L.sketch, t, L.pads, cx - kPadSize / 2, cy - kPadSize / 2, pad); - else if (L.kind[cell] == TwoPads) + const int i = localTile(L, cell, u, v); + stampFarmPlot(L.sketch, t, L.pads, i % t.w - kPadSize / 2, i / t.w - kPadSize / 2, pad); + }; + for (int cell = 0; cell < L.g.cellCount(); ++cell) + switch (L.kind[cell]) { - stampFarmPlot(L.sketch, t, L.pads, cx - kPadsApart / 2 - kPadSize / 2, - cy - kPadSize / 2, pad); - stampFarmPlot(L.sketch, t, L.pads, cx + kPadsApart / 2 - kPadSize / 2, - cy - kPadSize / 2, pad); + case Homestead: + case Fort: + case Bastion: + case Moat: + stampPad(cell, 0, 0); + break; + case Hamlet: + stampPad(cell, -kPadsApart / 2, 0); + stampPad(cell, kPadsApart / 2, 0); + break; + case Funnel: + stampPad(cell, 0, kPadSize / 2); + break; + default: + break; } - } L.water = L.pads.water; + // The walls (blockWalls), once the pads' sand is in the sketch, so none stands on it. + L.stone.assign(n, 0); + for (int cell = 0; cell < L.g.cellCount(); ++cell) + blockWalls(L, cell, wallReach(o)); return L; } @@ -342,6 +575,11 @@ bool generate(Game &game, GenerationContext &context) if (L.bridge[i] && L.canal[i]) terrain[i] = SAND; writeUndermap(map, terrain); + // The built kinds' walls: stone on every designed wall tile (all pure grass, by the design's own + // check), before anything else is placed. + for (int i = 0; i < n; ++i) + if (L.stone[i] && map.isGrass(i % t.w, i / t.w)) + map.setResource(i % t.w, i / t.w, STONE, 1); context.stage = "canals colonies"; const auto homeMask = [&](int team) @@ -381,10 +619,14 @@ bool generate(Game &game, GenerationContext &context) clearGround(map, i % t.w, i / t.w); }); // Every block is fertile (its canal is within the growth probe's reach of all of it), so the - // ambient farmland is a modest share of the plain, lake and home blocks, in patches; the other - // kinds carry their own thing instead, and the bridges' landings stay clear so no field closes - // a bridge. + // ambient farmland is a share of every block but the kinds that carry their own cover (orchard, + // quarry, woodlot, wheatfield, dune), in patches: the pad kinds and the built kinds too, since a pad + // only matters with something to farm beside it (second play), 15 and 10 in 100 of the fertile tiles + // for wheat and wood (12 and 8 while only plain, lake and home blocks had them). The bridges' + // landings stay clear so no field closes a bridge, and a tile's width round every wall, so the + // stone stays in plain view. const std::vector landings = dilate(t, bridges, 2); + const std::vector wallMargin = dilate(t, L.stone, 1); const Fertility::Field fertility = Fertility::forMap(map, false); const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("canals-patch")); const std::vector split = periodicNoise(t.w, t.h, 5, context.stream("canals-split")); @@ -396,7 +638,8 @@ bool generate(Game &game, GenerationContext &context) const auto eligible = [&](int i) { const int kind = L.kind[L.cell[i]]; - return open(i) && (kind == Plain || kind == Lake || kind == Home); + return open(i) && !wallMargin[i] && kind != Orchard && kind != Quarry && kind != Woodlot && + kind != Wheatfield && kind != Dune; }; int fertile = 0; for (int i = 0; i < n; ++i) @@ -406,13 +649,17 @@ bool generate(Game &game, GenerationContext &context) [&](int i) { return split[i]; }, [&](int area) { - return GroundAmounts{int(scaledCount(fertile * 12 / 100, o.wheat)), - int(scaledCount(fertile * 8 / 100, o.wood)), + return GroundAmounts{int(scaledCount(fertile * 15 / 100, o.wheat)), + int(scaledCount(fertile * 10 / 100, o.wood)), int(scaledCount(area / 900, o.stone)), 0}; }, "canals-stone", "canals-fruit"); // Every block's own thing (first play): the three fruits round an orchard's pond, a quarry's - // stone, a woodlot's or wheatfield's cover on the block's most-noise share of ground. + // stone, a woodlot's or wheatfield's cover on the block's most-noise share of ground; and (second + // play) a wheat clump and a wood clump beside every pad, in the block's frame, outside its stone or + // water: a homestead's before and behind the pad, a hamlet's before and behind its pair, a fort's + // behind and beside it (never before the gate), a bastion's at two corners, a funnel's either side of + // the pad inside the V, a chicane's outside both walls, a moat's either side of the ring. const std::vector cover = periodicNoise(t.w, t.h, 6, context.stream("canals-cover")); for (int cell = 0; cell < L.g.cellCount(); ++cell) { @@ -435,6 +682,57 @@ bool generate(Game &game, GenerationContext &context) placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), STONE, kQuarryRadius); break; + case Homestead: + case Hamlet: + case Fort: + case Bastion: + case Funnel: + case Chicane: + case Moat: + { + struct Spot + { + int u, v, type, radius; + }; + const int r = wallReach(o), m = int(kMoatOuter) + 2, small = kClumpRadius - 1; + std::vector spots; + switch (L.kind[cell]) + { + case Homestead: + spots = {{0, -kClumpOut, CORN, kClumpRadius}, {0, kClumpOut, WOOD, small}}; + break; + case Hamlet: + spots = {{0, -(kClumpOut - 1), CORN, small}, {0, kClumpOut - 1, WOOD, small}}; + break; + case Fort: + spots = {{0, -(r + 2), CORN, small}, {-(r + 2), 0, WOOD, small}}; + break; + case Bastion: + spots = {{-(r + 2), -(r + 2), CORN, small}, {r + 2, r + 2, WOOD, small}}; + break; + case Funnel: + spots = {{-r, 3, CORN, small}, {r, 3, WOOD, small}}; + break; + case Chicane: + spots = {{0, -kClumpOut, CORN, small}, {0, kClumpOut, WOOD, small}}; + break; + default: + spots = {{-m, 0, CORN, small}, {m, 0, WOOD, small}}; + break; + } + for (const Spot &spot : spots) + { + if (scaledCount(1, spot.type == CORN ? o.wheat : o.wood) <= 0) + continue; + const int at = localTile(L, cell, spot.u, spot.v); + if (const int seed = seedNear(t, at % t.w, at / t.w, 3, + [&](int i) { return inBlock(i) && !wallMargin[i]; }); + seed >= 0) + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), + spot.type, spot.radius); + } + break; + } case Woodlot: case Wheatfield: { @@ -475,6 +773,21 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (const std::string mismatch = designMismatch(L, map, "canals"); !mismatch.empty()) return mismatch; const Torus &t = L.t; + const int n = t.size(); + // Every built kind's wall stands, and every moat holds its water round a dry islet. + for (int i = 0; i < n; ++i) + if (L.stone[i] && map.getResource(i % t.w, i / t.w).type != STONE) + return "A wall's stone is missing at (" + std::to_string(i % t.w) + ", " + + std::to_string(i / t.w) + ")."; + for (int cell = 0; cell < L.g.cellCount(); ++cell) + if (L.kind[cell] == Moat) + { + const int ring = localTile(L, cell, 0, -int(kMoatOuter - 1.5)); + const int middle = localTile(L, cell, 0, 0); + if (!map.isWater(ring % t.w, ring / t.w) || map.isWater(middle % t.w, middle / t.w)) + return "A moat block has lost its moat at (" + std::to_string(middle % t.w) + ", " + + std::to_string(middle / t.w) + ")."; + } // Home blocks are dry by design (first play); every lake block keeps its lake. for (int cell = 0; cell < L.g.cellCount(); ++cell) if (L.kind[cell] == Lake) @@ -504,7 +817,7 @@ GeneratorDefinition canalsDefinition() "canals", 29, "Canals", - 3, + 4, false, // Blocks of 24 give a 256 map about a hundred blocks; a canal of 3 corners (two tiles of // water) is sealed against diagonal steps and is reached by a level-2 tower, which is what diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index d40e81f2f..5c5859805 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -216,14 +216,14 @@ macos-arm64 28 4 8 8 4 3 ok 13102395041856171672 macos-arm64 28 4 8 8 8 1 ok 14091742024861043395 macos-arm64 28 4 9 8 4 1 ok 8120863798265800623 macos-arm64 28 4 9 9 4 1 ok 4370189412371078302 -macos-arm64 29 3 7 7 4 1 ok 1513765199421365488 -macos-arm64 29 3 8 8 2 1 ok 11827857063654687224 -macos-arm64 29 3 8 8 4 1 ok 10145298994600649149 -macos-arm64 29 3 8 8 4 2 ok 2534114081112037916 -macos-arm64 29 3 8 8 4 3 ok 4867298262581307149 -macos-arm64 29 3 8 8 8 1 ok 7706014024180394011 -macos-arm64 29 3 9 8 4 1 ok 17327182427130708652 -macos-arm64 29 3 9 9 4 1 ok 16102946102284687512 +macos-arm64 29 4 7 7 4 1 ok 4232995500160826297 +macos-arm64 29 4 8 8 2 1 ok 7399419839946439559 +macos-arm64 29 4 8 8 4 1 ok 9472114427354905765 +macos-arm64 29 4 8 8 4 2 ok 15267437393931662669 +macos-arm64 29 4 8 8 4 3 ok 16695674954699155816 +macos-arm64 29 4 8 8 8 1 ok 13911889789453582543 +macos-arm64 29 4 9 8 4 1 ok 7057480006238666143 +macos-arm64 29 4 9 9 4 1 ok 6519639175542525008 macos-arm64 30 3 7 7 4 1 ok 6823664725837608399 macos-arm64 30 3 8 8 2 1 ok 18262511519283505011 macos-arm64 30 3 8 8 4 1 ok 18435202348722167438 From 385de4b23530f5e898c13be11060bb9af81dfc81 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Sun, 13 Sep 2026 23:01:57 -0400 Subject: [PATCH 126/136] Map generators: cleanup pass, byte-identical A self-review of the generators built and revised this week, with the golden table unchanged (248 rows, 0 failures) as the proof that no map moved. Boilerplate every designed generator copied goes into the toolkit: designFailure (the registry's request check), and in Pipeline.h settleRoundColonies with homeGrassMask (round homes settled on their own grass) and homePondMissing (the validators' pond check); nearestSiteDistance (Orbits.h) replaces four copies of the same loop and tilePoint (Tessellation.h) two. Anthill keeps one list of chamber kinds (ChamberKind) instead of computing it in design and again in generate. Stale comments corrected: Switchbacks' pre-fill farm paragraph and "water between mountains" wording, Old town's wall (removed after the first play), Rain shadow's stream sizes, Old growth's pool-ring distance, Canals' "market blocks" that never existed, Polder's and Anthill's superseded defaults, Amphitheatre's removed roughness control; debug includes dropped. The four control keys left by the deleted Marches and Patchwork generators go from all 32 tables that held them, and the docs name the new helpers. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 8 --- data/texts.br.txt | 8 --- data/texts.ca.txt | 8 --- data/texts.cz.txt | 8 --- data/texts.de.txt | 8 --- data/texts.dk.txt | 8 --- data/texts.eo.txt | 8 --- data/texts.es.txt | 8 --- data/texts.eu.txt | 8 --- data/texts.fa.txt | 8 --- data/texts.fi.txt | 8 --- data/texts.fr.txt | 8 --- data/texts.gr.txt | 8 --- data/texts.hu.txt | 8 --- data/texts.id.txt | 8 --- data/texts.it.txt | 8 --- data/texts.ja.txt | 8 --- data/texts.ko.txt | 8 --- data/texts.nl.txt | 8 --- data/texts.pl.txt | 8 --- data/texts.pt.txt | 8 --- data/texts.ro.txt | 8 --- data/texts.ru.txt | 8 --- data/texts.si.txt | 8 --- data/texts.sk.txt | 8 --- data/texts.sr.txt | 8 --- data/texts.sv.txt | 8 --- data/texts.tr.txt | 8 --- data/texts.uk.txt | 8 --- data/texts.vi.txt | 8 --- data/texts.zh-cn.txt | 8 --- data/texts.zh-tw.txt | 8 --- docs/map-generators/ADDING_A_GENERATOR.md | 4 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 4 +- .../generators/AmphitheatreGenerator.cpp | 9 ++- .../generator/generators/AnthillGenerator.cpp | 57 ++++++++++--------- .../generator/generators/CanalsGenerator.cpp | 32 +++-------- .../generators/FingerprintGenerator.cpp | 36 +++--------- .../generators/OldGrowthGenerator.cpp | 39 +++---------- .../generator/generators/OldTownGenerator.cpp | 39 ++++--------- .../generator/generators/PolderGenerator.cpp | 31 +++------- .../generators/RainShadowGenerator.cpp | 44 ++++---------- .../generators/SwitchbacksGenerator.cpp | 25 ++++---- src/map/generator/shared/Orbits.h | 14 +++++ src/map/generator/shared/Pipeline.cpp | 35 ++++++++++++ src/map/generator/shared/Pipeline.h | 27 +++++++++ src/map/generator/shared/Territories.h | 2 - src/map/generator/shared/Tessellation.h | 6 ++ 48 files changed, 183 insertions(+), 477 deletions(-) diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 5799c9775..3ab8498a8 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -1790,20 +1790,12 @@ OpenGL غير متوفر في هذا الإصدار. حجم البحيرة [Contested commons] الأراضي المشاع المتنازع عليها -[Lattice] -الشبكة [Maze] المتاهة [Fjord continent] قارة المضائق البحرية -[Islet size] -حجم الجزيرة الصغيرة -[Home radius] -نصف قطر القاعدة [Cell size] حجم الخلية -[Room size] -حجم الغرفة [Corridor width] عرض الممر [Spoke width] diff --git a/data/texts.br.txt b/data/texts.br.txt index 906ade05a..829896af9 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1788,20 +1788,12 @@ Largura da ponte de terra Tamanho do lago [Contested commons] Terras comuns disputadas -[Lattice] -Malha [Maze] Labirinto [Fjord continent] Continente de fiordes -[Islet size] -Tamanho da ilhota -[Home radius] -Raio da área inicial [Cell size] Tamanho da célula -[Room size] -Tamanho da sala [Corridor width] Largura do corredor [Spoke width] diff --git a/data/texts.ca.txt b/data/texts.ca.txt index b033d38a0..f45002f76 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -1800,20 +1800,12 @@ Amplada del pont de terra Mida del llac [Contested commons] Terres comunals disputades -[Lattice] -Retícula [Maze] Laberint [Fjord continent] Continent de fiords -[Islet size] -Mida de l'illot -[Home radius] -Radi de l'àrea inicial [Cell size] Mida de la cel·la -[Room size] -Mida de la sala [Corridor width] Amplada del passadís [Spoke width] diff --git a/data/texts.cz.txt b/data/texts.cz.txt index 88b34a886..f7ff9a1ff 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -1792,20 +1792,12 @@ Automaticky zobrazovat přehled mapy ve tvaru torusu při pohybu po mapě (OpenG Velikost jezera [Contested commons] Sporné společné území -[Lattice] -Mřížka [Maze] Bludiště [Fjord continent] Fjordový kontinent -[Islet size] -Velikost ostrůvku -[Home radius] -Poloměr domovské oblasti [Cell size] Velikost buňky -[Room size] -Velikost místnosti [Corridor width] Šířka chodby [Spoke width] diff --git a/data/texts.de.txt b/data/texts.de.txt index 5978cd151..8e040ba0b 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -1792,20 +1792,12 @@ Breite der Landbrücke Seegröße [Contested commons] Umkämpftes Gemeinschaftsland -[Lattice] -Gitternetz [Maze] Labyrinth [Fjord continent] Fjordkontinent -[Islet size] -Inselchengröße -[Home radius] -Radius des Startgebiets [Cell size] Zellengröße -[Room size] -Raumgröße [Corridor width] Korridorbreite [Spoke width] diff --git a/data/texts.dk.txt b/data/texts.dk.txt index e6c47d99d..fc05a7c52 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -1868,20 +1868,12 @@ Landbroens bredde Søstørrelse [Contested commons] Omstridt fællesområde -[Lattice] -Gitter [Maze] Labyrint [Fjord continent] Fjordkontinent -[Islet size] -Holmstørrelse -[Home radius] -Radius for startområde [Cell size] Cellestørrelse -[Room size] -Rumstørrelse [Corridor width] Korridorbredde [Spoke width] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index caa5e9d95..dd7bd9aea 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -1790,20 +1790,12 @@ Larĝo de terponto Grandeco de lago [Contested commons] Pridisputata komunareo -[Lattice] -Krado [Maze] Labirinto [Fjord continent] Fjorda kontinento -[Islet size] -Grandeco de insuleto -[Home radius] -Radiuso de hejmareo [Cell size] Grandeco de ĉelo -[Room size] -Grandeco de ĉambro [Corridor width] Larĝo de koridoro [Spoke width] diff --git a/data/texts.es.txt b/data/texts.es.txt index 4972c19cd..1e4cc90c6 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -1792,20 +1792,12 @@ Anchura del puente terrestre Tamaño del lago [Contested commons] Tierras comunes disputadas -[Lattice] -Retícula [Maze] Laberinto [Fjord continent] Continente de fiordos -[Islet size] -Tamaño del islote -[Home radius] -Radio del área inicial [Cell size] Tamaño de la celda -[Room size] -Tamaño de la sala [Corridor width] Anchura del pasillo [Spoke width] diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 199b5069b..69606556c 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -1802,20 +1802,12 @@ Lur-zubiaren zabalera Aintziraren tamaina [Contested commons] Lur komun lehiatuak -[Lattice] -Sareta [Maze] Labirintoa [Fjord continent] Fiordoen kontinentea -[Islet size] -Uharte txikiaren tamaina -[Home radius] -Hasierako eremuaren erradioa [Cell size] Gelaxkaren tamaina -[Room size] -Gelaren tamaina [Corridor width] Korridorearen zabalera [Spoke width] diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 713d6c21d..e706bdb75 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -1790,20 +1790,12 @@ OpenGL در این بیلد موجود نیست. اندازهٔ دریاچه [Contested commons] سرزمین مشترک مورد مناقشه -[Lattice] -شبکه [Maze] هزارتو [Fjord continent] قارهٔ آبدره‌ای -[Islet size] -اندازهٔ جزیرهٔ کوچک -[Home radius] -شعاع پایگاه [Cell size] اندازهٔ سلول -[Room size] -اندازهٔ اتاق [Corridor width] پهنای راهرو [Spoke width] diff --git a/data/texts.fi.txt b/data/texts.fi.txt index d3d25e9eb..55af16d0d 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -1792,20 +1792,12 @@ Maasillan leveys Järven koko [Contested commons] Kiistelty yhteisalue -[Lattice] -Ruudukko [Maze] Sokkelo [Fjord continent] Vuonomainen manner -[Islet size] -Luodon koko -[Home radius] -Kotialueen säde [Cell size] Solun koko -[Room size] -Huoneen koko [Corridor width] Käytävän leveys [Spoke width] diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 62f3c5c0e..0ed6d6431 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -1802,20 +1802,12 @@ Largeur du pont terrestre Taille des lacs [Contested commons] Terres communes disputées -[Lattice] -Quadrillage [Maze] Labyrinthe [Fjord continent] Continent à fjords -[Islet size] -Taille de l'îlot -[Home radius] -Rayon de la zone de départ [Cell size] Taille des cellules -[Room size] -Taille des salles [Corridor width] Largeur des couloirs [Spoke width] diff --git a/data/texts.gr.txt b/data/texts.gr.txt index e7d9d2de3..b2495bba8 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -1862,20 +1862,12 @@ Cortex Μέγεθος λίμνης [Contested commons] Διεκδικούμενη κοινή γη -[Lattice] -Πλέγμα [Maze] Λαβύρινθος [Fjord continent] Ήπειρος με φιόρδ -[Islet size] -Μέγεθος νησίδας -[Home radius] -Ακτίνα αρχικής περιοχής [Cell size] Μέγεθος κελιού -[Room size] -Μέγεθος δωματίου [Corridor width] Πλάτος διαδρόμου [Spoke width] diff --git a/data/texts.hu.txt b/data/texts.hu.txt index e621affd6..17a6c1dad 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -1792,20 +1792,12 @@ Földhíd szélessége Tó mérete [Contested commons] Vitatott közös terület -[Lattice] -Rácsozat [Maze] Labirintus [Fjord continent] Fjordos kontinens -[Islet size] -Szigetecske mérete -[Home radius] -Kezdőterület sugara [Cell size] Cella mérete -[Room size] -Szoba mérete [Corridor width] Folyosó szélessége [Spoke width] diff --git a/data/texts.id.txt b/data/texts.id.txt index 79aee3b12..80f62cf1a 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -1788,20 +1788,12 @@ Lebar jembatan darat Ukuran danau [Contested commons] Tanah bersama yang diperebutkan -[Lattice] -Kisi-kisi [Maze] Labirin [Fjord continent] Benua fjord -[Islet size] -Ukuran pulau kecil -[Home radius] -Radius markas [Cell size] Ukuran sel -[Room size] -Ukuran ruangan [Corridor width] Lebar koridor [Spoke width] diff --git a/data/texts.it.txt b/data/texts.it.txt index 00b8a5e94..fe2396cec 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -1854,20 +1854,12 @@ Larghezza del ponte terrestre Dimensione del lago [Contested commons] Territori comuni contesi -[Lattice] -Reticolo [Maze] Labirinto [Fjord continent] Continente dei fiordi -[Islet size] -Dimensione dell'isolotto -[Home radius] -Raggio dell'area iniziale [Cell size] Dimensione delle celle -[Room size] -Dimensione delle stanze [Corridor width] Larghezza dei corridoi [Spoke width] diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 75fa48c48..47471c59e 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -1788,20 +1788,12 @@ Globulation 2 のカーソルを表示します。 湖の大きさ [Contested commons] 争奪共有地 -[Lattice] -格子 [Maze] 迷路 [Fjord continent] フィヨルド大陸 -[Islet size] -小島の大きさ -[Home radius] -拠点の半径 [Cell size] マスの大きさ -[Room size] -部屋の大きさ [Corridor width] 通路の幅 [Spoke width] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 27649d9cd..e879cce6f 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -1788,20 +1788,12 @@ Globulation 2 커서를 표시합니다. 호수 크기 [Contested commons] 쟁탈 공유지 -[Lattice] -격자 [Maze] 미로 [Fjord continent] 피오르 대륙 -[Islet size] -작은 섬 크기 -[Home radius] -기지 반경 [Cell size] 칸 크기 -[Room size] -방 크기 [Corridor width] 통로 너비 [Spoke width] diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 6a095dd41..b25ec59be 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -1816,20 +1816,12 @@ Breedte van de landbrug Meergrootte [Contested commons] Betwist gemeenschappelijk gebied -[Lattice] -Raster [Maze] Doolhof [Fjord continent] Fjordencontinent -[Islet size] -Grootte van het eilandje -[Home radius] -Straal van het startgebied [Cell size] Celgrootte -[Room size] -Kamergrootte [Corridor width] Gangbreedte [Spoke width] diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 69226f2d7..ce9e31e23 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -1792,20 +1792,12 @@ Szerokość mostu lądowego Wielkość jeziora [Contested commons] Sporny teren wspólny -[Lattice] -Siatka [Maze] Labirynt [Fjord continent] Kontynent fiordowy -[Islet size] -Rozmiar wysepki -[Home radius] -Promień obszaru startowego [Cell size] Rozmiar komórki -[Room size] -Rozmiar pomieszczenia [Corridor width] Szerokość korytarza [Spoke width] diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 73c27b5d1..41591ac28 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -1798,20 +1798,12 @@ Largura da ponte terrestre Tamanho do lago [Contested commons] Terras comuns disputadas -[Lattice] -Grelha [Maze] Labirinto [Fjord continent] Continente de fiordes -[Islet size] -Tamanho do ilhéu -[Home radius] -Raio da área inicial [Cell size] Tamanho da célula -[Room size] -Tamanho da sala [Corridor width] Largura do corredor [Spoke width] diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 36456f693..c712476b9 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -1792,20 +1792,12 @@ Lățimea punții de uscat Mărimea lacului [Contested commons] Teritoriu comun disputat -[Lattice] -Grilă [Maze] Labirint [Fjord continent] Continent cu fiorduri -[Islet size] -Mărimea insuliței -[Home radius] -Raza bazei [Cell size] Mărimea celulei -[Room size] -Mărimea camerei [Corridor width] Lățimea coridorului [Spoke width] diff --git a/data/texts.ru.txt b/data/texts.ru.txt index cd8618af7..f8061f21d 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -1790,20 +1790,12 @@ OpenGL недоступен в этой сборке. Размер озера [Contested commons] Спорные общие земли -[Lattice] -Решётка [Maze] Лабиринт [Fjord continent] Материк с фьордами -[Islet size] -Размер островка -[Home radius] -Радиус базы [Cell size] Размер ячейки -[Room size] -Размер комнаты [Corridor width] Ширина коридора [Spoke width] diff --git a/data/texts.si.txt b/data/texts.si.txt index 1bccab6e7..c444d53af 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -1792,20 +1792,12 @@ Samodejno prikaži pregled zemljevida v obliki torusa med premikanjem po zemljev Velikost jezera [Contested commons] Sporno skupno ozemlje -[Lattice] -Mreža [Maze] Labirint [Fjord continent] Fjordska celina -[Islet size] -Velikost otočka -[Home radius] -Polmer baze [Cell size] Velikost celice -[Room size] -Velikost sobe [Corridor width] Širina hodnika [Spoke width] diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 299b31434..b0dd951c5 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -1800,20 +1800,12 @@ Automaticky zobraziť prehľad mapy v tvare torusu pri pohybe po mape (OpenGL). Veľkosť jazera [Contested commons] Sporné spoločné územie -[Lattice] -Mriežka [Maze] Bludisko [Fjord continent] Kontinent s fjordmi -[Islet size] -Veľkosť ostrovčeka -[Home radius] -Polomer základne [Cell size] Veľkosť bunky -[Room size] -Veľkosť miestnosti [Corridor width] Šírka chodby [Spoke width] diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 40eda4153..3cd9ceeab 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -1792,20 +1792,12 @@ OpenGL није доступан у овој верзији. Величина језера [Contested commons] Спорно заједничко подручје -[Lattice] -Мрежа [Maze] Лавиринт [Fjord continent] Континент са фјордовима -[Islet size] -Величина острвцета -[Home radius] -Полупречник базе [Cell size] Величина ћелије -[Room size] -Величина собе [Corridor width] Ширина ходника [Spoke width] diff --git a/data/texts.sv.txt b/data/texts.sv.txt index 1e17df9ea..a7b428385 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -1802,20 +1802,12 @@ Landbrons bredd Sjöstorlek [Contested commons] Omtvistad allmänning -[Lattice] -Rutnät [Maze] Labyrint [Fjord continent] Fjordkontinent -[Islet size] -Småöns storlek -[Home radius] -Basradie [Cell size] Cellstorlek -[Room size] -Rumsstorlek [Corridor width] Korridorbredd [Spoke width] diff --git a/data/texts.tr.txt b/data/texts.tr.txt index c090d65ec..d8d358307 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -1800,20 +1800,12 @@ Kara köprüsü genişliği Göl boyutu [Contested commons] Çekişmeli ortak alan -[Lattice] -Kafes [Maze] Labirent [Fjord continent] Fiyort kıtası -[Islet size] -Adacık boyutu -[Home radius] -Üs yarıçapı [Cell size] Hücre boyutu -[Room size] -Oda boyutu [Corridor width] Koridor genişliği [Spoke width] diff --git a/data/texts.uk.txt b/data/texts.uk.txt index e92fa4285..1e8914e37 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -1788,20 +1788,12 @@ OpenGL недоступний у цій збірці. Розмір озера [Contested commons] Спірні спільні землі -[Lattice] -Ґратка [Maze] Лабіринт [Fjord continent] Материк із фіордами -[Islet size] -Розмір острівця -[Home radius] -Радіус бази [Cell size] Розмір комірки -[Room size] -Розмір кімнати [Corridor width] Ширина коридору [Spoke width] diff --git a/data/texts.vi.txt b/data/texts.vi.txt index b4c9e45d3..11a57dd36 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -1788,20 +1788,12 @@ Tự động hiển thị tổng quan bản đồ hình xuyến khi di chuyển Kích thước hồ [Contested commons] Vùng đất chung tranh chấp -[Lattice] -Mạng lưới [Maze] Mê cung [Fjord continent] Lục địa vịnh hẹp -[Islet size] -Kích thước đảo nhỏ -[Home radius] -Bán kính căn cứ [Cell size] Kích thước ô -[Room size] -Kích thước phòng [Corridor width] Độ rộng hành lang [Spoke width] diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 6f218ad70..ab6a22858 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -1788,20 +1788,12 @@ OpenGL 在此版本中不可用。 湖泊大小 [Contested commons] 争夺公地 -[Lattice] -网格 [Maze] 迷宫 [Fjord continent] 峡湾大陆 -[Islet size] -小岛大小 -[Home radius] -基地半径 [Cell size] 单元格大小 -[Room size] -房间大小 [Corridor width] 通道宽度 [Spoke width] diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index a4923edb1..8291def6e 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -1864,20 +1864,12 @@ OpenGL 在此版本中不可用。 湖泊大小 [Contested commons] 爭奪公地 -[Lattice] -網格 [Maze] 迷宮 [Fjord continent] 峽灣大陸 -[Islet size] -小島大小 -[Home radius] -基地半徑 [Cell size] 單元格大小 -[Room size] -房間大小 [Corridor width] 通道寬度 [Spoke width] diff --git a/docs/map-generators/ADDING_A_GENERATOR.md b/docs/map-generators/ADDING_A_GENERATOR.md index 800c10995..2f8b39a17 100644 --- a/docs/map-generators/ADDING_A_GENERATOR.md +++ b/docs/map-generators/ADDING_A_GENERATOR.md @@ -21,7 +21,7 @@ Extract on the second use. When a routine you are about to write already exists 1. Add `ExampleGenerator.h/.cpp` under `src/map/generator/generators/`. Use a named options struct that reads its scalar values from the request by stable option ID. 2. Return a `GeneratorDefinition` from `exampleDefinition()`. Provide a stable string ID, unique nonnegative numeric ID, translation key, revision, editor-only availability, control list and callback. Numeric IDs are identifiers, never list positions; gaps are supported, and the id lives only in the definition (`GeneratorRegistry::idOf` looks one up by string; the `Method` enum names only the ids the legacy descriptor knows). `hasStartingColonies` defaults to true. Uniform alone disables normal starting-colony validation in the shipped catalog. 3. Add that definition to the single list in `GeneratorRegistry::builtins()` and its source to `src/SConscript`. Add the display name and control labels to the translation tables using the existing translation workflow. -4. Write the callback as `bool generate(Game&, GenerationContext&)`. Read the immutable request, build into the supplied fresh game, set `context.stage` before operations that may fail, and return false with a useful `context.detail` if the layout cannot be placed. The usual shape is a `design()` that computes the whole layout from the request and the context's streams without touching the map, a `generate()` that stamps it through the shared stages, and a `validateWorld` that calls `design()` again and checks the finished map against it (`designMismatch`, `walkFromFirstColony` in `Pipeline.h`). +4. Write the callback as `bool generate(Game&, GenerationContext&)`. Read the immutable request, build into the supplied fresh game, set `context.stage` before operations that may fail, and return false with a useful `context.detail` if the layout cannot be placed. The usual shape is a `design()` that computes the whole layout from the request and the context's streams without touching the map, a `generate()` that stamps it through the shared stages, and a `validateWorld` that calls `design()` again and checks the finished map against it (`designMismatch`, `homePondMissing`, `walkFromFirstColony` in `Pipeline.h`), with `designFailure` as the registry's request check. Each `GeneratorControl` defines `id`, translation label, minimum, maximum, step, default, group, power-of-two display formatting, whether the value participates in terrain weighting, and its `kind`. The all-zero-weight check reads this metadata, not generator-specific option names. IDs must be unique within a generator and cannot shadow shared controls. For irregular integer domains, supply a sorted, unique `allowedValues` list such as `{4, 8, 16}` with matching minimum and maximum. These are literal stored values; `powerOfTwo` instead formats stored exponents. Use `values()`, `indexOf()`, `valueAt()` and `displayValue()` in consumers, never selection-index arithmetic. Use `GeneratorControl::set()` for UI normalization. The service rejects malformed requests instead of silently changing their settings. @@ -81,7 +81,7 @@ Everything named here is in `shared/`; `homeMaskFor`, `anchorFor`, `kitFor` and ## Generation, randomness and failure -The stages are shared functions; call them in the order the map needs. A designed generator usually runs: `design()`, which deals its home sites to the colonies with `dealStarts` before anything is keyed by colony index (a fixed order would give the same team the same ground every map); stamp the layout into a `TerrainSketch`, `layBeaches` and `writeUndermap` (`Sketch.h`); `settleColonies` (`Pipeline.h`); the kits and ambient layers (`growPatch`, `seedNear`, `scatterResources`, `seedAlgae`, `stockIslands`); `clearAroundSwarms`; `guaranteeStartingResources`; `openRoad` for any walk the deposits closed; then `reopenCrampedStarts` at non-default amounts. The height-field modes run `generateHeightField`'s stages (`Terrain.h`) then `placeStarts`; the legacy point-dispersion modes place colonies before their final resource pass and interleave region division, resources and starts. Nothing forces one order, but a generator that needs a different one should say why in its header comment. +The stages are shared functions; call them in the order the map needs. A designed generator usually runs: `design()`, which deals its home sites to the colonies with `dealStarts` before anything is keyed by colony index (a fixed order would give the same team the same ground every map); stamp the layout into a `TerrainSketch`, `layBeaches` and `writeUndermap` (`Sketch.h`); `settleColonies` (`Pipeline.h`; `settleRoundColonies` for round homes on their own grass); the kits and ambient layers (`growPatch`, `seedNear`, `scatterResources`, `seedAlgae`, `stockIslands`); `clearAroundSwarms`; `guaranteeStartingResources`; `openRoad` for any walk the deposits closed; then `reopenCrampedStarts` at non-default amounts. The height-field modes run `generateHeightField`'s stages (`Terrain.h`) then `placeStarts`; the legacy point-dispersion modes place colonies before their final resource pass and interleave region division, resources and starts. Nothing forces one order, but a generator that needs a different one should say why in its header comment. Use `context.stream("name")` for a per-attempt `std::mt19937` stream, or `context.bounded("name", count)` for a rejection-sampled bounded choice. Seed derivation is defined in `GenerationContext.cpp`: unsigned FNV-1a mixed with the root seed, followed by a fixed integer avalanche. Do not use `std::hash`, wall-clock reseeding, `rand()`, `srand()` or the gameplay RNG in generator algorithms. Noise tables and stamp caches belong to each height-map instance. diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 238f1decd..6f805c3e3 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -34,7 +34,7 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Walls` | Walls a design builds and the checks that prove they hold: `seaVertices` and `seaMargin` (the land a swimmer can stand on), `sealCoasts` (stone on every grass tile touching that margin, so a coast is sealed; City states, Carousel, Switchbacks), `islandSeaMargin` and `sealedIslandStone` (the margin and the stone of walled land in the sea with ponds and sand roads, once for Carousel and Switchbacks), `labelBorders` (a wall between labelled regions no unit can cross even diagonally, one to three tiles thick, or with doors a design leaves open; Stone highlands' ridges, Amphitheatre's borders, Carousel's walls), `designedStone` (a designed band's stone and any gaps the beach pass left), `reachesWithShut`, `pieceLeak` and `seaEntry` (with the doors shut, does any part reach another, or the sea reach inside), `towerReach` (how close a tower on some ground comes to shooting at a target: towers scan square rings with no line of sight), `walkSpread` (every colony's walk to its own target, and how uneven they are) and `firstRegionLeak` (reachable ground where two labelled regions meet that a design says must not; Maze's walls) | | `Tessellation` | Polygon tilings of the torus with no terrain attached: `squareTessellation` and `hexTessellation` (cells, the corners they share and edges as identities, exact across the wrap even two cells wide), `edgeEnds`, `centreAcross`, `outline`, `transform` (the lattice's translations and mirrors), `labelTiles` (every tile's cell), `shortestEdgeSteps` and `centreClearance`; `warpLimit` and `warpCorners`, corners moved at random into irregular polygons that still tile, never closing the gap between walls that share no corner below a minimum (Maze) | | `GraphMaze` | A maze carved through any `CellGraph` (a tessellation's with `cellGraph(tiling)`, or scattered sites' with `cellGraph(torus, sites, siteNeighbours)`): `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | -| `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic, or shared by value when each claimant's ground has a worth per tile; `smoothLabels`, a majority filter over any radius that trims spurs or, at a radius of 3 or 4, straightens borders into curves; `separateTerritories`, a gap opened between neighbouring territories; `fillToNearest`, the reverse: unclaimed ground near labelled regions given to the nearest, so water between pieces becomes land; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `growLakeBeside`, one on either flank of a site, kept wholly to its side of the line from the way in; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | +| `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic, or shared by value when each claimant's ground has a worth per tile; `balancedTerritories`, a power diagram from one site per claimant with the weights tuned until the areas are equal, so every border is a straight line (Amphitheatre); `smoothLabels`, a majority filter over any radius that trims spurs or, at a radius of 3 or 4, straightens borders into curves; `separateTerritories`, a gap opened between neighbouring territories; `fillToNearest`, the reverse: unclaimed ground near labelled regions given to the nearest, so water between pieces becomes land; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `growLakeBeside`, one on either flank of a site, kept wholly to its side of the line from the way in; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | | `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges across the water rows every so many tiles along them, so workers need not walk round a long row (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives (and only ground whose eroded core joins the home's core stays, since two cores that do not touch can overlap once grown back through a waist the coast walls then close), and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | | `Towers` | Tower sites chosen for the ground they cover over the walls, other colonies' (offence) or their own (defence), each weighted: `startingTowerRequest` (a request from a map's level and count controls), `chooseTowerSites` (best first, a colony at a time in turn, optionally every site directly against a wall, no stocked tower in range of another colony's tower or swarm, plus open 2x2 pads for players to build more), `settleStartingTowers` (the sequence every arena map runs: `dropBlockingSites` so no site closes a colony's walk to a goal, `evenTowerPlan` so every colony has as many sites as the fewest got, then `raiseTowers`), `towerFootprints` and `roomyGround` (no tower on a strip it could close); a tower in range of another colony's tower starts empty | | `Homes` | Homes built alike: `stampRoundHome`, `homeSwarmSite` and `plantHomeKit` (a round home facing out from the middle, its swarm towards its door, optional ponds kept clear of its edge, and a wheat and wood kit near the swarm with no stone, since these homes are walled in stone; `homeHasRoom` is the size floor; Carousel, Switchbacks; the axis is any heading, out from the middle on a ring), `regionHome` (a home in ground of any shape: the swarm the same walk in from the door as every other colony's, on the roomiest such tile, facing away from the door), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | @@ -48,7 +48,7 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Room` | Building room as a design decision: `buildableTiles`, `buildAnchors` (every 4x4 footprint's top-left, across the wrap), `buildSites` (footprints in a region) and `growUntilSites` (a chamber grown until it holds exactly what it promised) | | `Biomes` | Kinds of land as data: `BiomeKit` (ponds, stone ring, farmland, wood share, outcrops, groves, cover, orchard island) with `fertilePlain`, `stoneFortress`, `orchardIsland` and `forest`; `biomeWorth` (an estimate from the start scorer's weights to share ground by, to be tuned with the fairness tournament); `sketchBiome` (its ponds, island and ring) and `furnishBiome` (its deposits) | | `BalancedStarts` | `chooseBalancedStarts`: boot tiles whose walks to wheat and wood are as nearly equal as the finished map allows | -| `Pipeline` | The stages round the others: `dealStarts` (the design's start sites dealt to the colonies at random, so a team number never gets the same ground map after map), `settleColonies`, `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | +| `Pipeline` | The stages round the others: `dealStarts` (the design's start sites dealt to the colonies at random, so a team number never gets the same ground map after map), `designFailure` (the registry's request check for a designed generator: the design's own failure), `settleColonies` and `settleRoundColonies` (round homes on their own grass, `homeGrassMask`), `homePondMissing` (a validator's check that every home kept its pond), `secureStartingCrops` (clear round the swarms, guarantee the crops, clear again), `reopenCrampedStarts`, `designMismatch` and `walkFromFirstColony` for validators, `ResourceAmounts` | | `Terrain` | The height-field pipeline as stages: `heightFieldTiling`, `classifyHeightField`, `paintHeightFieldTerrain`, `paintHeightFieldResources`, `chooseHeightFieldStarts`, `plantHeightFieldGroves`, composed by `generateHeightField` | | `StartQuality` | `scoreStarts`, the finished map's colony quality and fairness the service ranks candidates by | | `GenerationContext` | Named `std::mt19937` streams, `bounded` draws and `shuffle` | diff --git a/src/map/generator/generators/AmphitheatreGenerator.cpp b/src/map/generator/generators/AmphitheatreGenerator.cpp index 0d4bfc4cc..1f69c2ea9 100644 --- a/src/map/generator/generators/AmphitheatreGenerator.cpp +++ b/src/map/generator/generators/AmphitheatreGenerator.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -120,8 +119,8 @@ constexpr int kTowerPads = 3; constexpr int kTowerSpacing = 5; // A tower keeps this many steps from water on every side, so it never closes a strip of shore. constexpr int kTowerRoom = 4; -// The most the territories' areas may differ, in percent of the smallest: growth shares the ground -// out to the tile, and trimming the spurs off the borders moves a little of it. +// The most the territories' areas may differ, in percent of the smallest: the balance stops within +// kBalanceTolerance, and the walls and the seas take a little more. constexpr int kAreaTolerance = 5; // The most the colonies' walks to their ramps and to the pit may differ. constexpr int kWalkSpread = 12; @@ -721,8 +720,8 @@ GeneratorDefinition amphitheatreDefinition() 2, false, // Rings of wall; each ramp's width in tiles; the pit's radius and each terrace's width as - // shares of the half side; how far the territories' borders wander; every colony's inland seas - // together as a percentage of the smallest territory; the borders' thickness in tiles. + // shares of the half side; every colony's inland seas together as a percentage of the smallest + // territory; the borders' thickness in tiles. {{"rings", "Rings", 2, 4, 1, 3, ControlGroup::Layout}, {"ramp-width", "Ramp width", 5, 11, 2, 7, ControlGroup::Terrain}, {"pit-size", "Pit size", 8, 30, 2, 16, ControlGroup::Layout}, diff --git a/src/map/generator/generators/AnthillGenerator.cpp b/src/map/generator/generators/AnthillGenerator.cpp index 0a47c0d1d..130a7e3f4 100644 --- a/src/map/generator/generators/AnthillGenerator.cpp +++ b/src/map/generator/generators/AnthillGenerator.cpp @@ -105,12 +105,23 @@ constexpr double kWanderShare = 0.18, kWidthJitter = 0.25; // `queenRoom` building sites, never more than this many tiles. constexpr int kQueenExtra = 3, kQueenGrowthLimit = 400; +// What a chamber is: the queen chambers are the colonies' homes; farm chambers hold a pond and a +// field; treasure chambers are the tunnels' dead ends; the rest are plain. +enum ChamberKind +{ + PlainChamber = 0, + FarmChamber, + QueenChamber, + TreasureChamber +}; + struct Layout { Torus t{1, 1}; std::vector sites; std::vector label; // every tile's nearest site std::vector homeSite, farmSite, treasureSite; + std::vector kind; // every chamber's ChamberKind std::vector open, water, road; // tunnels and chambers; ponds; sand roads std::vector pondRadius; // every chamber's pond std::vector homeOf; @@ -166,12 +177,15 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (!isPocket[site] && !isEnd[site] && site % 2 == 0) L.farmSite.push_back(site); - // Every chamber's kind sets its size and its pond: 0 plain, 1 farm, 2 queen. - std::vector kind(L.sites.size(), 0); + // Every chamber's kind sets its size and its pond (a treasure chamber's are a plain chamber's). + L.kind.assign(L.sites.size(), PlainChamber); + for (int site : L.treasureSite) + L.kind[site] = TreasureChamber; for (int site : L.farmSite) - kind[site] = 1; + L.kind[site] = FarmChamber; for (int site : L.homeSite) - kind[site] = 2; + L.kind[site] = QueenChamber; + const std::vector &kind = L.kind; L.pondRadius.assign(L.sites.size(), pondRadiusFor(o.chamberSize)); for (int site : L.farmSite) L.pondRadius[site] += kFarmPondExtra; @@ -179,7 +193,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.pondRadius[site] = 0; // no water in a queen chamber (second play) // Where a road stops at each end: on a pond's beach, or short of a queen chamber's clear middle. const auto roadStop = [&](int site) - { return (kind[site] == 2 ? kQueenCore : L.pondRadius[site]) + kRoadStop; }; + { return (kind[site] == QueenChamber ? kQueenCore : L.pondRadius[site]) + kRoadStop; }; // Carving: every open edge a wandering tunnel between its two chambers' middles, with a sand road // down its middle that stops on the shore of the pond at each end (FEEDBACK 2026-09-13); then @@ -216,7 +230,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) { // Each chamber turned by a different angle, so one outline reads as many. const double turn = site * 2.399963; // the golden angle - const RadialShape &shape = kind[site] == 2 ? queen : kind[site] == 1 ? farm : chamber; + const RadialShape &shape = kind[site] == QueenChamber ? queen + : kind[site] == FarmChamber ? farm + : chamber; fillShape(L.open, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, shape, turn); } // A pond at the middle of every chamber but the queens' (FEEDBACK 2026-09-13, both plays): a @@ -226,9 +242,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const RadialShape pondFarm(pondRadiusFor(o.chamberSize) + kFarmPondExtra, 0.3, context, "anthill-ponds"); for (int site = 0; site < int(L.sites.size()); ++site) - if (kind[site] != 2) + if (kind[site] != QueenChamber) fillShape(L.water, t, L.sites[site].x + 0.5, L.sites[site].y + 0.5, - kind[site] == 1 ? pondFarm : pondPlain, 0.0); + kind[site] == FarmChamber ? pondFarm : pondPlain, 0.0); for (int i = 0; i < n; ++i) if (L.water[i]) L.road[i] = 0; @@ -350,13 +366,6 @@ bool generate(Game &game, GenerationContext &context) // Farm chambers ring the pond with wheat and a wider ring of wood beyond ("no sections empty of // growth"); every other chamber a wheat or a wood ring in turn; treasure chambers a fruit grove // and a wheat ring. - std::vector kind(L.sites.size(), 0); - for (int site : L.farmSite) - kind[site] = 1; - for (int site : L.homeSite) - kind[site] = 2; - for (int site : L.treasureSite) - kind[site] = 3; const auto ringPlant = [&](int site, double radius, int type, int count, const auto &here) { const Site s = L.sites[site]; @@ -378,14 +387,14 @@ bool generate(Game &game, GenerationContext &context) return L.open[i] && L.label[i] == site && !reserved[i] && clearGround(map, i % t.w, i / t.w); }; - if (kind[site] == 2) + if (L.kind[site] == QueenChamber) continue; - if (kind[site] == 1) + if (L.kind[site] == FarmChamber) { ringPlant(site, shore, CORN, int(scaledCount(kFarmWheat, o.wheat)), here); ringPlant(site, shore + 3, WOOD, int(scaledCount(kFarmWood, o.wood)), here); } - else if (kind[site] == 3) + else if (L.kind[site] == TreasureChamber) { if (scaledCount(1, o.fruit) > 0) if (const int seed = seedNear(t, s.x - int(shore) - 2, s.y, 3, here); seed >= 0) @@ -442,9 +451,9 @@ GeneratorDefinition anthillDefinition() "Anthill", 4, false, - // Chambers 28 apart give a 256 map about a hundred of them; a chamber of radius 5 holds - // three or four buildings; a queen chamber grown to 40 building sites (overlapping 4x4 - // footprints, the start scorer's measure) holds a swarm, an inn and a couple more; tunnels + // Chambers 28 apart give a 256 map about a hundred of them; a chamber of radius 7 holds a + // pond and a few buildings; a queen chamber grown to 60 building sites (overlapping 4x4 + // footprints, the start scorer's measure) holds a swarm, an inn and a few more; tunnels // three wide take a column of units and are closed by one tower; a fifth of the chambers' // count in loops keeps a way round most blockades. {{"chamber-spacing", "Chamber spacing", 20, 40, 2, 28, ControlGroup::Layout}, @@ -461,10 +470,6 @@ GeneratorDefinition anthillDefinition() GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, + designFailure, validateWorld}; } diff --git a/src/map/generator/generators/CanalsGenerator.cpp b/src/map/generator/generators/CanalsGenerator.cpp index 3e3c74a28..c237eb15a 100644 --- a/src/map/generator/generators/CanalsGenerator.cpp +++ b/src/map/generator/generators/CanalsGenerator.cpp @@ -37,8 +37,8 @@ using namespace MapGeneration; // out. // // The city has no centre: the colonies' blocks are the ones farthest apart on the block graph -// (spreadPockets), market blocks with orchards lie farthest from those, and fairness is statistical -// (the lobby keeps the best-scoring of several seeds). +// (spreadPockets), every other block is dealt its kind by a weighted draw, and fairness is +// statistical (the lobby keeps the best-scoring of several seeds). // // WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Water blocks walking until // a colony can swim, so the canals are a timing rule; a tower scans square rings with no line of @@ -146,12 +146,6 @@ struct Layout std::string failure; }; -// A subtile point in tile units. -ShapePoint tilePoint(SubtilePoint p) -{ - return {p.x / 16.0, p.y / 16.0}; -} - // A tile at (u, v) in a block's frame: from its middle, turned a quarter turn per facing. int localTile(const Layout &L, int cell, int u, int v) { @@ -187,7 +181,8 @@ int wallReach(const CanalsOptions &o) // THE WALLS of a built kind, as the tiles of the block they may stand on: on the block, clear of water // by two corners all round (so the beaches leave them pure grass and nothing stands on a bridge's // landing), and off any pad. The block must stay one piece with them in - its land and its bridges, -// eight ways, as a unit walks - else the block gets no walls and `fallback` instead. +// eight ways, as a unit walks - else the block gets no walls and the pad kind instead (a chicane, +// which has no pad, becomes plain). void blockWalls(Layout &L, int cell, int reach) { const Torus &t = L.t; @@ -323,8 +318,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) return L; } - // The home blocks: as far apart on the block graph as the tiling allows; then the market blocks, - // each the farthest block from every home and market so far. + // The home blocks: as far apart on the block graph as the tiling allows. const CellGraph graph = cellGraph(L.g); L.homeCell = spreadPockets(graph, teams); if (int(L.homeCell.size()) != teams) @@ -582,15 +576,7 @@ bool generate(Game &game, GenerationContext &context) map.setResource(i % t.w, i / t.w, STONE, 1); context.stage = "canals colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(n), 0); - for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; - const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; - if (!settleColonies(game, context, "canals-starts", homeMask, anchor)) + if (!settleRoundColonies(game, context, "canals-starts", L.homeOf, L.homes, L.homeRadius)) return false; // Towers on the banks. No tower or pad may close a colony's walk to a bridge, and every colony @@ -836,10 +822,6 @@ GeneratorDefinition canalsDefinition() GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, + designFailure, validateWorld}; } diff --git a/src/map/generator/generators/FingerprintGenerator.cpp b/src/map/generator/generators/FingerprintGenerator.cpp index fbcd27d07..bd5d3d8ea 100644 --- a/src/map/generator/generators/FingerprintGenerator.cpp +++ b/src/map/generator/generators/FingerprintGenerator.cpp @@ -96,12 +96,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) context.bounded("fingerprint-layout", std::uint32_t(t.h))) .sites; dealStarts(context, L.homes); // which colony gets which site is a draw, not the order - double spacing = std::min(t.w, t.h); - for (size_t a = 0; a < L.homes.size(); ++a) - for (size_t b = a + 1; b < L.homes.size(); ++b) - spacing = - std::min(spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), - t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + const double spacing = nearestSiteDistance(t, L.homes); // Clearings shrink to keep a band's width of pattern between neighbours; the smallest home that // holds a pond and a kit is the floor. L.homeRadius = @@ -194,15 +189,7 @@ bool generate(Game &game, GenerationContext &context) map.setResource(i % t.w, i / t.w, STONE, 1); context.stage = "fingerprint colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(n), 0); - for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; - const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; - if (!settleColonies(game, context, "fingerprint-starts", homeMask, anchor)) + if (!settleRoundColonies(game, context, "fingerprint-starts", L.homeOf, L.homes, L.homeRadius)) return false; context.stage = "fingerprint resources"; @@ -252,15 +239,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (const std::string mismatch = designMismatch(L, map, "fingerprint"); !mismatch.empty()) return mismatch; const Torus &t = L.t; - for (int k = 0; k < context.request.nbTeams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s clearing has lost its pond."; - } + if (const std::string lost = + homePondMissing(map, t, L.kits, context.request.nbTeams, "clearing", "pond"); + !lost.empty()) + return lost; return walkFromFirstColony(map, context.request.nbTeams, "the fingerprint", "through the pattern") .error; @@ -298,10 +280,6 @@ GeneratorDefinition fingerprintDefinition() GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, + designFailure, validateWorld}; } diff --git a/src/map/generator/generators/OldGrowthGenerator.cpp b/src/map/generator/generators/OldGrowthGenerator.cpp index 1dbb94c05..d786b762f 100644 --- a/src/map/generator/generators/OldGrowthGenerator.cpp +++ b/src/map/generator/generators/OldGrowthGenerator.cpp @@ -73,8 +73,8 @@ namespace constexpr int kHomeWheat = 24, kHomeQuarry = 2; // The ring of pools round a home (FEEDBACK 2026-09-13, "a lot more pools of water"): `homePools` // pools of this radius on a ring at this share of the home's radius, each with this much wheat on its -// shore. At the default radius 20 the ring is 12 tiles out, so every pool waters the ground between -// itself and the central pond and the whole clearing is fertile. +// shore. At the default radius 24 the ring is 14 tiles out, so every pool waters the ground between +// itself and the central pond (the growth probe reaches 15) and the whole clearing is fertile. constexpr double kPoolRadius = 2.5, kPoolRingShare = 0.6; constexpr int kPoolWheat = 32, kPoolSeeds = 4; // The forest starts this far beyond a home's rough disc, so the clearing's edge is open ground to @@ -123,12 +123,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) context.bounded("growth-layout", std::uint32_t(t.h))) .sites; dealStarts(context, L.homes); // which colony gets which site is a draw, not the order - L.spacing = std::min(t.w, t.h); - for (size_t a = 0; a < L.homes.size(); ++a) - for (size_t b = a + 1; b < L.homes.size(); ++b) - L.spacing = - std::min(L.spacing, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), - t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + L.spacing = nearestSiteDistance(t, L.homes); L.homeRadius = std::min(o.homeSize, std::floor(L.spacing / 3 - kClearingMargin - kSandRing)); if (!homeHasRoom(L.homeRadius)) @@ -310,15 +305,7 @@ bool generate(Game &game, GenerationContext &context) writeUndermap(map, terrain); context.stage = "old growth colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(n), 0); - for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; - const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; - if (!settleColonies(game, context, "growth-starts", homeMask, anchor)) + if (!settleRoundColonies(game, context, "growth-starts", L.homeOf, L.homes, L.homeRadius)) return false; context.stage = "old growth resources"; @@ -411,15 +398,9 @@ std::string validateWorld(const Game &game, const GenerationContext &context) return mismatch; const Torus &t = L.t; const int teams = context.request.nbTeams; - for (int k = 0; k < teams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s clearing has lost its pond."; - } + if (const std::string lost = homePondMissing(map, t, L.kits, teams, "clearing", "pond"); + !lost.empty()) + return lost; // The forest is dry: nothing but the home ponds and the lakes waters it, so wood beyond their // reach never regrows. Checked on the finished map's own growth field. const Fertility::Field fertility = Fertility::forMap(map, false); @@ -477,10 +458,6 @@ GeneratorDefinition oldGrowthDefinition() GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, + designFailure, validateWorld}; } diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index 74bec9e1c..ae2a04a89 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -38,7 +38,7 @@ using namespace MapGeneration; // // WHY IT PLAYS WELL (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md). Stone blocks are permanent // and unbuildable, so the city's shape is fixed and only the streets can change hands; crops regrow -// near water, and the only water inside the wall is the fountains, so a colony that wants to eat +// near water, and the only water inside the city is the fountains, so a colony that wants to eat // must farm the plazas or go out to the fields. A block stops walking but not shooting, so the // blocks are also cover for towers. // @@ -109,11 +109,6 @@ struct Layout std::string failure; }; -ShapePoint tilePoint(SubtilePoint p) -{ - return {p.x / 16.0, p.y / 16.0}; -} - Layout design(const GenerationRequest &request, GenerationContext &context) { const OldTownOptions o(request); @@ -123,7 +118,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const int n = t.size(), teams = std::max(1, request.nbTeams); L.cx = t.w / 2.0; L.cy = t.h / 2.0; - // The city is a disc of `citySize` percent of the half side; the wall stands at its edge. + // The city is a disc of `citySize` percent of the half side; the fields begin at its edge. L.cityRadius = o.citySize / 100.0 * std::min(t.w, t.h) / 2.0; // The blocks: a square tiling warped into irregular polygons. Every edge is a street, and the @@ -138,7 +133,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.failure = "The blocks could not be labelled."; return L; } - // The city's cells: those whose centre lies inside the wall's ring road. + // The city's cells: those whose whole block lies inside the city's edge. std::vector inCity(L.g.cellCount(), 0); int cityCells = 0; for (int cell = 0; cell < L.g.cellCount(); ++cell) @@ -381,13 +376,8 @@ bool generate(Game &game, GenerationContext &context) map.setResource(i % t.w, i / t.w, STONE, 1); context.stage = "old town colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(n), 0); - for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; + const auto homeMask = [&](int team) { return homeGrassMask(map, t, L.homeOf, team); }; + // The swarm on the annex block's middle, as the top-left tile of its 4x4 footprint. const auto anchor = [&](int team) { return MapGeneratorPoint(int(std::lround(L.homes[team].x)) - 2, @@ -494,15 +484,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (const std::string mismatch = designMismatch(L, map, "old town"); !mismatch.empty()) return mismatch; const Torus &t = L.t; - for (int k = 0; k < context.request.nbTeams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s plaza has lost its fountain."; - } + if (const std::string lost = + homePondMissing(map, t, L.kits, context.request.nbTeams, "plaza", "fountain"); + !lost.empty()) + return lost; return walkFromFirstColony(map, context.request.nbTeams, "the city", "through the streets") .error; } @@ -543,10 +528,6 @@ GeneratorDefinition oldTownDefinition() GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, + designFailure, validateWorld}; } diff --git a/src/map/generator/generators/PolderGenerator.cpp b/src/map/generator/generators/PolderGenerator.cpp index bb96a9371..de5fd0aca 100644 --- a/src/map/generator/generators/PolderGenerator.cpp +++ b/src/map/generator/generators/PolderGenerator.cpp @@ -141,12 +141,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) context.bounded("polder-layout", std::uint32_t(t.h))) .sites; dealStarts(context, L.homes); // which colony gets which site is a draw, not the order - double nearest = std::min(t.w, t.h); - for (size_t a = 0; a < L.homes.size(); ++a) - for (size_t b = a + 1; b < L.homes.size(); ++b) - nearest = - std::min(nearest, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), - t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + const double nearest = nearestSiteDistance(t, L.homes); L.villageRadius = std::min(o.villageSize, std::floor((nearest - L.spacing) / 2 - kVillageSand)); if (L.villageRadius < 8) @@ -260,16 +255,7 @@ bool generate(Game &game, GenerationContext &context) writeUndermap(map, terrain); context.stage = "polder colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(n), 0); - for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; - const auto anchor = [&](int team) - { return homeSwarmSite(L.homes[team], 0.0, L.villageRadius); }; - if (!settleColonies(game, context, "polder-starts", homeMask, anchor)) + if (!settleRoundColonies(game, context, "polder-starts", L.homeOf, L.homes, L.villageRadius)) return false; context.stage = "polder resources"; @@ -352,9 +338,10 @@ GeneratorDefinition polderDefinition() "Polder", 3, false, - // A dyke every 24 tiles is a lane every one and a half rows' walk; villages of radius 11 - // hold a swarm, its kit and a few more buildings and no more. - // FEEDBACK 2026-09-13: a random angle by default; villages of 14 (was 11) in a sand ring. + // A dyke every 24 tiles is a lane every one and a half rows' walk; villages of radius 14 + // hold a swarm, its kit and a few more buildings and no more (11 before the first play, and + // the growth crowded them; FEEDBACK 2026-09-13: a random angle by default, and villages of + // 14 in a sand ring). {GeneratorControl::choice("row-angle", "Row angle", {"Random", "Vertical", "Horizontal", "Diagonal"}, 0, ControlGroup::Terrain), @@ -368,10 +355,6 @@ GeneratorDefinition polderDefinition() GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, + designFailure, validateWorld}; } diff --git a/src/map/generator/generators/RainShadowGenerator.cpp b/src/map/generator/generators/RainShadowGenerator.cpp index 12a459a22..cb556046b 100644 --- a/src/map/generator/generators/RainShadowGenerator.cpp +++ b/src/map/generator/generators/RainShadowGenerator.cpp @@ -78,10 +78,10 @@ constexpr int kRidgeWarpPeriod = 40; // would take the ridge's windward row with it (measured: the ridge came out one row thick); four // tiles leaves all three rows standing. constexpr int kFootGap = 4, kFootDepth = 5; -// Pools along the foot: each this long along the ridge, every this many tiles. A pool 12 long and 3 -// deep is about 36 corners of water, enough for the engine's growth probe to find from a good part -// of the valley; the 12-tile gaps between pools are where the valley's beach lets units walk along -// the foot. +// Streams along the foot: each this long along the ridge, every this many tiles (first play: 20 every +// 24, from 12 every 24). A stream 20 long and 5 deep is about 100 corners of water, which the engine's +// growth probe finds from most of the valley; the four-tile gaps between streams, once their beaches +// meet, are where the valley's beach lets units walk along the foot. constexpr int kPoolLength = 20, kPoolSpacing = 24; // A sand road through every pass, running this far into the valley on either side of the stone // (first play: the passes "get clogged up too easily"): a line of single sand corners, which nothing @@ -284,12 +284,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.clearing[i] = 1; } dealStarts(context, L.homes); // which colony gets which valley site is a draw, not the order - double nearest = std::min(t.w, t.h); - for (size_t a = 0; a < L.homes.size(); ++a) - for (size_t b = a + 1; b < L.homes.size(); ++b) - nearest = - std::min(nearest, std::hypot(t.offsetX(int(L.homes[a].x), int(L.homes[b].x)), - t.offsetY(int(L.homes[a].y), int(L.homes[b].y)))); + const double nearest = nearestSiteDistance(t, L.homes); if (nearest < 2 * (L.homeRadius + kHomeMargin)) { L.failure = "Too many colonies for this map; use a bigger map or fewer colonies."; @@ -346,15 +341,7 @@ bool generate(Game &game, GenerationContext &context) map.setResource(i % t.w, i / t.w, STONE, 1); context.stage = "rain shadow colonies"; - const auto homeMask = [&](int team) - { - std::vector ground(size_t(n), 0); - for (int i = 0; i < n; ++i) - ground[i] = L.homeOf[i] == team && map.isGrass(i % t.w, i / t.w); - return ground; - }; - const auto anchor = [&](int team) { return homeSwarmSite(L.homes[team], 0.0, L.homeRadius); }; - if (!settleColonies(game, context, "rain-starts", homeMask, anchor)) + if (!settleRoundColonies(game, context, "rain-starts", L.homeOf, L.homes, L.homeRadius)) return false; context.stage = "rain shadow resources"; @@ -409,15 +396,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) if (const std::string mismatch = designMismatch(L, map, "rain shadow"); !mismatch.empty()) return mismatch; const Torus &t = L.t; - for (int k = 0; k < context.request.nbTeams; ++k) - { - bool pond = false; - for (int dy = -2; dy <= 2 && !pond; ++dy) - for (int dx = -2; dx <= 2 && !pond; ++dx) - pond = map.isWater(t.x(int(L.kits[k].x) + dx), t.y(int(L.kits[k].y) + dy)); - if (!pond) - return "Colony " + std::to_string(k) + "'s home has lost its pond."; - } + if (const std::string lost = + homePondMissing(map, t, L.kits, context.request.nbTeams, "home", "pond"); + !lost.empty()) + return lost; // Every designed ridge tile that could hold stone does: the ridges are the map's walls. for (int i = 0; i < t.size(); ++i) if (L.stone[i] && map.isGrass(i % t.w, i / t.w) && @@ -471,10 +453,6 @@ GeneratorDefinition rainShadowDefinition() GeneratorControl::percentage("fruit-amount", "Fruit amount")}, generate, true, - [](const GenerationRequest &r) -> std::string - { - GenerationContext probe(r); - return design(r, probe).failure; - }, + designFailure, validateWorld}; } diff --git a/src/map/generator/generators/SwitchbacksGenerator.cpp b/src/map/generator/generators/SwitchbacksGenerator.cpp index 01b2cfe2a..b8498b312 100644 --- a/src/map/generator/generators/SwitchbacksGenerator.cpp +++ b/src/map/generator/generators/SwitchbacksGenerator.cpp @@ -51,9 +51,9 @@ using namespace MapGeneration; // // HOW IT IS BUILT. Everything is designed once in the wedge's frame, along the colony's axis and across // it, and turned round the centre for every colony, so every colony's ground and trail are the same and -// the layout is fair for any colony count. The map stays round, in a circle on the shorter side, with sea -// filling a rectangle's ends. The design is a pure function of the request, so validateWorld rebuilds it -// and checks the finished world. +// the layout is fair for any colony count. The map stays round, in a circle on the shorter side, with +// farms filling a rectangle's ends. The design is a pure function of the request, so validateWorld +// rebuilds it and checks the finished world. // // GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be cleared, // so a mountain's walls are permanent and its trail is the only door; a tower scans round its footprint @@ -68,7 +68,8 @@ using namespace MapGeneration; namespace { -// Sea kept between the homes' outer coast and the map's wrap, as a share of the half side. +// The rim: ground kept between the homes' outer edge and the map's wrap, as a share of the half side +// (farm, since the fill takes every tile of sea). constexpr double kRimShare = 0.04; constexpr int kRimMinimum = 4; // A home's radius at 100% of home size: a share of the half side, never below this many tiles, never @@ -92,7 +93,9 @@ constexpr double kSideWall = 3; // A leg's centre line runs this far either side of the mountain's axis: a share of the half side, // but at least two trail widths. constexpr double kSpanShare = 0.1; -// Water kept between two mountains: enough for a beach and a coast wall on each. +// The gap kept between two mountains, and between a mountain and another colony's home: once sea +// with a beach and a coast wall on each side, now the guard's rock near the plateau and farm beyond, +// still what keeps the mountains from touching. constexpr double kPieceGap = 4; // Every home's starter kit: this much wheat and wood beside the swarm, unscaled; no stone, since the // mountains are stone. @@ -376,9 +379,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.road[i] = 0; } - // The mountains must keep water between each other and between each and every home but its own, - // beyond their innermost legs; inside those the climbs come together at the plateau, and the - // validator checks that no climb meets another's trail. + // The mountains must keep their gap from each other and from every home but their own, beyond + // their innermost legs; inside those the climbs come together at the plateau, and the validator + // checks that no climb meets another's trail. for (int k = 0; k < teams && teams > 1; ++k) { std::vector mine(n, 0); @@ -390,7 +393,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const int other = L.blockOf[i] >= 0 ? L.blockOf[i] : L.trailOf[i] >= 0 ? L.trailOf[i] : L.homeOf[i]; - // Chebyshev steps: two tiles of water between is enough for a beach on each side. + // Chebyshev steps: two tiles between is the least the gap allows. if (other >= 0 && other != k && !L.plateau[i] && steps[i] < kPieceGap - 1 && std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) > g.innerLeg + g.trailHalf) @@ -400,10 +403,6 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } } } - // The farms (growFarmFields): every home reaches out sideways, across its axis, to a farm on either - // flank, and the farms share the open sea between the mountains and round the rim by equal yield. Their - // rows run across the axis at the widths that yield most (bestFarmRows), with sand caps, bridges and a - // building plot (layFarm), and every farm's coast is sealed like the rest. L.farmOf.assign(n, -1); L.farmSand.assign(n, 0); // THE FARMS (growFarmFields, then the fill and the walls). Every home reaches out sideways, across diff --git a/src/map/generator/shared/Orbits.h b/src/map/generator/shared/Orbits.h index 0d86fb554..2432a308f 100644 --- a/src/map/generator/shared/Orbits.h +++ b/src/map/generator/shared/Orbits.h @@ -2,6 +2,8 @@ #pragma once #include "Geometry.h" #include "Grid.h" +#include +#include #include #include #include @@ -120,4 +122,16 @@ bool equaliseDeposits(Map &, const Symmetry &, std::string &detail); /// colony permutation. std::string orbitMismatch(const Game &, const Symmetry &, int teams, std::vector> *permutations = nullptr); +/// The least distance between any two of `sites`, the short way round the wrap, from whole-tile +/// positions (each site truncated to its tile); the shorter side when there is only one. What a +/// design shrinks its homes to fit between. +inline double nearestSiteDistance(const Torus &t, const std::vector &sites) +{ + double nearest = std::min(t.w, t.h); + for (size_t a = 0; a < sites.size(); ++a) + for (size_t b = a + 1; b < sites.size(); ++b) + nearest = std::min(nearest, std::hypot(t.offsetX(int(sites[a].x), int(sites[b].x)), + t.offsetY(int(sites[a].y), int(sites[b].y)))); + return nearest; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Pipeline.cpp b/src/map/generator/shared/Pipeline.cpp index f0ab31d1b..6695cc0b2 100644 --- a/src/map/generator/shared/Pipeline.cpp +++ b/src/map/generator/shared/Pipeline.cpp @@ -2,6 +2,7 @@ #include "Pipeline.h" #include "Game.h" #include "GenerationContext.h" +#include "Homes.h" #include "Planting.h" #include "Resources.h" namespace MapGeneration @@ -44,4 +45,38 @@ ColonyWalk walkFromFirstColony(const Map &map, int teams, const std::string &gro (route.empty() ? "" : " " + route) + "."; return walk; } + +std::vector homeGrassMask(const Map &map, const Torus &t, + const std::vector &homeOf, int team) +{ + std::vector ground(size_t(t.size()), 0); + for (int i = 0; i < t.size(); ++i) + ground[i] = homeOf[i] == team && map.isGrass(i % t.w, i / t.w); + return ground; +} + +bool settleRoundColonies(Game &game, GenerationContext &context, const char *stream, + const std::vector &homeOf, const std::vector &homes, + double radius) +{ + const Torus t(game.map.getW(), game.map.getH()); + return settleColonies( + game, context, stream, [&](int team) { return homeGrassMask(game.map, t, homeOf, team); }, + [&](int team) { return homeSwarmSite(homes[team], 0.0, radius); }); +} + +std::string homePondMissing(const Map &map, const Torus &t, const std::vector &kits, + int teams, const char *place, const char *water) +{ + for (int k = 0; k < teams; ++k) + { + bool pond = false; + for (int dy = -2; dy <= 2 && !pond; ++dy) + for (int dx = -2; dx <= 2 && !pond; ++dx) + pond = map.isWater(t.x(int(kits[k].x) + dx), t.y(int(kits[k].y) + dy)); + if (!pond) + return "Colony " + std::to_string(k) + "'s " + place + " has lost its " + water + "."; + } + return ""; +} } // namespace MapGeneration diff --git a/src/map/generator/shared/Pipeline.h b/src/map/generator/shared/Pipeline.h index 5eafb0feb..a46bc0a7f 100644 --- a/src/map/generator/shared/Pipeline.h +++ b/src/map/generator/shared/Pipeline.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "GenerationContext.h" +#include "Geometry.h" #include "Grid.h" #include "Regions.h" #include "Settlements.h" @@ -34,6 +35,26 @@ void dealStarts(GenerationContext &context, Sites &sites, const char *stream = " context.shuffle(sites.begin(), sites.end(), stream); } +/// The registry's validateRequest for a designed generator: rebuilds the design from the request on +/// a probe context and reports its failure, so the lobby refuses what the design refuses, with the +/// design's own message. Pass the generator's design function: designFailure. +template std::string designFailure(const GenerationRequest &r) +{ + GenerationContext probe(r); + return Design(r, probe).failure; +} + +/// The ground a colony's swarm and workers may stand on: its own tiles of `homeOf` that are pure +/// grass on the written map (a beach, a road or a plot's ring has spoiled the rest). +std::vector homeGrassMask(const Map &, const Torus &, const std::vector &homeOf, + int team); + +/// A validator's check that no colony's home pond was lost to the map's furnishing: water within two +/// tiles of every kit's centre (`kits`, one per colony). Returns "" or "Colony k's has lost +/// its ." for the first colony without. +std::string homePondMissing(const Map &, const Torus &, const std::vector &kits, + int teams, const char *place, const char *water); + /// A swarm and its workers for every colony, each inside its own home mask and as near as the /// mask allows to its anchor. Fails the candidate on the first colony that does not fit. template @@ -73,6 +94,12 @@ bool reopenCrampedStarts(Game &, GenerationContext &, const ResourceAmounts &, i int woodRange = 32, int clearRadius = 0, const std::vector *protectedWalls = nullptr); +/// settleColonies for round homes (stampRoundHomes): every colony on its own grass (homeGrassMask), +/// its swarm where homeSwarmSite puts it in a home of `radius` facing out from the middle. +bool settleRoundColonies(Game &, GenerationContext &, const char *stream, + const std::vector &homeOf, const std::vector &homes, + double radius); + /// validateWorld's first check on a design rebuilt from the request: it rebuilt, and it is the /// map's size. `L` needs a `failure` string and a `t` torus. template diff --git a/src/map/generator/shared/Territories.h b/src/map/generator/shared/Territories.h index d466cca5b..b53219f73 100644 --- a/src/map/generator/shared/Territories.h +++ b/src/map/generator/shared/Territories.h @@ -7,9 +7,7 @@ #include #include #include -#include #include -#include #include #include #include diff --git a/src/map/generator/shared/Tessellation.h b/src/map/generator/shared/Tessellation.h index 9f2b38bb7..895a5a484 100644 --- a/src/map/generator/shared/Tessellation.h +++ b/src/map/generator/shared/Tessellation.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "Drawing.h" +#include "Geometry.h" #include "Grid.h" #include "Topology.h" #include @@ -120,4 +121,9 @@ void warpCorners(Tessellation &, int reach, const std::vector &wa /// so every tile belongs to exactly one cell. Empty if some tile was left out or taken twice, which a /// valid tiling never does. std::vector labelTiles(const Tessellation &); +/// A subtile point (sixteenths of a tile) in tile units: where a cell's centre falls on the map. +inline ShapePoint tilePoint(SubtilePoint p) +{ + return {p.x / 16.0, p.y / 16.0}; +} } // namespace MapGeneration From 850ce2cecbd967361273911c7c3c9a99e844f501 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 07:26:58 -0400 Subject: [PATCH 127/136] Translate the landscape controls in every catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nine landscapes and three walled arenas registered 47 strings that were left as English fallbacks in all 32 catalogs, and CI's translation gate fails on the first fallback it sees. Every one now has a translation, aligned with each catalog's existing terms for ridges, passes, homes and resources. Where a catalog's own word is spelled as the English is (Polder in the Germanic and most Slavic catalogs, Coral in es/pt/ro, Spiral in dk/id/sv/tr, Plazas in es, Canals in ca, Grain in fr, Diagonal in de/id) the word is listed as shared vocabulary in test/translation_shared_values.json rather than reworded. Seven keys no generator refers to any more (Marches, March width, Patchwork, Orchards, Straight, Gates and the old home-size failure) are removed from texts.keys.txt and every catalog. texts.br.txt is Brazilian Portuguese; an earlier pass wrote about fifty of this branch's entries (Coral, Spider web and the walled arenas) in Breton. They are rewritten in Portuguese, following texts.pt.txt with Brazilian forms (colônia, trilha, fazenda, menor). Verified: data/check_translations.py --strict, test/test_translations.py, test/test_font_coverage.py and test/test_text_area_layout.py all pass. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 104 ++++++-------- data/texts.br.txt | 204 +++++++++++++--------------- data/texts.ca.txt | 102 ++++++-------- data/texts.cz.txt | 104 ++++++-------- data/texts.de.txt | 100 ++++++-------- data/texts.dk.txt | 102 ++++++-------- data/texts.en.txt | 14 -- data/texts.eo.txt | 104 ++++++-------- data/texts.es.txt | 102 ++++++-------- data/texts.eu.txt | 104 ++++++-------- data/texts.fa.txt | 104 ++++++-------- data/texts.fi.txt | 104 ++++++-------- data/texts.fr.txt | 100 ++++++-------- data/texts.gr.txt | 104 ++++++-------- data/texts.hu.txt | 102 ++++++-------- data/texts.id.txt | 100 ++++++-------- data/texts.it.txt | 102 ++++++-------- data/texts.ja.txt | 104 ++++++-------- data/texts.keys.txt | 7 - data/texts.ko.txt | 104 ++++++-------- data/texts.nl.txt | 100 ++++++-------- data/texts.pl.txt | 102 ++++++-------- data/texts.pt.txt | 104 ++++++-------- data/texts.ro.txt | 102 ++++++-------- data/texts.ru.txt | 104 ++++++-------- data/texts.si.txt | 102 ++++++-------- data/texts.sk.txt | 102 ++++++-------- data/texts.sr.txt | 104 ++++++-------- data/texts.sv.txt | 102 ++++++-------- data/texts.tr.txt | 102 ++++++-------- data/texts.uk.txt | 104 ++++++-------- data/texts.vi.txt | 104 ++++++-------- data/texts.zh-cn.txt | 104 ++++++-------- data/texts.zh-tw.txt | 104 ++++++-------- test/translation_shared_values.json | 37 ++++- 35 files changed, 1503 insertions(+), 1945 deletions(-) diff --git a/data/texts.ar.txt b/data/texts.ar.txt index 3ab8498a8..f9eba2c61 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2064,107 +2064,93 @@ OpenGL غير متوفر في هذا الإصدار. سداسيات [Warp] التواء -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +بصمة الإصبع [Wavelength] -Wavelength +الطول الموجي [Pattern] -Pattern +النمط [Labyrinth] -Labyrinth +متاهة [Channels] -Channels +الأخاديد [Barrier] -Barrier +الحاجز [Water] -Water +ماء [Grain] -Grain +التحبب [Rain shadow] -Rain shadow +ظل المطر [Ridges] -Ridges +الحواف الصخرية [Slant] -Slant +الانحراف [Ridge thickness] -Ridge thickness +سماكة الحواف الصخرية [Pass spacing] -Pass spacing +تباعد الممرات الجبلية [Lee width] -Lee width +عرض جانب الظل [Old growth] -Old growth +الغابة القديمة [Canals] -Canals +القنوات [Forest density] -Forest density +كثافة الغابة [Hidden groves] -Hidden groves +بساتين مخفية [Starting trails] -Starting trails +دروب البداية [Block size] -Block size +حجم الكتلة السكنية [Canal width] -Canal width +عرض القناة [Extra bridges] -Extra bridges +جسور إضافية [Polder] -Polder +بولدر [Old town] -Old town +المدينة القديمة [Anthill] -Anthill -[Patchwork] -Patchwork +عش النمل [Row angle] -Row angle -[Straight] -Straight +زاوية الصفوف [Diagonal] -Diagonal +قطري [Dyke spacing] -Dyke spacing +تباعد السدود [Village size] -Village size +حجم القرية [Hamlets] -Hamlets +القرى الصغيرة [City size] -City size +حجم المدينة [Street width] -Street width +عرض الشوارع [Plazas] -Plazas -[Gates] -Gates +الميادين [Chamber spacing] -Chamber spacing +تباعد الحجرات [Chamber size] -Chamber size +حجم الحجرات [Queen room] -Queen room +حجرة الملكة [Tunnel width] -Tunnel width +عرض الأنفاق [Loops] -Loops +مسارات دائرية [Home pools] -Home pools +برك الموطن [Farm plots] -Farm plots +قطع المزارع [Tendril roads] -Tendril roads +طرق متفرعة [Random] -Random +عشوائي [Vertical] -Vertical +عمودي [Horizontal] -Horizontal +أفقي [Inland lakes] -Inland lakes +بحيرات داخلية diff --git a/data/texts.br.txt b/data/texts.br.txt index 829896af9..b65e15f83 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -1797,7 +1797,7 @@ Tamanho da célula [Corridor width] Largura do corredor [Spoke width] -Ledander ar skinoù +Largura dos raios [Loopiness] Densidade de ciclos [The maze needs at least two cells across and down; use a bigger map or smaller cells.] @@ -1901,97 +1901,97 @@ Tamanho da clareira [Levee] Dique [Sloughs] -Geunioù +Charcos [Everglades need at least one colony.] Os pântanos precisam de pelo menos uma colônia. [Too many colonies for this map; use a bigger map or fewer colonies.] -Re a drevadennoù evit ar gartenn-mañ; implijit ur gartenn vrasoc'h pe nebeutoc'h a drevadennoù. +Colônias demais para este mapa; use um mapa maior ou menos colônias. [Coral] -Koural +Coral [Fork angle] -Korn ar skourradur +Ângulo de ramificação [Lean] -Stouadur +Inclinação [Home size] -Ment an diazez +Tamanho da base [The coral does not fit this map; use a bigger map, narrower straits or fewer colonies.] -Ne zeu ket ar c'houral war ar gartenn-mañ; implijit ur gartenn vrasoc'h, kanolioù moanoc'h pe nebeutoc'h a drevadennoù. +O coral não cabe neste mapa; use um mapa maior, estreitos mais estreitos ou menos colônias. [Branching] -Skourradurioù +Ramificação [Branch width] -Ledander ar skourroù +Largura dos ramos [Coral needs at least one colony.] -Ezhomm en deus ar c'houral eus un drevadenn da vihanañ. +O coral precisa de pelo menos uma colônia. [Coral needs a map at least 128 tiles across.] -Ezhomm en deus ar c'houral eus ur gartenn 128 karrezenn ledan da vihanañ. +O coral precisa de um mapa com pelo menos 128 casas de largura. [Carousel] -Karrousel +Carrossel [Amphitheatre] -Amfiteatr +Anfiteatro [Switchbacks] -Troioù-kamm +Ziguezagues [Court size] -Ment ar porzhioù +Tamanho dos pátios [Court wall] -Moger ar porzhioù +Muro dos pátios [Plaza size] -Ment ar blasenn +Tamanho da praça [Starting tower level] -Live an tourioù kentañ +Nível das torres iniciais [Towers per colony] -Tourioù dre drevadenn +Torres por colônia [Rings] -Kelc'hioù +Anéis [Ramp width] -Ledander ar pantennoù +Largura das rampas [Pit size] -Ment ar poull +Tamanho da arena [Terrace width] -Ledander ar bazennoù +Largura das bancadas [Bay size] -Ment ar baeoù +Tamanho das baías [Border wall] -Moger an harzoù +Muro de fronteira [Plateau size] -Ment ar plaen uhel +Tamanho do planalto [Trail width] -Ledander an hent +Largura da trilha [Wall between legs] -Moger etre an troioù +Muro entre trechos [Farm building plots] -Leurioù-savadur er fermoù +Lotes para construir nas fazendas [Carousels need at least two colonies.] -Ezhomm en deus ar c'harrousel eus div drevadenn da vihanañ. +O carrossel precisa de pelo menos duas colônias. [Switchbacks need at least two colonies.] -Ezhomm o deus an troioù-kamm eus div drevadenn da vihanañ. +Os ziguezagues precisam de pelo menos duas colônias. [An amphitheatre needs at least two colonies.] -Ezhomm en deus an amfiteatr eus div drevadenn da vihanañ. +O anfiteatro precisa de pelo menos duas colônias. [The carousel does not fit this map; use a bigger map, smaller homes or fewer colonies.] -N'eo ket mat ar c'harrousel evit ar gartenn-mañ; implijit ur gartenn vrasoc'h, diazezoù bihanoc'h pe nebeutoc'h a drevadennoù. +O carrossel não cabe neste mapa; use um mapa maior, bases menores ou menos colônias. [The switchbacks do not fit this map; use a bigger map, a smaller plateau, narrower trails or fewer colonies.] -N'eo ket mat an troioù-kamm evit ar gartenn-mañ; implijit ur gartenn vrasoc'h, ur plaen uhel bihanoc'h, hentoù strishoc'h pe nebeutoc'h a drevadennoù. +Os ziguezagues não cabem neste mapa; use um mapa maior, um planalto menor, trilhas mais estreitas ou menos colônias. [The amphitheatre does not fit this map; use a bigger map, fewer rings, narrower terraces or fewer colonies.] -N'eo ket mat an amfiteatr evit ar gartenn-mañ; implijit ur gartenn vrasoc'h, nebeutoc'h a gelc'hioù, pazennoù strishoc'h pe nebeutoc'h a drevadennoù. +O anfiteatro não cabe neste mapa; use um mapa maior, menos anéis, bancadas mais estreitas ou menos colônias. [Spider web] -Gwiad-kevnid +Teia de aranha [Spokes per colony] -Skinoù dre drevadenn +Raios por colônia [Ring spacing] -Esaouenn etre ar c'helc'hioù +Espaçamento dos anéis [Thread width] -Ledander an neudennoù +Largura dos fios [Sag] -Distegnadur +Flacidez [Torn strands] -Neudennoù freget +Fios rompidos [Hub size] -Ment ar c'hreiz +Tamanho do centro [Dew drops] -Takennoù glizh +Gotas de orvalho [Spiral] -Troellenn +Espiral [Spider webs need at least one colony.] -Ezhomm en deus ur gwiad-kevnid eus un drevadenn da vihanañ. +As teias de aranha precisam de pelo menos uma colônia. [Sandbars] Bancos de areia [Tide pools] @@ -2055,114 +2055,100 @@ Colônias em ambas as costas [Winding belt] Faixa sinuosa [Cell shape] -Stumm ar c'hellig +Forma das células [Squares] -Karrezioù +Quadrados [Hexagons] -C'hwec'hkornegoù +Hexágonos [Warp] -Distummañ -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards +Deformação [Fingerprint] -Fingerprint +Impressão digital [Wavelength] -Wavelength +Comprimento de onda [Pattern] -Pattern +Padrão [Labyrinth] -Labyrinth +Labirinto [Channels] -Channels +Sulcos [Barrier] -Barrier +Barreira [Water] -Water +Água [Grain] -Grain +Granulação [Rain shadow] -Rain shadow +Sombra de chuva [Ridges] -Ridges +Cristas [Slant] -Slant +Obliquidade [Ridge thickness] -Ridge thickness +Espessura das cristas [Pass spacing] -Pass spacing +Espaçamento das passagens [Lee width] -Lee width +Largura do sotavento [Old growth] -Old growth +Floresta primária [Canals] -Canals +Canais [Forest density] -Forest density +Densidade da floresta [Hidden groves] -Hidden groves +Bosques escondidos [Starting trails] -Starting trails +Trilhas iniciais [Block size] -Block size +Tamanho do quarteirão [Canal width] -Canal width +Largura dos canais [Extra bridges] -Extra bridges +Pontes extras [Polder] -Polder +Pôlder [Old town] -Old town +Cidade velha [Anthill] -Anthill -[Patchwork] -Patchwork +Formigueiro [Row angle] -Row angle -[Straight] -Straight +Ângulo das fileiras [Diagonal] -Diagonal +Diagonais [Dyke spacing] -Dyke spacing +Espaçamento dos diques [Village size] -Village size +Tamanho dos vilarejos [Hamlets] -Hamlets +Lugarejos [City size] -City size +Tamanho da cidade [Street width] -Street width +Largura das ruas [Plazas] -Plazas -[Gates] -Gates +Praças [Chamber spacing] -Chamber spacing +Espaçamento das câmaras [Chamber size] -Chamber size +Tamanho das câmaras [Queen room] -Queen room +Câmara da rainha [Tunnel width] -Tunnel width +Largura dos túneis [Loops] -Loops +Circuitos [Home pools] -Home pools +Poças da base [Farm plots] -Farm plots +Lotes agrícolas [Tendril roads] -Tendril roads +Estradas em gavinha [Random] -Random +Aleatório [Vertical] -Vertical +Verticais [Horizontal] -Horizontal +Horizontais [Inland lakes] -Inland lakes +Lagos interiores diff --git a/data/texts.ca.txt b/data/texts.ca.txt index f45002f76..5d4ccac39 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2074,107 +2074,93 @@ Quadrats Hexàgons [Warp] Deformació -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Empremta digital [Wavelength] -Wavelength +Longitud d'onda [Pattern] -Pattern +Patró [Labyrinth] -Labyrinth +Laberint [Channels] -Channels +Solcs [Barrier] -Barrier +Barrera [Water] -Water +Aigua [Grain] -Grain +Gra [Rain shadow] -Rain shadow +Ombra pluviomètrica [Ridges] -Ridges +Crestes [Slant] -Slant +Obliqüitat [Ridge thickness] -Ridge thickness +Gruix de les crestes [Pass spacing] -Pass spacing +Separació dels passos [Lee width] -Lee width +Amplada del sotavent [Old growth] -Old growth +Bosc primari [Canals] Canals [Forest density] -Forest density +Densitat del bosc [Hidden groves] -Hidden groves +Boscatges amagats [Starting trails] -Starting trails +Corriols inicials [Block size] -Block size +Mida de les illes de cases [Canal width] -Canal width +Amplada dels canals [Extra bridges] -Extra bridges +Ponts addicionals [Polder] -Polder +Pòlder [Old town] -Old town +Ciutat vella [Anthill] -Anthill -[Patchwork] -Patchwork +Formiguer [Row angle] -Row angle -[Straight] -Straight +Angle de les fileres [Diagonal] -Diagonal +Diagonals [Dyke spacing] -Dyke spacing +Separació dels dics [Village size] -Village size +Mida dels pobles [Hamlets] -Hamlets +Llogarets [City size] -City size +Mida de la ciutat [Street width] -Street width +Amplada dels carrers [Plazas] -Plazas -[Gates] -Gates +Places [Chamber spacing] -Chamber spacing +Separació de les cambres [Chamber size] -Chamber size +Mida de les cambres [Queen room] -Queen room +Cambra de la reina [Tunnel width] -Tunnel width +Amplada dels túnels [Loops] -Loops +Bucles [Home pools] -Home pools +Basses de la base [Farm plots] -Farm plots +Parcel·les de conreu [Tendril roads] -Tendril roads +Camins en circell [Random] -Random +Aleatori [Vertical] -Vertical +Verticals [Horizontal] -Horizontal +Horitzontals [Inland lakes] -Inland lakes +Llacs interiors diff --git a/data/texts.cz.txt b/data/texts.cz.txt index f7ff9a1ff..c37107938 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2066,107 +2066,93 @@ Tvar buněk Šestiúhelníky [Warp] Deformace -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Otisk prstu [Wavelength] -Wavelength +Vlnová délka [Pattern] -Pattern +Vzor [Labyrinth] -Labyrinth +Labyrint [Channels] -Channels +Brázdy [Barrier] -Barrier +Bariéra [Water] -Water +Voda [Grain] -Grain +Zrnitost [Rain shadow] -Rain shadow +Srážkový stín [Ridges] -Ridges +Hřebeny [Slant] -Slant +Zkosení [Ridge thickness] -Ridge thickness +Tloušťka hřebenů [Pass spacing] -Pass spacing +Rozestup průsmyků [Lee width] -Lee width +Šířka závětří [Old growth] -Old growth +Prales [Canals] -Canals +Kanály [Forest density] -Forest density +Hustota lesa [Hidden groves] -Hidden groves +Skryté háje [Starting trails] -Starting trails +Počáteční stezky [Block size] -Block size +Velikost bloku [Canal width] -Canal width +Šířka kanálů [Extra bridges] -Extra bridges +Další mosty [Polder] -Polder +Poldr [Old town] -Old town +Staré město [Anthill] -Anthill -[Patchwork] -Patchwork +Mraveniště [Row angle] -Row angle -[Straight] -Straight +Úhel řad [Diagonal] -Diagonal +Šikmo [Dyke spacing] -Dyke spacing +Rozestup hrází [Village size] -Village size +Velikost vesnic [Hamlets] -Hamlets +Osady [City size] -City size +Velikost města [Street width] -Street width +Šířka ulic [Plazas] -Plazas -[Gates] -Gates +Náměstí [Chamber spacing] -Chamber spacing +Rozestup komor [Chamber size] -Chamber size +Velikost komor [Queen room] -Queen room +Královnina komora [Tunnel width] -Tunnel width +Šířka tunelů [Loops] -Loops +Smyčky [Home pools] -Home pools +Tůně u domova [Farm plots] -Farm plots +Zemědělské parcely [Tendril roads] -Tendril roads +Úponkové cesty [Random] -Random +Náhodně [Vertical] -Vertical +Svisle [Horizontal] -Horizontal +Vodorovně [Inland lakes] -Inland lakes +Vnitrozemská jezera diff --git a/data/texts.de.txt b/data/texts.de.txt index 8e040ba0b..33f551bdb 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2066,107 +2066,93 @@ Quadrate Sechsecke [Warp] Verzerrung -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Fingerabdruck [Wavelength] -Wavelength +Wellenlänge [Pattern] -Pattern +Muster [Labyrinth] -Labyrinth +Irrgarten [Channels] -Channels +Rinnen [Barrier] -Barrier +Barriere [Water] -Water +Wasser [Grain] -Grain +Körnung [Rain shadow] -Rain shadow +Regenschatten [Ridges] -Ridges +Grate [Slant] -Slant +Schräge [Ridge thickness] -Ridge thickness +Gratdicke [Pass spacing] -Pass spacing +Passabstand [Lee width] -Lee width +Leebreite [Old growth] -Old growth +Urwald [Canals] -Canals +Kanäle [Forest density] -Forest density +Walddichte [Hidden groves] -Hidden groves +Versteckte Haine [Starting trails] -Starting trails +Startpfade [Block size] -Block size +Blockgröße [Canal width] -Canal width +Kanalbreite [Extra bridges] -Extra bridges +Zusätzliche Brücken [Polder] Polder [Old town] -Old town +Altstadt [Anthill] -Anthill -[Patchwork] -Patchwork +Ameisenhügel [Row angle] -Row angle -[Straight] -Straight +Reihenwinkel [Diagonal] Diagonal [Dyke spacing] -Dyke spacing +Deichabstand [Village size] -Village size +Dorfgröße [Hamlets] -Hamlets +Weiler [City size] -City size +Stadtgröße [Street width] -Street width +Straßenbreite [Plazas] -Plazas -[Gates] -Gates +Plätze [Chamber spacing] -Chamber spacing +Kammerabstand [Chamber size] -Chamber size +Kammergröße [Queen room] -Queen room +Königinnenkammer [Tunnel width] -Tunnel width +Tunnelbreite [Loops] -Loops +Schleifen [Home pools] -Home pools +Heimatteiche [Farm plots] -Farm plots +Ackerparzellen [Tendril roads] -Tendril roads +Rankenwege [Random] -Random +Zufällig [Vertical] -Vertical +Senkrecht [Horizontal] -Horizontal +Waagerecht [Inland lakes] -Inland lakes +Binnenseen diff --git a/data/texts.dk.txt b/data/texts.dk.txt index fc05a7c52..841d6d5d8 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2142,107 +2142,93 @@ Kvadrater Sekskanter [Warp] Forvrængning -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Fingeraftryk [Wavelength] -Wavelength +Bølgelængde [Pattern] -Pattern +Mønster [Labyrinth] -Labyrinth +Labyrint [Channels] -Channels +Render [Barrier] -Barrier +Barriere [Water] -Water +Vand [Grain] -Grain +Kornethed [Rain shadow] -Rain shadow +Regnskygge [Ridges] -Ridges +Højderygge [Slant] -Slant +Skråstilling [Ridge thickness] -Ridge thickness +Højderyggenes tykkelse [Pass spacing] -Pass spacing +Afstand mellem pas [Lee width] -Lee width +Læsidens bredde [Old growth] -Old growth +Urskov [Canals] -Canals +Kanaler [Forest density] -Forest density +Skovtæthed [Hidden groves] -Hidden groves +Skjulte lunde [Starting trails] -Starting trails +Startstier [Block size] -Block size +Karréstørrelse [Canal width] -Canal width +Kanalbredde [Extra bridges] -Extra bridges +Ekstra broer [Polder] Polder [Old town] -Old town +Gamle bydel [Anthill] -Anthill -[Patchwork] -Patchwork +Myretue [Row angle] -Row angle -[Straight] -Straight +Rækkernes vinkel [Diagonal] -Diagonal +Diagonalt [Dyke spacing] -Dyke spacing +Afstand mellem diger [Village size] -Village size +Landsbystørrelse [Hamlets] -Hamlets +Småbyer [City size] -City size +Byens størrelse [Street width] -Street width +Gadebredde [Plazas] -Plazas -[Gates] -Gates +Pladser [Chamber spacing] -Chamber spacing +Afstand mellem kamre [Chamber size] -Chamber size +Kammerstørrelse [Queen room] -Queen room +Dronningens kammer [Tunnel width] -Tunnel width +Tunnelbredde [Loops] -Loops +Løkker [Home pools] -Home pools +Hjemmets vandhuller [Farm plots] -Farm plots +Marklodder [Tendril roads] -Tendril roads +Slyngveje [Random] -Random +Tilfældig [Vertical] -Vertical +Lodret [Horizontal] -Horizontal +Vandret [Inland lakes] -Inland lakes +Indsøer diff --git a/data/texts.en.txt b/data/texts.en.txt index 2e85272b9..9068b8759 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2064,14 +2064,6 @@ Squares Hexagons [Warp] Warp -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] Fingerprint [Wavelength] @@ -2122,12 +2114,8 @@ Polder Old town [Anthill] Anthill -[Patchwork] -Patchwork [Row angle] Row angle -[Straight] -Straight [Diagonal] Diagonal [Dyke spacing] @@ -2142,8 +2130,6 @@ City size Street width [Plazas] Plazas -[Gates] -Gates [Chamber spacing] Chamber spacing [Chamber size] diff --git a/data/texts.eo.txt b/data/texts.eo.txt index dd7bd9aea..bb7b41cd1 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2064,107 +2064,93 @@ Kvadratoj Sesanguloj [Warp] Deformado -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Fingrospuro [Wavelength] -Wavelength +Ondolongo [Pattern] -Pattern +Motivo [Labyrinth] -Labyrinth +Labirinto [Channels] -Channels +Sulkoj [Barrier] -Barrier +Baro [Water] -Water +Akvo [Grain] -Grain +Grajneco [Rain shadow] -Rain shadow +Pluvombro [Ridges] -Ridges +Krestoj [Slant] -Slant +Oblikveco [Ridge thickness] -Ridge thickness +Dikeco de krestoj [Pass spacing] -Pass spacing +Interspaco de montpasejoj [Lee width] -Lee width +Larĝo de la ŝirmflanko [Old growth] -Old growth +Praarbaro [Canals] -Canals +Kanaloj [Forest density] -Forest density +Denseco de arbaro [Hidden groves] -Hidden groves +Kaŝitaj boskoj [Starting trails] -Starting trails +Komencaj padoj [Block size] -Block size +Grando de kvartaloj [Canal width] -Canal width +Larĝo de kanaloj [Extra bridges] -Extra bridges +Kromaj pontoj [Polder] -Polder +Poldero [Old town] -Old town +Malnova urbo [Anthill] -Anthill -[Patchwork] -Patchwork +Formikejo [Row angle] -Row angle -[Straight] -Straight +Angulo de vicoj [Diagonal] -Diagonal +Diagonala [Dyke spacing] -Dyke spacing +Interspaco de digoj [Village size] -Village size +Grando de vilaĝoj [Hamlets] -Hamlets +Vilaĝetoj [City size] -City size +Grando de la urbo [Street width] -Street width +Larĝo de stratoj [Plazas] -Plazas -[Gates] -Gates +Placoj [Chamber spacing] -Chamber spacing +Interspaco de ĉambroj [Chamber size] -Chamber size +Grando de ĉambroj [Queen room] -Queen room +Ĉambro de la reĝino [Tunnel width] -Tunnel width +Larĝo de tuneloj [Loops] -Loops +Bukloj [Home pools] -Home pools +Lagetoj de la hejmo [Farm plots] -Farm plots +Kampparceloj [Tendril roads] -Tendril roads +Ĉiraj vojoj [Random] -Random +Hazarda [Vertical] -Vertical +Vertikala [Horizontal] -Horizontal +Horizontala [Inland lakes] -Inland lakes +Enlandaj lagoj diff --git a/data/texts.es.txt b/data/texts.es.txt index 1e4cc90c6..a665fdff5 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2066,107 +2066,93 @@ Cuadrados Hexágonos [Warp] Deformación -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Huella dactilar [Wavelength] -Wavelength +Longitud de onda [Pattern] -Pattern +Patrón [Labyrinth] -Labyrinth +Laberinto [Channels] -Channels +Surcos [Barrier] -Barrier +Barrera [Water] -Water +Agua [Grain] -Grain +Granulado [Rain shadow] -Rain shadow +Sombra pluviométrica [Ridges] -Ridges +Crestas [Slant] -Slant +Oblicuidad [Ridge thickness] -Ridge thickness +Grosor de las crestas [Pass spacing] -Pass spacing +Separación de los pasos [Lee width] -Lee width +Anchura del sotavento [Old growth] -Old growth +Bosque primario [Canals] -Canals +Canales [Forest density] -Forest density +Densidad del bosque [Hidden groves] -Hidden groves +Arboledas ocultas [Starting trails] -Starting trails +Senderos iniciales [Block size] -Block size +Tamaño de las manzanas [Canal width] -Canal width +Anchura de los canales [Extra bridges] -Extra bridges +Puentes adicionales [Polder] -Polder +Pólder [Old town] -Old town +Ciudad vieja [Anthill] -Anthill -[Patchwork] -Patchwork +Hormiguero [Row angle] -Row angle -[Straight] -Straight +Ángulo de las hileras [Diagonal] -Diagonal +Diagonales [Dyke spacing] -Dyke spacing +Separación de los diques [Village size] -Village size +Tamaño de los pueblos [Hamlets] -Hamlets +Aldeas [City size] -City size +Tamaño de la ciudad [Street width] -Street width +Anchura de las calles [Plazas] Plazas -[Gates] -Gates [Chamber spacing] -Chamber spacing +Separación de las cámaras [Chamber size] -Chamber size +Tamaño de las cámaras [Queen room] -Queen room +Cámara de la reina [Tunnel width] -Tunnel width +Anchura de los túneles [Loops] -Loops +Bucles [Home pools] -Home pools +Charcas de la base [Farm plots] -Farm plots +Parcelas de cultivo [Tendril roads] -Tendril roads +Caminos en zarcillo [Random] -Random +Aleatorio [Vertical] -Vertical +Verticales [Horizontal] -Horizontal +Horizontales [Inland lakes] -Inland lakes +Lagos interiores diff --git a/data/texts.eu.txt b/data/texts.eu.txt index 69606556c..ecb53638a 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2076,107 +2076,93 @@ Karratuak Hexagonoak [Warp] Deformazioa -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Hatz-marka [Wavelength] -Wavelength +Uhin-luzera [Pattern] -Pattern +Eredua [Labyrinth] -Labyrinth +Labirintoa [Channels] -Channels +Ildoak [Barrier] -Barrier +Hesia [Water] -Water +Ura [Grain] -Grain +Pikortadura [Rain shadow] -Rain shadow +Euri-itzala [Ridges] -Ridges +Gailurrak [Slant] -Slant +Zeihartasuna [Ridge thickness] -Ridge thickness +Gailurren lodiera [Pass spacing] -Pass spacing +Mendateen arteko tartea [Lee width] -Lee width +Haizebearen zabalera [Old growth] -Old growth +Oihan zaharra [Canals] -Canals +Kanalak [Forest density] -Forest density +Basoaren dentsitatea [Hidden groves] -Hidden groves +Baso txiki ezkutuak [Starting trails] -Starting trails +Hasierako bideak [Block size] -Block size +Etxadien tamaina [Canal width] -Canal width +Kanalen zabalera [Extra bridges] -Extra bridges +Zubi gehigarriak [Polder] -Polder +Polderra [Old town] -Old town +Hiri zaharra [Anthill] -Anthill -[Patchwork] -Patchwork +Inurritegia [Row angle] -Row angle -[Straight] -Straight +Ilaren angelua [Diagonal] -Diagonal +Diagonala [Dyke spacing] -Dyke spacing +Dikeen arteko tartea [Village size] -Village size +Herrixken tamaina [Hamlets] -Hamlets +Auzoak [City size] -City size +Hiriaren tamaina [Street width] -Street width +Kaleen zabalera [Plazas] -Plazas -[Gates] -Gates +Plazak [Chamber spacing] -Chamber spacing +Ganberen arteko tartea [Chamber size] -Chamber size +Ganberen tamaina [Queen room] -Queen room +Erreginaren ganbera [Tunnel width] -Tunnel width +Tunelen zabalera [Loops] -Loops +Begiztak [Home pools] -Home pools +Etxeko putzuak [Farm plots] -Farm plots +Soro-lursailak [Tendril roads] -Tendril roads +Kiribil-bideak [Random] -Random +Ausazkoa [Vertical] -Vertical +Bertikala [Horizontal] -Horizontal +Horizontala [Inland lakes] -Inland lakes +Barnealdeko aintzirak diff --git a/data/texts.fa.txt b/data/texts.fa.txt index e706bdb75..4c96ac6fe 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2064,107 +2064,93 @@ OpenGL در این بیلد موجود نیست. شش‌ضلعی‌ها [Warp] پیچش -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +اثر انگشت [Wavelength] -Wavelength +طول موج [Pattern] -Pattern +الگو [Labyrinth] -Labyrinth +هزارتو [Channels] -Channels +شیارها [Barrier] -Barrier +مانع [Water] -Water +آب [Grain] -Grain +دانه‌بندی [Rain shadow] -Rain shadow +سایه بارانی [Ridges] -Ridges +ستیغ‌ها [Slant] -Slant +کجی [Ridge thickness] -Ridge thickness +ضخامت ستیغ‌ها [Pass spacing] -Pass spacing +فاصله گردنه‌ها [Lee width] -Lee width +پهنای پناهگاه باد [Old growth] -Old growth +جنگل کهن [Canals] -Canals +کانال‌ها [Forest density] -Forest density +تراکم جنگل [Hidden groves] -Hidden groves +بیشه‌های پنهان [Starting trails] -Starting trails +کوره‌راه‌های آغازین [Block size] -Block size +اندازه بلوک [Canal width] -Canal width +پهنای کانال [Extra bridges] -Extra bridges +پل‌های اضافی [Polder] -Polder +پولدر [Old town] -Old town +شهر قدیمی [Anthill] -Anthill -[Patchwork] -Patchwork +لانه مورچه [Row angle] -Row angle -[Straight] -Straight +زاویه ردیف‌ها [Diagonal] -Diagonal +قطری [Dyke spacing] -Dyke spacing +فاصله خاکریزها [Village size] -Village size +اندازه روستاها [Hamlets] -Hamlets +آبادی‌های کوچک [City size] -City size +اندازه شهر [Street width] -Street width +پهنای خیابان‌ها [Plazas] -Plazas -[Gates] -Gates +میدان‌ها [Chamber spacing] -Chamber spacing +فاصله حفره‌ها [Chamber size] -Chamber size +اندازه حفره‌ها [Queen room] -Queen room +حفره ملکه [Tunnel width] -Tunnel width +پهنای تونل‌ها [Loops] -Loops +مسیرهای حلقوی [Home pools] -Home pools +آبگیرهای پایگاه [Farm plots] -Farm plots +قطعه‌های زراعی [Tendril roads] -Tendril roads +جاده‌های پیچک‌وار [Random] -Random +تصادفی [Vertical] -Vertical +عمودی [Horizontal] -Horizontal +افقی [Inland lakes] -Inland lakes +دریاچه‌های داخلی diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 55af16d0d..4e286f292 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2066,107 +2066,93 @@ Neliöt Kuusikulmiot [Warp] Vääristymä -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Sormenjälki [Wavelength] -Wavelength +Aallonpituus [Pattern] -Pattern +Kuvio [Labyrinth] -Labyrinth +Labyrintti [Channels] -Channels +Uurteet [Barrier] -Barrier +Este [Water] -Water +Vesi [Grain] -Grain +Rakeisuus [Rain shadow] -Rain shadow +Sadevarjo [Ridges] -Ridges +Harjanteet [Slant] -Slant +Vinous [Ridge thickness] -Ridge thickness +Harjanteiden paksuus [Pass spacing] -Pass spacing +Solien väli [Lee width] -Lee width +Suojanpuolen leveys [Old growth] -Old growth +Aarniometsä [Canals] -Canals +Kanaalit [Forest density] -Forest density +Metsän tiheys [Hidden groves] -Hidden groves +Piilotetut lehdot [Starting trails] -Starting trails +Aloituspolut [Block size] -Block size +Korttelin koko [Canal width] -Canal width +Kanaalin leveys [Extra bridges] -Extra bridges +Lisäsillat [Polder] -Polder +Polderi [Old town] -Old town +Vanha kaupunki [Anthill] -Anthill -[Patchwork] -Patchwork +Muurahaiskeko [Row angle] -Row angle -[Straight] -Straight +Rivien kulma [Diagonal] -Diagonal +Vinottain [Dyke spacing] -Dyke spacing +Patojen väli [Village size] -Village size +Kylän koko [Hamlets] -Hamlets +Pikkukylät [City size] -City size +Kaupungin koko [Street width] -Street width +Katujen leveys [Plazas] -Plazas -[Gates] -Gates +Aukiot [Chamber spacing] -Chamber spacing +Kammioiden väli [Chamber size] -Chamber size +Kammioiden koko [Queen room] -Queen room +Kuningattaren kammio [Tunnel width] -Tunnel width +Tunnelien leveys [Loops] -Loops +Silmukat [Home pools] -Home pools +Kotialueen lammikot [Farm plots] -Farm plots +Peltopalstat [Tendril roads] -Tendril roads +Rönsytiet [Random] -Random +Satunnainen [Vertical] -Vertical +Pystysuoraan [Horizontal] -Horizontal +Vaakasuoraan [Inland lakes] -Inland lakes +Sisämaan järvet diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 0ed6d6431..938a3d5f3 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2076,107 +2076,93 @@ Carrés Hexagones [Warp] Déformation -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Empreinte digitale [Wavelength] -Wavelength +Longueur d'onde [Pattern] -Pattern +Motif [Labyrinth] -Labyrinth +Labyrinthe [Channels] -Channels +Sillons [Barrier] -Barrier +Barrière [Water] -Water +Eau [Grain] Grain [Rain shadow] -Rain shadow +Ombre pluviométrique [Ridges] -Ridges +Crêtes [Slant] -Slant +Obliquité [Ridge thickness] -Ridge thickness +Épaisseur des crêtes [Pass spacing] -Pass spacing +Espacement des cols [Lee width] -Lee width +Largeur sous le vent [Old growth] -Old growth +Forêt primaire [Canals] -Canals +Canaux [Forest density] -Forest density +Densité de la forêt [Hidden groves] -Hidden groves +Bosquets cachés [Starting trails] -Starting trails +Sentiers de départ [Block size] -Block size +Taille des pâtés de maisons [Canal width] -Canal width +Largeur des canaux [Extra bridges] -Extra bridges +Ponts supplémentaires [Polder] Polder [Old town] -Old town +Vieille ville [Anthill] -Anthill -[Patchwork] -Patchwork +Fourmilière [Row angle] -Row angle -[Straight] -Straight +Angle des rangées [Diagonal] -Diagonal +Diagonales [Dyke spacing] -Dyke spacing +Espacement des digues [Village size] -Village size +Taille des villages [Hamlets] -Hamlets +Hameaux [City size] -City size +Taille de la ville [Street width] -Street width +Largeur des rues [Plazas] -Plazas -[Gates] -Gates +Places [Chamber spacing] -Chamber spacing +Espacement des chambres [Chamber size] -Chamber size +Taille des chambres [Queen room] -Queen room +Chambre de la reine [Tunnel width] -Tunnel width +Largeur des tunnels [Loops] -Loops +Boucles [Home pools] -Home pools +Mares de la base [Farm plots] -Farm plots +Parcelles agricoles [Tendril roads] -Tendril roads +Chemins en vrilles [Random] -Random +Aléatoire [Vertical] -Vertical +Verticales [Horizontal] -Horizontal +Horizontales [Inland lakes] -Inland lakes +Lacs intérieurs diff --git a/data/texts.gr.txt b/data/texts.gr.txt index b2495bba8..3e7ef23f8 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2136,107 +2136,93 @@ Cortex Εξάγωνα [Warp] Παραμόρφωση -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Δακτυλικό αποτύπωμα [Wavelength] -Wavelength +Μήκος κύματος [Pattern] -Pattern +Μοτίβο [Labyrinth] -Labyrinth +Λαβύρινθος [Channels] -Channels +Αυλάκια [Barrier] -Barrier +Φράγμα [Water] -Water +Νερό [Grain] -Grain +Κοκκίωση [Rain shadow] -Rain shadow +Ομβροσκιά [Ridges] -Ridges +Κορυφογραμμές [Slant] -Slant +Λοξότητα [Ridge thickness] -Ridge thickness +Πάχος κορυφογραμμών [Pass spacing] -Pass spacing +Απόσταση διάσελων [Lee width] -Lee width +Πλάτος υπήνεμης πλευράς [Old growth] -Old growth +Πρωτογενές δάσος [Canals] -Canals +Κανάλια [Forest density] -Forest density +Πυκνότητα δάσους [Hidden groves] -Hidden groves +Κρυφά άλση [Starting trails] -Starting trails +Αρχικά μονοπάτια [Block size] -Block size +Μέγεθος οικοδομικού τετραγώνου [Canal width] -Canal width +Πλάτος καναλιών [Extra bridges] -Extra bridges +Επιπλέον γέφυρες [Polder] -Polder +Πόλντερ [Old town] -Old town +Παλιά πόλη [Anthill] -Anthill -[Patchwork] -Patchwork +Μυρμηγκοφωλιά [Row angle] -Row angle -[Straight] -Straight +Γωνία σειρών [Diagonal] -Diagonal +Διαγώνια [Dyke spacing] -Dyke spacing +Απόσταση αναχωμάτων [Village size] -Village size +Μέγεθος χωριών [Hamlets] -Hamlets +Οικισμοί [City size] -City size +Μέγεθος πόλης [Street width] -Street width +Πλάτος δρόμων [Plazas] -Plazas -[Gates] -Gates +Πλατείες [Chamber spacing] -Chamber spacing +Απόσταση θαλάμων [Chamber size] -Chamber size +Μέγεθος θαλάμων [Queen room] -Queen room +Θάλαμος βασίλισσας [Tunnel width] -Tunnel width +Πλάτος σηράγγων [Loops] -Loops +Βρόχοι [Home pools] -Home pools +Λιμνούλες βάσης [Farm plots] -Farm plots +Αγροτεμάχια [Tendril roads] -Tendril roads +Δρόμοι-έλικες [Random] -Random +Τυχαία [Vertical] -Vertical +Κάθετα [Horizontal] -Horizontal +Οριζόντια [Inland lakes] -Inland lakes +Εσωτερικές λίμνες diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 17a6c1dad..74d9c66ac 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2066,107 +2066,93 @@ Négyzetek Hatszögek [Warp] Torzítás -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Ujjlenyomat [Wavelength] -Wavelength +Hullámhossz [Pattern] -Pattern +Minta [Labyrinth] -Labyrinth +Labirintus [Channels] -Channels +Barázdák [Barrier] -Barrier +Akadály [Water] -Water +Víz [Grain] -Grain +Szemcsésség [Rain shadow] -Rain shadow +Esőárnyék [Ridges] -Ridges +Gerincek [Slant] -Slant +Ferdeség [Ridge thickness] -Ridge thickness +Gerincek vastagsága [Pass spacing] -Pass spacing +Hágók távolsága [Lee width] -Lee width +Szélárnyék szélessége [Old growth] -Old growth +Őserdő [Canals] -Canals +Csatornák [Forest density] -Forest density +Erdő sűrűsége [Hidden groves] -Hidden groves +Rejtett ligetek [Starting trails] -Starting trails +Kezdő ösvények [Block size] -Block size +Háztömb mérete [Canal width] -Canal width +Csatorna szélessége [Extra bridges] -Extra bridges +További hidak [Polder] Polder [Old town] -Old town +Óváros [Anthill] -Anthill -[Patchwork] -Patchwork +Hangyaboly [Row angle] -Row angle -[Straight] -Straight +Sorok szöge [Diagonal] -Diagonal +Átlós [Dyke spacing] -Dyke spacing +Gátak távolsága [Village size] -Village size +Falvak mérete [Hamlets] -Hamlets +Tanyák [City size] -City size +Város mérete [Street width] -Street width +Utcák szélessége [Plazas] -Plazas -[Gates] -Gates +Terek [Chamber spacing] -Chamber spacing +Kamrák távolsága [Chamber size] -Chamber size +Kamrák mérete [Queen room] -Queen room +Királynő kamrája [Tunnel width] -Tunnel width +Járatok szélessége [Loops] -Loops +Hurkok [Home pools] -Home pools +Otthoni tavacskák [Farm plots] -Farm plots +Szántóparcellák [Tendril roads] -Tendril roads +Kacsutak [Random] -Random +Véletlenszerű [Vertical] -Vertical +Függőleges [Horizontal] -Horizontal +Vízszintes [Inland lakes] -Inland lakes +Belső tavak diff --git a/data/texts.id.txt b/data/texts.id.txt index 80f62cf1a..a84a3dc84 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2062,107 +2062,93 @@ Persegi Segi enam [Warp] Distorsi -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Sidik jari [Wavelength] -Wavelength +Panjang gelombang [Pattern] -Pattern +Pola [Labyrinth] -Labyrinth +Labirin [Channels] -Channels +Alur [Barrier] -Barrier +Penghalang [Water] -Water +Air [Grain] -Grain +Butiran [Rain shadow] -Rain shadow +Bayangan hujan [Ridges] -Ridges +Punggung bukit [Slant] -Slant +Kecondongan [Ridge thickness] -Ridge thickness +Ketebalan punggung bukit [Pass spacing] -Pass spacing +Jarak celah gunung [Lee width] -Lee width +Lebar sisi teduh [Old growth] -Old growth +Hutan purba [Canals] -Canals +Kanal [Forest density] -Forest density +Kerapatan hutan [Hidden groves] -Hidden groves +Rumpun tersembunyi [Starting trails] -Starting trails +Jalan setapak awal [Block size] -Block size +Ukuran blok [Canal width] -Canal width +Lebar kanal [Extra bridges] -Extra bridges +Jembatan tambahan [Polder] Polder [Old town] -Old town +Kota tua [Anthill] -Anthill -[Patchwork] -Patchwork +Sarang semut [Row angle] -Row angle -[Straight] -Straight +Sudut baris [Diagonal] Diagonal [Dyke spacing] -Dyke spacing +Jarak tanggul [Village size] -Village size +Ukuran desa [Hamlets] -Hamlets +Dusun [City size] -City size +Ukuran kota [Street width] -Street width +Lebar jalan [Plazas] -Plazas -[Gates] -Gates +Alun-alun [Chamber spacing] -Chamber spacing +Jarak ruang [Chamber size] -Chamber size +Ukuran ruang [Queen room] -Queen room +Ruang ratu [Tunnel width] -Tunnel width +Lebar terowongan [Loops] -Loops +Jalur melingkar [Home pools] -Home pools +Kolam markas [Farm plots] -Farm plots +Petak ladang [Tendril roads] -Tendril roads +Jalan sulur [Random] -Random +Acak [Vertical] -Vertical +Tegak [Horizontal] -Horizontal +Mendatar [Inland lakes] -Inland lakes +Danau pedalaman diff --git a/data/texts.it.txt b/data/texts.it.txt index fe2396cec..54ee99d05 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2128,107 +2128,93 @@ Quadrati Esagoni [Warp] Deformazione -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Impronta digitale [Wavelength] -Wavelength +Lunghezza d'onda [Pattern] -Pattern +Motivo [Labyrinth] -Labyrinth +Labirinto [Channels] -Channels +Solchi [Barrier] -Barrier +Barriera [Water] -Water +Acqua [Grain] -Grain +Grana [Rain shadow] -Rain shadow +Ombra pluviometrica [Ridges] -Ridges +Creste [Slant] -Slant +Obliquità [Ridge thickness] -Ridge thickness +Spessore delle creste [Pass spacing] -Pass spacing +Distanza tra i passi [Lee width] -Lee width +Larghezza del sottovento [Old growth] -Old growth +Foresta primaria [Canals] -Canals +Canali [Forest density] -Forest density +Densità della foresta [Hidden groves] -Hidden groves +Boschetti nascosti [Starting trails] -Starting trails +Sentieri iniziali [Block size] -Block size +Dimensione degli isolati [Canal width] -Canal width +Larghezza dei canali [Extra bridges] -Extra bridges +Ponti aggiuntivi [Polder] Polder [Old town] -Old town +Città vecchia [Anthill] -Anthill -[Patchwork] -Patchwork +Formicaio [Row angle] -Row angle -[Straight] -Straight +Angolo delle file [Diagonal] -Diagonal +Diagonali [Dyke spacing] -Dyke spacing +Distanza tra gli argini [Village size] -Village size +Dimensione dei villaggi [Hamlets] -Hamlets +Borghi [City size] -City size +Dimensione della città [Street width] -Street width +Larghezza delle strade [Plazas] -Plazas -[Gates] -Gates +Piazze [Chamber spacing] -Chamber spacing +Distanza tra le camere [Chamber size] -Chamber size +Dimensione delle camere [Queen room] -Queen room +Camera della regina [Tunnel width] -Tunnel width +Larghezza dei tunnel [Loops] -Loops +Circuiti [Home pools] -Home pools +Stagni della base [Farm plots] -Farm plots +Appezzamenti agricoli [Tendril roads] -Tendril roads +Strade a viticcio [Random] -Random +Casuale [Vertical] -Vertical +Verticali [Horizontal] -Horizontal +Orizzontali [Inland lakes] -Inland lakes +Laghi interni diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 47471c59e..65c2d8f9a 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2062,107 +2062,93 @@ Globulation 2 のカーソルを表示します。 六角形 [Warp] ゆがみ -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +指紋 [Wavelength] -Wavelength +波長 [Pattern] -Pattern +パターン [Labyrinth] -Labyrinth +迷宮 [Channels] -Channels +溝 [Barrier] -Barrier +障壁 [Water] -Water +水 [Grain] -Grain +粒状感 [Rain shadow] -Rain shadow +雨陰 [Ridges] -Ridges +尾根 [Slant] -Slant +傾斜 [Ridge thickness] -Ridge thickness +尾根の厚さ [Pass spacing] -Pass spacing +峠の間隔 [Lee width] -Lee width +風下側の幅 [Old growth] -Old growth +原生林 [Canals] -Canals +運河 [Forest density] -Forest density +森の密度 [Hidden groves] -Hidden groves +隠れた木立 [Starting trails] -Starting trails +初期の小道 [Block size] -Block size +街区の大きさ [Canal width] -Canal width +運河の幅 [Extra bridges] -Extra bridges +追加の橋 [Polder] -Polder +ポルダー [Old town] -Old town +旧市街 [Anthill] -Anthill -[Patchwork] -Patchwork +アリの巣 [Row angle] -Row angle -[Straight] -Straight +列の角度 [Diagonal] -Diagonal +斜め [Dyke spacing] -Dyke spacing +堤防の間隔 [Village size] -Village size +村の大きさ [Hamlets] -Hamlets +集落 [City size] -City size +街の大きさ [Street width] -Street width +通りの幅 [Plazas] -Plazas -[Gates] -Gates +広場 [Chamber spacing] -Chamber spacing +部屋の間隔 [Chamber size] -Chamber size +部屋の大きさ [Queen room] -Queen room +女王の部屋 [Tunnel width] -Tunnel width +トンネルの幅 [Loops] -Loops +環状路 [Home pools] -Home pools +本拠地の池 [Farm plots] -Farm plots +農地の区画 [Tendril roads] -Tendril roads +つる状の道 [Random] -Random +ランダム [Vertical] -Vertical +縦 [Horizontal] -Horizontal +横 [Inland lakes] -Inland lakes +内陸の湖 diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 6a4ec152d..61ba419ad 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1030,10 +1030,6 @@ [Squares] [Hexagons] [Warp] -[Marches] -[The homes are too small; use a bigger home size.] -[March width] -[Orchards] [Fingerprint] [Wavelength] [Pattern] @@ -1059,9 +1055,7 @@ [Polder] [Old town] [Anthill] -[Patchwork] [Row angle] -[Straight] [Diagonal] [Dyke spacing] [Village size] @@ -1069,7 +1063,6 @@ [City size] [Street width] [Plazas] -[Gates] [Chamber spacing] [Chamber size] [Queen room] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index e879cce6f..6e98534d6 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2062,107 +2062,93 @@ Globulation 2 커서를 표시합니다. 육각형 [Warp] 뒤틀림 -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +지문 [Wavelength] -Wavelength +파장 [Pattern] -Pattern +패턴 [Labyrinth] -Labyrinth +미궁 [Channels] -Channels +고랑 [Barrier] -Barrier +장벽 [Water] -Water +물 [Grain] -Grain +입자 [Rain shadow] -Rain shadow +비 그늘 [Ridges] -Ridges +산등성이 [Slant] -Slant +비스듬함 [Ridge thickness] -Ridge thickness +산등성이 두께 [Pass spacing] -Pass spacing +고갯길 간격 [Lee width] -Lee width +바람그늘 너비 [Old growth] -Old growth +원시림 [Canals] -Canals +운하 [Forest density] -Forest density +숲 밀도 [Hidden groves] -Hidden groves +숨겨진 숲 [Starting trails] -Starting trails +시작 산길 [Block size] -Block size +블록 크기 [Canal width] -Canal width +운하 너비 [Extra bridges] -Extra bridges +추가 다리 [Polder] -Polder +간척지 [Old town] -Old town +옛 도시 [Anthill] -Anthill -[Patchwork] -Patchwork +개미집 [Row angle] -Row angle -[Straight] -Straight +열 각도 [Diagonal] -Diagonal +대각선 [Dyke spacing] -Dyke spacing +제방 간격 [Village size] -Village size +마을 크기 [Hamlets] -Hamlets +작은 마을 [City size] -City size +도시 크기 [Street width] -Street width +거리 너비 [Plazas] -Plazas -[Gates] -Gates +광장 [Chamber spacing] -Chamber spacing +방 간격 [Chamber size] -Chamber size +방 크기 [Queen room] -Queen room +여왕 방 [Tunnel width] -Tunnel width +터널 너비 [Loops] -Loops +순환로 [Home pools] -Home pools +본거지 연못 [Farm plots] -Farm plots +농경지 [Tendril roads] -Tendril roads +덩굴길 [Random] -Random +무작위 [Vertical] -Vertical +세로 [Horizontal] -Horizontal +가로 [Inland lakes] -Inland lakes +내륙 호수 diff --git a/data/texts.nl.txt b/data/texts.nl.txt index b25ec59be..9096b46a1 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2090,107 +2090,93 @@ Vierkanten Zeshoeken [Warp] Vervorming -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Vingerafdruk [Wavelength] -Wavelength +Golflengte [Pattern] -Pattern +Patroon [Labyrinth] -Labyrinth +Labyrint [Channels] -Channels +Geulen [Barrier] -Barrier +Barrière [Water] Water [Grain] -Grain +Korrel [Rain shadow] -Rain shadow +Regenschaduw [Ridges] -Ridges +Ruggen [Slant] -Slant +Scheefstand [Ridge thickness] -Ridge thickness +Dikte van de ruggen [Pass spacing] -Pass spacing +Pasafstand [Lee width] -Lee width +Breedte van de lijzijde [Old growth] -Old growth +Oerbos [Canals] -Canals +Grachten [Forest density] -Forest density +Bosdichtheid [Hidden groves] -Hidden groves +Verborgen bosjes [Starting trails] -Starting trails +Startpaden [Block size] -Block size +Grootte van de bouwblokken [Canal width] -Canal width +Grachtbreedte [Extra bridges] -Extra bridges +Extra bruggen [Polder] Polder [Old town] -Old town +Oude stad [Anthill] -Anthill -[Patchwork] -Patchwork +Mierenhoop [Row angle] -Row angle -[Straight] -Straight +Rijhoek [Diagonal] -Diagonal +Diagonaal [Dyke spacing] -Dyke spacing +Dijkafstand [Village size] -Village size +Dorpsgrootte [Hamlets] -Hamlets +Gehuchten [City size] -City size +Grootte van de stad [Street width] -Street width +Straatbreedte [Plazas] -Plazas -[Gates] -Gates +Pleinen [Chamber spacing] -Chamber spacing +Kamerafstand [Chamber size] -Chamber size +Kamergrootte [Queen room] -Queen room +Koninginnekamer [Tunnel width] -Tunnel width +Tunnelbreedte [Loops] -Loops +Lussen [Home pools] -Home pools +Poelen bij de thuisbasis [Farm plots] -Farm plots +Akkerpercelen [Tendril roads] -Tendril roads +Rankwegen [Random] -Random +Willekeurig [Vertical] -Vertical +Verticaal [Horizontal] -Horizontal +Horizontaal [Inland lakes] -Inland lakes +Binnenmeren diff --git a/data/texts.pl.txt b/data/texts.pl.txt index ce9e31e23..5cd04a776 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2066,107 +2066,93 @@ Kwadraty Sześciokąty [Warp] Zniekształcenie -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Odcisk palca [Wavelength] -Wavelength +Długość fali [Pattern] -Pattern +Wzór [Labyrinth] -Labyrinth +Labirynt [Channels] -Channels +Bruzdy [Barrier] -Barrier +Bariera [Water] -Water +Woda [Grain] -Grain +Ziarnistość [Rain shadow] -Rain shadow +Cień opadowy [Ridges] -Ridges +Grzbiety [Slant] -Slant +Skos [Ridge thickness] -Ridge thickness +Grubość grzbietów [Pass spacing] -Pass spacing +Odstęp przełęczy [Lee width] -Lee width +Szerokość strony zawietrznej [Old growth] -Old growth +Puszcza [Canals] -Canals +Kanały [Forest density] -Forest density +Gęstość lasu [Hidden groves] -Hidden groves +Ukryte gaje [Starting trails] -Starting trails +Ścieżki startowe [Block size] -Block size +Rozmiar kwartałów [Canal width] -Canal width +Szerokość kanałów [Extra bridges] -Extra bridges +Dodatkowe mosty [Polder] Polder [Old town] -Old town +Stare miasto [Anthill] -Anthill -[Patchwork] -Patchwork +Mrowisko [Row angle] -Row angle -[Straight] -Straight +Kąt rzędów [Diagonal] -Diagonal +Ukośne [Dyke spacing] -Dyke spacing +Odstęp grobli [Village size] -Village size +Rozmiar wiosek [Hamlets] -Hamlets +Przysiółki [City size] -City size +Rozmiar miasta [Street width] -Street width +Szerokość ulic [Plazas] -Plazas -[Gates] -Gates +Place [Chamber spacing] -Chamber spacing +Odstęp komór [Chamber size] -Chamber size +Rozmiar komór [Queen room] -Queen room +Komora królowej [Tunnel width] -Tunnel width +Szerokość tuneli [Loops] -Loops +Pętle [Home pools] -Home pools +Stawy bazy [Farm plots] -Farm plots +Działki rolne [Tendril roads] -Tendril roads +Drogi wąsowe [Random] -Random +Losowo [Vertical] -Vertical +Pionowe [Horizontal] -Horizontal +Poziome [Inland lakes] -Inland lakes +Jeziora śródlądowe diff --git a/data/texts.pt.txt b/data/texts.pt.txt index 41591ac28..c5ef41b2e 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2072,107 +2072,93 @@ Quadrados Hexágonos [Warp] Deformação -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Impressão digital [Wavelength] -Wavelength +Comprimento de onda [Pattern] -Pattern +Padrão [Labyrinth] -Labyrinth +Labirinto [Channels] -Channels +Sulcos [Barrier] -Barrier +Barreira [Water] -Water +Água [Grain] -Grain +Granulação [Rain shadow] -Rain shadow +Sombra de chuva [Ridges] -Ridges +Cristas [Slant] -Slant +Obliquidade [Ridge thickness] -Ridge thickness +Espessura das cristas [Pass spacing] -Pass spacing +Espaçamento das passagens [Lee width] -Lee width +Largura do sotavento [Old growth] -Old growth +Floresta primária [Canals] -Canals +Canais [Forest density] -Forest density +Densidade da floresta [Hidden groves] -Hidden groves +Bosques escondidos [Starting trails] -Starting trails +Trilhos iniciais [Block size] -Block size +Tamanho dos quarteirões [Canal width] -Canal width +Largura dos canais [Extra bridges] -Extra bridges +Pontes extra [Polder] -Polder +Pôlder [Old town] -Old town +Cidade velha [Anthill] -Anthill -[Patchwork] -Patchwork +Formigueiro [Row angle] -Row angle -[Straight] -Straight +Ângulo das filas [Diagonal] -Diagonal +Diagonais [Dyke spacing] -Dyke spacing +Espaçamento dos diques [Village size] -Village size +Tamanho das aldeias [Hamlets] -Hamlets +Lugarejos [City size] -City size +Tamanho da cidade [Street width] -Street width +Largura das ruas [Plazas] -Plazas -[Gates] -Gates +Praças [Chamber spacing] -Chamber spacing +Espaçamento das câmaras [Chamber size] -Chamber size +Tamanho das câmaras [Queen room] -Queen room +Câmara da rainha [Tunnel width] -Tunnel width +Largura dos túneis [Loops] -Loops +Circuitos [Home pools] -Home pools +Poças da base [Farm plots] -Farm plots +Parcelas agrícolas [Tendril roads] -Tendril roads +Estradas em gavinha [Random] -Random +Aleatório [Vertical] -Vertical +Verticais [Horizontal] -Horizontal +Horizontais [Inland lakes] -Inland lakes +Lagos interiores diff --git a/data/texts.ro.txt b/data/texts.ro.txt index c712476b9..5212b59b1 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2066,107 +2066,93 @@ Pătrate Hexagoane [Warp] Deformare -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Amprentă [Wavelength] -Wavelength +Lungime de undă [Pattern] -Pattern +Model [Labyrinth] -Labyrinth +Labirint [Channels] -Channels +Brazde [Barrier] -Barrier +Barieră [Water] -Water +Apă [Grain] -Grain +Granulație [Rain shadow] -Rain shadow +Umbră de ploaie [Ridges] -Ridges +Creste [Slant] -Slant +Oblicitate [Ridge thickness] -Ridge thickness +Grosimea crestelor [Pass spacing] -Pass spacing +Distanța dintre trecători [Lee width] -Lee width +Lățimea zonei adăpostite [Old growth] -Old growth +Pădure seculară [Canals] -Canals +Canale [Forest density] -Forest density +Densitatea pădurii [Hidden groves] -Hidden groves +Crânguri ascunse [Starting trails] -Starting trails +Poteci de start [Block size] -Block size +Mărimea cvartalelor [Canal width] -Canal width +Lățimea canalelor [Extra bridges] -Extra bridges +Poduri suplimentare [Polder] Polder [Old town] -Old town +Orașul vechi [Anthill] -Anthill -[Patchwork] -Patchwork +Furnicar [Row angle] -Row angle -[Straight] -Straight +Unghiul rândurilor [Diagonal] -Diagonal +Diagonale [Dyke spacing] -Dyke spacing +Distanța dintre diguri [Village size] -Village size +Mărimea satelor [Hamlets] -Hamlets +Cătune [City size] -City size +Mărimea orașului [Street width] -Street width +Lățimea străzilor [Plazas] -Plazas -[Gates] -Gates +Piețe [Chamber spacing] -Chamber spacing +Distanța dintre camere [Chamber size] -Chamber size +Mărimea camerelor [Queen room] -Queen room +Camera reginei [Tunnel width] -Tunnel width +Lățimea tunelurilor [Loops] -Loops +Bucle [Home pools] -Home pools +Iazuri la bază [Farm plots] -Farm plots +Parcele agricole [Tendril roads] -Tendril roads +Drumuri-cârcei [Random] -Random +Aleatoriu [Vertical] -Vertical +Verticale [Horizontal] -Horizontal +Orizontale [Inland lakes] -Inland lakes +Lacuri interioare diff --git a/data/texts.ru.txt b/data/texts.ru.txt index f8061f21d..44899b2b8 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2064,107 +2064,93 @@ OpenGL недоступен в этой сборке. Шестиугольники [Warp] Искажение -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Отпечаток пальца [Wavelength] -Wavelength +Длина волны [Pattern] -Pattern +Узор [Labyrinth] -Labyrinth +Лабиринт [Channels] -Channels +Борозды [Barrier] -Barrier +Преграда [Water] -Water +Вода [Grain] -Grain +Зернистость [Rain shadow] -Rain shadow +Дождевая тень [Ridges] -Ridges +Хребты [Slant] -Slant +Скос [Ridge thickness] -Ridge thickness +Толщина хребтов [Pass spacing] -Pass spacing +Шаг перевалов [Lee width] -Lee width +Ширина подветренной полосы [Old growth] -Old growth +Старый лес [Canals] -Canals +Каналы [Forest density] -Forest density +Густота леса [Hidden groves] -Hidden groves +Скрытые рощи [Starting trails] -Starting trails +Начальные тропы [Block size] -Block size +Размер кварталов [Canal width] -Canal width +Ширина каналов [Extra bridges] -Extra bridges +Дополнительные мосты [Polder] -Polder +Польдер [Old town] -Old town +Старый город [Anthill] -Anthill -[Patchwork] -Patchwork +Муравейник [Row angle] -Row angle -[Straight] -Straight +Угол рядов [Diagonal] -Diagonal +По диагонали [Dyke spacing] -Dyke spacing +Шаг дамб [Village size] -Village size +Размер деревень [Hamlets] -Hamlets +Хутора [City size] -City size +Размер города [Street width] -Street width +Ширина улиц [Plazas] -Plazas -[Gates] -Gates +Площади [Chamber spacing] -Chamber spacing +Шаг камер [Chamber size] -Chamber size +Размер камер [Queen room] -Queen room +Камера королевы [Tunnel width] -Tunnel width +Ширина туннелей [Loops] -Loops +Петли [Home pools] -Home pools +Пруды у базы [Farm plots] -Farm plots +Земельные наделы [Tendril roads] -Tendril roads +Дороги-усики [Random] -Random +Случайно [Vertical] -Vertical +Вертикально [Horizontal] -Horizontal +Горизонтально [Inland lakes] -Inland lakes +Внутренние озёра diff --git a/data/texts.si.txt b/data/texts.si.txt index c444d53af..e9b7b067c 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2066,107 +2066,93 @@ Kvadrati Šestkotniki [Warp] Popačenje -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Prstni odtis [Wavelength] -Wavelength +Valovna dolžina [Pattern] -Pattern +Vzorec [Labyrinth] -Labyrinth +Labirint [Channels] -Channels +Brazde [Barrier] -Barrier +Pregrada [Water] -Water +Voda [Grain] -Grain +Zrnatost [Rain shadow] -Rain shadow +Padavinska senca [Ridges] -Ridges +Grebeni [Slant] -Slant +Poševnost [Ridge thickness] -Ridge thickness +Debelina grebenov [Pass spacing] -Pass spacing +Razmik prelazov [Lee width] -Lee width +Širina zavetrne strani [Old growth] -Old growth +Pragozd [Canals] -Canals +Kanali [Forest density] -Forest density +Gostota gozda [Hidden groves] -Hidden groves +Skriti gaji [Starting trails] -Starting trails +Začetne poti [Block size] -Block size +Velikost blokov [Canal width] -Canal width +Širina kanalov [Extra bridges] -Extra bridges +Dodatni mostovi [Polder] Polder [Old town] -Old town +Staro mesto [Anthill] -Anthill -[Patchwork] -Patchwork +Mravljišče [Row angle] -Row angle -[Straight] -Straight +Kot vrst [Diagonal] -Diagonal +Diagonalno [Dyke spacing] -Dyke spacing +Razmik nasipov [Village size] -Village size +Velikost vasi [Hamlets] -Hamlets +Zaselki [City size] -City size +Velikost mesta [Street width] -Street width +Širina ulic [Plazas] -Plazas -[Gates] -Gates +Trgi [Chamber spacing] -Chamber spacing +Razmik komor [Chamber size] -Chamber size +Velikost komor [Queen room] -Queen room +Kraljičina komora [Tunnel width] -Tunnel width +Širina rovov [Loops] -Loops +Zanke [Home pools] -Home pools +Mlake ob domu [Farm plots] -Farm plots +Njivske parcele [Tendril roads] -Tendril roads +Vitičaste poti [Random] -Random +Naključno [Vertical] -Vertical +Navpično [Horizontal] -Horizontal +Vodoravno [Inland lakes] -Inland lakes +Celinska jezera diff --git a/data/texts.sk.txt b/data/texts.sk.txt index b0dd951c5..11fdc9fa0 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2074,107 +2074,93 @@ Tvar buniek Šesťuholníky [Warp] Deformácia -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Odtlačok prsta [Wavelength] -Wavelength +Vlnová dĺžka [Pattern] -Pattern +Vzor [Labyrinth] -Labyrinth +Labyrint [Channels] -Channels +Brázdy [Barrier] -Barrier +Bariéra [Water] -Water +Voda [Grain] -Grain +Zrnitosť [Rain shadow] -Rain shadow +Zrážkový tieň [Ridges] -Ridges +Hrebene [Slant] -Slant +Skosenie [Ridge thickness] -Ridge thickness +Hrúbka hrebeňov [Pass spacing] -Pass spacing +Rozostup priesmykov [Lee width] -Lee width +Šírka záveternej strany [Old growth] -Old growth +Prales [Canals] -Canals +Kanály [Forest density] -Forest density +Hustota lesa [Hidden groves] -Hidden groves +Skryté háje [Starting trails] -Starting trails +Počiatočné chodníky [Block size] -Block size +Veľkosť blokov [Canal width] -Canal width +Šírka kanálov [Extra bridges] -Extra bridges +Ďalšie mosty [Polder] Polder [Old town] -Old town +Staré mesto [Anthill] -Anthill -[Patchwork] -Patchwork +Mravenisko [Row angle] -Row angle -[Straight] -Straight +Uhol radov [Diagonal] -Diagonal +Šikmo [Dyke spacing] -Dyke spacing +Rozostup hrádzí [Village size] -Village size +Veľkosť dedín [Hamlets] -Hamlets +Osady [City size] -City size +Veľkosť mesta [Street width] -Street width +Šírka ulíc [Plazas] -Plazas -[Gates] -Gates +Námestia [Chamber spacing] -Chamber spacing +Rozostup komôr [Chamber size] -Chamber size +Veľkosť komôr [Queen room] -Queen room +Kráľovnina komora [Tunnel width] -Tunnel width +Šírka tunelov [Loops] -Loops +Slučky [Home pools] -Home pools +Jazierka pri domove [Farm plots] -Farm plots +Poľné parcely [Tendril roads] -Tendril roads +Úponkové cesty [Random] -Random +Náhodne [Vertical] -Vertical +Zvisle [Horizontal] -Horizontal +Vodorovne [Inland lakes] -Inland lakes +Vnútrozemské jazerá diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 3cd9ceeab..1cdfe52b6 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2066,107 +2066,93 @@ OpenGL није доступан у овој верзији. Шестоуглови [Warp] Изобличење -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Отисак прста [Wavelength] -Wavelength +Таласна дужина [Pattern] -Pattern +Шаблон [Labyrinth] -Labyrinth +Лавиринт [Channels] -Channels +Бразде [Barrier] -Barrier +Препрека [Water] -Water +Вода [Grain] -Grain +Зрнавост [Rain shadow] -Rain shadow +Кишна сенка [Ridges] -Ridges +Гребени [Slant] -Slant +Косина [Ridge thickness] -Ridge thickness +Дебљина гребена [Pass spacing] -Pass spacing +Размак превоја [Lee width] -Lee width +Ширина заветрине [Old growth] -Old growth +Прашума [Canals] -Canals +Канали [Forest density] -Forest density +Густина шуме [Hidden groves] -Hidden groves +Скривени гајеви [Starting trails] -Starting trails +Почетне стазе [Block size] -Block size +Величина блокова [Canal width] -Canal width +Ширина канала [Extra bridges] -Extra bridges +Додатни мостови [Polder] -Polder +Полдер [Old town] -Old town +Стари град [Anthill] -Anthill -[Patchwork] -Patchwork +Мравињак [Row angle] -Row angle -[Straight] -Straight +Угао редова [Diagonal] -Diagonal +Дијагонално [Dyke spacing] -Dyke spacing +Размак насипа [Village size] -Village size +Величина села [Hamlets] -Hamlets +Засеоци [City size] -City size +Величина града [Street width] -Street width +Ширина улица [Plazas] -Plazas -[Gates] -Gates +Тргови [Chamber spacing] -Chamber spacing +Размак комора [Chamber size] -Chamber size +Величина комора [Queen room] -Queen room +Краљичина комора [Tunnel width] -Tunnel width +Ширина тунела [Loops] -Loops +Петље [Home pools] -Home pools +Баре код базе [Farm plots] -Farm plots +Пољопривредне парцеле [Tendril roads] -Tendril roads +Путеви-витице [Random] -Random +Насумично [Vertical] -Vertical +Усправно [Horizontal] -Horizontal +Водоравно [Inland lakes] -Inland lakes +Унутрашња језера diff --git a/data/texts.sv.txt b/data/texts.sv.txt index a7b428385..f75c1f5b6 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2076,107 +2076,93 @@ Kvadrater Hexagoner [Warp] Förvrängning -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Fingeravtryck [Wavelength] -Wavelength +Våglängd [Pattern] -Pattern +Mönster [Labyrinth] -Labyrinth +Labyrint [Channels] -Channels +Rännor [Barrier] -Barrier +Barriär [Water] -Water +Vatten [Grain] -Grain +Kornighet [Rain shadow] -Rain shadow +Regnskugga [Ridges] -Ridges +Åsar [Slant] -Slant +Snedhet [Ridge thickness] -Ridge thickness +Åsarnas tjocklek [Pass spacing] -Pass spacing +Passavstånd [Lee width] -Lee width +Läsidans bredd [Old growth] -Old growth +Urskog [Canals] -Canals +Kanaler [Forest density] -Forest density +Skogstäthet [Hidden groves] -Hidden groves +Dolda dungar [Starting trails] -Starting trails +Startstigar [Block size] -Block size +Kvartersstorlek [Canal width] -Canal width +Kanalbredd [Extra bridges] -Extra bridges +Extra broar [Polder] Polder [Old town] -Old town +Gamla stan [Anthill] -Anthill -[Patchwork] -Patchwork +Myrstack [Row angle] -Row angle -[Straight] -Straight +Radernas vinkel [Diagonal] -Diagonal +Diagonalt [Dyke spacing] -Dyke spacing +Vallavstånd [Village size] -Village size +Bystorlek [Hamlets] -Hamlets +Småbyar [City size] -City size +Stadens storlek [Street width] -Street width +Gatubredd [Plazas] -Plazas -[Gates] -Gates +Torg [Chamber spacing] -Chamber spacing +Kammaravstånd [Chamber size] -Chamber size +Kammarstorlek [Queen room] -Queen room +Drottningens kammare [Tunnel width] -Tunnel width +Tunnelbredd [Loops] -Loops +Slingor [Home pools] -Home pools +Hemmets gölar [Farm plots] -Farm plots +Åkerlotter [Tendril roads] -Tendril roads +Slingervägar [Random] -Random +Slumpmässig [Vertical] -Vertical +Lodrätt [Horizontal] -Horizontal +Vågrätt [Inland lakes] -Inland lakes +Insjöar diff --git a/data/texts.tr.txt b/data/texts.tr.txt index d8d358307..20d86775d 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2074,107 +2074,93 @@ Kareler Altıgenler [Warp] Bükülme -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Parmak izi [Wavelength] -Wavelength +Dalga boyu [Pattern] -Pattern +Desen [Labyrinth] -Labyrinth +Labirent [Channels] -Channels +Oluklar [Barrier] -Barrier +Engel [Water] -Water +Su [Grain] -Grain +Pürüzlülük [Rain shadow] -Rain shadow +Yağmur gölgesi [Ridges] -Ridges +Sırtlar [Slant] -Slant +Yatıklık [Ridge thickness] -Ridge thickness +Sırt kalınlığı [Pass spacing] -Pass spacing +Geçit aralığı [Lee width] -Lee width +Rüzgâraltı genişliği [Old growth] -Old growth +Yaşlı orman [Canals] -Canals +Kanallar [Forest density] -Forest density +Orman yoğunluğu [Hidden groves] -Hidden groves +Gizli korular [Starting trails] -Starting trails +Başlangıç patikaları [Block size] -Block size +Blok boyutu [Canal width] -Canal width +Kanal genişliği [Extra bridges] -Extra bridges +Ek köprüler [Polder] Polder [Old town] -Old town +Eski şehir [Anthill] -Anthill -[Patchwork] -Patchwork +Karınca yuvası [Row angle] -Row angle -[Straight] -Straight +Sıra açısı [Diagonal] -Diagonal +Çapraz [Dyke spacing] -Dyke spacing +Set aralığı [Village size] -Village size +Köy boyutu [Hamlets] -Hamlets +Mezralar [City size] -City size +Şehir boyutu [Street width] -Street width +Sokak genişliği [Plazas] -Plazas -[Gates] -Gates +Meydanlar [Chamber spacing] -Chamber spacing +Oda aralığı [Chamber size] -Chamber size +Oda boyutu [Queen room] -Queen room +Kraliçe odası [Tunnel width] -Tunnel width +Tünel genişliği [Loops] -Loops +Döngüler [Home pools] -Home pools +Üs gölcükleri [Farm plots] -Farm plots +Tarla parselleri [Tendril roads] -Tendril roads +Sarmaşık yollar [Random] -Random +Rastgele [Vertical] -Vertical +Dikey [Horizontal] -Horizontal +Yatay [Inland lakes] -Inland lakes +İç göller diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 1e8914e37..5fe8e8302 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2062,107 +2062,93 @@ OpenGL недоступний у цій збірці. Шестикутники [Warp] Викривлення -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Відбиток пальця [Wavelength] -Wavelength +Довжина хвилі [Pattern] -Pattern +Візерунок [Labyrinth] -Labyrinth +Лабіринт [Channels] -Channels +Борозни [Barrier] -Barrier +Перешкода [Water] -Water +Вода [Grain] -Grain +Зернистість [Rain shadow] -Rain shadow +Дощова тінь [Ridges] -Ridges +Хребти [Slant] -Slant +Скіс [Ridge thickness] -Ridge thickness +Товщина хребтів [Pass spacing] -Pass spacing +Крок перевалів [Lee width] -Lee width +Ширина підвітряної смуги [Old growth] -Old growth +Праліс [Canals] -Canals +Канали [Forest density] -Forest density +Густота лісу [Hidden groves] -Hidden groves +Приховані гаї [Starting trails] -Starting trails +Початкові стежки [Block size] -Block size +Розмір кварталів [Canal width] -Canal width +Ширина каналів [Extra bridges] -Extra bridges +Додаткові мости [Polder] -Polder +Польдер [Old town] -Old town +Старе місто [Anthill] -Anthill -[Patchwork] -Patchwork +Мурашник [Row angle] -Row angle -[Straight] -Straight +Кут рядів [Diagonal] -Diagonal +По діагоналі [Dyke spacing] -Dyke spacing +Крок дамб [Village size] -Village size +Розмір сіл [Hamlets] -Hamlets +Хутори [City size] -City size +Розмір міста [Street width] -Street width +Ширина вулиць [Plazas] -Plazas -[Gates] -Gates +Площі [Chamber spacing] -Chamber spacing +Крок камер [Chamber size] -Chamber size +Розмір камер [Queen room] -Queen room +Камера королеви [Tunnel width] -Tunnel width +Ширина тунелів [Loops] -Loops +Петлі [Home pools] -Home pools +Ставки біля бази [Farm plots] -Farm plots +Земельні наділи [Tendril roads] -Tendril roads +Дороги-вусики [Random] -Random +Випадково [Vertical] -Vertical +Вертикально [Horizontal] -Horizontal +Горизонтально [Inland lakes] -Inland lakes +Внутрішні озера diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 11a57dd36..777fe2fc0 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2062,107 +2062,93 @@ Hình vuông Lục giác [Warp] Biến dạng -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +Dấu vân tay [Wavelength] -Wavelength +Bước sóng [Pattern] -Pattern +Kiểu mẫu [Labyrinth] -Labyrinth +Mê cung [Channels] -Channels +Rãnh [Barrier] -Barrier +Rào cản [Water] -Water +Nước [Grain] -Grain +Độ hạt [Rain shadow] -Rain shadow +Bóng mưa [Ridges] -Ridges +Dãy núi [Slant] -Slant +Độ xiên [Ridge thickness] -Ridge thickness +Độ dày dãy núi [Pass spacing] -Pass spacing +Khoảng cách đèo [Lee width] -Lee width +Độ rộng khuất gió [Old growth] -Old growth +Rừng già [Canals] -Canals +Kênh đào [Forest density] -Forest density +Mật độ rừng [Hidden groves] -Hidden groves +Lùm cây ẩn [Starting trails] -Starting trails +Lối mòn ban đầu [Block size] -Block size +Kích thước khu phố [Canal width] -Canal width +Độ rộng kênh [Extra bridges] -Extra bridges +Cầu bổ sung [Polder] -Polder +Đất lấn biển [Old town] -Old town +Phố cổ [Anthill] -Anthill -[Patchwork] -Patchwork +Tổ kiến [Row angle] -Row angle -[Straight] -Straight +Góc hàng [Diagonal] -Diagonal +Chéo [Dyke spacing] -Dyke spacing +Khoảng cách đê [Village size] -Village size +Kích thước làng [Hamlets] -Hamlets +Xóm [City size] -City size +Kích thước thành phố [Street width] -Street width +Độ rộng đường phố [Plazas] -Plazas -[Gates] -Gates +Quảng trường [Chamber spacing] -Chamber spacing +Khoảng cách khoang [Chamber size] -Chamber size +Kích thước khoang [Queen room] -Queen room +Phòng kiến chúa [Tunnel width] -Tunnel width +Độ rộng đường hầm [Loops] -Loops +Vòng lặp [Home pools] -Home pools +Ao căn cứ [Farm plots] -Farm plots +Lô đất canh tác [Tendril roads] -Tendril roads +Đường dây leo [Random] -Random +Ngẫu nhiên [Vertical] -Vertical +Dọc [Horizontal] -Horizontal +Ngang [Inland lakes] -Inland lakes +Hồ nội địa diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index ab6a22858..71947b4e6 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2062,107 +2062,93 @@ OpenGL 在此版本中不可用。 六边形 [Warp] 扭曲 -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +指纹 [Wavelength] -Wavelength +波长 [Pattern] -Pattern +图案 [Labyrinth] -Labyrinth +迷宫 [Channels] -Channels +沟槽 [Barrier] -Barrier +屏障 [Water] -Water +水 [Grain] -Grain +颗粒度 [Rain shadow] -Rain shadow +雨影 [Ridges] -Ridges +山脊 [Slant] -Slant +倾斜度 [Ridge thickness] -Ridge thickness +山脊厚度 [Pass spacing] -Pass spacing +山口间距 [Lee width] -Lee width +背风带宽度 [Old growth] -Old growth +原始森林 [Canals] -Canals +运河 [Forest density] -Forest density +森林密度 [Hidden groves] -Hidden groves +隐蔽林地 [Starting trails] -Starting trails +初始小径 [Block size] -Block size +街区大小 [Canal width] -Canal width +运河宽度 [Extra bridges] -Extra bridges +额外桥梁 [Polder] -Polder +圩田 [Old town] -Old town +老城 [Anthill] -Anthill -[Patchwork] -Patchwork +蚁丘 [Row angle] -Row angle -[Straight] -Straight +行列角度 [Diagonal] -Diagonal +斜向 [Dyke spacing] -Dyke spacing +堤坝间距 [Village size] -Village size +村庄大小 [Hamlets] -Hamlets +小村落 [City size] -City size +城市大小 [Street width] -Street width +街道宽度 [Plazas] -Plazas -[Gates] -Gates +广场 [Chamber spacing] -Chamber spacing +巢室间距 [Chamber size] -Chamber size +巢室大小 [Queen room] -Queen room +蚁后室 [Tunnel width] -Tunnel width +隧道宽度 [Loops] -Loops +环路 [Home pools] -Home pools +基地水池 [Farm plots] -Farm plots +农田地块 [Tendril roads] -Tendril roads +藤蔓状道路 [Random] -Random +随机 [Vertical] -Vertical +纵向 [Horizontal] -Horizontal +横向 [Inland lakes] -Inland lakes +内陆湖 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 8291def6e..4d656cbbf 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2138,107 +2138,93 @@ OpenGL 在此版本中不可用。 六邊形 [Warp] 扭曲 -[Marches] -Marches -[The homes are too small; use a bigger home size.] -The homes are too small; use a bigger home size. -[March width] -March width -[Orchards] -Orchards [Fingerprint] -Fingerprint +指紋 [Wavelength] -Wavelength +波長 [Pattern] -Pattern +圖案 [Labyrinth] -Labyrinth +迷宮 [Channels] -Channels +溝槽 [Barrier] -Barrier +屏障 [Water] -Water +水 [Grain] -Grain +顆粒度 [Rain shadow] -Rain shadow +雨影 [Ridges] -Ridges +山脊 [Slant] -Slant +傾斜度 [Ridge thickness] -Ridge thickness +山脊厚度 [Pass spacing] -Pass spacing +隘口間距 [Lee width] -Lee width +背風帶寬度 [Old growth] -Old growth +原始森林 [Canals] -Canals +運河 [Forest density] -Forest density +森林密度 [Hidden groves] -Hidden groves +隱蔽林地 [Starting trails] -Starting trails +初始小徑 [Block size] -Block size +街區大小 [Canal width] -Canal width +運河寬度 [Extra bridges] -Extra bridges +額外橋樑 [Polder] -Polder +圩田 [Old town] -Old town +老城 [Anthill] -Anthill -[Patchwork] -Patchwork +蟻丘 [Row angle] -Row angle -[Straight] -Straight +行列角度 [Diagonal] -Diagonal +斜向 [Dyke spacing] -Dyke spacing +堤壩間距 [Village size] -Village size +村莊大小 [Hamlets] -Hamlets +小村落 [City size] -City size +城市大小 [Street width] -Street width +街道寬度 [Plazas] -Plazas -[Gates] -Gates +廣場 [Chamber spacing] -Chamber spacing +巢室間距 [Chamber size] -Chamber size +巢室大小 [Queen room] -Queen room +蟻后室 [Tunnel width] -Tunnel width +隧道寬度 [Loops] -Loops +環路 [Home pools] -Home pools +基地水池 [Farm plots] -Farm plots +農田地塊 [Tendril roads] -Tendril roads +藤蔓狀道路 [Random] -Random +隨機 [Vertical] -Vertical +縱向 [Horizontal] -Horizontal +橫向 [Inland lakes] -Inland lakes +內陸湖 diff --git a/test/translation_shared_values.json b/test/translation_shared_values.json index 31474bd75..029429607 100644 --- a/test/translation_shared_values.json +++ b/test/translation_shared_values.json @@ -57,6 +57,7 @@ "shared_vocabulary": { "texts.ar.txt": [], "texts.br.txt": [ + "Coral", "Editor", "Hospital", "Magenta", @@ -65,6 +66,7 @@ ], "texts.ca.txt": [ "Audio", + "Canals", "Editor", "Hospital", "Magenta", @@ -84,12 +86,14 @@ "Audio", "Briefing", "Cyan", + "Diagonal", "Editor", "Hunger", "Layout", "Magenta", "Maximum", "Name", + "Polder", "Prestige", "Radius", "Sand", @@ -109,9 +113,11 @@ "Magenta", "Medium", "Pink", + "Polder", "Prestige", "Radius", "Sand", + "Spiral", "Standard", "Start", "Team", @@ -123,10 +129,12 @@ "Magenta" ], "texts.es.txt": [ + "Coral", "Editor", "Hospital", "Magenta", "No", + "Plazas", "Total", "Tutorial" ], @@ -146,9 +154,11 @@ "Colonies", "Construction", "Cyan", + "Grain", "Magenta", "Options", "Orange", + "Polder", "Prestige", "Proportions", "Standard", @@ -162,12 +172,16 @@ "texts.hu.txt": [ "Editor", "Info", - "Magenta" + "Magenta", + "Polder" ], "texts.id.txt": [ "Audio", + "Diagonal", "Editor", + "Polder", "Radius", + "Spiral", "Total", "Tutorial" ], @@ -177,6 +191,7 @@ "Editor", "Magenta", "No", + "Polder", "Standard", "Start" ], @@ -189,6 +204,7 @@ "Info", "Lobby", "Magenta", + "Polder", "School", "Team", "Tutorial", @@ -197,9 +213,11 @@ ], "texts.pl.txt": [ "Audio", - "Magenta" + "Magenta", + "Polder" ], "texts.pt.txt": [ + "Coral", "Editor", "Hospital", "Magenta", @@ -209,10 +227,12 @@ "texts.ro.txt": [ "Audio", "Control", + "Coral", "Cyan", "Editor", "Info", "Magenta", + "Polder", "Standard", "Total", "Tutorial" @@ -220,11 +240,13 @@ "texts.ru.txt": [], "texts.si.txt": [ "Cyan", - "Magenta" + "Magenta", + "Polder" ], "texts.sk.txt": [ "Editor", - "Info" + "Info", + "Polder" ], "texts.sr.txt": [], "texts.sv.txt": [ @@ -237,13 +259,18 @@ "Lobby", "Magenta", "Medium", + "Polder", "Prestige", "Sand", + "Spiral", "Standard", "Team", "Version" ], - "texts.tr.txt": [], + "texts.tr.txt": [ + "Polder", + "Spiral" + ], "texts.uk.txt": [], "texts.vi.txt": [], "texts.zh-cn.txt": [], From 953fe431c6b104187eecacae99552b52b0f0e163 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 07:26:58 -0400 Subject: [PATCH 128/136] Review fixes: mingw build, test link surface, Stone Highlands passes, style Four items from the review of b86c59b: - Tessellation.cpp named a vector `near`, a legacy windef.h macro that expands to nothing on mingw and broke the Windows build. Renamed `nearby`. - makeHomogenMap, controlSand and smoothResources had been folded into MapTerrain.cpp, which test/SConstruct links against a stripped server-mode libgag with no globalContainer, so the unit-test build no longer linked. They are back in their own translation unit, src/map/generator/MapHomogen.cpp, listed with the generator sources; nothing outside the generators and their tests calls them. - Stone Highlands' nearbyValleys returned before its sort when a fourth valley was in the window, so a valley pair could key the anyPair map in either order and the fallback pass-carving saw split candidate lists. The helper now keeps a sorted three-slot set and always returns it sorted. The same shape removes the std::sort over a runtime-bounded raw array that GCC 15 flagged with -Warray-bounds. The generator's revision moves to 3; the golden rows for the sampled seeds are unchanged, so the fix only bites where the fallback ran. - Glob2.cpp had two includes above its SPDX header and three space-indented lines in a tab-indented file. Verified: scons release=1 builds with no warnings; MapGeneratorDefaultsTest passes; MapGeneratorGoldenTest passes after --update (only the Stone Highlands revision column changed); test/MapExploredAreaSaveLoadTest builds and passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- src/Glob2.cpp | 12 +-- src/SConscript | 1 + src/map/MapTerrain.cpp | 98 ------------------- src/map/generator/MapHomogen.cpp | 92 +++++++++++++++++ .../generators/StoneHighlandsGenerator.cpp | 54 ++++++---- src/map/generator/shared/Tessellation.cpp | 8 +- test/map-generator-golden.txt | 16 +-- 7 files changed, 148 insertions(+), 133 deletions(-) create mode 100644 src/map/generator/MapHomogen.cpp diff --git a/src/Glob2.cpp b/src/Glob2.cpp index ee42b9c16..08521e056 100644 --- a/src/Glob2.cpp +++ b/src/Glob2.cpp @@ -1,5 +1,3 @@ -#include "GeneratorRegistry.h" -#include "GenerationContext.h" // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière @@ -15,9 +13,11 @@ #include "EditorMainMenu.h" #include "Engine.h" #include "Game.h" +#include "GenerationContext.h" +#include "GenerationService.h" +#include "GeneratorRegistry.h" #include "LANMenuScreen.h" #include "MainMenuScreen.h" -#include "GenerationService.h" #include "SettingsScreen.h" #include #include "Utilities.h" @@ -177,7 +177,7 @@ int Glob2::runTestMapGeneration() using D = GenerationRequest; const auto methods=GeneratorRegistry::builtins().methods(false); - auto method=methods[syncRand()%methods.size()]; + auto method=methods[syncRand()%methods.size()]; descriptor.setMethodDefaults(method); auto controls = D::sharedControls(); const auto& specific = D::controls(method); @@ -194,8 +194,8 @@ int Glob2::runTestMapGeneration() GenerationService generator; Game game(NULL); descriptor.seed=syncRand(); - auto result=generator.generate(game, descriptor); - if(!result) std::cerr << result.diagnostic() << std::endl; + auto result=generator.generate(game, descriptor); + if(!result) std::cerr << result.diagnostic() << std::endl; } return 0; } diff --git a/src/SConscript b/src/SConscript index 89b99e345..9df565b14 100644 --- a/src/SConscript +++ b/src/SConscript @@ -174,6 +174,7 @@ LandscapePreviewer.cpp MainMenuScreen.cpp map/Map.cpp map/generator/compatibility/LegacyGenerationDescriptor.cpp +map/generator/MapHomogen.cpp map/generator/core/GenerationContext.cpp map/generator/core/GenerationService.cpp map/generator/core/GenerationValidation.cpp diff --git a/src/map/MapTerrain.cpp b/src/map/MapTerrain.cpp index 044941980..7ef7b460b 100644 --- a/src/map/MapTerrain.cpp +++ b/src/map/MapTerrain.cpp @@ -232,101 +232,3 @@ Uint16 Map::lookup(Uint8 tl, Uint8 tr, Uint8 bl, Uint8 br) const return terrainLookupTable[index][0]+(syncRand()%terrainLookupTable[index][1]); } - - - -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "GlobalContainer.h" -#include "Map.h" -#include "MapInternal.h" -#include "Unit.h" -#include "Utilities.h" - -///generates a map that is of one terrain type only -void Map::makeHomogenMap(TerrainType terrainType) -{ - for (int y=0; yresourcesTypes.get(r.type); - if (int(r.amount)-RESOURCE_INITIAL_AMOUNT<=int(syncRand()&3)) - { - if (r.amountsizesCount) - r.amount++; - } - else - { - int dx, dy; - Unit::dxDyFromDirection(syncRand()&7, &dx, &dy); - const int nx=normalizeX(x+dx), ny=normalizeY(y+dy); - if (getResource(nx, ny).type==NO_RES_TYPE && isResourceAllowed(nx, ny, r.type)) - { - Resource &sprout=tiles[coordToIndex(nx, ny)].resource; - sprout.type=r.type; - sprout.variety=syncRand()%rt->varietiesCount; - sprout.amount=RESOURCE_INITIAL_AMOUNT; - sprout.animation=0; - } - } - } -} - diff --git a/src/map/generator/MapHomogen.cpp b/src/map/generator/MapHomogen.cpp new file mode 100644 index 000000000..d8f60d320 --- /dev/null +++ b/src/map/generator/MapHomogen.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière + +// The whole-map passes the generators share: a uniform terrain to start from, the sand rule that +// keeps water off grass, and the resource-fraying pass. Kept out of MapTerrain.cpp on purpose: +// smoothResources needs globalContainer's resource types, and the test build links MapTerrain.cpp +// against a stripped server-mode libgag that has no globalContainer (test/SConstruct). +#include "GlobalContainer.h" +#include "Map.h" +#include "MapInternal.h" +#include "Unit.h" +#include "Utilities.h" + +///generates a map that is of one terrain type only +void Map::makeHomogenMap(TerrainType terrainType) +{ + for (int y=0; yresourcesTypes.get(r.type); + if (int(r.amount)-RESOURCE_INITIAL_AMOUNT<=int(syncRand()&3)) + { + if (r.amountsizesCount) + r.amount++; + } + else + { + int dx, dy; + Unit::dxDyFromDirection(syncRand()&7, &dx, &dy); + const int nx=normalizeX(x+dx), ny=normalizeY(y+dy); + if (getResource(nx, ny).type==NO_RES_TYPE && isResourceAllowed(nx, ny, r.type)) + { + Resource &sprout=tiles[coordToIndex(nx, ny)].resource; + sprout.type=r.type; + sprout.variety=syncRand()%rt->varietiesCount; + sprout.amount=RESOURCE_INITIAL_AMOUNT; + sprout.animation=0; + } + } + } +} diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 987539347..0514a4527 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -159,22 +159,43 @@ struct Disjoint } }; -// Distinct valley ids within Chebyshev radius r of a tile, sorted, at most `limit` of them. -int nearbyValleys(const Layout &L, int x, int y, int r, int ids[], int limit) +// Up to three distinct valley ids, in ascending order, so a pair of valleys keys the same map entry +// whichever of the two a scan met first. +struct ValleyIds { + int id[3] = {-1, -1, -1}; int count = 0; + bool has(int v) const + { + return (count > 0 && id[0] == v) || (count > 1 && id[1] == v) || (count > 2 && id[2] == v); + } + // Insertion into the sorted prefix; the set holds at most three ids, so this is the whole sort + // and the compiler can see every index stays inside the array. + void add(int v) + { + int i = count++; + for (; i > 0 && id[i - 1] > v; --i) + id[i] = id[i - 1]; + id[i] = v; + } +}; + +// The distinct valley ids within Chebyshev radius r of a tile, sorted, stopping at three: the +// callers only ask whether a tile sees two valleys or a junction of three or more. +ValleyIds nearbyValleys(const Layout &L, int x, int y, int r) +{ + ValleyIds ids; for (int dy = -r; dy <= r; ++dy) for (int dx = -r; dx <= r; ++dx) { const int v = L.valley[L.t.at(x + dx, y + dy)]; - if (v < 0 || std::find(ids, ids + count, v) != ids + count) + if (v < 0 || ids.has(v)) continue; - if (count == limit) - return count; - ids[count++] = v; + if (ids.count == 3) + return ids; + ids.add(v); } - std::sort(ids, ids + count); - return count; + return ids; } // Whether valleys a and b meet inside the window around (cx, cy), walking through non-ridge @@ -619,15 +640,14 @@ Layout design(const GenerationRequest &request, GenerationContext &context) // Not near/far: both are legacy macros in Windows' windef.h that expand to nothing, // which turns `int near[3]` into a structured binding declaration and fails to compile // on mingw. - int nearIds[3], farIds[3]; - const int count = nearbyValleys(L, x, y, 2, nearIds, 3); - if (count >= 3) + const ValleyIds close = nearbyValleys(L, x, y, 2); + if (close.count >= 3) junction[i] = 1; - for (int p = 0; p < count; ++p) - for (int q = p + 1; q < count; ++q) - anyPair[{nearIds[p], nearIds[q]}].push_back(i); - if (count == 2 && nearbyValleys(L, x, y, reach, farIds, 3) == 2) - exclusive[{nearIds[0], nearIds[1]}].push_back(i); + for (int p = 0; p < close.count; ++p) + for (int q = p + 1; q < close.count; ++q) + anyPair[{close.id[p], close.id[q]}].push_back(i); + if (close.count == 2 && nearbyValleys(L, x, y, reach).count == 2) + exclusive[{close.id[0], close.id[1]}].push_back(i); } const std::vector alongRidge = stepsFrom(t, junction, L.ridge); @@ -1132,7 +1152,7 @@ GeneratorDefinition stoneHighlandsDefinition() return {"stone-highlands", 14, "Stone highlands", - 2, + 3, false, {// Average spacing between valley centres, in tiles. {"valley-size", "Valley size", 20, 44, 4, 32, ControlGroup::Layout}, diff --git a/src/map/generator/shared/Tessellation.cpp b/src/map/generator/shared/Tessellation.cpp index b1c1afc83..38fa2fbc4 100644 --- a/src/map/generator/shared/Tessellation.cpp +++ b/src/map/generator/shared/Tessellation.cpp @@ -337,7 +337,7 @@ void warpCorners(Tessellation &g, int reach, const std::vector &w // Pairs that could come within minimumGap once both move, and the gap each must keep: its // unwarped gap, or minimumGap if that is less. Obstacles sharing a corner meet there by design. const int drift = 2 * (reach / kSubtile + 2); - std::vector>> near(obstacles.size()); + std::vector>> nearby(obstacles.size()); for (size_t a = 0; a < obstacles.size(); ++a) for (size_t b = a + 1; b < obstacles.size(); ++b) { @@ -351,8 +351,8 @@ void warpCorners(Tessellation &g, int reach, const std::vector &w if (apart >= minimumGap + drift) continue; const int floor = std::min(apart, minimumGap); - near[a].push_back({int(b), floor}); - near[b].push_back({int(a), floor}); + nearby[a].push_back({int(b), floor}); + nearby[b].push_back({int(a), floor}); } const auto clearanceOf = [&](int cell) @@ -384,7 +384,7 @@ void warpCorners(Tessellation &g, int reach, const std::vector &w for (size_t k = 0; fits && k < cornerObstacles[corner].size(); ++k) { const int o = cornerObstacles[corner][k]; - for (const auto &pair : near[o]) + for (const auto &pair : nearby[o]) if (gap(obstacles[o], obstacles[pair.first], pair.second) < pair.second) { fits = false; diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 5c5859805..2d55e8954 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -104,14 +104,14 @@ macos-arm64 13 1 8 8 4 3 ok 13134881442491555389 macos-arm64 13 1 8 8 8 1 ok 16484172863350979963 macos-arm64 13 1 9 8 4 1 ok 7377329900378929112 macos-arm64 13 1 9 9 4 1 ok 9971144189844359399 -macos-arm64 14 2 7 7 4 1 ok 17202733506137778788 -macos-arm64 14 2 8 8 2 1 ok 16088282592338295379 -macos-arm64 14 2 8 8 4 1 ok 11925006621409592431 -macos-arm64 14 2 8 8 4 2 ok 414349330464996680 -macos-arm64 14 2 8 8 4 3 ok 760189253045661613 -macos-arm64 14 2 8 8 8 1 ok 3322640947906419646 -macos-arm64 14 2 9 8 4 1 ok 2403928672037668991 -macos-arm64 14 2 9 9 4 1 ok 1552916736750568752 +macos-arm64 14 3 7 7 4 1 ok 17202733506137778788 +macos-arm64 14 3 8 8 2 1 ok 16088282592338295379 +macos-arm64 14 3 8 8 4 1 ok 11925006621409592431 +macos-arm64 14 3 8 8 4 2 ok 414349330464996680 +macos-arm64 14 3 8 8 4 3 ok 760189253045661613 +macos-arm64 14 3 8 8 8 1 ok 3322640947906419646 +macos-arm64 14 3 9 8 4 1 ok 2403928672037668991 +macos-arm64 14 3 9 9 4 1 ok 1552916736750568752 macos-arm64 15 1 7 7 4 1 ok 18061507907613243474 macos-arm64 15 1 8 8 2 1 ok 14205129108449246087 macos-arm64 15 1 8 8 4 1 ok 1822623745155517541 From c6fb9d51c32d36cd8cb18e96b812d042c84588eb Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 07:37:29 -0400 Subject: [PATCH 129/136] Golden check can require rows; Old growth's lake size survives preferences - MapGeneratorGoldenTest gains --require-rows: the same check, but a platform with no rows in the table fails instead of reporting and passing. The plain check keeps passing on a rowless platform so a new machine can run it before its rows exist. CI will pass the flag once the linux-x86_64 rows are committed; until then the workflow runs --print before the check, so the rows a platform would record are in the log even when the check fails. - CustomGamePreferences bounded the legacy riverDiameter slot at 100, and Old growth's lake size, which shares that slot, reaches 160: a saved lobby with a big lake failed to load and fell back to defaults, and CustomGameSetupHarness's whole-range round trip caught it. The envelope is now 0-160. Verified: CustomGameSetupHarness passes in default, preferences-write and preferences-read modes; MapGeneratorGoldenTest passes with and without --require-rows on macos-arm64; --sweep reports 0 failing combinations; test/TestsRunner and every test/ harness pass; MenuColonyHarness check and navigation pass. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .github/workflows/build.yml | 7 ++++--- src/CustomGamePreferences.h | 10 +++++----- test/MapGeneratorGoldenTest.cpp | 31 ++++++++++++++++++------------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64a12c45a..06b42039e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,10 +109,11 @@ jobs: run: | scons -j$(nproc) release=1 server=0 custom-setup-test map-generator-defaults-test map-generator-golden-test timeout 60s ./build/src/MapGeneratorDefaultsTest glob2-map-defaults-ci - # Golden maps: rows exist per platform; a platform without rows reports and passes, - # and --print shows the rows it would record so they can be committed from the log. - timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci + # Golden maps: rows exist per platform. --print first, so the rows this platform would + # record are in the log even when the check that follows fails and can be committed + # from there. timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --print + timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci timeout 600s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --sweep timeout 180s ./build/src/CustomGameSetupHarness mkdir -p artifacts/custom-game-ci diff --git a/src/CustomGamePreferences.h b/src/CustomGamePreferences.h index 4bf1f60b2..b1a89591b 100644 --- a/src/CustomGamePreferences.h +++ b/src/CustomGamePreferences.h @@ -34,10 +34,10 @@ struct CustomGamePreferences { // Bounds are a safe envelope across every generator method's current // registry range for the reused legacy field (e.g. riverDiameter also - // stands in for river width, lake size - Fjord's reaches 0 and 90 - - // channel width and bridge width; craterDensity also stands in for - // Ring world's lake density, which reaches 0; grassRatio also stands - // in for Isles' island size). This only needs to be at least as wide + // stands in for river width, lake size - Fjord's reaches 0 and 90, Old + // growth's 160 - channel width and bridge width; craterDensity also + // stands in for Ring world's lake density, which reaches 0; grassRatio + // also stands in for Isles' island size). This only needs to be at least as wide // as what setMethodDefaults() and every registered control can // produce; it is not a tight per-method bound. Controls with no // legacy field are saved in the options section instead. @@ -48,7 +48,7 @@ struct CustomGamePreferences FIELD(grassRatio, 0, 100), FIELD(desertRatio, 0, 100), FIELD(wheatRatio, 0, 64), FIELD(woodRatio, 0, 64), FIELD(fruitRatio, 0, 64), FIELD(algaeRatio, 0, 64), - FIELD(stoneRatio, 0, 64), FIELD(riverDiameter, 0, 100), + FIELD(stoneRatio, 0, 64), FIELD(riverDiameter, 0, 160), FIELD(craterDensity, 0, 64), FIELD(extraIslands, 0, 8), FIELD(oldIslandSize, 1, 70), FIELD(oldBeach, 0, 4), FIELD(smooth, 1, 8), FIELD(nbTeams, 2, Team::MAX_COUNT), diff --git a/test/MapGeneratorGoldenTest.cpp b/test/MapGeneratorGoldenTest.cpp index 9f0b42121..4cfb020e9 100644 --- a/test/MapGeneratorGoldenTest.cpp +++ b/test/MapGeneratorGoldenTest.cpp @@ -1,18 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Golden maps and a colony-count sweep for every registered generator. // -// MapGeneratorGoldenTest check this platform's rows of the table -// MapGeneratorGoldenTest --update regenerate this platform's rows -// MapGeneratorGoldenTest --print print this platform's rows to stdout -// MapGeneratorGoldenTest --sweep every playable landscape at the colony -// counts and sizes the lobby offers +// MapGeneratorGoldenTest check this platform's rows of the table +// MapGeneratorGoldenTest --require-rows the same, failing when this platform +// has no rows at all (what CI runs) +// MapGeneratorGoldenTest --update regenerate this platform's rows +// MapGeneratorGoldenTest --print print this platform's rows to stdout +// MapGeneratorGoldenTest --sweep every playable landscape at the colony +// counts and sizes the lobby offers // // The table (test/map-generator-golden.txt) records, per platform, the fingerprint of the map // each generator produces for a few seeds, sizes and colony counts, keyed by the generator's // revision. A generator whose output changes without its revision moving fails the check; a // revision bump makes --update accept the new maps. Generation is deterministic per platform, -// not across platforms, so rows carry the platform they were made on and a platform without -// rows is reported, not failed. +// not across platforms, so rows carry the platform they were made on. A platform without rows +// is reported and passes by default, so a new machine can run the check before its rows exist; +// CI passes --require-rows so that the platforms it runs on cannot pass vacuously. #include "Game.h" #include "GenerationContext.h" #include "GenerationService.h" @@ -158,7 +161,7 @@ std::vector goldenRows() return rows; } -int check(const std::string &path) +int check(const std::string &path, bool requireRows) { const auto table = readTable(path); const auto platform = platformTag(); @@ -191,9 +194,10 @@ int check(const std::string &path) if (expected.empty()) { std::printf("No golden rows for %s; nothing to compare. Run --update on this platform" - " and commit its rows.\n", - platform.c_str()); - return failures ? 1 : 0; + " and commit its rows%s.\n", + platform.c_str(), + requireRows ? " (--require-rows: this counts as a failure)" : ""); + return failures || requireRows ? 1 : 0; } int compared = 0; for (const auto &[key, row] : expected) @@ -343,7 +347,8 @@ int main(int argc, char **argv) { if (argc < 2) { - std::fprintf(stderr, "usage: %s [--update [--force]|--print|--sweep]\n", + std::fprintf(stderr, + "usage: %s [--require-rows|--update [--force]|--print|--sweep]\n", argv[0]); return 2; } @@ -361,5 +366,5 @@ int main(int argc, char **argv) return update(kTablePath, false, force); if (mode == "--print") return update(kTablePath, true, false); - return check(kTablePath); + return check(kTablePath, mode == "--require-rows"); } From 3b53ec5a179d1204f867437e0a8bb5b235b06131 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 08:01:14 -0400 Subject: [PATCH 130/136] Golden rows for linux-x86_64, required by CI The 248 linux-x86_64 rows come from the --print output of CI run 34839155040 on c6fb9d51c; the ubuntu 22.04 (g++ 11) and 24.04 (g++ 13) jobs printed identical rows, so one platform tag covers both. 156 of the rows match macos-arm64 hash for hash; the other 92 are the generators that go through floating point (Fjord continent, Watershed at 512, Ring world and others), which is the per-platform caveat the table was designed around. With the rows in, the workflow runs the check as --require-rows, so a Linux job can no longer pass this step with nothing to compare, and the test README and framework reference say where a new platform's rows come from. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .github/workflows/build.yml | 8 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 5 +- test/README.md | 9 +- test/map-generator-golden.txt | 248 ++++++++++++++++++ 4 files changed, 263 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06b42039e..3be4ec3f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,11 +109,11 @@ jobs: run: | scons -j$(nproc) release=1 server=0 custom-setup-test map-generator-defaults-test map-generator-golden-test timeout 60s ./build/src/MapGeneratorDefaultsTest glob2-map-defaults-ci - # Golden maps: rows exist per platform. --print first, so the rows this platform would - # record are in the log even when the check that follows fails and can be committed - # from there. + # Golden maps: rows exist per platform and this platform must have them. --print first, + # so the rows a new platform would record are in the log even when the check that + # follows fails and can be committed from there. timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --print - timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci + timeout 300s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --require-rows timeout 600s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --sweep timeout 180s ./build/src/CustomGameSetupHarness mkdir -p artifacts/custom-game-ci diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index 6f805c3e3..feb675f48 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -1195,7 +1195,10 @@ taking the next chamber down the tunnel. following; `--update` rewrites this platform's rows and refuses (without `--force`) to record a changed map under an unchanged revision; `--print` writes the rows to stdout, which is how a platform's rows are first bootstrapped from a CI log. Generation is deterministic per - platform, not across platforms, so rows carry the platform they were made on. `--sweep` rolls + platform, not across platforms, so rows carry the platform they were made on. A platform with + no rows reports and passes, so a new machine can run the check before its rows exist; CI + passes `--require-rows`, which fails instead, so the table has to carry rows for every + platform CI builds on (`linux-x86_64` beside the maintainers' `macos-arm64`). `--sweep` rolls every playable landscape at the colony counts and sizes the lobby offers, five seeds at 128 and 256 and three at 512, prints the success rate per cell and fails any valid cell where no seed generated: that is what a player would see as a failed generation. CI runs all three. diff --git a/test/README.md b/test/README.md index ed22ad2d6..dec2d6845 100644 --- a/test/README.md +++ b/test/README.md @@ -47,8 +47,13 @@ From the repository root, `scons -j8 release=1 server=0 map-generator-golden-tes `build/src/MapGeneratorGoldenTest`. Run it as `./build/src/MapGeneratorGoldenTest ` to compare this platform's rows of `test/map-generator-golden.txt` against fresh rolls, with `--update` after a revision bump, `--print` to bootstrap a platform's rows from a log, and -`--sweep` to roll every playable landscape at the lobby's colony counts and sizes. The -framework reference under `docs/map-generators/` describes the rules it enforces. +`--sweep` to roll every playable landscape at the lobby's colony counts and sizes. A platform +with no rows reports and passes, so a new machine can run the check before its rows exist; +`--require-rows` makes that a failure instead, which is what CI runs, so the table must carry +rows for every platform CI builds on (`linux-x86_64` today, next to the maintainers' +`macos-arm64`). Rows for a platform you cannot build on come from the `--print` output in its +CI log, which the workflow prints before the check. The framework reference under +`docs/map-generators/` describes the rules it enforces. ## Map subclass test pattern diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 2d55e8954..1bb9a8540 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -1,5 +1,253 @@ # Golden map fingerprints per generator: platform id revision wDec hDec teams seed status hash # Regenerate this platform's rows with MapGeneratorGoldenTest --update after a revision bump. +linux-x86_64 0 1 7 7 4 1 ok 3707480985526347828 +linux-x86_64 0 1 8 8 2 1 ok 1882570766644760050 +linux-x86_64 0 1 8 8 4 1 ok 1882570766644760050 +linux-x86_64 0 1 8 8 4 2 ok 13897470516389625799 +linux-x86_64 0 1 8 8 4 3 ok 3342469568135303530 +linux-x86_64 0 1 8 8 8 1 ok 1882570766644760050 +linux-x86_64 0 1 9 8 4 1 ok 5952847682975674388 +linux-x86_64 0 1 9 9 4 1 ok 9537656230795372076 +linux-x86_64 1 2 7 7 4 1 ok 2056248083838577766 +linux-x86_64 1 2 8 8 2 1 ok 4513806375703215835 +linux-x86_64 1 2 8 8 4 1 ok 15266936846292760467 +linux-x86_64 1 2 8 8 4 2 ok 14755082496502706273 +linux-x86_64 1 2 8 8 4 3 ok 7766703425182182688 +linux-x86_64 1 2 8 8 8 1 ok 15211747697609312326 +linux-x86_64 1 2 9 8 4 1 ok 17904614402584393968 +linux-x86_64 1 2 9 9 4 1 ok 13823901920601588993 +linux-x86_64 2 2 7 7 4 1 ok 15072902734700655403 +linux-x86_64 2 2 8 8 2 1 ok 4851692128995844304 +linux-x86_64 2 2 8 8 4 1 ok 2883468895713543674 +linux-x86_64 2 2 8 8 4 2 ok 4530039562136596405 +linux-x86_64 2 2 8 8 4 3 ok 8829998086451332904 +linux-x86_64 2 2 8 8 8 1 ok 5058966551671087466 +linux-x86_64 2 2 9 8 4 1 ok 3284099059923127358 +linux-x86_64 2 2 9 9 4 1 ok 11508891954623314185 +linux-x86_64 3 2 7 7 4 1 ok 8448347508778630572 +linux-x86_64 3 2 8 8 2 1 ok 18055009306025368179 +linux-x86_64 3 2 8 8 4 1 ok 9615729526066216419 +linux-x86_64 3 2 8 8 4 2 ok 8908276107237789891 +linux-x86_64 3 2 8 8 4 3 ok 7682200520761145636 +linux-x86_64 3 2 8 8 8 1 ok 16434396702117749324 +linux-x86_64 3 2 9 8 4 1 ok 8810915806685158013 +linux-x86_64 3 2 9 9 4 1 ok 528287643730928710 +linux-x86_64 4 2 7 7 4 1 ok 16799842763352457672 +linux-x86_64 4 2 8 8 2 1 ok 11276811933984094039 +linux-x86_64 4 2 8 8 4 1 ok 3361731306080595113 +linux-x86_64 4 2 8 8 4 2 ok 4883867572660367167 +linux-x86_64 4 2 8 8 4 3 ok 3642147494547083564 +linux-x86_64 4 2 8 8 8 1 ok 7277529821103791285 +linux-x86_64 4 2 9 8 4 1 ok 13633424028759709559 +linux-x86_64 4 2 9 9 4 1 ok 6305132251765376442 +linux-x86_64 5 1 7 7 4 1 ok 13850702411921678476 +linux-x86_64 5 1 8 8 2 1 ok 16875811593585503047 +linux-x86_64 5 1 8 8 4 1 ok 15081591080055302398 +linux-x86_64 5 1 8 8 4 2 ok 14333463520897961954 +linux-x86_64 5 1 8 8 4 3 ok 1927162392993477983 +linux-x86_64 5 1 8 8 8 1 ok 1417569272582492023 +linux-x86_64 5 1 9 8 4 1 ok 12064970734501370021 +linux-x86_64 5 1 9 9 4 1 ok 6078319397555065000 +linux-x86_64 6 2 7 7 4 1 ok 10962798937752830582 +linux-x86_64 6 2 8 8 2 1 ok 13908422176345369582 +linux-x86_64 6 2 8 8 4 1 ok 12156463255653919960 +linux-x86_64 6 2 8 8 4 2 ok 8488290807097875252 +linux-x86_64 6 2 8 8 4 3 ok 13493861953378784496 +linux-x86_64 6 2 8 8 8 1 ok 5023916742560360564 +linux-x86_64 6 2 9 8 4 1 ok 16957866786516727954 +linux-x86_64 6 2 9 9 4 1 ok 18148498138515235675 +linux-x86_64 7 3 7 7 4 1 ok 17606064467284084149 +linux-x86_64 7 3 8 8 2 1 ok 13195220428486063337 +linux-x86_64 7 3 8 8 4 1 ok 15902815871131779878 +linux-x86_64 7 3 8 8 4 2 ok 18406432865124272517 +linux-x86_64 7 3 8 8 4 3 ok 2922633456080255781 +linux-x86_64 7 3 8 8 8 1 ok 15346042196395066810 +linux-x86_64 7 3 9 8 4 1 ok 16173449937850616615 +linux-x86_64 7 3 9 9 4 1 ok 5698846428085401433 +linux-x86_64 8 2 7 7 4 1 ok 4658555237636300612 +linux-x86_64 8 2 8 8 2 1 ok 14143132148239399264 +linux-x86_64 8 2 8 8 4 1 ok 10445762206825023853 +linux-x86_64 8 2 8 8 4 2 ok 11928184670408982532 +linux-x86_64 8 2 8 8 4 3 ok 11414080712598699583 +linux-x86_64 8 2 8 8 8 1 ok 17098982314650317944 +linux-x86_64 8 2 9 8 4 1 ok 219026690225498048 +linux-x86_64 8 2 9 9 4 1 ok 11398981807552515483 +linux-x86_64 9 2 7 7 4 1 ok 2277607816406551252 +linux-x86_64 9 2 8 8 2 1 ok 6984614415357599638 +linux-x86_64 9 2 8 8 4 1 ok 1045725267928572154 +linux-x86_64 9 2 8 8 4 2 ok 1933790560431647187 +linux-x86_64 9 2 8 8 4 3 ok 5877899263698877100 +linux-x86_64 9 2 8 8 8 1 ok 4204618854271989380 +linux-x86_64 9 2 9 8 4 1 ok 13364805290811868144 +linux-x86_64 9 2 9 9 4 1 ok 17370525382688177735 +linux-x86_64 11 13 7 7 4 1 ok 7573201930155389176 +linux-x86_64 11 13 8 8 2 1 ok 1580814293182404478 +linux-x86_64 11 13 8 8 4 1 ok 6132248727033081699 +linux-x86_64 11 13 8 8 4 2 ok 17571897192944793353 +linux-x86_64 11 13 8 8 4 3 ok 17321560833037622047 +linux-x86_64 11 13 8 8 8 1 ok 9101533112797905055 +linux-x86_64 11 13 9 8 4 1 ok 8608092470811997215 +linux-x86_64 11 13 9 9 4 1 ok 5365458970422917083 +linux-x86_64 12 11 7 7 4 1 ok 10085480575762183996 +linux-x86_64 12 11 8 8 2 1 ok 15910518443514311603 +linux-x86_64 12 11 8 8 4 1 ok 5459653342558093269 +linux-x86_64 12 11 8 8 4 2 ok 14685303563245965840 +linux-x86_64 12 11 8 8 4 3 ok 11130544737337244989 +linux-x86_64 12 11 8 8 8 1 ok 5837239095605474547 +linux-x86_64 12 11 9 8 4 1 ok 9747063510013385840 +linux-x86_64 12 11 9 9 4 1 ok 17035956360268025535 +linux-x86_64 13 1 7 7 4 1 ok 8112226036058122700 +linux-x86_64 13 1 8 8 2 1 ok 9154120752318257696 +linux-x86_64 13 1 8 8 4 1 ok 2078930885184706426 +linux-x86_64 13 1 8 8 4 2 ok 13659661778465349112 +linux-x86_64 13 1 8 8 4 3 ok 13134881442491555389 +linux-x86_64 13 1 8 8 8 1 ok 16484172863350979963 +linux-x86_64 13 1 9 8 4 1 ok 7377329900378929112 +linux-x86_64 13 1 9 9 4 1 ok 3669618330451908633 +linux-x86_64 14 3 7 7 4 1 ok 17202733506137778788 +linux-x86_64 14 3 8 8 2 1 ok 16088282592338295379 +linux-x86_64 14 3 8 8 4 1 ok 11925006621409592431 +linux-x86_64 14 3 8 8 4 2 ok 414349330464996680 +linux-x86_64 14 3 8 8 4 3 ok 760189253045661613 +linux-x86_64 14 3 8 8 8 1 ok 3322640947906419646 +linux-x86_64 14 3 9 8 4 1 ok 2403928672037668991 +linux-x86_64 14 3 9 9 4 1 ok 1552916736750568752 +linux-x86_64 15 1 7 7 4 1 ok 18061507907613243474 +linux-x86_64 15 1 8 8 2 1 ok 14205129108449246087 +linux-x86_64 15 1 8 8 4 1 ok 1822623745155517541 +linux-x86_64 15 1 8 8 4 2 ok 13627699444976556672 +linux-x86_64 15 1 8 8 4 3 ok 4558451268999198918 +linux-x86_64 15 1 8 8 8 1 ok 6662980182189545293 +linux-x86_64 15 1 9 8 4 1 ok 13301401750330037833 +linux-x86_64 15 1 9 9 4 1 ok 4317157755929348617 +linux-x86_64 16 1 7 7 4 1 ok 7206623774422624421 +linux-x86_64 16 1 8 8 2 1 ok 13822333607975965821 +linux-x86_64 16 1 8 8 4 1 ok 11438745084637433400 +linux-x86_64 16 1 8 8 4 2 ok 517692217825115231 +linux-x86_64 16 1 8 8 4 3 ok 8018514250963441197 +linux-x86_64 16 1 8 8 8 1 ok 11001992185533046958 +linux-x86_64 16 1 9 8 4 1 ok 12695777392728758889 +linux-x86_64 16 1 9 9 4 1 ok 15361103174928577925 +linux-x86_64 17 7 7 7 4 1 ok 16384619148338487512 +linux-x86_64 17 7 8 8 2 1 ok 12055923282160576445 +linux-x86_64 17 7 8 8 4 1 ok 14456968977872880957 +linux-x86_64 17 7 8 8 4 2 ok 15957250270316988236 +linux-x86_64 17 7 8 8 4 3 ok 1726581497331910085 +linux-x86_64 17 7 8 8 8 1 ok 4164461749169536866 +linux-x86_64 17 7 9 8 4 1 ok 504687273871298522 +linux-x86_64 17 7 9 9 4 1 ok 614102320811357231 +linux-x86_64 18 5 7 7 4 1 ok 2131984367767490485 +linux-x86_64 18 5 8 8 2 1 ok 3078796380374205351 +linux-x86_64 18 5 8 8 4 1 ok 7736833362775075996 +linux-x86_64 18 5 8 8 4 2 ok 3733351741626345607 +linux-x86_64 18 5 8 8 4 3 ok 4515656875628782478 +linux-x86_64 18 5 8 8 8 1 ok 12075523445877924400 +linux-x86_64 18 5 9 8 4 1 ok 2230785934352114637 +linux-x86_64 18 5 9 9 4 1 ok 10761736745352955182 +linux-x86_64 19 2 7 7 4 1 ok 2704824689328339712 +linux-x86_64 19 2 8 8 2 1 ok 18374433395111261761 +linux-x86_64 19 2 8 8 4 1 ok 9505660943407981793 +linux-x86_64 19 2 8 8 4 2 ok 10374245496345104115 +linux-x86_64 19 2 8 8 4 3 ok 6643792788093279102 +linux-x86_64 19 2 8 8 8 1 ok 638962071964486508 +linux-x86_64 19 2 9 8 4 1 ok 5247699519866569761 +linux-x86_64 19 2 9 9 4 1 ok 14084993306773314060 +linux-x86_64 20 2 7 7 4 1 ok 2762554265195917781 +linux-x86_64 20 2 8 8 2 1 ok 174514327114224496 +linux-x86_64 20 2 8 8 4 1 ok 17867996144716273583 +linux-x86_64 20 2 8 8 4 2 ok 17425198876722941958 +linux-x86_64 20 2 8 8 4 3 ok 15787579039604162098 +linux-x86_64 20 2 8 8 8 1 ok 1565465011606233801 +linux-x86_64 20 2 9 8 4 1 ok 896474791327603754 +linux-x86_64 20 2 9 9 4 1 ok 16074835094380761605 +linux-x86_64 21 1 7 7 4 1 ok 14660091690297466223 +linux-x86_64 21 1 8 8 2 1 ok 10121960244518094221 +linux-x86_64 21 1 8 8 4 1 ok 16436121170398078198 +linux-x86_64 21 1 8 8 4 2 ok 4584887454756350840 +linux-x86_64 21 1 8 8 4 3 ok 6175686376568023634 +linux-x86_64 21 1 8 8 8 1 ok 13558584930804210761 +linux-x86_64 21 1 9 8 4 1 ok 8092089350815172597 +linux-x86_64 21 1 9 9 4 1 ok 6220231772661939919 +linux-x86_64 22 1 7 7 4 1 ok 8819869978616031071 +linux-x86_64 22 1 8 8 2 1 ok 730574824765298860 +linux-x86_64 22 1 8 8 4 1 ok 9691963763756535666 +linux-x86_64 22 1 8 8 4 2 ok 12280328850293820652 +linux-x86_64 22 1 8 8 4 3 ok 16156515469674000849 +linux-x86_64 22 1 8 8 8 1 ok 12707724408115629147 +linux-x86_64 22 1 9 8 4 1 ok 16181991639738054527 +linux-x86_64 22 1 9 9 4 1 ok 18170981972027515569 +linux-x86_64 23 2 7 7 4 1 ok 549496627108919023 +linux-x86_64 23 2 8 8 2 1 ok 10411914600145665080 +linux-x86_64 23 2 8 8 4 1 ok 10630205222293629395 +linux-x86_64 23 2 8 8 4 2 ok 3769887846353985102 +linux-x86_64 23 2 8 8 4 3 ok 14438238323646079445 +linux-x86_64 23 2 8 8 8 1 ok 7486873941404335262 +linux-x86_64 23 2 9 8 4 1 ok 9501696698924728828 +linux-x86_64 23 2 9 9 4 1 ok 8928207243182979823 +linux-x86_64 24 3 7 7 4 1 ok 8766269131759353289 +linux-x86_64 24 3 8 8 2 1 ok 11694144927456181775 +linux-x86_64 24 3 8 8 4 1 ok 12461145917554129793 +linux-x86_64 24 3 8 8 4 2 ok 14956981799443403869 +linux-x86_64 24 3 8 8 4 3 ok 16634610244346170796 +linux-x86_64 24 3 8 8 8 1 ok 14595537376914576811 +linux-x86_64 24 3 9 8 4 1 ok 16587253345581904688 +linux-x86_64 24 3 9 9 4 1 ok 12750052710567335197 +linux-x86_64 26 3 7 7 4 1 ok 7030856013259944714 +linux-x86_64 26 3 8 8 2 1 ok 3845517928739616223 +linux-x86_64 26 3 8 8 4 1 ok 5648058033288605271 +linux-x86_64 26 3 8 8 4 2 ok 2890577651377242724 +linux-x86_64 26 3 8 8 4 3 ok 18218990530981657750 +linux-x86_64 26 3 8 8 8 1 ok 2357741973286312376 +linux-x86_64 26 3 9 8 4 1 ok 7748628063269926229 +linux-x86_64 26 3 9 9 4 1 ok 12177232439900443665 +linux-x86_64 27 3 7 7 4 1 ok 12643361820633102920 +linux-x86_64 27 3 8 8 2 1 ok 17439807471090677068 +linux-x86_64 27 3 8 8 4 1 ok 17647686618338591999 +linux-x86_64 27 3 8 8 4 2 ok 5785940387948126089 +linux-x86_64 27 3 8 8 4 3 ok 5375052871419675847 +linux-x86_64 27 3 8 8 8 1 ok 4736068021352568885 +linux-x86_64 27 3 9 8 4 1 ok 1938306971359313264 +linux-x86_64 27 3 9 9 4 1 ok 4857220545332469578 +linux-x86_64 28 4 7 7 4 1 ok 17460766322823457384 +linux-x86_64 28 4 8 8 2 1 ok 11514638504458263270 +linux-x86_64 28 4 8 8 4 1 ok 11307450576559481972 +linux-x86_64 28 4 8 8 4 2 ok 5688860787945563985 +linux-x86_64 28 4 8 8 4 3 ok 5051877275687580956 +linux-x86_64 28 4 8 8 8 1 ok 6662509859672209696 +linux-x86_64 28 4 9 8 4 1 ok 12260507135522363815 +linux-x86_64 28 4 9 9 4 1 ok 7745553466171171583 +linux-x86_64 29 4 7 7 4 1 ok 4232995500160826297 +linux-x86_64 29 4 8 8 2 1 ok 7399419839946439559 +linux-x86_64 29 4 8 8 4 1 ok 9472114427354905765 +linux-x86_64 29 4 8 8 4 2 ok 15267437393931662669 +linux-x86_64 29 4 8 8 4 3 ok 16695674954699155816 +linux-x86_64 29 4 8 8 8 1 ok 13911889789453582543 +linux-x86_64 29 4 9 8 4 1 ok 7057480006238666143 +linux-x86_64 29 4 9 9 4 1 ok 6519639175542525008 +linux-x86_64 30 3 7 7 4 1 ok 16282398610937407829 +linux-x86_64 30 3 8 8 2 1 ok 3709056129881639799 +linux-x86_64 30 3 8 8 4 1 ok 9174613388237436961 +linux-x86_64 30 3 8 8 4 2 ok 8462895324070553571 +linux-x86_64 30 3 8 8 4 3 ok 8076866925420152192 +linux-x86_64 30 3 8 8 8 1 ok 7750531187217742676 +linux-x86_64 30 3 9 8 4 1 ok 9383240492603169903 +linux-x86_64 30 3 9 9 4 1 ok 5015472615197108784 +linux-x86_64 31 6 7 7 4 1 ok 7932853597508094162 +linux-x86_64 31 6 8 8 2 1 ok 11763462524905175676 +linux-x86_64 31 6 8 8 4 1 ok 10294467211899142010 +linux-x86_64 31 6 8 8 4 2 ok 2468014486729326948 +linux-x86_64 31 6 8 8 4 3 ok 13708832934710116111 +linux-x86_64 31 6 8 8 8 1 ok 6715384199869579049 +linux-x86_64 31 6 9 8 4 1 ok 10414113953351534244 +linux-x86_64 31 6 9 9 4 1 ok 4824924432611332992 +linux-x86_64 32 4 7 7 4 1 ok 7540126588246018722 +linux-x86_64 32 4 8 8 2 1 ok 8633803889491191707 +linux-x86_64 32 4 8 8 4 1 ok 17633894656724945162 +linux-x86_64 32 4 8 8 4 2 ok 17969857465891887079 +linux-x86_64 32 4 8 8 4 3 ok 14927243951147519255 +linux-x86_64 32 4 8 8 8 1 ok 14387082642563060217 +linux-x86_64 32 4 9 8 4 1 ok 15882581644053452132 +linux-x86_64 32 4 9 9 4 1 ok 5175145437434078792 macos-arm64 0 1 7 7 4 1 ok 3707480985526347828 macos-arm64 0 1 8 8 2 1 ok 1882570766644760050 macos-arm64 0 1 8 8 4 1 ok 1882570766644760050 From 1617f7ecb2ded557917cd0cc5df5ce5b2464ebb0 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 08:01:14 -0400 Subject: [PATCH 131/136] Rename the last near and far identifiers for mingw windef.h defines both as empty macros, and the Windows job stopped at Territories.h's `const int far` once Tessellation.cpp compiled. Every remaining use as an identifier is renamed: farthest in Morphology.cpp and Territories.h, clearance for the distance-to-keep-clear vectors in Old growth, Old town and Polder, and nearby/distant in the two test headers. Pure renames; the golden table is unchanged and the defaults test passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../generators/OldGrowthGenerator.cpp | 4 ++-- .../generator/generators/OldTownGenerator.cpp | 4 ++-- .../generator/generators/PolderGenerator.cpp | 6 +++--- src/map/generator/shared/Morphology.cpp | 4 ++-- src/map/generator/shared/Territories.h | 5 +++-- test/MapGeneratorLandscapeChecks.h | 18 ++++++++--------- test/MapGeneratorToolkitChecks.h | 20 +++++++++---------- 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/map/generator/generators/OldGrowthGenerator.cpp b/src/map/generator/generators/OldGrowthGenerator.cpp index 923ea6a43..649102935 100644 --- a/src/map/generator/generators/OldGrowthGenerator.cpp +++ b/src/map/generator/generators/OldGrowthGenerator.cpp @@ -181,10 +181,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) std::vector queued(n, 0); for (int lake = 0; lake < lakes; ++lake) { - const std::vector far = distanceSquaredTo(t, keepClear); + const std::vector clearance = distanceSquaredTo(t, keepClear); int seed = -1; for (int i = 0; i < n; ++i) - if (!keepClear[i] && (seed < 0 || far[i] > far[seed])) + if (!keepClear[i] && (seed < 0 || clearance[i] > clearance[seed])) seed = i; if (seed < 0) break; diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index ae2a04a89..810c5b68b 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -304,10 +304,10 @@ Layout design(const GenerationRequest &request, GenerationContext &context) keepClear[i] = !L.farmRegion[i]; for (int p = 0; p < o.farmPlots * teams; ++p) { - const std::vector far = distanceSquaredTo(t, keepClear); + const std::vector clearance = distanceSquaredTo(t, keepClear); int site = -1; for (int i = 0; i < n; ++i) - if (roomy[i] && (site < 0 || far[i] > far[site])) + if (roomy[i] && (site < 0 || clearance[i] > clearance[site])) site = i; if (site < 0) break; diff --git a/src/map/generator/generators/PolderGenerator.cpp b/src/map/generator/generators/PolderGenerator.cpp index de5fd0aca..8b150d9f3 100644 --- a/src/map/generator/generators/PolderGenerator.cpp +++ b/src/map/generator/generators/PolderGenerator.cpp @@ -215,12 +215,12 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const int plots = (teams * kPlotsPerTwoColonies + 1) / 2; for (int p = 0; p < plots; ++p) { - const std::vector far = distanceSquaredTo(t, keepClear); + const std::vector clearance = distanceSquaredTo(t, keepClear); int site = -1; for (int i = 0; i < n; ++i) - if (!keepClear[i] && (site < 0 || far[i] > far[site])) + if (!keepClear[i] && (site < 0 || clearance[i] > clearance[site])) site = i; - if (site < 0 || far[site] < std::int64_t(kPlotMargin) * kPlotMargin) + if (site < 0 || clearance[site] < std::int64_t(kPlotMargin) * kPlotMargin) break; const int x0 = site % t.w - plot.width / 2, y0 = site / t.w - plot.height / 2; stampFarmPlot(L.sketch, t, L.farm, x0, y0, plot); diff --git a/src/map/generator/shared/Morphology.cpp b/src/map/generator/shared/Morphology.cpp index 159d0a3cc..1754911d2 100644 --- a/src/map/generator/shared/Morphology.cpp +++ b/src/map/generator/shared/Morphology.cpp @@ -208,9 +208,9 @@ std::vector clearance(const Torus &t, const std::vector &mas for (size_t i = 0; i < mask.size(); ++i) outside[i] = !mask[i]; std::vector steps = stepsFrom(t, outside); - const int far = std::max(t.w, t.h) / 2; + const int farthest = std::max(t.w, t.h) / 2; for (size_t i = 0; i < mask.size(); ++i) - steps[i] = !mask[i] ? 0 : steps[i] < 0 ? far : steps[i]; + steps[i] = !mask[i] ? 0 : steps[i] < 0 ? farthest : steps[i]; return steps; } diff --git a/src/map/generator/shared/Territories.h b/src/map/generator/shared/Territories.h index b53219f73..a3b36f64b 100644 --- a/src/map/generator/shared/Territories.h +++ b/src/map/generator/shared/Territories.h @@ -407,13 +407,14 @@ int growFarLake(const Torus &t, std::vector &water, const std::ve seed = i; if (seed < 0) return 0; - const int far = depth[seed]; + // Not `far`: like `near`, a legacy macro in Windows' windef.h that expands to nothing. + const int farthest = depth[seed]; return growWater( t, water, seed, target, [&](int i) { return roomy[i] != 0; }, [&](int i) { const double d = std::sqrt(double(t.dist2(seed % t.w, seed / t.w, i % t.w, i / t.w))); - return std::int64_t(d * 1000) + (far - depth[i]) * 250LL + + return std::int64_t(d * 1000) + (farthest - depth[i]) * 250LL + std::int64_t(noiseAt(i) * 2500); }, queued, stamp); diff --git a/test/MapGeneratorLandscapeChecks.h b/test/MapGeneratorLandscapeChecks.h index ff36d95cd..52bf9f3bd 100644 --- a/test/MapGeneratorLandscapeChecks.h +++ b/test/MapGeneratorLandscapeChecks.h @@ -168,12 +168,12 @@ inline void growthChecks() layBeaches(sketch, t); Fertility::Field field = cropGrowthField(sketch, t); assert(field.at(26, 31) > 0 && field.at(0, 0) == 0); - std::vector far(t.size(), 0), near(t.size(), 0); - far[t.at(0, 0)] = far[t.at(63, 63)] = 1; - near[t.at(25, 31)] = 1; - assert(wetTiles(field, far) == 0 && wetTiles(field, near) == 1); - assert(wateredShare(field, near) == 1.0 && wateredShare(field, far) == 0.0); - assert(dryZone(t, near)[t.at(40, 31)] && !dryZone(t, near)[t.at(41, 31)]); + std::vector distant(t.size(), 0), nearby(t.size(), 0); + distant[t.at(0, 0)] = distant[t.at(63, 63)] = 1; + nearby[t.at(25, 31)] = 1; + assert(wetTiles(field, distant) == 0 && wetTiles(field, nearby) == 1); + assert(wateredShare(field, nearby) == 1.0 && wateredShare(field, distant) == 0.0); + assert(dryZone(t, nearby)[t.at(40, 31)] && !dryZone(t, nearby)[t.at(41, 31)]); std::vector region(t.size(), 0); region[t.at(24, 31)] = 1; TerrainSketch drained(t.size(), GRASS); @@ -403,11 +403,11 @@ inline void channelChecks() for (int x = 20; x < 26; ++x) bridge[t.at(x, 4)] = 1; // The wrap joins the banks round the far side too, so only the bridge's own row is checked. - std::vector near(t.size(), 0); + std::vector nearby(t.size(), 0); for (int y = 0; y < t.h; ++y) for (int x = 16; x < 30; ++x) - near[t.at(x, y)] = open[t.at(x, y)]; - assert(stepsFrom(t, tileMask(t, {t.at(17, 4)}), near)[t.at(28, 4)] > 0); + nearby[t.at(x, y)] = open[t.at(x, y)]; + assert(stepsFrom(t, tileMask(t, {t.at(17, 4)}), nearby)[t.at(28, 4)] > 0); std::vector sides(t.size(), -1); for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) diff --git a/test/MapGeneratorToolkitChecks.h b/test/MapGeneratorToolkitChecks.h index 25bf7f2ef..cacc850e7 100644 --- a/test/MapGeneratorToolkitChecks.h +++ b/test/MapGeneratorToolkitChecks.h @@ -176,12 +176,12 @@ inline void plantingChecks() } assert(reached.size() == 12); const auto clear = [&](int i) { return clearGround(map, i % t.w, i / t.w); }; - const int near = seedNear(t, 20, 20, 6, clear); - assert(near >= 0 && clear(near)); + const int nearby = seedNear(t, 20, 20, 6, clear); + assert(nearby >= 0 && clear(nearby)); for (int dy = -6; dy <= 6; ++dy) for (int dx = -6; dx <= 6; ++dx) if (clear(t.at(20 + dx, 20 + dy))) - assert(dx * dx + dy * dy >= t.dist2(20, 20, near % t.w, near / t.w)); + assert(dx * dx + dy * dy >= t.dist2(20, 20, nearby % t.w, nearby / t.w)); assert(seedNear(t, 20, 20, 2, [](int) { return false; }) == -1); // Only eligible tiles are planted, and the count stops where eligibility does: a 2 by 4 box. const auto box = [&](int i) @@ -746,8 +746,8 @@ inline void stretchChecks() assert(same.x == 10.1 && same.y == 3.7); const Stretch wide = Stretch::toFill(64, 16); assert(wide.sx == 4 && wide.sy == 1 && wide.longest() == 4); - const ShapePoint far = wide.apply(32, 8, {40, 8}); - assert(far.x == 64 && far.y == 8); + const ShapePoint distant = wide.apply(32, 8, {40, 8}); + assert(distant.x == 64 && distant.y == 8); assert(std::abs(wide.heading(kPi / 4) - std::atan2(1.0, 4.0)) < 1e-12); const Torus t(64, 16); @@ -1078,14 +1078,14 @@ inline void arenaChecks() edge[t.at(x, y)] = 0; const std::vector stripRoom = stepsFrom(t, edge); assert(growFarLake(t, lake, stripDepth, stripRoom, 3, 60, [](int) { return 0.0; }, queued, 1) == 60); - int far = 0, near = 0; + int distant = 0, nearby = 0; for (int i = 0; i < t.size(); ++i) if (lake[i]) { assert(stripRoom[i] >= 3); - (i % t.w > 30 ? far : near)++; + (i % t.w > 30 ? distant : nearby)++; } - assert(far == 60 && near == 0); + assert(distant == 60 && nearby == 0); assert(growFarLake(t, lake, stripDepth, stripRoom, 12, 60, [](int) { return 0.0; }, queued, 2) == 0); std::vector jagged(t.size(), 0); @@ -1499,8 +1499,8 @@ inline void farmAndTowerChecks() std::vector gap(sea.size(), 0); for (int i = 0; i < sea.size(); ++i) gap[i] = halves[i] < 0; - std::vector near = halves; - assert(fillToNearest(sea, near, gap, 1)[sea.at(30, 5)] && near[sea.at(30, 5)] == 0 && near[sea.at(31, 5)] == -1); + std::vector nearby = halves; + assert(fillToNearest(sea, nearby, gap, 1)[sea.at(30, 5)] && nearby[sea.at(30, 5)] == 0 && nearby[sea.at(31, 5)] == -1); const std::vector filled = fillToNearest(sea, halves, gap, 4); assert(filled[sea.at(31, 5)] && halves[sea.at(31, 5)] == 0 && halves[sea.at(32, 5)] == 1); for (int i = 0; i < sea.size(); ++i) From ab95800c2cf65718608f4799f350682644cf5353 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 08:37:37 -0400 Subject: [PATCH 132/136] Reach the sync RNG through a function, not an extern thread_local object The Windows job failed SavegameSafetyHarness's check that a save leaves the sync RNG untouched, with the same harness and save code master passes there; the only change to the RNG on this branch is that `randomGenerator` became `thread_local`. A thread_local with a constructor that other translation units name directly is initialised through a weak per-unit wrapper, which mingw's emulated TLS is the one platform to get wrong. The engine is now a function-local thread_local behind `syncRandEngine()`, initialised in one place in Utilities.cpp, and every former use of the object (save and load, the menu colony's swap, the generation service's scope, the harnesses) calls the accessor. Same semantics everywhere else: golden maps, the defaults test, the savegame and trapped-unit harnesses and the setup harness all pass unchanged on macOS. The savegame harness now prints both RNG states when they differ instead of a bare assert, so the next platform that disagrees says how. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- src/Game_io.cpp | 4 ++-- src/MenuColony.cpp | 6 +++--- src/Utilities.cpp | 12 ++++++++---- src/Utilities.h | 9 +++++++-- src/map/generator/core/GenerationService.cpp | 4 ++-- test/MapGeneratorDefaultsTest.cpp | 8 ++++---- test/MapGeneratorFrameworkChecks.h | 4 ++-- test/SavegameSafetyHarness.cpp | 15 ++++++++++++--- test/TrappedUnitLifecycleTest.cpp | 4 ++-- tools/MenuColonyHarness.cpp | 4 ++-- 10 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/Game_io.cpp b/src/Game_io.cpp index 69ccc001e..cdf5e9685 100644 --- a/src/Game_io.cpp +++ b/src/Game_io.cpp @@ -305,7 +305,7 @@ bool Game::load(GAGCore::InputStream *stream) if (versionMinor >= FILE_FORMAT_VERSION_CONTINUATION_STATE && mapHeader.getIsSavedGame()) { - randomGenerator = savedRandom; + syncRandEngine() = savedRandom; hasSavedRandomState = true; } @@ -550,7 +550,7 @@ void Game::save(GAGCore::OutputStream *stream, bool fileIsAMap, const std::strin { std::ostringstream randomState; randomState.imbue(std::locale::classic()); - randomState << randomGenerator; + randomState << syncRandEngine(); std::istringstream state(randomState.str()); state.imbue(std::locale::classic()); stream->writeEnterSection("randomState"); diff --git a/src/MenuColony.cpp b/src/MenuColony.cpp index 959c6768a..c50ff49fc 100644 --- a/src/MenuColony.cpp +++ b/src/MenuColony.cpp @@ -25,10 +25,10 @@ struct ColonyContext explicit ColonyContext(boost::mt19937& state) : rng(state), replay(std::move(globalContainer->replayWriter)), dataset(std::move(globalContainer->datasetWriter)) - { std::swap(randomGenerator, rng); } + { std::swap(syncRandEngine(), rng); } ~ColonyContext() { - std::swap(randomGenerator, rng); + std::swap(syncRandEngine(), rng); globalContainer->replayWriter = std::move(replay); globalContainer->datasetWriter = std::move(dataset); } @@ -51,7 +51,7 @@ bool MenuColony::load(const std::string& path) !loaded->players[0]->ai || loaded->players[0]->ai->implementationID != AI::ECONO) throw std::runtime_error("incompatible menu colony"); std::istringstream state(input.readText("rng") + " "); - if (!(state >> randomGenerator)) throw std::runtime_error("invalid colony RNG"); + if (!(state >> syncRandEngine())) throw std::runtime_error("invalid colony RNG"); loaded->setWaitingOnMask(0); // The map round-robin updater expects at least one lazy gradient. loaded->map.getResourceGradient(0, WHEAT, 0); diff --git a/src/Utilities.cpp b/src/Utilities.cpp index bd31fe415..2b96c9ef3 100644 --- a/src/Utilities.cpp +++ b/src/Utilities.cpp @@ -26,7 +26,11 @@ using ssize_t = SSIZE_T; #endif -thread_local boost::mt19937 randomGenerator; +boost::mt19937 &syncRandEngine() +{ + thread_local boost::mt19937 engine; + return engine; +} int distSquare(int x1, int y1, int x2, int y2) { @@ -38,16 +42,16 @@ int distSquare(int x1, int y1, int x2, int y2) void setSyncRandSeed() { ///Sets the default seed - randomGenerator.seed(); + syncRandEngine().seed(); } void setSyncRandSeed(Uint32 seed) { - randomGenerator.seed(seed); + syncRandEngine().seed(seed); } void setRandomSyncRandSeed() { - randomGenerator.seed(std::random_device{}()); + syncRandEngine().seed(std::random_device{}()); } namespace Utilities diff --git a/src/Utilities.h b/src/Utilities.h index 7f8f3d5fd..640aa1b28 100644 --- a/src/Utilities.h +++ b/src/Utilities.h @@ -21,11 +21,16 @@ namespace GAGCore // The synchronized gameplay stream. One state per thread: the simulation only ever runs on // one thread, so its sequence is unchanged, while a background map generation (which seeds // this stream itself) neither disturbs nor races the menu's live colony on the UI thread. -extern thread_local boost::mt19937 randomGenerator; +// Reached through a function rather than an `extern thread_local` object: a thread_local +// with a constructor that other translation units name directly is initialised through a +// weak per-unit wrapper, which mingw's emulated TLS does not get right, and the savegame +// harness saw the engine stream change state under it on Windows. A function-local +// thread_local is initialised in one place, in this unit. +boost::mt19937 &syncRandEngine(); inline Uint32 syncRand(void) { - return randomGenerator(); + return syncRandEngine()(); } // 32-bit right-rotate by 1 bit. Used to mix per-section checksums into the diff --git a/src/map/generator/core/GenerationService.cpp b/src/map/generator/core/GenerationService.cpp index 4e4306002..5bc24f3b4 100644 --- a/src/map/generator/core/GenerationService.cpp +++ b/src/map/generator/core/GenerationService.cpp @@ -42,8 +42,8 @@ GenerationResult GenerationService::generate(Game &game, const GenerationRequest // This synchronous, scoped bridge is not a concurrency API. struct EngineRandomScope { - boost::mt19937 saved = randomGenerator; - ~EngineRandomScope() { randomGenerator = saved; } + boost::mt19937 saved = syncRandEngine(); + ~EngineRandomScope() { syncRandEngine() = saved; } } rngScope; setSyncRandSeed(GenerationContext::deriveSeed(request.seed, "engine")); GenerationContext context(request); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 2dbc0d961..29b3a1346 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -97,10 +97,10 @@ class MapGeneratorDefaultsTest request.setMethodDefaults(method); request.seed = 22001; setSyncRandSeed(177); - auto surrounding = randomGenerator; + auto surrounding = syncRandEngine(); Game first(nullptr); auto a = service.generate(first, request); - assert(randomGenerator == surrounding); + assert(syncRandEngine() == surrounding); auto hash = mapFingerprint(first); auto checksum = first.checkSum(nullptr, nullptr, nullptr, true); D intervening; @@ -169,7 +169,7 @@ class MapGeneratorDefaultsTest } // Scoped RNG restoration must also hold when placement fails. setSyncRandSeed(711); - auto savedFailureRng = randomGenerator; + auto savedFailureRng = syncRandEngine(); D invalid; invalid.method = 99999; Game fresh(nullptr); @@ -186,7 +186,7 @@ class MapGeneratorDefaultsTest auto failed = service.generate(fresh, invalid); assert(!failed && failed.error == GenerationError::PlacementFailed && !failed.stage.empty()); - assert(randomGenerator == savedFailureRng); + assert(syncRandEngine() == savedFailureRng); // Legacy sentinel conversion belongs exclusively to the adapter. for (auto pair : {std::pair{D::eCRATERLAKES, 30}, std::pair{D::eCONCRETEISLANDS, 6}, std::pair{D::eISLES, 4}}) diff --git a/test/MapGeneratorFrameworkChecks.h b/test/MapGeneratorFrameworkChecks.h index 75ae6da56..d3171b1f6 100644 --- a/test/MapGeneratorFrameworkChecks.h +++ b/test/MapGeneratorFrameworkChecks.h @@ -252,7 +252,7 @@ inline void frameworkChecks() GenerationService service(registry); request.setMethodDefaults(305, registry); request.nbTeams = 1; - auto surrounding = randomGenerator; + auto surrounding = syncRandEngine(); { Game game(nullptr); assert(service.generate(game, request)); @@ -280,7 +280,7 @@ inline void frameworkChecks() assert(failure.error == GenerationError::InvalidWorld && failure.stage == "generator validation"); } - assert(randomGenerator == surrounding); + assert(syncRandEngine() == surrounding); // Invalid weighted inputs fail explicitly; weighted metadata follows the shuffle. Game game(nullptr); game.map.setSize(6, 6); diff --git a/test/SavegameSafetyHarness.cpp b/test/SavegameSafetyHarness.cpp index 18d8ce91f..ad3e25aeb 100644 --- a/test/SavegameSafetyHarness.cpp +++ b/test/SavegameSafetyHarness.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #ifdef WIN32 @@ -199,7 +200,7 @@ static void checkRandomContinuation(bool text, bool ai) game.syncStep(0); }; for (int i=0; i<100; ++i) step(gui.game); - const auto savedRandom = randomGenerator; + const auto savedRandom = syncRandEngine(); auto *backend = new MemoryStreamBackend(); class CheckpointOutput : public BinaryOutputStream { @@ -224,7 +225,15 @@ static void checkRandomContinuation(bool text, bool ai) runtimeText.assign(storage->getBuffer(),storage->getPosition()); } - assert(randomGenerator == savedRandom); + if (!(syncRandEngine() == savedRandom)) + { + std::ostringstream now, was; + now << syncRandEngine(); + was << savedRandom; + std::cerr << "sync RNG changed across save:\n was " << was.str().substr(0, 96) + << "\n now " << now.str().substr(0, 96) << std::endl; + assert(false); + } auto simulationState = [](Game &game) { std::vector result, buildings, units; game.checkSum(&result, &buildings, &units, true); @@ -274,7 +283,7 @@ static void checkRandomContinuation(bool text, bool ai) restored.game.setGameHeader(header, true); auto expected = savedRandom; for (int i=0; i<2000; ++i) assert(syncRand() == expected()); - randomGenerator = savedRandom; + syncRandEngine() = savedRandom; for (int i=0; i<300; ++i) { step(restored.game); diff --git a/test/TrappedUnitLifecycleTest.cpp b/test/TrappedUnitLifecycleTest.cpp index f6134f1da..f9d9cea9e 100644 --- a/test/TrappedUnitLifecycleTest.cpp +++ b/test/TrappedUnitLifecycleTest.cpp @@ -127,7 +127,7 @@ Trace eliminationAndSave(int resource, int purpose) { GAGCore::BinaryOutputStream output(backend); f.game.save(&output, false, "trapped unit fixture"); std::string bytes(backend->getBuffer(), backend->getPosition()); - const auto checkpointRng = randomGenerator; + const auto checkpointRng = syncRandEngine(); const auto before = state(f.game, f.id); const Trace original = finish(f.game, f.id); GameGUI restored; @@ -135,7 +135,7 @@ Trace eliminationAndSave(int resource, int purpose) { input.seekFromStart(0); assert(restored.game.load(&input)); restored.game.setWaitingOnMask(0); - randomGenerator = checkpointRng; + syncRandEngine() = checkpointRng; const auto after = state(restored.game, f.id); for (size_t i = 0; i < before.size(); ++i) if (after.at(i) != before[i]) std::cerr << "state[" << i << "] " << before[i] << " -> " << after[i] << std::endl; diff --git a/tools/MenuColonyHarness.cpp b/tools/MenuColonyHarness.cpp index 5eb2cc3ba..7e799a688 100644 --- a/tools/MenuColonyHarness.cpp +++ b/tools/MenuColonyHarness.cpp @@ -53,7 +53,7 @@ GlobalContainer* globalContainer=nullptr; using namespace GAGCore; std::string replayFilenameToName(const std::string&); -std::string getSyncRandState() { std::ostringstream out; out << randomGenerator; return out.str(); } +std::string getSyncRandState() { std::ostringstream out; out << syncRandEngine(); return out.str(); } void require(bool condition, const char* message) { @@ -393,7 +393,7 @@ std::cout << "global_assets_ms=" << std::chrono::duration(std GameGUI gui; BinaryInputStream in(Toolkit::getFileManager()->openInputStreamBackend(argv[2])); in.readText("format"); require(gui.game.load(&in),"load real-game fixture"); - std::istringstream state(in.readText("rng")+" "); state >> randomGenerator; + std::istringstream state(in.readText("rng")+" "); state >> syncRandEngine(); gui.game.map.getResourceGradient(0,WHEAT,0); gui.localTeamNo=0; gui.localPlayer=0; gui.adjustLocalTeam(); globals.replayWriter=std::make_unique(); From 81622ca2aa11c07a1b7534d82b7294b2dc37b7e0 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 10:54:56 -0400 Subject: [PATCH 133/136] Tune ten generators from the 2026-09-14 play notes Play verdicts on the branch, each explained in the generator's header: - Tidal flats: six oases and eight sandbars per wedge by default (were three), drawn smaller and rounder with tighter gaps, so crowded wedges seat more of them; the smallest oasis keeps its pond in a ring of wheat. - Carousel: the sea inside the ring stays a lagoon with algae, the inward farm and the home kit are gone, and every lane or court on the lagoon keeps a 5-tile margin that is all stone (laneBand): on the 3-tile margin a diagonal spoke lost every grass tile down its middle to the beach and its seal, which cut colonies off from the plaza. - City states: an archipelago of round islets in the strait and the channels, each with the farms' 10x4 building plot and a small prize, two either side of every causeway on a four-colony map and more on a longer arc; drawn on the map about frame-designed middles so a bowed channel does not shear them, and a slot that would break its 3-tile moat in any wedge is left out of every wedge. The Barrens home layout is gone. Commons 45%, strait 11%. - Standard farms (Farmland): the sand bridges run clean across the whole farm, crop rows and water alike. - Amphitheatre: starting towers stand against the arena's outer wall within 18 steps of the colony's own ramp, covering the arena, not along the borders. - Starting tower level defaults to 1 on Carousel, Amphitheatre, Switchbacks and Canals. - Switchbacks, Carousel and Polder homes have no wheat and wood kit; Polder's wood share is 8% (was 15%). - Rain shadow: streams 7 deep (were 5), pass roads 18 tiles into each valley (were 12), ending in a lane of sand that joins the streams' beaches. - Canals: a `block-shape` choice of squares or hexagons, as on Maze. - Maze and Stone highlands amounts are percentages like every other landscape; the resource-amount audit is recorded in the framework doc. - The catalog order was shuffled once, so the list carries no bias towards the landscapes written first; the lobby and editor open on its first entry. Revisions bumped for every generator whose maps changed (Old town through the shared farm bridges); golden rows regenerated for macos-arm64 and linux-x86_64 (therig, gcc), --sweep 0 failing combinations. The saved-preferences bound for extra islands covers Tidal flats' new range. Docs updated in docs/map-generators/MAP_GENERATOR_FRAMEWORK.md. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 233 +++++++----- src/CustomGamePreferences.h | 5 +- src/map/generator/core/GenerationRequest.h | 6 + src/map/generator/core/GeneratorControls.cpp | 23 ++ src/map/generator/core/GeneratorRegistry.cpp | 52 +-- .../generators/AmphitheatreGenerator.cpp | 67 +++- .../generator/generators/CanalsGenerator.cpp | 42 ++- .../generator/generators/CanalsGenerator.h | 2 +- .../generators/CarouselGenerator.cpp | 166 +++++---- .../generators/CityStatesGenerator.cpp | 297 ++++++++++++--- .../generators/CityStatesGenerator.h | 3 +- .../generator/generators/MazeGenerator.cpp | 38 +- src/map/generator/generators/MazeGenerator.h | 2 +- .../generator/generators/OldTownGenerator.cpp | 2 +- .../generator/generators/PolderGenerator.cpp | 22 +- .../generators/RainShadowGenerator.cpp | 37 +- .../generators/StoneHighlandsGenerator.cpp | 16 +- .../generators/StoneHighlandsGenerator.h | 4 +- .../generators/SwitchbacksGenerator.cpp | 30 +- .../generators/TidalFlatsGenerator.cpp | 86 +++-- src/map/generator/shared/Farmland.cpp | 13 +- src/map/generator/shared/Farmland.h | 13 +- test/MapGeneratorDefaultsTest.cpp | 14 +- test/MapGeneratorToolkitChecks.h | 8 +- test/map-generator-golden.txt | 352 +++++++++--------- 25 files changed, 991 insertions(+), 542 deletions(-) diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index feb675f48..e92c2236a 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -35,7 +35,7 @@ and Terrain take a `Game` because placing buildings and units needs its mutation | `Tessellation` | Polygon tilings of the torus with no terrain attached: `squareTessellation` and `hexTessellation` (cells, the corners they share and edges as identities, exact across the wrap even two cells wide), `edgeEnds`, `centreAcross`, `outline`, `transform` (the lattice's translations and mirrors), `labelTiles` (every tile's cell), `shortestEdgeSteps` and `centreClearance`; `warpLimit` and `warpCorners`, corners moved at random into irregular polygons that still tile, never closing the gap between walls that share no corner below a minimum (Maze) | | `GraphMaze` | A maze carved through any `CellGraph` (a tessellation's with `cellGraph(tiling)`, or scattered sites' with `cellGraph(torus, sites, siteNeighbours)`): `pocketsFit` and `spreadPockets` (cul-de-sac cells spread as far apart as still leaves a maze), `carveSpanningTree` (recursive backtracker), `openPocketDoors`, `openLoops`, `deadEnds` (Maze) | | `Territories` | Ground shared out where wedges cannot be fair (a square map's corners, the wrap, odd colony counts): `growTerritories`, equal-area regions grown together from seed tiles, the smallest always taking the next cheapest tile, connected and deterministic, or shared by value when each claimant's ground has a worth per tile; `balancedTerritories`, a power diagram from one site per claimant with the weights tuned until the areas are equal, so every border is a straight line (Amphitheatre); `smoothLabels`, a majority filter over any radius that trims spurs or, at a radius of 3 or 4, straightens borders into curves; `separateTerritories`, a gap opened between neighbouring territories; `fillToNearest`, the reverse: unclaimed ground near labelled regions given to the nearest, so water between pieces becomes land; `strandedGround`, land a flood cannot reach; `growFarLake`, a lake of an exact size at the roomy far end of a region; `growLakeBeside`, one on either flank of a site, kept wholly to its side of the line from the way in; `siteAtDepth`, the roomiest site a given number of steps from a region's way in | -| `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges across the water rows every so many tiles along them, so workers need not walk round a long row (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives (and only ground whose eroded core joins the home's core stays, since two cores that do not touch can overlap once grown back through a waist the coast walls then close), and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | +| `Farmland` | Farms laid like real ones, in long rows of crops with water between: `bestFarmRows`, the crop and water widths that yield most for rows at an angle (10 and 8 tiles along an axis, 12 and 9 on the diagonal, a rough line through the exact regrowth sums `tools/farm_row_fit.py` computes); `farmYield`, the yield per tile at that angle from the same fit (0.149 along an axis, 0.113 on the diagonal); `layFarm`, rows over a region with a rim of land kept round them and, optionally, a 10x4 building plot of grass ringed with sand at its most inland point and sand bridges clean across the farm every so many tiles along the rows, water and crop rows alike (2026-09-14; before, only the water rows), so workers cross the whole field on one road (a plot's grass always wins over a bridge), and a ring of sand closing the crop rows so wheat and wood never spread out of the farm; `plantFarm`, wheat along the water on every crop row and a small woodlot along one; `clearFarmPlots`; `farmReachable`, the share of a farm's crop land a colony can walk to; `growFarmFields`, farm fields grown into open water straight out of their own homes by equal yield for their row angles, held off all other land and each other, opened so no strip too narrow for its beaches survives (and only ground whose eroded core joins the home's core stays, since two cores that do not touch can overlap once grown back through a waist the coast walls then close), and joined to their homes by a broad neck. Walls round a farm are the map's business, not the farm's | | `Towers` | Tower sites chosen for the ground they cover over the walls, other colonies' (offence) or their own (defence), each weighted: `startingTowerRequest` (a request from a map's level and count controls), `chooseTowerSites` (best first, a colony at a time in turn, optionally every site directly against a wall, no stocked tower in range of another colony's tower or swarm, plus open 2x2 pads for players to build more), `settleStartingTowers` (the sequence every arena map runs: `dropBlockingSites` so no site closes a colony's walk to a goal, `evenTowerPlan` so every colony has as many sites as the fewest got, then `raiseTowers`), `towerFootprints` and `roomyGround` (no tower on a strip it could close); a tower in range of another colony's tower starts empty | | `Homes` | Homes built alike: `stampRoundHome`, `homeSwarmSite` and `plantHomeKit` (a round home facing out from the middle, its swarm towards its door, optional ponds kept clear of its edge, and a wheat and wood kit near the swarm with no stone, since these homes are walled in stone; `homeHasRoom` is the size floor; Carousel, Switchbacks; the axis is any heading, out from the middle on a ring), `regionHome` (a home in ground of any shape: the swarm the same walk in from the door as every other colony's, on the roomiest such tile, facing away from the door), and `furnishGround` (farmland in patches on a ground's fertile tiles, outcrops and groves; City states, Carousel, Switchbacks, Amphitheatre) | | `Orbits` | Fairness by symmetry groups rather than wedges: `Symmetry` (signed permutation matrices on doubled centred coordinates plus a whole-tile translation, mapping tiles and corners exactly), `pointSymmetry` (the half turn, quarter turns, mirrors and all eight square symmetries; Symmetric arena), `translationSymmetry` (the roomiest lattice of 2, 4, 8... colonies on the torus, square, staggered or sheared, with no centre and served as well on a rectangular map), `latticeSites` (colonies on that lattice, or evenly staggered rows when the count has no exact group), `stampOrbits`, `orbitSum`, `orbitNoise` and `topShare` (features and fields every symmetry leaves unchanged), `equaliseDeposits` (the gameplay RNG's amounts made equal across each orbit) and `orbitMismatch` (the finished world checked exactly symmetric, colonies permuted one to one) | @@ -94,40 +94,43 @@ piece itself and says why in its header comment. `GeneratorRegistry::builtins()` orders these for the product-facing catalog; the numeric legacy id is a separate, stable compatibility identifier that never changes once assigned and is never -reused after a generator is retired. +reused after a generator is retired. The order was shuffled once, on 2026-09-14, so the list +carries no bias towards the landscapes that happened to be written first; the lobby opens on the +first playable entry and the editor on the first entry (both Fingerprint), and a saved lobby +restores whatever landscape it had. | id | legacy id | Display name | Family | |---|---|---|---| -| `contested-commons` | 9 | Contested commons | Point dispersion (`shared/legacy/Regions`) | -| `maze` | 11 | Maze | Its own — see below | -| `fjord-continent` | 12 | Fjord continent | Its own — see below | -| `shattered-coast` | 7 | Old random | Iterative water/sand/grass balancer, own resource search | +| `fingerprint` | 26 | Fingerprint | Its own — see below | +| `old-town` | 31 | Old town | Its own — see below | +| `symmetric-arena` | 15 | Symmetric arena | Its own — see below | +| `swamp` | 1 | Swamp | Height-field noise; water interleaved with land | +| `tidal-flats` | 18 | Tidal flats | Its own — see below | +| `river` | 2 | River | Height-field noise; a winding river through connected land | | `isles` | 6 | Isles | Point dispersion; islands linked by land bridges | +| `ring-world` | 16 | Ring world | Its own — see below | +| `amphitheatre` | 23 | Amphitheatre | Its own — see below | +| `shattered-coast` | 7 | Old random | Iterative water/sand/grass balancer, own resource search | +| `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | +| `fjord-continent` | 12 | Fjord continent | Its own — see below | +| `spider-web` | 20 | Spider web | Its own — see below | +| `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | | `watershed` | 13 | Watershed | Its own — see below | +| `maze` | 11 | Maze | Its own — see below | +| `islands` | 3 | Islands | Height-field noise; organic islands with no inter-island passage | | `stone-highlands` | 14 | Stone highlands | Its own — see below | -| `symmetric-arena` | 15 | Symmetric arena | Its own — see below | -| `ring-world` | 16 | Ring world | Its own — see below | +| `switchbacks` | 24 | Switchbacks | Its own — see below | | `city-states` | 17 | City states | Its own — see below | -| `tidal-flats` | 18 | Tidal flats | Its own — see below | +| `canals` | 29 | Canals | Its own — see below | +| `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | +| `contested-commons` | 9 | Contested commons | Point dispersion (`shared/legacy/Regions`) | +| `rain-shadow` | 27 | Rain shadow | Its own — see below | | `everglades` | 19 | Everglades | Its own — see below | -| `spider-web` | 20 | Spider web | Its own — see below | -| `coral` | 21 | Coral | Its own — see below | +| `polder` | 30 | Polder | Its own — see below | | `carousel` | 22 | Carousel | Its own — see below | -| `amphitheatre` | 23 | Amphitheatre | Its own — see below | -| `switchbacks` | 24 | Switchbacks | Its own — see below | -| `fingerprint` | 26 | Fingerprint | Its own — see below | -| `rain-shadow` | 27 | Rain shadow | Its own — see below | | `old-growth` | 28 | Old growth | Its own — see below | -| `canals` | 29 | Canals | Its own — see below | -| `polder` | 30 | Polder | Its own — see below | -| `old-town` | 31 | Old town | Its own — see below | | `anthill` | 32 | Anthill | Its own — see below | -| `rugged-archipelago` | 8 | Old islands | Island growth + beach passes, own resource search | -| `concrete-islands` | 5 | Concrete islands | Point dispersion; islands linked by channels | -| `crater-lakes` | 4 | Crater lakes | Height-field noise; round lakes in otherwise connected land | -| `islands` | 3 | Islands | Height-field noise; organic islands with no inter-island passage | -| `swamp` | 1 | Swamp | Height-field noise; water interleaved with land | -| `river` | 2 | River | Height-field noise; a winding river through connected land | +| `coral` | 21 | Coral | Its own — see below | | `uniform` | 0 | uniform terrain | Editor-only; one terrain type, unstructured | Retired ids, never reused: 25 (Marches, a lattice of homelands with wooded border bands, dropped @@ -154,7 +157,16 @@ sub-behaviours. An amount is a percentage of the generator's default, 100, from of 25 unless noted. It scales the numbers that already decided that amount, and at 100 every map is exactly what it was. Fairness placements (starter kits, 1:1 guaranteed wheat and wood, the reachability backstop) stay unscaled wherever a generator has them, so an amount of 0 empties the -ambient layer but still leaves every colony a start. Maze keeps its explicit densities. +ambient layer but still leaves every colony a start. + +Audited 2026-09-14 for consistency: every landscape carries `wheat-amount`, `wood-amount`, +`stone-amount`, `algae-amount` and `fruit-amount` as percentages, with these exceptions, each +because the landscape has no such layer or names it differently: Anthill has no `stone-amount` +(its only stone is its walls); Old growth has no `wood-amount` (its `forest-density` is the wood +control); Isles, Old random and Old islands have no `fruit-amount` (they place no fruit); the +height-field generators (Swamp, River, Islands, Crater lakes) share `heightFieldResourceControls` +and keep their `fruit` control as a count of groves, 0 to 64. Maze's and Stone highlands' amounts +were counts of their own until that audit and are percentages since. | Generator | What the amounts scale | Switches (default) | |---|---|---| @@ -164,20 +176,20 @@ ambient layer but still leaves every colony a start. Maze keeps its explicit den | Old random | The area of each colony's wheat, wood, stone and algae squares | Colony meadows (on): the cleared grass square round each colony | | Old islands | The area of each island's deposits | Extra starting deposit (on): the fourth deposit, of whichever of wheat or wood came out smaller | | Contested commons | How many of the commons' zones are wheat, wood or fruit, its quarry's size and the moat's algae; home islands' fields are unscaled | Moat bridges (on); Jagged coastlines (on) | -| Maze | No new amounts: its existing Wheat, Wood, Stone, Algae and Fruit controls already set them | Sand roads (on); Treasure in dead ends (on): off, the same fruit is scattered along the passages' shores | +| Maze | The shore scatter's wheat, wood and stone (48, 24 and 16 tiles per 256 shore tiles at 100), every dead end's treasure (9 fruit tiles) and the channels' algae (24 per 400 water tiles); the homes' kits are unscaled | Sand roads (on); Treasure in dead ends (on): off, the same fruit is scattered along the passages' shores | | Fjord continent | The ambient scatter, the core's stone and fruit clumps, the lake's and open sea's algae, and the banks' extra clumps; starter kits and bank guarantees are unscaled | Lake connects to fjords (off); Sandy lake shore (on); Fjord bank deposits (on) | | Watershed | Separate wheat and wood farmland budgets, stone outcrops, confluence fruit groves, and algae at the mouths and in the shallows; starter kits are unscaled | River delta (on); Meandering rivers (on) | -| Stone highlands | The ponds' farmland (wheat and wood) and algae, and how much of the ridgeline is two tiles thick (stone, 0 to 200); kits are unscaled and Fruit still counts groves | Fruit in home valleys (off) | +| Stone highlands | The ponds' farmland (wheat and wood) and algae, how much of the ridgeline is two tiles thick (stone, 0 to 200), and the valleys' fruit groves (four per 128×128 at 100); kits are unscaled | Fruit in home valleys (off) | | Symmetric arena | The farmland's wheat and wood, stone outcrops and algae, on top of Resource richness; fruit (0 to 100) keeps that share of the orchard's groves nearest the centre, never fewer than three | Moat (on); Stone in the orchard (on) | | Ring world | The ambient scatter's wheat, wood, stone and fruit, and the shallows' algae; starter patches and island prizes are unscaled | Winding belt (on); Colonies on both coasts (on) | -| City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard) and the sea's algae and island prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | +| City states | Every home's ambient fields, outcrops and grove, everything on the commons (farmland, outcrops, groves, the orchard), the sea's algae and the islets' prizes; every home's kit and the walls' stone are unscaled | Stone walls (on): off, the causeways are plain roads and the homes' coasts are open | | Tidal flats | Every island's ambient fields and outcrops and every island's prize; each home's kit is unscaled | Central island (on): off, the middle of the map is flats and there is no orchard | | Everglades | The swamp's standing wood and wheat, its outcrops and groves, and the pools' algae; every home's kit is unscaled | None | | Spider web | The threads' standing wheat and wood, the share of knots carrying stone, whether the dew drops and the hub carry fruit and stone, and the shallows' algae; every pad's kit is unscaled | Spiral (on): off, the capture threads are closed rings; Sand roads (on): off, the threads are grass from shore to shore | | Coral | The branches' standing wheat and wood, the share of forks carrying stone, the tips' fruit groves and the shallows' algae; every pad's kit is unscaled | Sand roads (on): off, the branches are grass from shore to shore | -| Carousel | Every home's ambient fields, the farms' wheat and woodlots, the courts' and the plaza orchard's fruit, and the algae; every home's kit, the walls' stone and the starting towers are unscaled | Sand roads (on): off, the corridors and spokes are grass from wall to wall | +| Carousel | Every home's ambient fields, the farms' wheat and woodlots, the courts' and the plaza orchard's fruit, and the lagoon's algae; the walls' stone and the starting towers are unscaled (a home has no kit since 2026-09-14) | Sand roads (on): off, the corridors and spokes are grass from wall to wall | | Amphitheatre | Every territory's ambient fields and grove, the arena's groves and terrace outcrops, and the bays' algae; every home's kit, the walls' stone and the starting towers are unscaled | None | -| Switchbacks | Every home's ambient fields and grove, the farms' wheat and woodlots, the plateau's orchard, and the algae; every home's kit, the mountains' stone and the starting towers are unscaled | Sand roads (on): off, the trails are grass from wall to wall | +| Switchbacks | Every home's ambient fields and grove, the farms' wheat and woodlots, the plateau's orchard, and the algae; the mountains' stone and the starting towers are unscaled (a home has no kit since 2026-09-14) | Sand roads (on): off, the trails are grass from wall to wall | `scaledCount` and `scaledShare` (`shared/Resources.h`) apply a percentage to a count or a share and return it unchanged at 100. `setScaledResource` scales one `Map::setResource` square to a share of @@ -412,8 +424,9 @@ cul-de-sac. its tiles the same way on every platform. Outside the homes, clumps of wheat, wood and stone (densities per 256 shore tiles) are scattered along every passage's shores, never more than three tiles in, so each passage keeps a clear lane down its middle however the maze turns. - Fruit is treasure: every dead end that isn't a home gets one compact patch of `fruit` tiles near - its far end, with fruit types dealt round-robin so every kind is somewhere in the maze. With + Fruit is treasure: every dead end that isn't a home gets one compact patch of nine fruit tiles + (at 100% `fruit-amount`) near its far end, with fruit types dealt round-robin so every kind is + somewhere in the maze. With `dead-end-treasure` off (on by default) the same fruit is scattered along the passages' shores instead. Algae is seeded along the channels. - **Checked, not assumed.** `validateWorld` floods walkable tiles (water, buildings and every @@ -471,8 +484,9 @@ the whole game. interiors away from ridges, passes and homes, with two-tile beaches so algae can regrow; valleys under 120 tiles get none. - **Resources.** Identical 1:1 wheat and wood kits beside every home, 2:1 farmland ringing the - ponds, algae in the water, and `fruit` groves (per 128×128) only in valleys no colony starts in - (unless `home-valley-fruit`, off by default, lets them grow in home valleys too). + ponds, algae in the water, and fruit groves (four per 128×128 at 100% `fruit-amount`) only in + valleys no colony starts in (unless `home-valley-fruit`, off by default, lets them grow in home + valleys too). `guaranteeStartingResources` runs with the ridges as protected walls, and any resource left in or beside a pass is removed. `validateRequest` rejects maps smaller than four valleys or with fewer than 1,024 tiles per colony. @@ -552,9 +566,9 @@ fairness falls from about 0.97 to 0.65–0.8, because a home's fields and fertil shape. - **Geometry.** A pure function of the request (`geometryFor`). The commons' radius is - `commons-size` percent of half the shorter side, the strait `strait-width` percent of the shorter - side, and the homes reach out to the map's half side less a rim of sea, so opposite homes never - meet across the wrap. Both coasts are radial shapes (`coast-roughness`); the strait follows the + `commons-size` percent of half the shorter side (45 since 2026-09-14, from 55), the strait + `strait-width` percent of the shorter side (11, from 4: room for the islets), and the homes reach + out to the map's half side less a rim of sea, so opposite homes never meet across the wrap. Both coasts are radial shapes (`coast-roughness`); the strait follows the commons' coast. Every home is the same wedge turned round the centre, so the layout is fair for any colony count. `validateRequest` needs at least 20 tiles of home between the strait and the sea, and at each home's inner coast at least the causeway plus ten tiles of arc beyond its @@ -584,7 +598,8 @@ shape. around each causeway; every channel bows sideways by the same random amount. One home layout and one heart layout are drawn per map. Homes: Lakeland (one lake), Riverside (a creek from the lake towards one flank with a sand ford), Highland (two stone ridges out to the sea with a pass - each), Marsh (four ponds) and Barrens (a band of sand with a corridor through it). Hearts: a + each) and Marsh (four ponds); Barrens, both flanks of the home under sand, was dropped + 2026-09-14 ("that one is just a giant desert"). Hearts: a lake with the orchard on its shore, a stone crag with a gap towards every landing, an island in the lake reached by a ford from every landing, a delta of rivers from the lake to the strait between the landings with a ford each, and a belt of forest round the orchard. `sand` adds @@ -597,13 +612,26 @@ shape. - **Commons.** A central lake of `kHeartShare` of its radius, `valleys` extra lakes (per 128×128 of commons) likelier towards the centre, farmland on fertile ground weighted by depth inward per `frontier-richness`, outcrops and groves by the same weight, and the orchard of the three fruits - on the central lake's shore. `resource-islands` (per 128×128 of sea) raises islets with a prize - each out in the sea. Algae seeds every shallows. `guaranteeStartingResources` runs with the + on the central lake's shore. Algae seeds every shallows. `guaranteeStartingResources` runs with the walls' stone protected, causeways and approaches are cleared, and a cheapest-walk pass keeps a way open from every swarm to its causeway and from every landing to the heart. +- **The archipelago** (2026-09-14: "WAY more islands out filling the no mans land", each with "the + 10x4 little sand building plots"). Round islets of radius 9 dot the strait and the channels: + `islands` (0-4, 2) either side of every causeway on a four-colony 256 map's arc, proportionally + more on a longer arc (a two-colony map gets four a side) and never more than fit at three tiles + apart, spread evenly along the strait's midline between the causeway's clear stretch and the + channel, and one in every channel between two homes, half way along it. Their middles are designed in the wedge frame, so every colony has the same islets + at the same distances, and each is drawn as a round disc on the map about its middle, so a bowed + channel does not shear it. Every islet carries the farms' 10x4 building plot (`stampFarmPlot`, a + clearing of grass in a two-vertex ring of sand) at its middle, kept clear of everything, and a + small prize on its grass beyond the ring, stone, fruit and wheat in turn round the wedge. An islet + keeps three tiles of water from every coast, causeway and other islet, so it is only ever reached + by swimming: a forward post, not a stepping stone. An islet whose moat the strait's bulge at a + causeway or a channel's bow would break is left out of every wedge alike. A 128 map's strait is + too narrow for any. - **Checked, not assumed.** `validateWorld` rebuilds the design and requires every causeway road and ford walkable and every designed stone tile present (shoulders, walls, ridges and crag), every colony and the heart reachable on foot from - colony 0, no colony reachable from any beach with the roads shut, no home able to reach the + colony 0, every islet's plot buildable and no islet reachable on foot, no colony reachable from any beach with the roads shut, no home able to reach the commons or another home with the causeways shut, and the colonies' walks to their landings within twelve steps of each other. `validateRequest` needs at least 20 tiles of home depth. @@ -627,9 +655,18 @@ on its island's edge, and expansion means taking another island whole. pond's two sides and a quarry towards the map's centre, then scaled ambient farmland on its fertile ground and an outcrop. Every neutral island is an oasis: a pond, and every other tile of it under unscaled wheat, so taking one means clearing it first, with one prize inside, a - fruit grove or a stone deposit in turn. With `central-island` (on) an island at the centre carries a pond, - the orchard of all three fruits and a quarry. Algae seeds every pool and lagoon, which is exactly - where it regrows. Nothing is kept clear because the flats hold nothing. + fruit grove or a stone deposit in turn. Since 2026-09-14 ("more of those pond + wheat or pond + + wood oases, especially on smaller maps with more players", and "a few more of the random green + patches"), `extra-islands` defaults to six per wedge (was three) and `sandbars` to eight (was + three); an oasis is 30 to 45 percent of a home island's radius, never under 3.5 tiles, rounder + than a home island, and keeps three tiles of sand from other features and four from a home island + (every feature used to keep eight); sandbars, the green patches a forward post stands on, are 3.5 + to 5 tiles and keep the same gaps; and the scatter tries 240 times per feature (was 80). A 128 map + with six colonies is the limit: its home islands nearly touch, and only an oasis or two of the + smallest size fits between them and the central island. With `central-island` (on) an island at + the centre carries a pond, the orchard of all three fruits and a quarry. Algae seeds every pool + and lagoon, which is exactly where it regrows. Nothing is kept clear because the flats hold + nothing. - **Checked, not assumed.** `validateWorld` rebuilds the design and requires every home's pond present and every colony and the central island reachable on foot from colony 0, with water, buildings and every resource blocking. @@ -802,7 +839,9 @@ the other. A colony's single door opens onto a narrow corridor that follows the plaza. The court and that home are parted by a thin wall of stone that no unit crosses but a tower shoots over, so every colony's towers, in its own home against that wall, cover its neighbour's only way out. The concept depends on the lanes being tight, so they default narrow and every wall is a -single line of stone with no water beside it. +single line of stone with no water beside it. Inside the ring the sea stays: the plaza is an island +in a lagoon with algae in it, and the spokes cross the lagoon between bands of stone (2026-09-14: +farms reaching in towards the plaza made it "hard to know where you are on the map"). - **Geometry** (`geometryFor`, `drawLanes`). Designed once in the wedge's frame and turned for every colony; round on a rectangular map. The homes' outer edge is `kRingShare` of the half side (a little @@ -816,30 +855,40 @@ single line of stone with no water beside it. the court wall is just the land within `court-wall` tiles (default 2) of both the court and the next home; the plaza with its pond. A ring too crowded for a wall between a lane and another home, or for two colonies' lanes to stay apart, is refused. -- **Farms** (`claimFarmFields`, `layFarmRows`). `growFarmFields` grows every colony one field in towards - the plaza and one out beyond the ring, each set shared out by equal yield for the colony's row angle, - so a colony whose rows fall on the diagonal gets more ground. A set is kept only where every colony's - field has room. Once the walls stand (below), `layFarm` lays rows along the colony's axis at - `bestFarmRows` widths over the whole walled field, keeping three tiles of land round the water (six - against open sea), with a sand cap closing the crop rows, a sand bridge across the water every 16 - tiles and, with `farm-plots` (on), a 10x4 building plot. `plantFarm` plants wheat along the water - with one small woodlot. The farms are the homes' only water. -- **Filling in and walls** (`fillAndWall`). The sea within `kFillReach` (24) tiles of a home or farm - becomes that colony's ground (`fillToNearest`); lanes, courts and the plaza never grow, so they keep - exactly their drawn width. Sea left beyond keeps a `kSeaMargin` (3) strip of the nearest piece before - its coast, which is sealed (`sealCoasts`). Every tile then has a side (the plaza; a colony's court and - lanes; a colony's home and farms), and a single line of stone stands wherever two sides meet - (`labelBorders` with doors), always on the home's or farm's side, so no wall narrows a lane. Two - borders stay open: each home's door onto its own corridor, and each spoke's way into the plaza. +- **Farms** (`claimFarmFields`, `layFarmRows`). `growFarmFields` grows every colony one field out + beyond the ring (the inward field towards the plaza went with the lagoon, 2026-09-14), shared out + by equal yield for the colony's row angle, so a colony whose rows fall on the diagonal gets more + ground, and kept only where every colony's field has room. Once the walls stand (below), `layFarm` + lays rows along the colony's axis at `bestFarmRows` widths over the whole walled field, keeping + three tiles of land round the water (six against open sea), with a sand cap closing the crop rows, + a sand bridge clean across the whole farm, water and crop rows alike, every 16 tiles (2026-09-14: + "so they go clean across the whole farm") and, with `farm-plots` (on), a 10x4 building plot. + `plantFarm` plants wheat along the water with one small woodlot. The farms are the homes' only + water. +- **Filling in and walls** (`fillAndWall`). The sea outside the ring within `kFillReach` (24) tiles + of a home or farm becomes that colony's ground (`fillToNearest`); the lagoon, every tile of sea + inside the ring through the homes' centres, is never filled; lanes, courts and the plaza never + grow, so they keep exactly their drawn width. Sea left keeps a `kSeaMargin` (3) strip of the + nearest piece before its coast, which is sealed (`sealCoasts`); a lane or court keeps + `kLaneMargin` (5) instead, and all of that margin is stone (`laneBand`), since a beach's mixed + tiles and a sealed coast reach three tiles in where a coast runs diagonally, which left a diagonal + spoke on the 3-tile margin with no grass down its middle. Every tile then has a side (the plaza; a + colony's court and lanes; a colony's home and farms), and a single line of stone stands wherever + two sides meet (`labelBorders` with doors), always on the home's or farm's side, so no wall + narrows a lane. Two borders stay open: each home's door onto its own corridor, and each spoke's + way into the plaza. - **Roads** (`finishRoads`). With `sand-roads` (on), a sand road runs down the corridor, through the court, down the spoke and across the plaza to its pond, so the way in can never be fully overgrown. Roads keep `kRoadSeaGap` from water and never touch a wall tile. -- **Homes and prizes** (`furnish`). A wheat and wood kit beside every swarm (`plantHomeKit`, no stone: - the walls are stone) and scattered farmland (`furnishGround`); nothing is planted within six steps of - a lane. One grove of one fruit per court, and on the plaza only fruit: an orchard of the three fruits - between every two spokes' arrivals, with no wheat or wood to smother it. +- **Homes and prizes** (`furnish`). Scattered farmland on every home's fertile ground + (`furnishGround`) and no starter kit of wheat and wood blocks (2026-09-14: "there's already too + much food on this map"; the farm feeds the home, and `secureStartingCrops` is the backstop); + nothing is planted within six steps of a lane. One grove of one fruit per court, and on the plaza + only fruit: an orchard of the three fruits between every two spokes' arrivals, with no wheat or + wood to smother it. Algae seeds the lagoon's shallows. - **Starting towers** (`planTowers`). `starting-towers` sets their level (0 none, when every site is - an open pad; default 2, level 1) and `tower-count` how many (default 3), with three open pads. Every + an open pad; default 1 since 2026-09-14, so players upgrade their own) and `tower-count` how many + (default 3), with three open pads. Every site stands in the colony's own home, directly against stone and within six tiles of a wall, chosen for how much of the previous colony's elbow (its court and lanes within twelve tiles) it covers (`chooseTowerSites` counting other colonies' elbows only). `settleStartingTowers` drops any site @@ -878,10 +927,13 @@ is a meeting at a known place. territories. - **Prizes.** Orchards of the three fruits in the pit and on the innermost terrace, and an outcrop on every terrace, the same at every colony's angle. -- **Starting towers.** `tower-count` towers (default 3, at `starting-towers` level) and three open - pads per colony in its territory, each directly against stone and away from its ramp, covering the - most of its neighbours' territories over the border walls (`chooseTowerSites`, - `settleStartingTowers`). +- **Starting towers.** `tower-count` towers (default 3, at `starting-towers` level, default 1 since + 2026-09-14) and three open pads per colony in its own territory, each directly against the + arena's outer wall within 18 steps of the colony's ramp mouth and clear of the ramp itself, + covering the most of the arena (terraces, pit and ramps) over the stone (`chooseTowerSites`, + `settleStartingTowers`). Until 2026-09-14 they stood along the border walls covering the + neighbours' territories, which "make no sense": the starting towers hold the door, and a colony + that wants towers on its borders builds them. - **Checked, not assumed.** Every designed stone present, every ramp walkable, territories within `kAreaTolerance` percent, every colony's seas the same size, no territory reaching another or the arena with the outer ramps shut, and even walks to the ramps and to the pit. @@ -910,16 +962,20 @@ next leg up. Every home reaches a walled wheat farm on either flank. colony's ground and another's (`labelBorders`): nothing but stone parts two colonies, as on Carousel (third play, 2026-09-13; before this the fields kept three tiles of water from each other and the mountains, which on a 256x128 map took over half the sea). Rows run across the axis at `bestFarmRows` - widths (`layFarm`, rim 3), with a sand cap, a sand bridge across the water every 16 tiles and a 10x4 - building plot under `farm-plots` (on); wheat with one woodlot (`plantFarm`). + widths (`layFarm`, rim 3), with a sand cap, a sand bridge clean across the whole farm every 16 + tiles (2026-09-14; before, only the water rows) and a 10x4 building plot under `farm-plots` (on); + wheat with one woodlot (`plantFarm`). - **Walls.** The mountains' rock, the border lines between colonies, and a sealed coast on whatever sea a design leaves (none at the defaults). Sea within a level-3 tower's range of the plateau becomes rock before the fill, so the mountains' inner ends join round the plateau and nothing outside the trails comes near it. - **Homes and plateau.** Round homes (`stampRoundHome`) whose farms are their water on every size of - map (128 maps had two home ponds instead until 2026-09-13); a wheat and wood kit with no stone. The plateau has a pond and only - fruit, an orchard of the three fruits between every two summits, so nothing overgrows it. -- **Starting towers.** `tower-count` towers (default 3) and four open pads per colony, all on its trail + map (128 maps had two home ponds instead until 2026-09-13), with no starter kit of wheat and wood + blocks since 2026-09-14 (the farms on either flank feed the home; `secureStartingCrops` is the + backstop). The plateau has a pond and only fruit, an orchard of the three fruits between every + two summits, so nothing overgrows it. +- **Starting towers.** `tower-count` towers (default 3, at `starting-towers` level, default 1 since + 2026-09-14) and four open pads per colony, all on its trail beside the walkway down its middle, each directly against stone on the inner side of a wall - the side towards the middle of the map - so it shoots across the stone at the next leg up, where attackers coming down from the plateau pass (`chooseTowerSites` counting the colony's own trail). A trail too @@ -970,15 +1026,18 @@ at intervals staggered from ridge to ridge, so moving along a valley is easy and reach across (`towerReach`). - **Passes.** Every `pass-spacing` tiles (24-96, 48) along the ridge, `pass-width` (3-9, 5) wide, laid out by the phase along the ridges (`alongStripes`), so they repeat seamlessly. -- **Wind.** Streams run along the windward foot, 20 tiles long every 24 and 5 deep (the first play - wanted the ponds "deeper and more connected"), four tiles out from the stone +- **Wind.** Streams run along the windward foot, 20 tiles long every 24 and 7 deep (the first play + wanted the ponds "deeper and more connected"; the second, 2026-09-14, "a couple tiles larger" + still), four tiles out from the stone (three took the ridge's windward row with them: a pool's beach spoils the tiles round it and stone stands only on pure grass); the lee sand starts two tiles behind the ridge for the same reason and runs `lee-width` (2-12, 6) tiles, measured downwind from the stone itself (`upwindSteps`) so it stops where a pass lets the rain through. -- **Pass roads, lakes and rivers.** A line of sand runs through every pass and 12 tiles into the - valley on either side, so a pass never grows shut; no stream lies within 4 tiles of a pass along the - ridge. `inland-lakes` (on) throws lake sites 56 apart, keeps those within 18% of a valley's middle +- **Pass roads, lakes and rivers.** A line of sand runs through every pass and 18 tiles into the + valley on either side (12 until 2026-09-14), so a pass never grows shut, and on the windward side + it ends in a lane of sand along the middle of the foot out to the streams either side of the pass, + so the road runs into their beaches instead of stopping short of them in the grass; no stream lies + within 4 tiles of a pass along the ridge. `inland-lakes` (on) throws lake sites 56 apart, keeps those within 18% of a valley's middle phase, grows a rough lake of radius 5 at each and joins it to the nearest stream by a wandering river one tile wide (`wanderingPath`); `sand-patches` (0-20, 6) percent of the valleys' grass is sprinkled into sand patches (`sprinkleSand`). All from the first play: "the valleys feel too empty". @@ -1028,7 +1087,9 @@ a unit and just narrow enough for a tower on one bank to shoot the other, and on bridges. Towers reach across from the first minute and armies cannot, so where the first towers go is the opening; once swimming pools are built every canal is a road. -- **Blocks.** A square tiling of `block-size` (16-40, 24) warped by `warp` (0-100, 80), every edge an +- **Blocks.** A tiling of `block-size` (16-40, 24) - squares, or with `block-shape` hexagons + (2026-09-14, "an option similar to the maze generator"; a hexagon's pitch is 150% of the size, so + its blocks hold about as much as the squares) - warped by `warp` (0-100, 80), every edge an obstacle the warp keeps apart (`warpCorners`), every edge stroked `canal-width` corners of water (3-5, 3). A canal's water is a corner narrower than the stroke and a diagonal canal is sealed against a diagonal step only when its water is two tiles thick, which is why the narrowest offered is 3; a @@ -1053,8 +1114,8 @@ opening; once swimming pools are built every canal is a road. colony's, so every colony can be walked to, then `extra-bridges` percent (0-100, 30) of the blocks' count more at random (`openLoops`), so most blocks stay islands until someone swims. A bridge is a line of sand corners across the canal (a tile with a sand corner is no longer pure water) reaching onto both banks. -- **Towers.** `tower-count` towers at `starting-towers` level (default 2 of level 2) and two pads per - colony, on its own bank against the beach (`chooseTowerSites` with `against`), covering the most of +- **Towers.** `tower-count` towers at `starting-towers` level (default 2 of level 1, from level 2 on + 2026-09-14) and two pads per colony, on its own bank against the beach (`chooseTowerSites` with `against`), covering the most of other blocks' land across the canal; none may close the colony's walk to a bridge (`settleStartingTowers`). - **Fields.** Every block is fertile (its canal is within the growth probe's reach of all of it), so a @@ -1078,12 +1139,14 @@ until someone can swim. rows (`alongStripes`). - **Villages.** Grass discs of `village-size` (8-20, 14) on a lattice, shrunk so a whole row and ditch lie between two, each in a two-tile ring of sand the crops cannot cross (first play: growth "quickly - crowds out the base"); each holds a swarm, its kit and a few buildings. Two and a half 10x4 farm + crowds out the base"); each holds a swarm, its quarry and a few buildings, and no wheat or wood + blocks since 2026-09-14 (the rows a few tiles away are the polder's food). Two and a half 10x4 farm plots per colony (`stampFarmPlot`) are spread through the rows, each the farthest a plot can stand from every village, hamlet and earlier plot, and at least 18 tiles from any village. `hamlets` (on) puts a grass disc of radius 5 half way between neighbouring villages, room for a forward inn and a tower, with a fruit grove. -- **Fields.** 55% of the fertile row ground under wheat and 15% under wood, in patches; every row tile is +- **Fields.** 55% of the fertile row ground under wheat and 8% under wood (15% until 2026-09-14, + "turn down the default amount of wood"), in patches; every row tile is a few tiles from water, so it all regrows. Only crops could close a lane, so `openColonyRoutes` clears only crops. @@ -1110,7 +1173,7 @@ into the next. the outer blocks where it meets them (the second and third plays asked for "little tendril sand roads extending inwards", short and frequent). - **Fields.** Outside the city, farm rows along the map's axis (`layFarm` at `bestFarmRows`, bridged - every 16) with `farm-plots` (0-6, 3) 10x4 building plots per colony spread through them + clean across every 16) with `farm-plots` (0-6, 3) 10x4 building plots per colony spread through them (`stampFarmPlot`, each the farthest a plot can stand from the city and the plots before it), half their fertile ground under wheat and a twentieth under wood, no outcrops; the plazas inside get a light share so they stay open. Every kit has no quarry: the blocks are stone. diff --git a/src/CustomGamePreferences.h b/src/CustomGamePreferences.h index b1a89591b..dc629296b 100644 --- a/src/CustomGamePreferences.h +++ b/src/CustomGamePreferences.h @@ -37,7 +37,8 @@ struct CustomGamePreferences // stands in for river width, lake size - Fjord's reaches 0 and 90, Old // growth's 160 - channel width and bridge width; craterDensity also // stands in for Ring world's lake density, which reaches 0; grassRatio - // also stands in for Isles' island size). This only needs to be at least as wide + // also stands in for Isles' island size; extraIslands also counts Tidal + // flats' oases, which reach 10). This only needs to be at least as wide // as what setMethodDefaults() and every registered control can // produce; it is not a tight per-method bound. Controls with no // legacy field are saved in the options section instead. @@ -49,7 +50,7 @@ struct CustomGamePreferences FIELD(wheatRatio, 0, 64), FIELD(woodRatio, 0, 64), FIELD(fruitRatio, 0, 64), FIELD(algaeRatio, 0, 64), FIELD(stoneRatio, 0, 64), FIELD(riverDiameter, 0, 160), - FIELD(craterDensity, 0, 64), FIELD(extraIslands, 0, 8), + FIELD(craterDensity, 0, 64), FIELD(extraIslands, 0, 10), FIELD(oldIslandSize, 1, 70), FIELD(oldBeach, 0, 4), FIELD(smooth, 1, 8), FIELD(nbTeams, 2, Team::MAX_COUNT), FIELD(nbWorkers, 1, 8) diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h index 3f00e8e17..f41bd4caf 100644 --- a/src/map/generator/core/GenerationRequest.h +++ b/src/map/generator/core/GenerationRequest.h @@ -52,6 +52,12 @@ struct GenerationRequest static const std::vector &sharedControls() { return sharedGeneratorControls(); } static const Control &control(int method, const std::string &id); static const char *methodName(int method); + /// Draws every one of this landscape's own controls at random - terrain, resources, switches, + /// layout alike - and keeps only a draw the generator accepts up front (validateRequest); the + /// shared controls (size, colonies, workers) frame the game the player asked for and are left + /// alone. Deterministic for a seed. Up to `attempts` draws; false, and the request unchanged, + /// when none was accepted. A draw the world then refuses is the caller's to redraw. + bool randomizeControls(std::uint32_t seed, int attempts = 64); }; class GenerationHistory { diff --git a/src/map/generator/core/GeneratorControls.cpp b/src/map/generator/core/GeneratorControls.cpp index e1b8bf76c..7a73b04b4 100644 --- a/src/map/generator/core/GeneratorControls.cpp +++ b/src/map/generator/core/GeneratorControls.cpp @@ -2,8 +2,10 @@ #include "GeneratorControls.h" #include "GenerationRequest.h" #include "GeneratorRegistry.h" +#include "GenerationValidation.h" #include "Team.h" #include +#include #include GeneratorControl GeneratorControl::toggle(std::string id, const char *label, bool on, ControlGroup group) @@ -129,6 +131,27 @@ const char *GenerationRequest::methodName(int id) { return GeneratorRegistry::builtins().at(id).nameKey; } +bool GenerationRequest::randomizeControls(std::uint32_t seed, int attempts) +{ + // A stream of its own: this is lobby randomness, nothing the simulation ever sees. + std::mt19937 rng(seed); + const GeneratorDefinition &definition = GeneratorRegistry::builtins().at(method); + for (int attempt = 0; attempt < attempts; ++attempt) + { + GenerationRequest draft = *this; + for (const auto &c : definition.controls) + { + const std::vector domain = c.values(); + c.set(draft, domain[rng() % domain.size()]); + } + if (validateGenerationRequest(draft, definition).empty()) + { + *this = draft; + return true; + } + } + return false; +} const GeneratorControl &GenerationRequest::control(int method, const std::string &id) { for (const auto &c : controls(method)) diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 6dd0cea08..635516792 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -114,39 +114,41 @@ int GeneratorRegistry::selectionIndex(int id, bool editor) const } const GeneratorRegistry &GeneratorRegistry::builtins() { - // This is also the product-facing catalog order. Keep the most polished and - // distinctive playable maps first; numeric IDs remain stable compatibility - // identifiers and do not determine presentation order. - static const GeneratorRegistry registry({contestedCommonsDefinition(), - mazeDefinition(), - fjordContinentDefinition(), - shatteredCoastDefinition(), + // This is also the product-facing catalog order. It was shuffled once, on 2026-09-14 + // (std::mt19937-free: Python's random.Random(20260914).shuffle over the playable list as it + // then stood), so browsing the lobby favours no landscape over another; Uniform, editor-only, + // stays last. Numeric IDs remain stable compatibility identifiers and do not determine + // presentation order. Add a new landscape wherever you like; do not reorder the rest. + static const GeneratorRegistry registry({fingerprintDefinition(), + oldTownDefinition(), + symmetricArenaDefinition(), + swampDefinition(), + tidalFlatsDefinition(), + riverDefinition(), islesDefinition(), + ringWorldDefinition(), + amphitheatreDefinition(), + shatteredCoastDefinition(), + craterLakesDefinition(), + fjordContinentDefinition(), + spiderWebDefinition(), + concreteIslandsDefinition(), watershedDefinition(), + mazeDefinition(), + islandsDefinition(), stoneHighlandsDefinition(), - symmetricArenaDefinition(), - ringWorldDefinition(), + switchbacksDefinition(), cityStatesDefinition(), - tidalFlatsDefinition(), + canalsDefinition(), + ruggedArchipelagoDefinition(), + contestedCommonsDefinition(), + rainShadowDefinition(), evergladesDefinition(), - spiderWebDefinition(), - coralDefinition(), + polderDefinition(), carouselDefinition(), - amphitheatreDefinition(), - switchbacksDefinition(), - fingerprintDefinition(), - rainShadowDefinition(), oldGrowthDefinition(), - canalsDefinition(), - polderDefinition(), - oldTownDefinition(), anthillDefinition(), - ruggedArchipelagoDefinition(), - concreteIslandsDefinition(), - craterLakesDefinition(), - islandsDefinition(), - swampDefinition(), - riverDefinition(), + coralDefinition(), uniformDefinition()}); return registry; } diff --git a/src/map/generator/generators/AmphitheatreGenerator.cpp b/src/map/generator/generators/AmphitheatreGenerator.cpp index 1f69c2ea9..fac5bd682 100644 --- a/src/map/generator/generators/AmphitheatreGenerator.cpp +++ b/src/map/generator/generators/AmphitheatreGenerator.cpp @@ -41,8 +41,9 @@ using namespace MapGeneration; // amphitheatre. There is no sea: each territory's water is two inland seas of its own, one on either // side of its home (or, where a territory has no room beside its home, one at its far end), enclosed by // its own land, so its algae and its regrowing fields are its own and no swimmer can use them to cross -// into another territory. Every colony's towers stand against its border walls, covering the -// neighbouring territories over them. +// into another territory. Every colony's towers stand against the arena's outer wall beside its own +// ramp, covering the terrace inside and the ramp itself over the stone, so the way in is held from the +// first minute (FEEDBACK 2026-09-14: towers spread along the border walls "make no sense"). // // The arena is designed round the centre and turned for every colony, like a wedge design. The // territories cannot be: a square map's corners and the ground across its wrap belong to no wedge, @@ -65,14 +66,14 @@ using namespace MapGeneration; // cleared, so the walls and the ramps are permanent; a unit may step diagonally, which is why every // border is walled where any neighbour has another label (labelBorders); water blocks walking until a // colony swims, which is why a sea must lie wholly inside its territory; a defence tower shoots over -// stone with no line of sight, which is why towers stand against the border walls; wheat and wood regrow +// stone with no line of sight, which is why the towers stand against the arena wall; wheat and wood regrow // only near water, which is why every territory's fields grow round its seas and the terraces are dry; and // fruit lets an inn pull hungry enemy units across, which makes the pit's orchard the prize at the // bottom of every ramp. // // THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a pit 15 tiles in radius, three walls 13 tiles // apart with ramps 7 wide, so the outer wall stands 41 tiles out; four territories of about 11,000 -// tiles each, each with 1,500 tiles of inland sea; three towers per colony. +// tiles each, each with 1,500 tiles of inland sea; three level-1 towers per colony by the ramp. namespace { @@ -117,6 +118,9 @@ constexpr int kRampClearance = 5; // Open 2x2 pads beside every colony's `tower-count` towers, and the spacing between sites. constexpr int kTowerPads = 3; constexpr int kTowerSpacing = 5; +// Towers stand within this many walking steps of the colony's own ramp mouth, so they cluster at the +// door rather than along the borders. +constexpr int kEntranceReach = 18; // A tower keeps this many steps from water on every side, so it never closes a strip of shore. constexpr int kTowerRoom = 4; // The most the territories' areas may differ, in percent of the smallest: the balance stops within @@ -397,29 +401,51 @@ std::vector stoneTiles(const Map &map, const Layout &L) return designed.stone; } -// The towers every colony starts with and the open pads beside them (chooseTowerSites): sites in a -// colony's territory, directly against stone and away from its ramp, that cover the most of -// neighbouring territories over the border walls, none in range of another colony's swarm. +// The towers every colony starts with and the open pads beside them (chooseTowerSites): all in the +// colony's own territory, directly against the arena's outer wall and within kEntranceReach steps of +// the colony's ramp mouth, chosen for how much of the arena - the terraces, the pit and the ramps, +// where an attacker comes from - each covers over the stone. Nothing scores for covering the colony's +// own ground or its neighbours': the starting towers hold the door, and a colony that wants towers on +// its borders builds them (FEEDBACK 2026-09-14: towers scattered along the border walls "make no +// sense"; they should be "concentrated around the inner stone wall ... nearby the entrance"). TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &context, const AmphitheatreOptions &o, const std::vector &stone) { const Torus &t = L.t; - const int n = t.size(); - std::vector ramps(n, 0); + const int n = t.size(), teams = L.g.teams; + std::vector ramps(n, 0), ringStone(n, 0); for (int i = 0; i < n; ++i) + { ramps[i] = L.rampOf[i] >= 0 || L.innerRamp[i]; + ringStone[i] = L.ring[i] && stone[i]; + } const std::vector fromRamps = stepsFrom(t, ramps); const std::vector swarms = swarmSurroundings(t, context, 0); const std::vector reserved = swarmSurroundings(t, context); + // How far every tile of a territory is from its own ramp mouth, walking the territory. + std::vector fromMouth(n, -1); + for (int k = 0; k < teams; ++k) + { + std::vector ground(n, 0); + for (int i = 0; i < n; ++i) + ground[i] = L.territory[i] == k; + const std::vector steps = stepsFrom(t, tileMask(t, L.mouths[k]), ground); + for (int i = 0; i < n; ++i) + if (ground[i]) + fromMouth[i] = steps[i]; + } std::vector owner(n, -1); std::vector buildable(n, 0), target(n, 0); for (int i = 0; i < n; ++i) { const int x = i % t.w, y = i / t.w; - owner[i] = L.territory[i]; - buildable[i] = map.isGrass(x, y) && !stone[i] && !reserved[i] && - fromRamps[i] > kRampClearance && !map.isResource(x, y); - target[i] = !map.isWater(x, y) && !stone[i]; + // The arena belongs to "everyone else", so every colony's towers score for covering it. + const bool arena = ramps[i] || (L.zone[i] >= 0 && L.zone[i] < L.g.rings); + owner[i] = arena ? teams : L.territory[i]; + target[i] = arena && !map.isWater(x, y) && !stone[i]; + buildable[i] = L.territory[i] >= 0 && map.isGrass(x, y) && !stone[i] && !reserved[i] && + fromRamps[i] > kRampClearance && fromMouth[i] >= 0 && + fromMouth[i] <= kEntranceReach && !map.isResource(x, y); } // No tower on a shore strip narrow enough for it to close. std::vector land(n, 0); @@ -429,8 +455,10 @@ TowerPlan planTowers(const Map &map, const Layout &L, const GenerationContext &c for (int i = 0; i < n; ++i) buildable[i] = buildable[i] && roomy[i]; TowerRequest request = startingTowerRequest(o.towers, o.towerCount, kTowerPads, kTowerSpacing); - request.against = &stone; - return chooseTowerSites(t, owner, buildable, target, swarms, L.g.teams, request); + request.otherWeight = 1; + request.ownWeight = 0; + request.against = &ringStone; + return chooseTowerSites(t, owner, buildable, target, swarms, teams, request); } bool generate(Game &game, GenerationContext &context) @@ -717,7 +745,7 @@ GeneratorDefinition amphitheatreDefinition() "amphitheatre", 23, "Amphitheatre", - 2, + 3, false, // Rings of wall; each ramp's width in tiles; the pit's radius and each terrace's width as // shares of the half side; every colony's inland seas together as a percentage of the smallest @@ -728,9 +756,10 @@ GeneratorDefinition amphitheatreDefinition() {"terrace-width", "Terrace width", 6, 20, 1, 14, ControlGroup::Layout}, {"bay-size", "Bay size", 6, 24, 2, 14, ControlGroup::Terrain}, {"border-wall", "Border wall", 1, 3, 1, 2, ControlGroup::Terrain}, - // The towers every colony starts with, all against its walls: their level (0 for none, just - // open pads) and how many. - {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + // The towers every colony starts with, all against the arena wall by its ramp: their level + // (0 for none, just open pads; level 1 by default since 2026-09-14, so players upgrade + // their own towers) and how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 1, ControlGroup::Layout}, {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, // Every territory's ambient fields and grove, the arena's fruit and outcrops, and the // bays' algae; every home's kit and the walls' stone are unscaled. diff --git a/src/map/generator/generators/CanalsGenerator.cpp b/src/map/generator/generators/CanalsGenerator.cpp index c2be6b92a..b20120c58 100644 --- a/src/map/generator/generators/CanalsGenerator.cpp +++ b/src/map/generator/generators/CanalsGenerator.cpp @@ -27,9 +27,10 @@ #include using namespace MapGeneration; -// Canals: a lagoon city. The map is cut into blocks by a grid of narrow canals, warped so the blocks -// are irregular, and every canal is just wide enough to stop a unit and just narrow enough for a -// tower on one bank to shoot the other. Every colony starts on a block of its own with a pond and a +// Canals: a lagoon city. The map is cut into blocks by a grid of narrow canals - squares, or hexagons +// (FEEDBACK 2026-09-14: "support a hexagon lattice ... an option similar to the maze generator") - +// warped so the blocks are irregular, and every canal is just wide enough to stop a unit and just +// narrow enough for a tower on one bank to shoot the other. Every colony starts on a block of its own with a pond and a // kit, and a handful of sand bridges join the blocks: only the bridges a tree needs to connect the // colonies' blocks, plus a few more at random, so most blocks are islands until someone can swim. // Towers reach across the canals from the first minute and armies cannot, so where the first towers @@ -64,6 +65,10 @@ namespace // Every home's starting kit, unscaled whatever the amounts say. constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// Hexagonal blocks sit this share of the block size apart, as Maze's cells do: a hexagon's edge is only +// about 0.58 of its pitch, so at the square's spacing its canals would leave blocks too small for their +// kinds. +constexpr int kHexPitchPercent = 150; // A bridge is a line of sand corners kBridgeHalfWidth wide across the canal (a 1-corner line already // carries units two tiles wide, since a tile with a sand corner is no longer pure water), reaching // this far past the canal's edge onto each bank so it lands on grass, not beach. @@ -297,11 +302,15 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const Torus &t = L.t; const int n = t.size(), teams = std::max(1, request.nbTeams); - // The blocks: a square tiling, warped. Every edge is a canal, so every edge is an obstacle the - // warp keeps apart: opposite sides of a block stay at least the canal plus eight tiles apart, so - // a block always keeps a few tiles of grass across its narrowest, and a block's centre keeps at - // least a fifth of the block from its sides, so a pond fits. - L.g = squareTessellation(t.w, t.h, o.blockSize); + // The blocks: a square or hexagonal tiling, warped. Every edge is a canal, so every edge is an + // obstacle the warp keeps apart: opposite sides of a block stay at least the canal plus eight tiles + // apart, so a block always keeps a few tiles of grass across its narrowest, and a block's centre + // keeps at least a fifth of the block from its sides, so a pond fits. The built kinds are drawn in + // a square frame whatever the block's shape; on a hexagon a wall tile that falls outside the block + // is simply left out (blockWalls). + L.g = o.blockShape == int(Tessellation::Shape::Hexagon) + ? hexTessellation(t.w, t.h, o.blockSize * kHexPitchPercent / 100) + : squareTessellation(t.w, t.h, o.blockSize); if (L.g.columns < 2 || L.g.rows < 2) { L.failure = "The canals need at least two blocks across and down; use a bigger map or " @@ -788,7 +797,8 @@ std::string validateWorld(const Game &game, const GenerationContext &context) } // namespace CanalsOptions::CanalsOptions(const GenerationRequest &r) - : blockSize(r.option("block-size")), canalWidth(r.option("canal-width")), + : blockShape(r.option("block-shape")), blockSize(r.option("block-size")), + canalWidth(r.option("canal-width")), warp(r.option("warp")), extraBridges(r.option("extra-bridges")), towers(r.option("starting-towers")), towerCount(r.option("tower-count")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), @@ -803,17 +813,21 @@ GeneratorDefinition canalsDefinition() "canals", 29, "Canals", - 4, + 5, false, // Blocks of 24 give a 256 map about a hundred blocks; a canal of 3 corners (two tiles of - // water) is sealed against diagonal steps and is reached by a level-2 tower, which is what - // the colonies start with; a fifth again in extra bridges keeps most blocks islands. - {{"block-size", "Block size", 16, 40, 2, 24, ControlGroup::Layout}, + // water) is sealed against diagonal steps and is reached by a level-2 tower, one upgrade + // from what the colonies start with; a fifth again in extra bridges keeps most blocks + // islands. Hexagonal blocks sit kHexPitchPercent of the block size apart. + {GeneratorControl::choice("block-shape", "Block shape", {"Squares", "Hexagons"}, 0, + ControlGroup::Layout), + {"block-size", "Block size", 16, 40, 2, 24, ControlGroup::Layout}, {"canal-width", "Canal width", 3, 5, 1, 3, ControlGroup::Terrain}, // FEEDBACK 2026-09-13: warp 80 and extra bridges 30 (were 40 and 20). {"warp", "Warp", 0, 100, 10, 80, ControlGroup::Terrain}, {"extra-bridges", "Extra bridges", 0, 100, 10, 30, ControlGroup::Layout}, - {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + // Level 1 by default since 2026-09-14, so players upgrade their own towers. + {"starting-towers", "Starting tower level", 0, 3, 1, 1, ControlGroup::Layout}, {"tower-count", "Towers per colony", 1, 4, 1, 2, ControlGroup::Layout}, GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), diff --git a/src/map/generator/generators/CanalsGenerator.h b/src/map/generator/generators/CanalsGenerator.h index 4122f02ce..097717d97 100644 --- a/src/map/generator/generators/CanalsGenerator.h +++ b/src/map/generator/generators/CanalsGenerator.h @@ -4,7 +4,7 @@ #include "GeneratorDefinition.h" struct CanalsOptions { - int blockSize, canalWidth, warp, extraBridges, towers, towerCount; + int blockShape, blockSize, canalWidth, warp, extraBridges, towers, towerCount; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit CanalsOptions(const GenerationRequest &r); }; diff --git a/src/map/generator/generators/CarouselGenerator.cpp b/src/map/generator/generators/CarouselGenerator.cpp index 24e84e732..52dac5878 100644 --- a/src/map/generator/generators/CarouselGenerator.cpp +++ b/src/map/generator/generators/CarouselGenerator.cpp @@ -44,19 +44,25 @@ using namespace MapGeneration; // in radius, and every wall is a single line of stone with no water beside it, so a tower behind a // wall stands as close as possible to the lane it covers. // -// WHY THE FARMS. Each home reaches two walled wheat farms, one in towards the plaza and one out beyond -// the ring, laid in rows at the widths that regrow best (Farmland). The farms are the homes' water: -// there are no ponds, which would only take building room. Every set of farms is shared out by equal -// yield for its rows' angle, so no colony's farms grow less for facing the wrong way. The plaza has -// only fruit - an orchard for inns to pull hungry enemies across - with no wheat or wood to smother it. +// WHY THE FARMS. Each home reaches one walled wheat farm, out beyond the ring, laid in rows at the +// widths that regrow best (Farmland). The farm is the home's water: there are no ponds, which would +// only take building room, and no starter kit of wheat and wood blocks either (FEEDBACK 2026-09-14: +// "there's already too much food on this map", and the blocks clutter the base): a home's own +// scattered fields and its farm feed it. The farms are shared out by equal yield for their rows' angle, +// so no colony's farm grows less for facing the wrong way. Inside the ring, between the spokes, the sea +// is left as sea with algae in it (FEEDBACK 2026-09-14: farms reaching in towards the plaza made it +// "hard to know where you are on the map", and "less beautiful"), so the plaza reads as an island in a +// lagoon and the ring of homes as its shore. The plaza has only fruit - an orchard for inns to pull +// hungry enemies across - with no wheat or wood to smother it. // // HOW IT IS BUILT. Everything is designed once, in the wedge's frame (angle round the centre and // distance out from it), and turned round the centre for every colony, so every colony's ground is the // same and the layout is fair for any colony count. The pieces - homes, lanes, courts and the plaza - -// are drawn first, then the farms grown into the sea between them. Then all the sea near a home or farm -// fills in with that colony's ground, and a line of stone is drawn wherever two colonies' ground meets -// (see fillAndWall). What sea is left beyond the outermost pieces is sealed off: stone stands on every -// grass tile touching its beaches, so a swimmer can land but never walk in. The design is a pure +// are drawn first, then the farms grown into the sea beyond the ring. Then all the sea outside the ring +// near a home or farm fills in with that colony's ground, and a line of stone is drawn wherever two +// colonies' ground meets (see fillAndWall). What sea is left - the lagoon inside the ring, and any beyond +// the outermost pieces - is sealed off: stone stands on every grass tile touching its beaches, so a +// swimmer can land but never walk in. The design is a pure // function of the request, so validateWorld rebuilds it and checks the finished world against it. // // GAME RULES BEHIND IT (docs/map-generators/GAME_RULES_FOR_MAP_DESIGN.md): stone can never be cleared, so @@ -69,7 +75,7 @@ using namespace MapGeneration; // // THE SIZES AT THE DEFAULTS (256x256, 4 colonies): homes 26 tiles in radius 94 tiles out from the // centre, corridors 3 wide sweeping about 50 degrees, courts 3 in radius, a court wall 2 thick, spokes 3 -// wide and a plaza 31 in radius with a pond and an orchard; three towers per colony. +// wide and a plaza 31 in radius with a pond and an orchard; three level-1 towers per colony. namespace { @@ -87,10 +93,9 @@ constexpr double kHomeShare = 0.2; constexpr double kHomeArcShare = 0.2; constexpr double kMinimumHome = 14; constexpr double kHomeRoughness = 0.12; -// Every home's starter kit: this much wheat and wood beside the swarm, unscaled, 1:1. -constexpr int kHomeWheat = 30; -constexpr int kHomeWood = 30; -// A home's scattered farmland, as percentages of its tiles at 100% wheat and wood. +// A home has no starter kit of wheat and wood blocks (FEEDBACK 2026-09-14; the farm and the scattered +// fields feed it, and secureStartingCrops is the backstop). Its scattered farmland, as percentages of its +// tiles at 100% wheat and wood. constexpr int kHomeWheatShare = 4; constexpr int kHomeWoodShare = 2; @@ -114,8 +119,8 @@ constexpr double kPlazaPondShare = 0.28; constexpr double kPlazaPondShore = 5; // THE FARMS. While they grow, farms keep this many steps from other colonies' ground (the gap fills in -// afterwards), join their home through a neck this wide, and are laid in a set (every inner farm, or -// every outer one) only if every colony's field in it is at least this share of a home's area. +// afterwards), join their home through a neck this wide, and are laid only if every colony's field is +// at least this share of a home's area. constexpr int kFarmGap = 3; constexpr double kNeckHalf = 8; constexpr double kMinimumFarmShare = 0.5; @@ -135,6 +140,10 @@ constexpr double kFarmReach = 0.95; // beyond it keeps this many tiles of the nearest piece's ground before its coast. constexpr int kFillReach = 24; constexpr int kSeaMargin = 3; +// The margin a lane or court keeps from the lagoon instead, all of it stone (laneBand): a beach's +// mixed tiles reach two tiles in and a sealed coast one more, doubled where the coast runs diagonally, +// which on the 3-tile margin left a diagonal spoke with no grass down its middle at all. +constexpr int kLaneMargin = 5; // A road's corridor sand keeps this many steps from any water, so it never joins a beach. constexpr int kRoadSeaGap = 5; // Deposits keep this many steps from every corridor and spoke, so no field ever grows a door shut. @@ -241,6 +250,8 @@ struct Layout std::vector farms; std::vector farmColony; std::vector farmSand; + // The lanes' and courts' margins on the lagoon, kept as stone so the lane stays as drawn (fillAndWall). + std::vector laneBand; std::string failure; }; @@ -426,64 +437,61 @@ struct FarmSet std::vector origins; }; -// THE FARM FIELDS (growFarmFields): every colony's home reaches one field in towards the plaza, in the -// sea between its corridor and the spokes, and one out beyond the ring. Each set is shared out by equal -// yield for every colony's row angle (its axis), so a colony whose rows fall on the diagonal gets more -// ground. A set is kept only if every colony's field in it has room, so a small or crowded map loses a -// set for everyone rather than favouring anyone. The rows are laid later, once the fields' walls stand. +// THE FARM FIELDS (growFarmFields): every colony's home reaches one field out beyond the ring, in the +// sea between it and the map's wrap. The fields are shared out by equal yield for every colony's row +// angle (its axis), so a colony whose rows fall on the diagonal gets more ground. The set is kept only +// if every colony's field has room, so a small or crowded map loses the farms for everyone rather than +// favouring anyone. Nothing grows inside the ring: the sea between the spokes stays the lagoon (see the +// header; the inner farms went on 2026-09-14). The set keeps the outer set's index, 1, so the fill +// labels below read as they always did. The rows are laid later, once the fields' walls stand. std::vector claimFarmFields(Layout &L) { const Torus &t = L.t; const Geometry &g = L.g; const int n = t.size(), teams = g.teams; - std::vector inner(n, 0), outer(n, 0); + constexpr int kOuterSet = 1; + std::vector outer(n, 0); for (int i = 0; i < n; ++i) - { - const double d = std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)); - inner[i] = d < g.homeRadius; - outer[i] = d >= g.homeRadius; - } - std::vector innerSeeds, outerSeeds; + outer[i] = std::hypot(t.offsetX(int(L.cx), i % t.w), t.offsetY(int(L.cy), i / t.w)) >= + g.homeRadius; + std::vector seeds; std::vector owners; for (int k = 0; k < teams; ++k) { - innerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius - g.homeReach - 2, L.axis[k])); - outerSeeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius + g.homeReach + 2, L.axis[k])); + seeds.push_back(polarPoint(L.cx, L.cy, g.homeRadius + g.homeReach + 2, L.axis[k])); owners.push_back(k); } - const std::vector fields[2] = { - growFarmFields(t, L.land, L.homeOf, inner, innerSeeds, owners, L.homes, kFarmGap, kNeckHalf, - L.axis), - growFarmFields(t, L.land, L.homeOf, outer, outerSeeds, owners, L.homes, kFarmGap, kNeckHalf, - L.axis)}; + const std::vector fields = + growFarmFields(t, L.land, L.homeOf, outer, seeds, owners, L.homes, kFarmGap, kNeckHalf, L.axis); const double homeArea = kPi * g.homeR * g.homeR; + std::vector size(teams, 0); + for (int i = 0; i < n; ++i) + if (fields[i] >= 0) + ++size[fields[i]]; std::vector sets; - for (int set = 0; set < 2; ++set) - { - std::vector size(teams, 0); - for (int i = 0; i < n; ++i) - if (fields[set][i] >= 0) - ++size[fields[set][i]]; - if (*std::min_element(size.begin(), size.end()) < kMinimumFarmShare * homeArea) - continue; - sets.push_back({set, set == 0 ? innerSeeds : outerSeeds}); - for (int i = 0; i < n; ++i) - if (fields[set][i] >= 0 && L.homeOf[i] < 0) - { - L.farmOf[i] = fields[set][i]; - L.farmSet[i] = set; - L.land[i] = 1; - } - } + if (*std::min_element(size.begin(), size.end()) < kMinimumFarmShare * homeArea) + return sets; + sets.push_back({kOuterSet, seeds}); + for (int i = 0; i < n; ++i) + if (fields[i] >= 0 && L.homeOf[i] < 0) + { + L.farmOf[i] = fields[i]; + L.farmSet[i] = kOuterSet; + L.land[i] = 1; + } return sets; } // FILLING IN AND WALLING. Nothing but stone parts two colonies' ground, so every wall stands right beside // what it guards: -// - The sea within kFillReach of a home or farm becomes that home's or farm's ground, whichever is -// nearest (fillToNearest). Lanes, courts and the plaza never grow, so they keep exactly the width they -// were drawn. Sea left beyond that keeps a kSeaMargin strip of the nearest piece before its beach, so -// a beach and its sealed coast never eat into a lane. +// - The sea outside the ring within kFillReach of a home or farm becomes that home's or farm's ground, +// whichever is nearest (fillToNearest). The lagoon - every tile of sea inside the ring through the +// homes' centres - is never filled, so the middle of the map stays water between the spokes. Lanes, +// courts and the plaza never grow, so they keep exactly the width they were drawn. Sea left over, +// the lagoon included, keeps a kSeaMargin strip of the nearest piece before its beach, so a beach and +// its sealed coast never eat into a piece; a lane or court keeps kLaneMargin instead, and all of that +// margin is stone (laneBand, laid by stoneTiles), so a lane crossing the lagoon is the 3 grass tiles +// it was drawn between two bands of stone, its beaches and the water, however diagonally it runs. // - Every tile gets a side: the plaza, a colony's lanes and court, or a colony's home and farms. A // single line of stone stands wherever two sides meet (labelBorders), on the higher side - always the // home's or farm's, so no wall narrows a lane. Two borders stay open (borderOpen): each home's door @@ -496,7 +504,7 @@ void fillAndWall(Layout &L) const int farmLabel = teams, courtLabel = 3 * teams, pathLabel = 4 * teams, plazaLabel = 5 * teams; std::vector piece(n, -1), grown(n, -1); - std::vector sea(n, 0), home(n, 0); + std::vector sea(n, 0), home(n, 0), fillable(n, 0); for (int i = 0; i < n; ++i) { piece[i] = L.wall[i] ? -1 @@ -509,15 +517,29 @@ void fillAndWall(Layout &L) grown[i] = piece[i] < courtLabel ? piece[i] : -1; sea[i] = !L.land[i]; home[i] = L.homeOf[i] >= 0; + // Only the sea outside the ring fills in; inside it is the lagoon. + fillable[i] = sea[i] && std::hypot(t.offsetX(int(L.cx), i % t.w), + t.offsetY(int(L.cy), i / t.w)) >= L.g.homeRadius; } - std::vector filled = fillToNearest(t, grown, sea, kFillReach); + std::vector filled = fillToNearest(t, grown, fillable, kFillReach); for (int i = 0; i < n; ++i) { if (filled[i]) piece[i] = grown[i]; sea[i] = sea[i] && !filled[i]; } - const std::vector margin = fillToNearest(t, piece, sea, kSeaMargin); + // The margins: kSeaMargin of the nearest piece, or kLaneMargin where that piece is a lane or a court, + // the whole of which is the lane's band. + std::vector nearest = piece; + const std::vector wide = fillToNearest(t, nearest, sea, kLaneMargin); + std::vector margin = fillToNearest(t, piece, sea, kSeaMargin); + for (int i = 0; i < n; ++i) + if (wide[i] && nearest[i] >= courtLabel && nearest[i] < plazaLabel) + { + margin[i] = 1; + piece[i] = nearest[i]; + L.laneBand[i] = 1; + } L.homeSteps = stepsFrom(t, home); for (int i = 0; i < n; ++i) { @@ -632,6 +654,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.wall.assign(n, 0); L.road.assign(n, 0); L.farmSand.assign(n, 0); + L.laneBand.assign(n, 0); // One outline for every home, turned to each home's axis; the court and plaza are true circles. const RadialShape homeShape(g.homeR, kHomeRoughness, context, "carousel-home"); const RadialShape courtShape(g.courtR, 0.0001, context, "carousel-court"); @@ -659,14 +682,20 @@ std::vector seaMargin(const Map &map, const Layout &L) return islandSeaMargin(map, L.t, L.pond, L.roadTile, L.farmSand); } -// The design's stone, once the terrain is laid: every coast of the remaining sea sealed, and every wall. +// The design's stone, once the terrain is laid: every coast of the remaining sea sealed, every wall, and +// every grass tile of a lane's band (fillAndWall), so a lane on the lagoon keeps its drawn width and its +// coast is sealed however it runs. std::vector stoneTiles(const Map &map, const Layout &L) { - return sealedIslandStone(map, L.t, seaMargin(map, L), L.land, L.wall); + std::vector stone = sealedIslandStone(map, L.t, seaMargin(map, L), L.land, L.wall); + for (int i = 0; i < L.t.size(); ++i) + if (L.laneBand[i] && map.getTerrainType(i % L.t.w, i / L.t.w) == GRASS) + stone[i] = 1; + return stone; } -// The resources: every home's kit and scattered farmland, a fruit grove in every court, the plaza's -// orchard (fruit only, so nothing smothers it), and the farms' wheat and woodlots. +// The resources: every home's scattered farmland (no kit: see the header), a fruit grove in every +// court, the plaza's orchard (fruit only, so nothing smothers it), and the farms' wheat and woodlots. void furnish(Map &map, const Layout &L, GenerationContext &context, const CarouselOptions &o, const std::vector &pads) { @@ -692,8 +721,6 @@ void furnish(Map &map, const Layout &L, GenerationContext &context, const Carous for (int k = 0; k < g.teams; ++k) { const auto eligible = [&](int i) { return L.homeOf[i] == k && free(i); }; - plantHomeKit(map, t, context, L.homes[k], L.axis[k], g.homeR, kHomeWheat, kHomeWood, - eligible); furnishGround( map, t, context, fertility, eligible, patch, split, [&](int area) @@ -1005,7 +1032,7 @@ GeneratorDefinition carouselDefinition() "carousel", 22, "Carousel", - 1, + 2, false, // Each home's radius and each court's as percentages of the standard; the corridors' and the // spokes' widths and the wall between a court and the next home in tiles; the plaza's radius as @@ -1017,8 +1044,9 @@ GeneratorDefinition carouselDefinition() {"court-wall", "Court wall", 1, 5, 1, 2, ControlGroup::Terrain}, {"plaza-size", "Plaza size", 14, 34, 2, 24, ControlGroup::Layout}, // The towers every colony starts with, all against its walls: their level (0 for none, just - // open pads) and how many. - {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + // open pads; level 1 by default since 2026-09-14, so players upgrade their own towers) and + // how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 1, ControlGroup::Layout}, {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, // Off, the lanes and the plaza are grass from wall to wall. GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), @@ -1026,7 +1054,7 @@ GeneratorDefinition carouselDefinition() // an inn. GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), // Every home's scattered fields, the farms' wheat and woodlots, the courts' and the orchard's - // fruit, and the algae; every home's kit, the walls' stone and the towers are unscaled. + // fruit, and the algae; the walls' stone and the towers are unscaled. GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), GeneratorControl::percentage("stone-amount", "Stone amount"), diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 3b53a0724..0ec5530fc 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -5,6 +5,7 @@ #include "GenerationContext.h" #include "Grid.h" #include "Drawing.h" +#include "Farmland.h" #include "Geometry.h" #include "HeightMap.h" #include "Homes.h" @@ -40,12 +41,33 @@ using namespace MapGeneration; // only way in. // // Every roll differs: the coasts have bays and headlands, the channels bend, the homes take one of -// several layouts (a lake, a creek with a ford, stone ridges with passes, a marsh, a band of sand) -// and the heart of the commons one of several (a lake, a stone crag, an island, a river delta, a -// forest), and sand lies in patches over the ground. Everything a home is made of is designed in -// the wedge's own frame - arc across it and radius out from the centre - and stamped into every -// home alike, so however a roll comes out, every colony gets the same home turned round the -// centre, and the layout is fair for any colony count. +// several layouts (a lake, a creek with a ford, stone ridges with passes, a marsh) and the heart of +// the commons one of several (a lake, a stone crag, an island, a river delta, a forest), and sand +// lies in patches over the ground. Everything a home is made of is designed in the wedge's own +// frame - arc across it and radius out from the centre - and stamped into every home alike, so +// however a roll comes out, every colony gets the same home turned round the centre, and the layout +// is fair for any colony count. +// +// THE ARCHIPELAGO (FEEDBACK 2026-09-14: "WAY more islands out filling the no mans land where there +// is currently only ocean. it should be like a mini archipelago dotting the landscape. Additionally +// I want those islands to all contain the 10x4 little sand building plots with no resources in +// them so that users have a spot to place buildings"). The strait and the channels carry islets: +// round, kIsletRadius across, each with an axis-aligned 10x4 clearing of grass in a ring of sand +// at its middle - the same building plot the farms use (Farmland.h) - and a small prize on its +// grass beyond the ring. Their middles are designed in the wedge frame like everything else: +// `islands` of them either side of every causeway in the strait, evenly spread over the arc between +// the causeway's clear stretch and the channel, and one in every channel between two homes, half +// way along it, so every colony has the same islets at the same distances; each islet is then +// drawn as a round disc on the map about its middle, so a bowed channel does not shear it. An +// islet keeps kIsletMoat of water from every coast and every other islet, so it is only ever +// reached by swimming: a forward post for whoever swims first, not a stepping stone. A slot whose +// islet would break that moat in any wedge is left out of every wedge (rasterize), so the colonies +// always match. The strait (11% of the side) and the commons (45% of the half side) are sized so +// the islets fit on a 256 map; a 128 map's strait is too narrow for any, and has none. The former +// random "resource islands" went with this. +// +// The Barrens home layout - both flanks of the home under sand - went the same day ("that one +// just feels lame when you receive it"), so a home is one of four kinds. // // The whole design is a pure function of the request, so validateWorld rebuilds it and checks the // finished world. Terrain is written straight to the undermap with an order-independent beach @@ -64,9 +86,10 @@ using namespace MapGeneration; // have different shapes at different angles, and the fairness score fell from 0.97 to between // 0.65 and 0.8. Equal homes matter more here than filling the map. // -// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a commons 70 tiles in radius, a strait 10 wide, -// homes 42 tiles deep from the strait out to a 6-tile rim of sea at the wrap, and a home lake about -// 4 tiles in radius. +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a commons 58 tiles in radius, a strait 28 wide, +// homes 36 tiles deep from the strait out to a 6-tile rim of sea at the wrap, a home lake about 4 +// tiles in radius, and twenty islets of radius 9: two either side of every causeway and one in +// every channel (a two-colony map's longer strait gets four either side). namespace { @@ -125,6 +148,13 @@ constexpr double kRoadDepthShare = 0.5; constexpr double kStreetInset = 7.0; // Sand vertices across a road. constexpr int kRoadWidth = 2; +// The islets (see the header): radius and roughness (nearly round, so an axis-aligned 10x4 plot of +// grass, whose far corner is 5.4 tiles out, sits well inside the beach: the grass reaches +// 8.55 - 1 beach = 7.5 at the roughest, and the plot's sand ring may lie on the beach), the water +// kept between an islet and +// any coast or other islet, and how far from the middle the prize stands, clear of the ring. +constexpr double kIsletRadius = 9.0, kIsletRoughness = 0.05; +constexpr int kIsletMoat = 3, kIsletPrizeOut = 6; // What a home is made of, and what the heart of the commons is. One of each per map. enum HomeKind @@ -133,7 +163,6 @@ enum HomeKind Riverside, Highland, Marsh, - Barrens, kHomeKinds }; enum HeartKind @@ -218,11 +247,15 @@ Geometry geometryFor(const GenerationRequest &r) const int side = std::min(1 << r.wDec, 1 << r.hDec); g.half = side / 2; g.rim = std::max(kRimMinimum, int(std::lround(kRimShare * g.half))); + g.commonsRadius = std::lround(o.commonsSize / 100.0 * g.half); + g.outerRadius = g.half - g.rim; + // The strait is a share of the side, but never so wide that the homes lose the depth a home needs: + // on a 128 map the default 11% would leave 14 tiles, so the strait shrinks to 8 there (and the + // islets, which need 27, are left out). g.strait = std::max(4, int(std::lround(o.straitWidth / 100.0 * side))); + g.strait = std::max(4, std::min(g.strait, int(g.outerRadius - g.commonsRadius) - kMinimumDepth)); g.causeway = o.causewayWidth; g.walls = o.stoneWalls; - g.commonsRadius = std::lround(o.commonsSize / 100.0 * g.half); - g.outerRadius = g.half - g.rim; g.amplitude = o.coastRoughness / 100.0; return g; } @@ -252,7 +285,8 @@ enum Region : signed char { Sea = 0, Commons = 1, - HomeLand = 2 + HomeLand = 2, + Islet = 3 }; // Where the swarm stands along its home's axis, from the centre: 9 tiles short of the lake's near @@ -282,6 +316,13 @@ struct Layout // The sand roads: undermap vertices turned to sand, and the tiles with a corner on one. std::vector sandRoad, roadTile; std::vector radius, angle; + // The islets' middles, where their 10x4 plots are stamped and their prizes stand, with each + // islet's place among its wedge's (the prize kind goes round by it). + struct IsletSite + { + int x, y, place; + }; + std::vector plots; std::string failure; }; @@ -362,15 +403,21 @@ struct Features std::vector> creek; int fordSegment = -1; // Highland ridges run at 28% and 72% across the wedge, splitting the home into a middle valley - // with the lake and two side valleys; Barrens leave the middle 45% of the half arc as grass and - // put sand beyond it. + // with the lake and two side valleys. double ridgeU[2] = {0.28, 0.72}; double passR = 0; - double barrenShare = 0.45; double deltaFordR = 0; RadialShape heart; std::vector commonsSand; - Features(RadialShape heart, double deltaFordR) : deltaFordR(deltaFordR), heart(heart) {} + // The archipelago: the strait's islets in the wedge frame, all of one outline; and the radius at + // which the channel islet sits (0 for none). + RadialShape islet; + std::vector islets; + double channelIsletR = 0; + Features(RadialShape heart, RadialShape islet, double deltaFordR) + : deltaFordR(deltaFordR), heart(heart), islet(islet) + { + } }; // Roll the features. Each kind of feature draws from a stream of its own, so the heart, rolled @@ -400,6 +447,7 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext // A delta heart's rivers are forded 60% of the way out from the centre, between the heart and // the landings. Features f(RadialShape(L.lakeR, L.heartKind == IslandHeart ? 0.2 : 0.3, context, "city-heart"), + RadialShape(kIsletRadius, kIsletRoughness, context, "city-islets"), 0.6 * g.commonsRadius); // The home's features in the wedge's frame: f.lakes first (the main one first), then whatever // the kind adds. A feature keeps clear of the coasts as bays can bring them, of the channels, @@ -454,8 +502,6 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext } // Highland: two ridges out from the strait to the sea, a pass through each. f.passR = inner + 0.5 * depth; - // Barrens: both flanks of the home are sand, leaving a strip of grass down the middle from the - // causeway past the lake; the sand keeps clear of every coast so the wall stays on the shore. // Sand patches, in every kind of home. const double homeArea = wedge * (g.outerRadius * g.outerRadius - inner * inner) / 2; // Sand patches per 8192 tiles (the default 3 gives about 2 per home and 6 on the commons at @@ -507,6 +553,63 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext t.y(L.cy + int(std::lround(r0 * std::sin(a)))), shape}); } + // The archipelago (see the header). Strait islets: `islands` of them either side of the causeway, + // spread evenly between the causeway's clear stretch and the channel, each on the strait's + // midline at its own arc position, and dropped - in every wedge alike, since the coasts are + // periodic in the wedge - where the strait there is too narrow for it with its moat on both + // sides. The islet's outline is one shape for all, so every islet is the same islet. + const double reach = f.islet.maximumRadius(); + const double room = 2 * reach + 2 * kIsletMoat; + if (o.islands > 0) + { + const double dMid = g.commonsRadius + g.strait / 2.0; + const double first = c.flatArc + kIsletMoat + reach; + const double last = g.arcHalf(dMid) - kIsletMoat - reach; + if (last >= first) + { + // `islands` is the count either side on a four-colony 256 map's arc; a longer arc + // (fewer colonies, a bigger map) gets proportionally more, a shorter one fewer, and + // never more than fit at the least spacing. + const double spacing = room - kIsletMoat; + const int roomFit = 1 + int((last - first) / spacing); + const int wanted = std::max( + 1, int(std::lround(o.islands * (last - first + spacing) / (2 * spacing)))); + const int fit = std::min(roomFit, wanted); + for (int k = 0; k < fit; ++k) + { + const double s = fit == 1 ? (first + last) / 2 : first + (last - first) * k / (fit - 1); + for (const double sign : {-1.0, 1.0}) + { + const double s0 = sign * s; + const double u0 = 0.5 + s0 / (wedge * dMid); + const double lo = c.coastAt(u0, dMid), hi = c.innerAt(u0, dMid); + if (hi - lo < room) + continue; + // Centred in the strait at its middle, the islet must also clear both coasts + // with its moat where its sides lie: the coasts bulge towards the causeway just + // where the first islet's near side is, and the strait follows the commons' + // coast, so both may lean across the islet's span. + const double r0 = (lo + hi) / 2; + bool clear = true; + for (const double off : {-0.7 * reach, 0.7 * reach}) + { + const double u = 0.5 + (s0 + off) / (wedge * dMid); + const double half = std::sqrt(reach * reach - off * off); + clear = clear && r0 - half >= c.coastAt(u, dMid) + kIsletMoat && + r0 + half <= c.innerAt(u, dMid) - kIsletMoat; + } + if (!clear) + continue; + f.islets.push_back({s0, r0, 1.0, 0.0, f.islet}); + } + } + } + // The channel islet: on the boundary between two homes, half way from the strait to the outer + // sea, when the channel is wide enough and long enough for it. + if (g.teams >= 2 && g.strait >= room && depth >= room) + f.channelIsletR = inner + depth / 2; + } + return f; } @@ -527,6 +630,7 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const const double roadHalf = g.causeway / 2.0; const double stripHalf = roadHalf + kShoulder; std::vector landingAlong(teams, -1.0), shoreAlong(teams, 1e9); + L.plots.clear(); const WedgeFrame frame(t, L.phase, teams); for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) @@ -648,11 +752,6 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const L.clear[i] = 1; } } - // Barrens' sand keeps 8 tiles from both coasts and the channels, so the home's wall - // still stands on grass. - if (L.homeKind == Barrens && d >= in + 8 && d <= out - 8 && - std::abs(s) > f.barrenShare * g.arcHalf(d) && std::abs(s) < g.arcHalf(d) - 8) - L.sand[i] = 1; for (const Blob &b : f.sandBlobs) if (b.holds(s - b.s, d - b.r)) L.sand[i] = 1; @@ -713,6 +812,71 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const } } } + // The archipelago (see the header). Every islet's middle in every wedge - a strait islet's at + // the home's angle plus its arc offset, the channel's on the bent boundary clockwise of the home + // (the frame bends a tile's angle by bendAt(d) / d, so the boundary sits that far the other way) + // - and the islet drawn round that middle on the map itself, not in the frame, so it is a round + // islet however the channel bows. A slot whose islet would come within kIsletMoat of a coast or + // a causeway in any wedge (the bow narrows a channel beside its islet) is left out of every + // wedge, so every colony still has the same islets. Three is the least moat that leaves a tile + // of pure water: the beach pass sands the land vertices beside water, and a tile with a sand + // corner is no longer water to walk on. The plots and prizes stand at the middles kept; the + // prize kind goes round by the islet's place among its wedge's. + { + struct Centre + { + int x, y, slot; + }; + std::vector centres; + const int slots = int(f.islets.size()) + 1, channelSlot = slots - 1; + for (int k = 0; k < teams; ++k) + { + const Home &h = L.homes[k]; + for (size_t j = 0; j < f.islets.size(); ++j) + { + const Blob &b = f.islets[j]; + const double a = h.angle + b.s / b.r; + centres.push_back({t.x(L.cx + int(std::lround(b.r * std::cos(a)))), + t.y(L.cy + int(std::lround(b.r * std::sin(a)))), int(j)}); + } + if (f.channelIsletR > 0 && teams >= 2) + { + const double r = f.channelIsletR; + const double a = L.phase + wedge * (k + 1) + coasts.bendAt(r) / r; + centres.push_back({t.x(L.cx + int(std::lround(r * std::cos(a)))), + t.y(L.cy + int(std::lround(r * std::sin(a)))), channelSlot}); + } + } + const int moat = kIsletMoat; + const int span = int(std::ceil(f.islet.maximumRadius())) + moat; + std::vector slotOk(slots, 1); + for (const Centre &c : centres) + for (int dy = -span; dy <= span && slotOk[c.slot]; ++dy) + for (int dx = -span; dx <= span && slotOk[c.slot]; ++dx) + if (std::hypot(dx, dy) < f.islet.radiusAt(std::atan2(dy, dx)) + moat && + L.region[t.at(c.x + dx, c.y + dy)] != Sea) + slotOk[c.slot] = 0; + for (const Centre &c : centres) + if (slotOk[c.slot]) + for (int dy = -span; dy <= span; ++dy) + for (int dx = -span; dx <= span; ++dx) + if (std::hypot(dx, dy) < f.islet.radiusAt(std::atan2(dy, dx))) + L.region[t.at(c.x + dx, c.y + dy)] = Islet; + L.plots.clear(); + int place = 0, wedgeOf = -1; + for (size_t index = 0; index < centres.size(); ++index) + { + const Centre &c = centres[index]; + const int k = int(index) * teams / int(centres.size()); + if (k != wedgeOf) + { + wedgeOf = k; + place = 0; + } + if (slotOk[c.slot]) + L.plots.push_back({c.x, c.y, place++}); + } + } // The commons-side end is the end nearest the centre; the home-side end the farthest. for (int k = 0; k < teams; ++k) { @@ -895,8 +1059,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) L.heartKind = int(context.bounded("city-kind", kHeartKinds)); if (L.heartKind == Delta && teams < 2) L.heartKind = LakeHeart; - if (depth < kFeatureDepth && - (L.homeKind == Riverside || L.homeKind == Highland || L.homeKind == Barrens)) + if (depth < kFeatureDepth && (L.homeKind == Riverside || L.homeKind == Highland)) L.homeKind = Lakeland; const Coasts c(g, context); @@ -1222,6 +1385,32 @@ void stockCommons(Map &map, const Layout &L, GenerationContext &context, const C map.setResource(i % t.w, i / t.w, WOOD, 1); } +// Every islet's prize: one small clump on its grass beyond the plot's ring, kIsletPrizeOut tiles +// below the middle (the same side on every islet, so every wedge's islets match), of a kind that +// goes round by the islet's place in its wedge - stone, then a fruit (the three fruits in turn), +// then wheat - each scaled by its amount. Small, since the islet is a place to build, not a mine. +void stockIslets(Map &map, const Layout &L, GenerationContext &context, const CityStatesOptions &o, + const Farm &plots) +{ + const Torus &t = L.t; + for (const Layout::IsletSite &site : L.plots) + { + const int kind = site.place % 3; + const int type = kind == 0 ? STONE : kind == 1 ? CHERRY + (site.place / 3) % 3 : WHEAT; + const int amount = kind == 0 ? o.stone : kind == 1 ? o.fruit : o.wheat; + if (scaledCount(1, amount) <= 0) + continue; + const int seed = seedNear(t, site.x, site.y + kIsletPrizeOut, 3, + [&](int i) + { + return L.region[i] == Islet && !plots.plot[i] && + !plots.sand[i] && clearGround(map, i % t.w, i / t.w); + }); + if (seed >= 0) + placeResourceClump(map, context, MapGeneratorPoint(seed % t.w, seed / t.w), type, 1); + } +} + // Causeways and their approaches hold nothing but the causeways' own stone lines. void clearRoads(Map &map, const Layout &L, const std::vector &line) { @@ -1289,12 +1478,18 @@ bool generate(Game &game, GenerationContext &context) terrain[i] = SAND; } carveValleys(terrain, L, context, o.valleys); - // Resource islands are counted per 128x128 of sea. - const std::vector islands = raiseIslands( - terrain, t, context, - {"city-islands", - int(std::lround(o.resourceIslands * double(countTiles(terrain, WATER)) / 16384.0)), 60, - kLakeShore + 1}); + // The islets' building plots (stampFarmPlot, as the farms lay theirs): a 10x4 clearing of grass + // in a two-vertex ring of sand at every islet's middle, which nothing is planted on and the beach + // pass leaves alone, so a colony that swims out finds a spot to build on at once. + Farm isletPlots; + isletPlots.water.assign(n, 0); + isletPlots.sand.assign(n, 0); + isletPlots.plot.assign(n, 0); + isletPlots.row.assign(n, -1); + const FarmPlot plot; + for (const Layout::IsletSite &site : L.plots) + stampFarmPlot(terrain, t, isletPlots, site.x - plot.width / 2, site.y - plot.height / 2, + plot); layBeaches(terrain, t); writeUndermap(map, terrain); const std::vector line = stoneTiles(map, L); @@ -1327,12 +1522,13 @@ bool generate(Game &game, GenerationContext &context) context.stage = "city resources"; furnishHomes(map, L, context, o); stockCommons(map, L, context, o); - stockIslands(map, context, islands, "city-islands"); + stockIslets(map, L, context, o, isletPlots); seedAlgae(map, context, t, "city-algae", o.algae, AlgaeBand::shallows(2, 4)); // The kits already put wheat and wood a short walk from every swarm; this is only the backstop, // and the walls and the causeways' stone are designed and must never be cleared. secureStartingCrops(game, context, t, 24, 32, 0, &line); clearRoads(map, L, line); + clearFarmPlots(map, t, {isletPlots}); context.stage = "city roads"; return openRoads(map, L, line, context); } @@ -1391,6 +1587,19 @@ std::string validateWorld(const Game &game, const GenerationContext &context) heartReached = heart[i] && fromFirst[i] >= 0; if (!heartReached) return "The heart of the commons cannot be reached on foot."; + // Every islet keeps its plot clear and buildable, and no islet can be walked to: the moat holds. + for (const Layout::IsletSite &site : L.plots) + { + for (int dy = 0; dy < 4; ++dy) + for (int dx = 0; dx < 10; ++dx) + { + const int x = t.x(site.x - 5 + dx), y = t.y(site.y - 2 + dy); + if (!map.isGrass(x, y) || map.isResource(x, y) || map.getBuilding(x, y) != NOGBID) + return "The islet plot at " + where(t.at(site.x, site.y)) + " is not buildable."; + } + if (fromFirst[t.at(site.x, site.y)] >= 0) + return "The islet at " + where(t.at(site.x, site.y)) + " can be walked to."; + } // Shut every causeway road: with the walls up, nothing landing from the sea may get in. if (L.g.walls) @@ -1433,8 +1642,8 @@ std::string validateWorld(const Game &game, const GenerationContext &context) CityStatesOptions::CityStatesOptions(const GenerationRequest &r) : commonsSize(r.option("commons-size")), straitWidth(r.option("strait-width")), causewayWidth(r.option("causeway-width")), coastRoughness(r.option("coast-roughness")), - valleys(r.option("valleys")), resourceIslands(r.option("resource-islands")), - sand(r.option("sand")), frontier(r.option("frontier-richness")), + valleys(r.option("valleys")), islands(r.option("islands")), sand(r.option("sand")), + frontier(r.option("frontier-richness")), stoneWalls(r.option("stone-walls") != 0), sandRoads(r.option("sand-roads") != 0), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), stone(r.option("stone-amount")), algae(r.option("algae-amount")), @@ -1448,27 +1657,29 @@ GeneratorDefinition cityStatesDefinition() "city-states", 17, "City states", - 7, + 8, false, - // The commons' radius as a share of half the shorter side, the strait's width as a - // share of the shorter side, the causeway's road in tiles; valleys per 128x128 of - // commons; resource islands per 128x128 of sea. - {{"commons-size", "Commons size", 30, 65, 5, 55, ControlGroup::Terrain}, - {"strait-width", "Strait width", 3, 8, 1, 4, ControlGroup::Terrain}, + // The commons' radius as a share of half the shorter side (45 since 2026-09-14, from 55), + // the strait's width as a share of the shorter side (11, from 4: room for the islets), + // the causeway's road in tiles; valleys per 128x128 of commons; islets either side of + // every causeway in the strait on a four-colony 256 map, more on a longer arc (the + // channels get one each whenever this is above 0). + {{"commons-size", "Commons size", 30, 65, 5, 45, ControlGroup::Terrain}, + {"strait-width", "Strait width", 3, 14, 1, 11, ControlGroup::Terrain}, {"causeway-width", "Causeway width", 5, 11, 2, 7, ControlGroup::Layout}, // Bays and headlands on every coast and the bow in the channels. {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, {"valleys", "Valleys", 0, 8, 1, 3, ControlGroup::Terrain}, // Patches of sand over the homes and the commons, per 64x128 tiles of land. {"sand", "Sand patches", 0, 8, 1, 3, ControlGroup::Terrain}, - {"resource-islands", "Resource islands", 0, 6, 1, 2, ControlGroup::Resources}, + {"islands", "Islands", 0, 4, 1, 2, ControlGroup::Terrain}, // How much richer the commons' heart is than its shores. {"frontier-richness", "Frontier richness", 0, 100, 10, 60, ControlGroup::Resources}, // Off, no stone: the causeways are plain roads and the homes' coasts are open. GeneratorControl::toggle("stone-walls", "Stone walls", true, ControlGroup::Layout), // Off, no sand roads: the causeways and the commons are grass from shore to shore. GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), - // Every home's ambient fields, outcrops and grove, the commons and the islands' prizes; + // Every home's ambient fields, outcrops and grove, the commons and the islets' prizes; // every home's kit and the causeways' stone stay as they are. GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), diff --git a/src/map/generator/generators/CityStatesGenerator.h b/src/map/generator/generators/CityStatesGenerator.h index d5f8efe2b..39a91de69 100644 --- a/src/map/generator/generators/CityStatesGenerator.h +++ b/src/map/generator/generators/CityStatesGenerator.h @@ -4,8 +4,7 @@ #include "GeneratorDefinition.h" struct CityStatesOptions { - int commonsSize, straitWidth, causewayWidth, coastRoughness, valleys, resourceIslands, sand, - frontier; + int commonsSize, straitWidth, causewayWidth, coastRoughness, valleys, islands, sand, frontier; bool stoneWalls, sandRoads; int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit CityStatesOptions(const GenerationRequest &r); diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp index bcc649e4d..f4570d5f3 100644 --- a/src/map/generator/generators/MazeGenerator.cpp +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -77,6 +77,11 @@ constexpr int kHexPitchPercent = 150; constexpr int kHomeFarmland = 32; constexpr int kHomeStone = 12; +// The ambient layer at 100%: wheat, wood and stone tiles per 256 shore tiles, fruit tiles in every +// dead end's treasure, and algae tiles per 400 water tiles. The amount controls are percentages of +// these, as on every other generator (until 2026-09-14 they were these counts themselves). +constexpr int kShoreWheat = 48, kShoreWood = 24, kShoreStone = 16, kTreasureFruit = 9, kAlgae = 24; + // Deposits hug each passage's shores, at most this many tiles in. That leaves a clear lane down // the middle of every passage however the maze turns, so no deposit can seal a route. constexpr int kShoreBand = 3; @@ -403,8 +408,9 @@ void scatterThroughMaze(Map &map, GenerationContext &context, const MazeDesign & }; const int shore = int(pool.size()); - for (const auto &layer : {std::pair{WHEAT, o.wheat}, std::pair{WOOD, o.wood}, - std::pair{STONE, o.stone}}) + for (const auto &layer : {std::pair{WHEAT, int(scaledCount(kShoreWheat, o.wheat))}, + std::pair{WOOD, int(scaledCount(kShoreWood, o.wood))}, + std::pair{STONE, int(scaledCount(kShoreStone, o.stone))}}) { // Density per 256 shore tiles, so the defaults (wheat 48, wood 24, stone 16) cover about // 19%, 9% and 6% of the shore band, in clumps of 4 to 12 tiles: big enough to be worth a @@ -579,15 +585,16 @@ bool generate(Game &game, GenerationContext &context) // fight for the ones it lacks. Placed before the shore scatter, which works around them. // Without treasure the same fruit is scattered along the passages' shores instead. std::vector deadEnds = d.deadEnds; - if (o.fruit > 0 && o.treasure) + const int fruit = int(scaledCount(kTreasureFruit, o.fruit)); + if (fruit > 0 && o.treasure) { context.shuffle(deadEnds.begin(), deadEnds.end(), "resources"); const int firstType = context.bounded("resources", 3); for (size_t i = 0; i < deadEnds.size(); ++i) - placeTreasure(map, t, d, deadEnds[i], o.fruit, CHERRY + int((firstType + i) % 3)); + placeTreasure(map, t, d, deadEnds[i], fruit, CHERRY + int((firstType + i) % 3)); } - scatterThroughMaze(map, context, d, onRoad, o, o.treasure ? 0 : int(deadEnds.size()) * o.fruit); - seedAlgae(map, context, o.algae); + scatterThroughMaze(map, context, d, onRoad, o, o.treasure ? 0 : int(deadEnds.size()) * fruit); + seedAlgae(map, context, int(scaledCount(kAlgae, o.algae))); return true; } @@ -659,8 +666,9 @@ std::string validateWorld(const Game &game, const GenerationContext &context) MazeOptions::MazeOptions(const GenerationRequest &r) : cellShape(r.option("cell-shape")), cellSize(r.option("cell-size")), channelWidth(r.option("channel-width")), loopiness(r.option("loopiness")), - warp(r.option("warp")), wheat(r.option("wheat")), wood(r.option("wood")), - stone(r.option("stone")), algae(r.option("algae")), fruit(r.option("fruit")), + warp(r.option("warp")), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), + stone(r.option("stone-amount")), algae(r.option("algae-amount")), + fruit(r.option("fruit-amount")), sandRoads(r.option("sand-roads") != 0), treasure(r.option("dead-end-treasure") != 0) { } @@ -671,7 +679,7 @@ GeneratorDefinition mazeDefinition() "maze", 11, "Maze", - 13, + 14, false, {GeneratorControl::choice("cell-shape", "Cell shape", {"Squares", "Hexagons"}, 0), {"cell-size", @@ -708,11 +716,13 @@ GeneratorDefinition mazeDefinition() // Densities for the deposits scattered along the passages (per 256 shore tiles); homes // always get the same fixed amounts. Fruit is the size, in tiles, of the treasure at // every dead end that isn't a home. - {"wheat", "Wheat", 0, 64, 1, 48, ControlGroup::Resources}, - {"wood", "Wood", 0, 64, 1, 24, ControlGroup::Resources}, - {"stone", "Stone", 0, 64, 1, 16, ControlGroup::Resources}, - {"algae", "Algae", 0, 64, 1, 24, ControlGroup::Resources}, - {"fruit", "Fruit", 0, 25, 1, 9, ControlGroup::Resources}, + // Percentages of the shore scatter, the dead-end treasure and the channel algae (kShoreWheat + // and company); every home's kit is unscaled. + GeneratorControl::percentage("wheat-amount", "Wheat amount"), + GeneratorControl::percentage("wood-amount", "Wood amount"), + GeneratorControl::percentage("stone-amount", "Stone amount"), + GeneratorControl::percentage("algae-amount", "Algae amount"), + GeneratorControl::percentage("fruit-amount", "Fruit amount"), // Off, passages are grass from shore to shore, with no sand road down the middle. GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), // Off, the treasure's fruit is scattered along the passages' shores instead. diff --git a/src/map/generator/generators/MazeGenerator.h b/src/map/generator/generators/MazeGenerator.h index e8c66163d..8b83777d1 100644 --- a/src/map/generator/generators/MazeGenerator.h +++ b/src/map/generator/generators/MazeGenerator.h @@ -5,7 +5,7 @@ struct MazeOptions { int cellShape, cellSize, channelWidth, loopiness, warp; - int wheat, wood, stone, algae, fruit; + int wheat, wood, stone, algae, fruit; // percentages of the default amounts bool sandRoads, treasure; explicit MazeOptions(const GenerationRequest &r); }; diff --git a/src/map/generator/generators/OldTownGenerator.cpp b/src/map/generator/generators/OldTownGenerator.cpp index 810c5b68b..02487027c 100644 --- a/src/map/generator/generators/OldTownGenerator.cpp +++ b/src/map/generator/generators/OldTownGenerator.cpp @@ -508,7 +508,7 @@ GeneratorDefinition oldTownDefinition() return {"old-town", 31, "Old town", - 6, + 7, false, // A city of 70% of the half side leaves a belt of fields round it; blocks of 14 with // streets of 4 give a 256 map about a hundred blocks and streets a column of units wide diff --git a/src/map/generator/generators/PolderGenerator.cpp b/src/map/generator/generators/PolderGenerator.cpp index 8b150d9f3..f4910273a 100644 --- a/src/map/generator/generators/PolderGenerator.cpp +++ b/src/map/generator/generators/PolderGenerator.cpp @@ -58,10 +58,11 @@ using namespace MapGeneration; namespace { -// Every village's starting kit, unscaled whatever the amounts say: wheat and wood at the village's -// edge (they regrow there, the ditches being a few tiles away) and a quarry, the only stone the -// polder has besides the odd outcrop. -constexpr int kHomeWheat = 14, kHomeWood = 12, kHomeQuarry = 2; +// Every village's starting kit, unscaled whatever the amounts say: a quarry, the only stone the polder +// has besides the odd outcrop. No wheat or wood blocks (FEEDBACK 2026-09-14): the rows a few tiles from +// every village are the polder's food, and the village's ground stays open to build on; +// secureStartingCrops is the backstop should a village ever be cut off from them. +constexpr int kHomeQuarry = 2; // Row widths, across the rows in tiles. Straight rows: 10 of crops and 6 of water, a period of 16 so // the pattern divides every map side and wraps without a seam (the yield fit's 10 and 8 would give // 18, which does not, and the difference in yield is under 3%). Diagonal rows: 11 of crops and 6 of @@ -262,13 +263,14 @@ bool generate(Game &game, GenerationContext &context) const std::vector reserved = swarmSurroundings(t, context); for (int k = 0; k < teams; ++k) plantOpenHomeKit( - map, t, context, L.kits[k], 0.0, L.villageRadius, kHomeWheat, kHomeWood, kHomeQuarry, + map, t, context, L.kits[k], 0.0, L.villageRadius, 0, 0, kHomeQuarry, [&](int i) { return L.homeOf[i] == k && !reserved[i] && clearGround(map, i % t.w, i / t.w); }); // The fields: crops on the rows' grass, nearly all of it. Wheat on 55% of the fertile row ground - // and wood on 15%, in patches, so the rows are mostly under crop (this is the polder) while the - // gaps between patches leave room to build a mill or a tower on a row; the villages and hamlets - // stay open. Every row tile is within a few tiles of water, so it all regrows. + // and wood on 8% (15% until 2026-09-14: "turn down the default amount of wood"), in patches, so + // the rows are mostly under crop (this is the polder) while the gaps between patches leave room to + // build a mill or a tower on a row; the villages and hamlets stay open. Every row tile is within a + // few tiles of water, so it all regrows. const Fertility::Field fertility = Fertility::forMap(map, false); const std::vector patch = periodicNoise(t.w, t.h, 8, context.stream("polder-patch")); const std::vector split = periodicNoise(t.w, t.h, 4, context.stream("polder-split")); @@ -286,7 +288,7 @@ bool generate(Game &game, GenerationContext &context) [&](int area) { return GroundAmounts{int(scaledCount(fertile * 55 / 100, o.wheat)), - int(scaledCount(fertile * 15 / 100, o.wood)), + int(scaledCount(fertile * 8 / 100, o.wood)), int(scaledCount(area / 2500, o.stone)), 0}; }, "polder-stone", "polder-fruit"); @@ -336,7 +338,7 @@ GeneratorDefinition polderDefinition() "polder", 30, "Polder", - 3, + 4, false, // A dyke every 24 tiles is a lane every one and a half rows' walk; villages of radius 14 // hold a swarm, its kit and a few more buildings and no more (11 before the first play, and diff --git a/src/map/generator/generators/RainShadowGenerator.cpp b/src/map/generator/generators/RainShadowGenerator.cpp index cb556046b..bae387c36 100644 --- a/src/map/generator/generators/RainShadowGenerator.cpp +++ b/src/map/generator/generators/RainShadowGenerator.cpp @@ -51,11 +51,19 @@ using namespace MapGeneration; // clogged up too easily. also in the valleys between the mountains it just feels too empty; toss in // a few rivers connected to some inland lakes connecting from the rain shadows, and some inland // desert or sporadic sand patches to break up the texture." So: the windward foot is a chain of -// streams 5 deep and 20 long every 24 (was 3 deep, 12 long: nearly continuous water now, with a +// streams 7 deep and 20 long every 24 (was 3 deep, 12 long: nearly continuous water now, with a // four-tile beach gap between streams); a sand road runs through every pass and kPassRoad tiles into // the valley on both sides; inland lakes lie scattered along the middle of every valley, each joined // by a wandering river to the nearest windward stream; and sand patches are sprinkled over the // valleys' grass (`sand-patches` percent). +// +// FEEDBACK 2026-09-14 (second play): "make the default lakes/ponds on the rain side of the mountain a +// couple tiles larger. still too small right now. Also the sand roads that go between the ridges need +// to extend further into either side of the base and should connect more smoothly to the beaches +// around the water instead of being distinct from them." So the streams are 7 deep (was 5), the pass +// roads run 18 tiles into the valley on both sides (was 12), and on the windward side each road ends +// in a lane of sand along the middle of the foot, out to the streams either side of the pass, so the +// road runs into their beaches instead of stopping short of them in the grass. namespace { @@ -77,17 +85,18 @@ constexpr int kRidgeWarpPeriod = 40; // one out from its water, and a corner spoils the four tiles round it, so a pool three tiles out // would take the ridge's windward row with it (measured: the ridge came out one row thick); four // tiles leaves all three rows standing. -constexpr int kFootGap = 4, kFootDepth = 5; +constexpr int kFootGap = 4, kFootDepth = 7; // Streams along the foot: each this long along the ridge, every this many tiles (first play: 20 every -// 24, from 12 every 24). A stream 20 long and 5 deep is about 100 corners of water, which the engine's -// growth probe finds from most of the valley; the four-tile gaps between streams, once their beaches -// meet, are where the valley's beach lets units walk along the foot. +// 24, from 12 every 24; second play: 7 deep, from 5). A stream 20 long and 7 deep is about 140 corners +// of water, which the engine's growth probe finds from most of the valley; the four-tile gaps between +// streams, once their beaches meet, are where the valley's beach lets units walk along the foot. constexpr int kPoolLength = 20, kPoolSpacing = 24; // A sand road through every pass, running this far into the valley on either side of the stone -// (first play: the passes "get clogged up too easily"): a line of single sand corners, which nothing -// grows onto and nothing is built across. No stream lies within kPassClear tiles along the ridge of a -// pass, so the road never runs into water. -constexpr int kPassRoad = 12, kPassClear = 4; +// (first play: the passes "get clogged up too easily"; second play: 18, from 12, to reach well into +// the bases): a line of single sand corners, which nothing grows onto and nothing is built across. No +// stream lies within kPassClear tiles along the ridge of a pass, so the road never runs into water; on +// the windward side it ends in a lane along the foot that joins the streams' beaches (second play). +constexpr int kPassRoad = 18, kPassClear = 4; // Inland lakes (first play: "a few rivers connected to some inland lakes"): sites at least this far // apart along the middle of each valley (the band within kLakeBand of the valley's middle phase), each // a rough disc of this radius, joined to the nearest windward stream by a river this many corners @@ -207,6 +216,7 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const int offset = ((along[i] + shift) % passPeriod + passPeriod) % passPeriod; return std::min(offset, passPeriod - offset); }; + const int footMiddle = (footNear + footFar) / 2; for (int i = 0; i < n; ++i) { const int below = 65536 - phase[i]; // how far below the next ridge's crest @@ -220,6 +230,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) if (passOffset(i) <= alongUnits(0.5) && stripeDistance(phase[i]) < ridgeHalf + acrossUnits(kPassRoad)) L.road[i] = 1; + // The road's junction with the foot (second play): a lane of sand two tiles across along the + // middle of the foot band, from the pass out to the streams on either side of it, so the road + // meets their beaches rather than stopping in the grass a few tiles short. It runs one tile + // past kPassClear so it touches the first tile of each stream's beach. + if (passOffset(i) <= passClear + alongUnits(1) && below >= footMiddle - acrossUnits(1) && + below < footMiddle + acrossUnits(1) && !L.water[i]) + L.road[i] = 1; } // The sand starts two tiles behind the stone, not one: a sand corner spoils the tiles round it // and stone stands only on pure grass, so sand touching the ridge would strip its lee row. @@ -428,7 +445,7 @@ GeneratorDefinition rainShadowDefinition() "rain-shadow", 27, "Rain shadow", - 3, + 4, false, // Four ridges on a 256 map give 64-tile valleys: a 12-tile home, a pass every 48 tiles // and a lee band of 6 leave a valley wide enough to farm and to fight in. Ridges three diff --git a/src/map/generator/generators/StoneHighlandsGenerator.cpp b/src/map/generator/generators/StoneHighlandsGenerator.cpp index 1c821267c..2b71247f0 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.cpp +++ b/src/map/generator/generators/StoneHighlandsGenerator.cpp @@ -920,6 +920,9 @@ void seedAlgae(Map &map, GenerationContext &context, const Layout &L, int algaeP map.setResource(i % t.w, i / t.w, ALGA, 1); } +// Groves at 100% fruit; the amount control scales this count. +constexpr int kValleyFruit = 4; + // Fruit is rare: small groves beside the ponds of valleys no colony starts in, a prize for // whoever holds the passes to them - or in any valley, colonies' own included, if asked. void plantFruit(Map &map, GenerationContext &context, const Layout &L, @@ -1032,7 +1035,8 @@ bool generate(Game &game, GenerationContext &context) } scatterFarmland(map, context, L, pondDistance, keepClear, o.wheat, o.wood); seedAlgae(map, context, L, o.algae); - plantFruit(map, context, L, pondDistance, keepClear, homeValley, o.fruit, o.homeValleyFruit); + plantFruit(map, context, L, pondDistance, keepClear, homeValley, + int(scaledCount(kValleyFruit, o.fruit)), o.homeValleyFruit); // The kits above already put wheat and wood a few steps from every swarm; this is only the // backstop, and the ridges are designed walls it must never clear. guaranteeStartingResources(game, context, 24, 32, 0, &L.ridge); @@ -1140,10 +1144,10 @@ std::string validateWorld(const Game &game, const GenerationContext &context) StoneHighlandsOptions::StoneHighlandsOptions(const GenerationRequest &r) : valleySize(r.option("valley-size")), passWidth(r.option("pass-width")), - loopiness(r.option("loopiness")), pondSize(r.option("pond-size")), fruit(r.option("fruit")), + loopiness(r.option("loopiness")), pondSize(r.option("pond-size")), homeValleyFruit(r.option("home-valley-fruit") != 0), wheat(r.option("wheat-amount")), wood(r.option("wood-amount")), stone(r.option("stone-amount")), - algae(r.option("algae-amount")) + algae(r.option("algae-amount")), fruit(r.option("fruit-amount")) { } @@ -1152,7 +1156,7 @@ GeneratorDefinition stoneHighlandsDefinition() return {"stone-highlands", 14, "Stone highlands", - 3, + 4, false, {// Average spacing between valley centres, in tiles. {"valley-size", "Valley size", 20, 44, 4, 32, ControlGroup::Layout}, @@ -1162,7 +1166,9 @@ GeneratorDefinition stoneHighlandsDefinition() // Pond area as a percentage of each valley's area. {"pond-size", "Pond size", 4, 16, 2, 8, ControlGroup::Terrain}, // Fruit groves per 128x128 tiles of map. - {"fruit", "Fruit", 0, 12, 1, 4, ControlGroup::Resources}, + // Groves per map at 100, as a percentage of kValleyFruit (a count of 0 to 12 until + // 2026-09-14, when every generator's amounts became percentages). + GeneratorControl::percentage("fruit-amount", "Fruit amount"), // On, fruit groves may grow in the valleys colonies start in too. GeneratorControl::toggle("home-valley-fruit", "Fruit in home valleys", false, ControlGroup::Resources), diff --git a/src/map/generator/generators/StoneHighlandsGenerator.h b/src/map/generator/generators/StoneHighlandsGenerator.h index 668f7c35e..580cf4509 100644 --- a/src/map/generator/generators/StoneHighlandsGenerator.h +++ b/src/map/generator/generators/StoneHighlandsGenerator.h @@ -4,9 +4,9 @@ #include "GeneratorDefinition.h" struct StoneHighlandsOptions { - int valleySize, passWidth, loopiness, pondSize, fruit; + int valleySize, passWidth, loopiness, pondSize; bool homeValleyFruit; - int wheat, wood, stone, algae; // percentages of the default amounts + int wheat, wood, stone, algae, fruit; // percentages of the default amounts explicit StoneHighlandsOptions(const GenerationRequest &r); }; GeneratorDefinition stoneHighlandsDefinition(); diff --git a/src/map/generator/generators/SwitchbacksGenerator.cpp b/src/map/generator/generators/SwitchbacksGenerator.cpp index b8498b312..b76ff85e9 100644 --- a/src/map/generator/generators/SwitchbacksGenerator.cpp +++ b/src/map/generator/generators/SwitchbacksGenerator.cpp @@ -44,10 +44,11 @@ using namespace MapGeneration; // yield (Farmland), and the fields grow to fill every tile of sea between the mountains and round the // rim, with a single line of stone on every border between colonies, so a defence tower behind it // shoots across into the neighbour's rows and no unit walks through. The farms are the homes' water, on -// every size of map. Whatever sea a design leaves (none at the defaults) keeps a sealed coast, stone on -// every grass tile touching its margin, so a swimmer can land on a beach but never get inside; the -// trails are the only way anywhere for the whole game. The plateau has a pond and only fruit, the -// prize every trail climbs to, with no wheat or wood to smother it. +// every size of map, and their food: a home has no starter kit of wheat and wood blocks (FEEDBACK +// 2026-09-14), only its own scattered fields. Whatever sea a design leaves (none at the defaults) +// keeps a sealed coast, stone on every grass tile touching its margin, so a swimmer can land on a +// beach but never get inside; the trails are the only way anywhere for the whole game. The plateau +// has a pond and only fruit, the prize every trail climbs to, with no wheat or wood to smother it. // // HOW IT IS BUILT. Everything is designed once in the wedge's frame, along the colony's axis and across // it, and turned round the centre for every colony, so every colony's ground and trail are the same and @@ -64,7 +65,7 @@ using namespace MapGeneration; // // THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a plateau 28 tiles in radius; mountains about 40 // tiles wide, each with a trail of several legs about 26 tiles long and 2 tiles of stone between them; -// homes 24 tiles in radius on the rim, each with a farm on either flank; three towers per colony. +// homes 24 tiles in radius on the rim, each with a farm on either flank; three level-1 towers per colony. namespace { @@ -97,11 +98,9 @@ constexpr double kSpanShare = 0.1; // with a beach and a coast wall on each side, now the guard's rock near the plateau and farm beyond, // still what keeps the mountains from touching. constexpr double kPieceGap = 4; -// Every home's starter kit: this much wheat and wood beside the swarm, unscaled; no stone, since the -// mountains are stone. -constexpr int kHomeWheat = 30; -constexpr int kHomeWood = 30; -// A home's scattered farmland, as percentages of its tiles at 100% wheat and wood. +// A home has no starter kit of wheat and wood blocks (FEEDBACK 2026-09-14: the farms on either flank +// feed it, and secureStartingCrops is the backstop), and no stone, since the mountains are stone. Its +// scattered farmland, as percentages of its tiles at 100% wheat and wood. constexpr int kHomeWheatShare = 4; constexpr int kHomeWoodShare = 2; // Deposits keep this many steps from every trail, so no field grows across a trail's mouth. @@ -563,8 +562,6 @@ void furnish(Map &map, const Layout &L, GenerationContext &context, const Switch for (int k = 0; k < g.teams; ++k) { const auto eligible = [&](int i) { return L.homeOf[i] == k && free(i); }; - plantHomeKit(map, t, context, L.kitCentre[k], L.axis[k], g.homeR, kHomeWheat, kHomeWood, - eligible); furnishGround( map, t, context, fertility, eligible, patch, split, [&](int area) @@ -855,7 +852,7 @@ GeneratorDefinition switchbacksDefinition() "switchbacks", 24, "Switchbacks", - 3, + 4, false, // The trail's width and the stone between its legs in tiles; the plateau's radius as a // share of the half side (the mountains fill the rest with as many legs as fit); each home's @@ -865,8 +862,9 @@ GeneratorDefinition switchbacksDefinition() {"plateau-size", "Plateau size", 14, 34, 2, 22, ControlGroup::Layout}, {"home-size", "Home size", 60, 160, 10, 100, ControlGroup::Layout}, // The towers every colony starts with, all against its walls: their level (0 for none, just - // open pads) and how many. - {"starting-towers", "Starting tower level", 0, 3, 1, 2, ControlGroup::Layout}, + // open pads; level 1 by default since 2026-09-14, so players upgrade their own towers) and + // how many. + {"starting-towers", "Starting tower level", 0, 3, 1, 1, ControlGroup::Layout}, {"tower-count", "Towers per colony", 0, 12, 1, 3, ControlGroup::Layout}, // Off, the trails are grass from wall to wall. GeneratorControl::toggle("sand-roads", "Sand roads", true, ControlGroup::Layout), @@ -874,7 +872,7 @@ GeneratorDefinition switchbacksDefinition() // swarm or an inn. GeneratorControl::toggle("farm-plots", "Farm building plots", true, ControlGroup::Layout), // Every home's scattered fields and grove, the farms' wheat and woodlots, the plateau's - // orchard, and the algae; every home's kit, the mountains' stone and the towers are unscaled. + // orchard, and the algae; the mountains' stone and the towers are unscaled. GeneratorControl::percentage("wheat-amount", "Wheat amount"), GeneratorControl::percentage("wood-amount", "Wood amount"), GeneratorControl::percentage("stone-amount", "Stone amount"), diff --git a/src/map/generator/generators/TidalFlatsGenerator.cpp b/src/map/generator/generators/TidalFlatsGenerator.cpp index 547a36044..a5cc06317 100644 --- a/src/map/generator/generators/TidalFlatsGenerator.cpp +++ b/src/map/generator/generators/TidalFlatsGenerator.cpp @@ -48,7 +48,17 @@ using namespace MapGeneration; // // THE SIZES AT THE DEFAULTS (256x256, 4 colonies, island size 20): home islands about 25 tiles in // radius on a ring 74 tiles from the centre, a central island about 17 in radius, and a pond of -// radius 5 on each home island. +// radius 5 on each home island; six oasis islands and eight sandbars per wedge. +// +// FEEDBACK 2026-09-14: "it needs to have more of those pond + wheat or pond + wood oases, especially on +// smaller maps with more players ... also want to see a few more of the random 'green patches' within +// the sandy desert which can provide small building spaces." So the oases default to six per wedge +// (was three), are drawn smaller and rounder, and keep less sand from each other (kOasisGap) and +// from the home islands (kOasisHomeGap, where every feature used to keep kIslandGap), so more of +// them fit a narrow wedge; and the sandbars, which are those green patches, default to eight per +// wedge (was three), a little bigger, and keep the same smaller gaps. The scatter also tries three +// times as hard to seat each one. A 128 map with six colonies is the limit: its home islands nearly +// touch, and only an oasis or two of the smallest size fits between them and the central island. namespace { @@ -60,14 +70,21 @@ constexpr int kIslandGap = 8; constexpr int kHomeWheat = 40; constexpr int kHomeWood = 30; // Neutral islands' radii as shares of the home islands' (never below a size that holds a pond and -// a field), and the central island's as a share of the half side. -constexpr double kNeutralLow = 0.45, kNeutralHigh = 0.6; -constexpr double kNeutralMinimum = 7.0; +// a ring of wheat round it; smaller since 2026-09-14 so more fit a crowded wedge), how rough their +// coasts are (rounder than the home islands, so a small one is not all beach), and the central +// island's radius as a share of the half side. +constexpr double kNeutralLow = 0.30, kNeutralHigh = 0.45; +constexpr double kNeutralMinimum = 3.5, kNeutralRoughness = 0.25; +// Oases and sandbars keep this much sand from every other feature, and from a home island +// kOasisHomeGap (water features keep kIslandGap from a home, so its beach stays dry): the less they +// keep, the more of them a narrow wedge seats, which is what a small map with many colonies needs +// (2026-09-14). +constexpr int kOasisGap = 3, kSandbarGap = 3, kOasisHomeGap = 4; // Every neutral island is an oasis: a pond, and every other tile of it under wheat, so taking one // means clearing it first. constexpr double kCentralShare = 0.13; -// Sandbars are just big enough to hold a tower or an inn. -constexpr double kSandbarLow = 3.0, kSandbarHigh = 4.5; +// Sandbars are just big enough to hold an inn and a tower: a green patch a forward post can stand on. +constexpr double kSandbarLow = 3.5, kSandbarHigh = 5.0; struct Geometry { @@ -168,11 +185,11 @@ Layout design(const GenerationRequest &request, GenerationContext &context) 0, -1}); blobs.back().reach = blobs.back().shape.maximumRadius(); - const auto fits = [&](double s0, double r0, double reach, double gap) + const auto fits = [&](double s0, double r0, double reach, double gap, double homeGap) { // A feature keeps 6 tiles from the very centre (plus the central island and a gap when // there is one) and 4 from the edge of the design circle, stays inside its wedge by half a - // gap, and keeps its gap from every feature already placed (kIslandGap from a home island). + // gap, and keeps its gap from every feature already placed (homeGap from a home island). if (r0 - reach < 6 + g.centralRadius * 1.3 + (g.centralRadius > 0 ? gap : 0)) return false; if (r0 + reach > g.half - 4) @@ -181,21 +198,21 @@ Layout design(const GenerationRequest &request, GenerationContext &context) return false; for (const Feature &b : blobs) { - const double need = - reach + b.reach + std::max(gap, b.kind == HomeIsland ? double(kIslandGap) : gap); + const double need = reach + b.reach + std::max(gap, b.kind == HomeIsland ? homeGap : gap); if (std::hypot(s0 - b.s, r0 - b.r) < need) return false; } return true; }; const auto scatter = [&](Kind kind, int wanted, double low, double high, double roughness, - double gap, const char *stream, bool stretched) + double gap, double homeGap, const char *stream, bool stretched) { // Features are drawn at random across the wedge, from 8 tiles out from the centre to 8 - // tiles short of the design circle, with 80 tries each; one that fits nowhere is left out, - // the same for every wedge. Lagoons are stretched 1.2 to 2 times and turned, so they read - // as long tidal channels rather than round pools. - for (int attempt = 0, placed = 0; attempt < wanted * 80 && placed < wanted; ++attempt) + // tiles short of the design circle, with 240 tries each (80 until 2026-09-14: a crowded + // wedge needs the extra draws to find its last few spots); one that fits nowhere is left + // out, the same for every wedge. Lagoons are stretched 1.2 to 2 times and turned, so they + // read as long tidal channels rather than round pools. + for (int attempt = 0, placed = 0; attempt < wanted * 240 && placed < wanted; ++attempt) { const double r0 = 8 + context.bounded(stream, 1000) / 1000.0 * (g.half - 16); const double s0 = (context.bounded(stream, 2001) / 1000.0 - 1) * arcHalfAt * r0; @@ -204,22 +221,23 @@ Layout design(const GenerationRequest &request, GenerationContext &context) const double turn = stretched ? context.bounded(stream, 3600) / 3600.0 * kPi : 0.0; RadialShape shape(radius, roughness, context, stream); const double reach = shape.maximumRadius() * stretch; - if (!fits(s0, r0, reach, gap)) + if (!fits(s0, r0, reach, gap, homeGap)) continue; blobs.push_back({{s0, r0, stretch, turn, shape}, kind, reach, placed % 2}); ++placed; } }; scatter(Neutral, o.extraIslands, std::max(kNeutralMinimum, kNeutralLow * g.homeRadius), - std::max(kNeutralMinimum + 2, kNeutralHigh * g.homeRadius), 0.35, kIslandGap, - "flats-islands", false); - // Sandbars and lagoons keep 6 tiles of sand round them, tide pools 4: small water keeps the - // flats walkable round it. Lagoons are 8 to 14 tiles in radius; tide pools 1.5 to 4, counted - // per 16384 tiles of wedge so their density is the same on every map size. - scatter(Sandbar, o.sandbars, kSandbarLow, kSandbarHigh, 0.3, 6, "flats-sandbars", false); - scatter(Lagoon, o.lagoons, 8.0, 14.0, 0.35, 6, "flats-lagoons", true); + std::max(kNeutralMinimum + 1.5, kNeutralHigh * g.homeRadius), kNeutralRoughness, + kOasisGap, kOasisHomeGap, "flats-islands", false); + // Sandbars keep kSandbarGap tiles of sand round them and lagoons 6, tide pools 4: small water + // keeps the flats walkable round it. Lagoons are 8 to 14 tiles in radius; tide pools 1.5 to 4, + // counted per 16384 tiles of wedge so their density is the same on every map size. + scatter(Sandbar, o.sandbars, kSandbarLow, kSandbarHigh, 0.3, kSandbarGap, kOasisHomeGap, + "flats-sandbars", false); + scatter(Lagoon, o.lagoons, 8.0, 14.0, 0.35, 6, kIslandGap, "flats-lagoons", true); const double wedgeArea = wedge * g.half * g.half / 2; - scatter(Pool, int(std::lround(o.tidePools * wedgeArea / 16384.0)), 1.5, 4.0, 0.4, 4, + scatter(Pool, int(std::lround(o.tidePools * wedgeArea / 16384.0)), 1.5, 4.0, 0.4, 4, kIslandGap, "flats-pools", false); const RadialShape central(std::max(1.0, g.centralRadius), g.amplitude * 0.6, context, "flats-central"); @@ -244,10 +262,13 @@ Layout design(const GenerationRequest &request, GenerationContext &context) } // A home's pond is a fifth of its island's radius, 3 to 6 tiles: wide enough to stay water // inside its own beach, and small enough to leave the island its building room. Oasis ponds are - // 2.5. + // 2.5, shrunk in step with the island below kOasisPondReach tiles of reach (an island of + // radius 5.5 or so) so the smallest oasis, a crowded wedge's, is still a pond in a ring of + // wheat rather than a pond with a beach. const double pondRadius = std::clamp(0.2 * g.homeRadius, 3.0, 6.0); const RadialShape pond(pondRadius, 0.3, context, "flats-ponds"); const RadialShape oasis(2.5, 0.3, context, "flats-ponds"); + constexpr double kOasisPondReach = 6.5; const WedgeFrame frame(t, L.phase, teams, L.stretch); for (int y = 0; y < t.h; ++y) for (int x = 0; x < t.w; ++x) @@ -288,7 +309,9 @@ Layout design(const GenerationRequest &request, GenerationContext &context) std::hypot(s - b.s, d - b.r) < pond.radiusAt(std::atan2(d - b.r, s - b.s))) L.water[i] = 1; if (b.kind == Neutral && - std::hypot(s - b.s, d - b.r) < oasis.radiusAt(std::atan2(d - b.r, s - b.s))) + std::hypot(s - b.s, d - b.r) < + oasis.radiusAt(std::atan2(d - b.r, s - b.s)) * + std::min(1.0, b.reach / kOasisPondReach)) L.water[i] = 1; } } @@ -563,13 +586,14 @@ GeneratorDefinition tidalFlatsDefinition() "tidal-flats", 18, "Tidal flats", - 5, + 6, false, - // The home islands' radius as a share of the half side; extra islands, sandbars and - // lagoons per colony; tide pools per 128x128 of flats. + // The home islands' radius as a share of the half side; extra islands (the oases) and + // sandbars (the green patches) per colony, six and eight since 2026-09-14; lagoons per + // colony; tide pools per 128x128 of flats. {{"home-island-size", "Home island size", 14, 26, 1, 20, ControlGroup::Terrain}, - {"extra-islands", "Extra islands", 0, 4, 1, 3, ControlGroup::Terrain}, - {"sandbars", "Sandbars", 0, 4, 1, 3, ControlGroup::Terrain}, + {"extra-islands", "Extra islands", 0, 10, 1, 6, ControlGroup::Terrain}, + {"sandbars", "Sandbars", 0, 16, 1, 8, ControlGroup::Terrain}, {"tide-pools", "Tide pools", 0, 12, 1, 8, ControlGroup::Terrain}, {"lagoons", "Lagoons", 0, 4, 1, 3, ControlGroup::Terrain}, {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, diff --git a/src/map/generator/shared/Farmland.cpp b/src/map/generator/shared/Farmland.cpp index 666bb334e..300a1c32b 100644 --- a/src/map/generator/shared/Farmland.cpp +++ b/src/map/generator/shared/Farmland.cpp @@ -126,15 +126,20 @@ Farm layFarm(TerrainSketch &sketch, const Torus &t, const std::vector 0) { const double ax = std::cos(angle), ay = std::sin(angle); for (int i = 0; i < n; ++i) { - if (!farm.water[i]) + if (!region[i] || fromEdge[i] < rim - 1) continue; const double along = t.offsetX(ox, i % t.w) * ax + t.offsetY(oy, i / t.w) * ay; const double off = along - bridgeSpacing * std::round(along / bridgeSpacing); diff --git a/src/map/generator/shared/Farmland.h b/src/map/generator/shared/Farmland.h index 6b390ec84..2a62d3dd6 100644 --- a/src/map/generator/shared/Farmland.h +++ b/src/map/generator/shared/Farmland.h @@ -18,8 +18,9 @@ namespace MapGeneration // ground out into fields joined to their homes (by equal yield for their rows' angles), layFarm lays // the rows over a field, and plantFarm plants them once the terrain is written. Around a laid farm, // sand does three jobs: a cap ring closes the crop rows so wheat and wood never spread into the ground -// round the farm; bridges cross the water rows so workers need not walk round a long row; and a ring -// round the building plot keeps crops off it. Walls round a farm are the map's business, not the farm's. +// round the farm; bridges cross the whole farm, water rows and crop rows alike, so workers need not walk +// round a long row nor cut through one; and a ring round the building plot keeps crops off it. Walls +// round a farm are the map's business, not the farm's. /// The widths of a farm's crop rows and water rows, measured across the rows in tiles. struct FarmRows @@ -80,9 +81,11 @@ constexpr int kFarmOpening = 3; /// margin of land and every crop row joins the rim at both ends. Then, in order: /// - with `caps` (on), a ring of sand vertices just inside the rim, where the water rows' beaches begin, /// closes every crop row, so its wheat and wood never spread out of the farm; -/// - with a `bridgeSpacing`, a line of sand vertices crosses every water row each that many tiles along -/// the rows, starting at the origin: the tiles either side are no longer pure water, so workers walk -/// across, two tiles wide; +/// - with a `bridgeSpacing`, a line of sand vertices crosses the whole farm inside its cap, water rows +/// and crop rows alike, each that many tiles along the rows, starting at the origin: across a water +/// row the tiles either side are no longer pure water, so workers walk across, two tiles wide, and +/// across a crop row the line is a lane no crop grows over, so the farm is cut into bays a worker +/// walks round without clearing anything; /// - with a `plot`, a building clearing is stamped at the region's most inland point, clear of every /// water row by its sand ring and a vertex more and at least `rim` from the edge (a region too small /// for it gets none); its grass always wins over a cap or bridge running through it. diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 29b3a1346..e2d27ead5 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -396,12 +396,16 @@ class MapGeneratorDefaultsTest s.gfx = globalContainer->gfx; s.dispatchInit(); CustomGameSetup lobby; - D commons; - commons.setMethodDefaults(GeneratorRegistry::builtins().idOf("contested-commons")); - assert(GeneratorRegistry::builtins().methods(false).front() == GeneratorRegistry::builtins().idOf("contested-commons")); + // The catalog's order was shuffled once (FEEDBACK 2026-09-14: no bias towards the landscapes + // that happen to be listed first). The editor opens on the catalog's first entry and the + // lobby on its first playable one, whatever they are. + D editorFirst, lobbyFirst; + editorFirst.setMethodDefaults(GeneratorRegistry::builtins().methods().front()); + lobbyFirst.setMethodDefaults(GeneratorRegistry::builtins().methods(false).front()); + assert(GeneratorRegistry::builtins().methods(false).front() == GeneratorRegistry::builtins().idOf("fingerprint")); assert(s.methods->getSelectionIndex() == 0); - sameControls(s.descriptor, commons); - sameControls(lobby.generator, commons); + sameControls(s.descriptor, editorFirst); + sameControls(lobby.generator, lobbyFirst); for (int m : GeneratorRegistry::builtins().methods()) { auto method = static_cast(m); diff --git a/test/MapGeneratorToolkitChecks.h b/test/MapGeneratorToolkitChecks.h index cacc850e7..ac107bc91 100644 --- a/test/MapGeneratorToolkitChecks.h +++ b/test/MapGeneratorToolkitChecks.h @@ -1420,12 +1420,16 @@ inline void farmAndTowerChecks() } assert(farm.water[t.at(32, 36)] && !farm.water[t.at(32, 34)]); { - // Bridges: every 8 tiles along the rows from the origin, a line of sand across each water row. + // Bridges: every 8 tiles along the rows from the origin, a line of sand clean across the + // farm, through the water rows and the crop rows alike (FEEDBACK 2026-09-14), so a colony + // crosses the whole field on one road. Only the rim keeps its crops. TerrainSketch bridged(t.size(), GRASS); const Farm crossed = layFarm(bridged, t, region, 0, {32, 32}, 4, {6, 4}, nullptr, 8); assert(!crossed.water[t.at(32, 36)] && crossed.sand[t.at(32, 36)] && bridged[t.at(32, 36)] == SAND); assert(crossed.sand[t.at(24, 36)] && crossed.water[t.at(33, 36)] && crossed.water[t.at(28, 36)]); - assert(!crossed.sand[t.at(32, 32)] && crossed.rows == farm.rows); + assert(crossed.sand[t.at(32, 32)] && bridged[t.at(32, 32)] == SAND && crossed.rows == farm.rows); + assert(!crossed.sand[t.at(33, 32)] && bridged[t.at(33, 32)] == GRASS); + assert(!crossed.sand[t.at(32, 10)] && !crossed.sand[t.at(32, 53)]); // the rim stays grass // A building plot's grass trumps a bridge through it. TerrainSketch plotted(t.size(), GRASS); const FarmPlot clearing; diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index 1bb9a8540..db218249d 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -80,14 +80,14 @@ linux-x86_64 9 2 8 8 4 3 ok 5877899263698877100 linux-x86_64 9 2 8 8 8 1 ok 4204618854271989380 linux-x86_64 9 2 9 8 4 1 ok 13364805290811868144 linux-x86_64 9 2 9 9 4 1 ok 17370525382688177735 -linux-x86_64 11 13 7 7 4 1 ok 7573201930155389176 -linux-x86_64 11 13 8 8 2 1 ok 1580814293182404478 -linux-x86_64 11 13 8 8 4 1 ok 6132248727033081699 -linux-x86_64 11 13 8 8 4 2 ok 17571897192944793353 -linux-x86_64 11 13 8 8 4 3 ok 17321560833037622047 -linux-x86_64 11 13 8 8 8 1 ok 9101533112797905055 -linux-x86_64 11 13 9 8 4 1 ok 8608092470811997215 -linux-x86_64 11 13 9 9 4 1 ok 5365458970422917083 +linux-x86_64 11 14 7 7 4 1 ok 7573201930155389176 +linux-x86_64 11 14 8 8 2 1 ok 1580814293182404478 +linux-x86_64 11 14 8 8 4 1 ok 6132248727033081699 +linux-x86_64 11 14 8 8 4 2 ok 17571897192944793353 +linux-x86_64 11 14 8 8 4 3 ok 17321560833037622047 +linux-x86_64 11 14 8 8 8 1 ok 9101533112797905055 +linux-x86_64 11 14 9 8 4 1 ok 8608092470811997215 +linux-x86_64 11 14 9 9 4 1 ok 5365458970422917083 linux-x86_64 12 11 7 7 4 1 ok 10085480575762183996 linux-x86_64 12 11 8 8 2 1 ok 15910518443514311603 linux-x86_64 12 11 8 8 4 1 ok 5459653342558093269 @@ -104,14 +104,14 @@ linux-x86_64 13 1 8 8 4 3 ok 13134881442491555389 linux-x86_64 13 1 8 8 8 1 ok 16484172863350979963 linux-x86_64 13 1 9 8 4 1 ok 7377329900378929112 linux-x86_64 13 1 9 9 4 1 ok 3669618330451908633 -linux-x86_64 14 3 7 7 4 1 ok 17202733506137778788 -linux-x86_64 14 3 8 8 2 1 ok 16088282592338295379 -linux-x86_64 14 3 8 8 4 1 ok 11925006621409592431 -linux-x86_64 14 3 8 8 4 2 ok 414349330464996680 -linux-x86_64 14 3 8 8 4 3 ok 760189253045661613 -linux-x86_64 14 3 8 8 8 1 ok 3322640947906419646 -linux-x86_64 14 3 9 8 4 1 ok 2403928672037668991 -linux-x86_64 14 3 9 9 4 1 ok 1552916736750568752 +linux-x86_64 14 4 7 7 4 1 ok 17202733506137778788 +linux-x86_64 14 4 8 8 2 1 ok 16088282592338295379 +linux-x86_64 14 4 8 8 4 1 ok 11925006621409592431 +linux-x86_64 14 4 8 8 4 2 ok 414349330464996680 +linux-x86_64 14 4 8 8 4 3 ok 760189253045661613 +linux-x86_64 14 4 8 8 8 1 ok 3322640947906419646 +linux-x86_64 14 4 9 8 4 1 ok 2403928672037668991 +linux-x86_64 14 4 9 9 4 1 ok 1552916736750568752 linux-x86_64 15 1 7 7 4 1 ok 18061507907613243474 linux-x86_64 15 1 8 8 2 1 ok 14205129108449246087 linux-x86_64 15 1 8 8 4 1 ok 1822623745155517541 @@ -128,22 +128,22 @@ linux-x86_64 16 1 8 8 4 3 ok 8018514250963441197 linux-x86_64 16 1 8 8 8 1 ok 11001992185533046958 linux-x86_64 16 1 9 8 4 1 ok 12695777392728758889 linux-x86_64 16 1 9 9 4 1 ok 15361103174928577925 -linux-x86_64 17 7 7 7 4 1 ok 16384619148338487512 -linux-x86_64 17 7 8 8 2 1 ok 12055923282160576445 -linux-x86_64 17 7 8 8 4 1 ok 14456968977872880957 -linux-x86_64 17 7 8 8 4 2 ok 15957250270316988236 -linux-x86_64 17 7 8 8 4 3 ok 1726581497331910085 -linux-x86_64 17 7 8 8 8 1 ok 4164461749169536866 -linux-x86_64 17 7 9 8 4 1 ok 504687273871298522 -linux-x86_64 17 7 9 9 4 1 ok 614102320811357231 -linux-x86_64 18 5 7 7 4 1 ok 2131984367767490485 -linux-x86_64 18 5 8 8 2 1 ok 3078796380374205351 -linux-x86_64 18 5 8 8 4 1 ok 7736833362775075996 -linux-x86_64 18 5 8 8 4 2 ok 3733351741626345607 -linux-x86_64 18 5 8 8 4 3 ok 4515656875628782478 -linux-x86_64 18 5 8 8 8 1 ok 12075523445877924400 -linux-x86_64 18 5 9 8 4 1 ok 2230785934352114637 -linux-x86_64 18 5 9 9 4 1 ok 10761736745352955182 +linux-x86_64 17 8 7 7 4 1 ok 2202071218543666908 +linux-x86_64 17 8 8 8 2 1 ok 4428719747623839556 +linux-x86_64 17 8 8 8 4 1 ok 2965302471633099017 +linux-x86_64 17 8 8 8 4 2 ok 6136422521341729105 +linux-x86_64 17 8 8 8 4 3 ok 15470226854365351642 +linux-x86_64 17 8 8 8 8 1 ok 3694942298190354927 +linux-x86_64 17 8 9 8 4 1 ok 10736492416954691629 +linux-x86_64 17 8 9 9 4 1 ok 16271468635570942766 +linux-x86_64 18 6 7 7 4 1 ok 13941455206050809724 +linux-x86_64 18 6 8 8 2 1 ok 5172958575872259794 +linux-x86_64 18 6 8 8 4 1 ok 685234629062878118 +linux-x86_64 18 6 8 8 4 2 ok 5218743790797528810 +linux-x86_64 18 6 8 8 4 3 ok 130655471980274306 +linux-x86_64 18 6 8 8 8 1 ok 16548719928209852767 +linux-x86_64 18 6 9 8 4 1 ok 311217146217050958 +linux-x86_64 18 6 9 9 4 1 ok 11233943223771623871 linux-x86_64 19 2 7 7 4 1 ok 2704824689328339712 linux-x86_64 19 2 8 8 2 1 ok 18374433395111261761 linux-x86_64 19 2 8 8 4 1 ok 9505660943407981793 @@ -168,30 +168,30 @@ linux-x86_64 21 1 8 8 4 3 ok 6175686376568023634 linux-x86_64 21 1 8 8 8 1 ok 13558584930804210761 linux-x86_64 21 1 9 8 4 1 ok 8092089350815172597 linux-x86_64 21 1 9 9 4 1 ok 6220231772661939919 -linux-x86_64 22 1 7 7 4 1 ok 8819869978616031071 -linux-x86_64 22 1 8 8 2 1 ok 730574824765298860 -linux-x86_64 22 1 8 8 4 1 ok 9691963763756535666 -linux-x86_64 22 1 8 8 4 2 ok 12280328850293820652 -linux-x86_64 22 1 8 8 4 3 ok 16156515469674000849 -linux-x86_64 22 1 8 8 8 1 ok 12707724408115629147 -linux-x86_64 22 1 9 8 4 1 ok 16181991639738054527 -linux-x86_64 22 1 9 9 4 1 ok 18170981972027515569 -linux-x86_64 23 2 7 7 4 1 ok 549496627108919023 -linux-x86_64 23 2 8 8 2 1 ok 10411914600145665080 -linux-x86_64 23 2 8 8 4 1 ok 10630205222293629395 -linux-x86_64 23 2 8 8 4 2 ok 3769887846353985102 -linux-x86_64 23 2 8 8 4 3 ok 14438238323646079445 -linux-x86_64 23 2 8 8 8 1 ok 7486873941404335262 -linux-x86_64 23 2 9 8 4 1 ok 9501696698924728828 -linux-x86_64 23 2 9 9 4 1 ok 8928207243182979823 -linux-x86_64 24 3 7 7 4 1 ok 8766269131759353289 -linux-x86_64 24 3 8 8 2 1 ok 11694144927456181775 -linux-x86_64 24 3 8 8 4 1 ok 12461145917554129793 -linux-x86_64 24 3 8 8 4 2 ok 14956981799443403869 -linux-x86_64 24 3 8 8 4 3 ok 16634610244346170796 -linux-x86_64 24 3 8 8 8 1 ok 14595537376914576811 -linux-x86_64 24 3 9 8 4 1 ok 16587253345581904688 -linux-x86_64 24 3 9 9 4 1 ok 12750052710567335197 +linux-x86_64 22 2 7 7 4 1 ok 3655866758271062779 +linux-x86_64 22 2 8 8 2 1 ok 12138492238448374580 +linux-x86_64 22 2 8 8 4 1 ok 1284471235474139044 +linux-x86_64 22 2 8 8 4 2 ok 8478284340363876001 +linux-x86_64 22 2 8 8 4 3 ok 3651272843185213508 +linux-x86_64 22 2 8 8 8 1 ok 12614988246288899091 +linux-x86_64 22 2 9 8 4 1 ok 17425447918742743653 +linux-x86_64 22 2 9 9 4 1 ok 9365457891684218114 +linux-x86_64 23 3 7 7 4 1 ok 16284233831079059960 +linux-x86_64 23 3 8 8 2 1 ok 13363436405671293892 +linux-x86_64 23 3 8 8 4 1 ok 17739866559139208687 +linux-x86_64 23 3 8 8 4 2 ok 15066220726718427366 +linux-x86_64 23 3 8 8 4 3 ok 6306903179004779394 +linux-x86_64 23 3 8 8 8 1 ok 8993378800483085102 +linux-x86_64 23 3 9 8 4 1 ok 12830180604398138241 +linux-x86_64 23 3 9 9 4 1 ok 13154479288816036136 +linux-x86_64 24 4 7 7 4 1 ok 15335564520221051714 +linux-x86_64 24 4 8 8 2 1 ok 1330458027073369597 +linux-x86_64 24 4 8 8 4 1 ok 11900636304707015638 +linux-x86_64 24 4 8 8 4 2 ok 15045971539824213999 +linux-x86_64 24 4 8 8 4 3 ok 14226682360002951214 +linux-x86_64 24 4 8 8 8 1 ok 3556977709737021092 +linux-x86_64 24 4 9 8 4 1 ok 7641797400793793992 +linux-x86_64 24 4 9 9 4 1 ok 8801641067316080015 linux-x86_64 26 3 7 7 4 1 ok 7030856013259944714 linux-x86_64 26 3 8 8 2 1 ok 3845517928739616223 linux-x86_64 26 3 8 8 4 1 ok 5648058033288605271 @@ -200,14 +200,14 @@ linux-x86_64 26 3 8 8 4 3 ok 18218990530981657750 linux-x86_64 26 3 8 8 8 1 ok 2357741973286312376 linux-x86_64 26 3 9 8 4 1 ok 7748628063269926229 linux-x86_64 26 3 9 9 4 1 ok 12177232439900443665 -linux-x86_64 27 3 7 7 4 1 ok 12643361820633102920 -linux-x86_64 27 3 8 8 2 1 ok 17439807471090677068 -linux-x86_64 27 3 8 8 4 1 ok 17647686618338591999 -linux-x86_64 27 3 8 8 4 2 ok 5785940387948126089 -linux-x86_64 27 3 8 8 4 3 ok 5375052871419675847 -linux-x86_64 27 3 8 8 8 1 ok 4736068021352568885 -linux-x86_64 27 3 9 8 4 1 ok 1938306971359313264 -linux-x86_64 27 3 9 9 4 1 ok 4857220545332469578 +linux-x86_64 27 4 7 7 4 1 ok 852968509575191579 +linux-x86_64 27 4 8 8 2 1 ok 10440175495935678077 +linux-x86_64 27 4 8 8 4 1 ok 12122023816867611083 +linux-x86_64 27 4 8 8 4 2 ok 13487939039474675492 +linux-x86_64 27 4 8 8 4 3 ok 13959888245801023830 +linux-x86_64 27 4 8 8 8 1 ok 15737033485164197857 +linux-x86_64 27 4 9 8 4 1 ok 4166265319375796381 +linux-x86_64 27 4 9 9 4 1 ok 14377410524771932695 linux-x86_64 28 4 7 7 4 1 ok 17460766322823457384 linux-x86_64 28 4 8 8 2 1 ok 11514638504458263270 linux-x86_64 28 4 8 8 4 1 ok 11307450576559481972 @@ -216,30 +216,30 @@ linux-x86_64 28 4 8 8 4 3 ok 5051877275687580956 linux-x86_64 28 4 8 8 8 1 ok 6662509859672209696 linux-x86_64 28 4 9 8 4 1 ok 12260507135522363815 linux-x86_64 28 4 9 9 4 1 ok 7745553466171171583 -linux-x86_64 29 4 7 7 4 1 ok 4232995500160826297 -linux-x86_64 29 4 8 8 2 1 ok 7399419839946439559 -linux-x86_64 29 4 8 8 4 1 ok 9472114427354905765 -linux-x86_64 29 4 8 8 4 2 ok 15267437393931662669 -linux-x86_64 29 4 8 8 4 3 ok 16695674954699155816 -linux-x86_64 29 4 8 8 8 1 ok 13911889789453582543 -linux-x86_64 29 4 9 8 4 1 ok 7057480006238666143 -linux-x86_64 29 4 9 9 4 1 ok 6519639175542525008 -linux-x86_64 30 3 7 7 4 1 ok 16282398610937407829 -linux-x86_64 30 3 8 8 2 1 ok 3709056129881639799 -linux-x86_64 30 3 8 8 4 1 ok 9174613388237436961 -linux-x86_64 30 3 8 8 4 2 ok 8462895324070553571 -linux-x86_64 30 3 8 8 4 3 ok 8076866925420152192 -linux-x86_64 30 3 8 8 8 1 ok 7750531187217742676 -linux-x86_64 30 3 9 8 4 1 ok 9383240492603169903 -linux-x86_64 30 3 9 9 4 1 ok 5015472615197108784 -linux-x86_64 31 6 7 7 4 1 ok 7932853597508094162 -linux-x86_64 31 6 8 8 2 1 ok 11763462524905175676 -linux-x86_64 31 6 8 8 4 1 ok 10294467211899142010 -linux-x86_64 31 6 8 8 4 2 ok 2468014486729326948 -linux-x86_64 31 6 8 8 4 3 ok 13708832934710116111 -linux-x86_64 31 6 8 8 8 1 ok 6715384199869579049 -linux-x86_64 31 6 9 8 4 1 ok 10414113953351534244 -linux-x86_64 31 6 9 9 4 1 ok 4824924432611332992 +linux-x86_64 29 5 7 7 4 1 ok 12200772585279551772 +linux-x86_64 29 5 8 8 2 1 ok 16251445089057044280 +linux-x86_64 29 5 8 8 4 1 ok 16622643961647829752 +linux-x86_64 29 5 8 8 4 2 ok 902254425595264091 +linux-x86_64 29 5 8 8 4 3 ok 15069842375605218424 +linux-x86_64 29 5 8 8 8 1 ok 11813498569711113796 +linux-x86_64 29 5 9 8 4 1 ok 92662381611664645 +linux-x86_64 29 5 9 9 4 1 ok 7969632618175026300 +linux-x86_64 30 4 7 7 4 1 ok 14407380987668449408 +linux-x86_64 30 4 8 8 2 1 ok 6798265038210319359 +linux-x86_64 30 4 8 8 4 1 ok 3765941716715094578 +linux-x86_64 30 4 8 8 4 2 ok 11036301984462668247 +linux-x86_64 30 4 8 8 4 3 ok 13410366176963007270 +linux-x86_64 30 4 8 8 8 1 ok 2775864693594616922 +linux-x86_64 30 4 9 8 4 1 ok 1830912849165295048 +linux-x86_64 30 4 9 9 4 1 ok 9572249900935733318 +linux-x86_64 31 7 7 7 4 1 ok 15103777741204988252 +linux-x86_64 31 7 8 8 2 1 ok 1671530153995168572 +linux-x86_64 31 7 8 8 4 1 ok 4227638302785670886 +linux-x86_64 31 7 8 8 4 2 ok 5227958771095877535 +linux-x86_64 31 7 8 8 4 3 ok 858939263087336773 +linux-x86_64 31 7 8 8 8 1 ok 15917102255954610647 +linux-x86_64 31 7 9 8 4 1 ok 4704259247486630826 +linux-x86_64 31 7 9 9 4 1 ok 9546018503956606156 linux-x86_64 32 4 7 7 4 1 ok 7540126588246018722 linux-x86_64 32 4 8 8 2 1 ok 8633803889491191707 linux-x86_64 32 4 8 8 4 1 ok 17633894656724945162 @@ -328,14 +328,14 @@ macos-arm64 9 2 8 8 4 3 ok 5877899263698877100 macos-arm64 9 2 8 8 8 1 ok 4204618854271989380 macos-arm64 9 2 9 8 4 1 ok 13364805290811868144 macos-arm64 9 2 9 9 4 1 ok 17370525382688177735 -macos-arm64 11 13 7 7 4 1 ok 7573201930155389176 -macos-arm64 11 13 8 8 2 1 ok 1580814293182404478 -macos-arm64 11 13 8 8 4 1 ok 6132248727033081699 -macos-arm64 11 13 8 8 4 2 ok 17571897192944793353 -macos-arm64 11 13 8 8 4 3 ok 17321560833037622047 -macos-arm64 11 13 8 8 8 1 ok 9101533112797905055 -macos-arm64 11 13 9 8 4 1 ok 8608092470811997215 -macos-arm64 11 13 9 9 4 1 ok 5365458970422917083 +macos-arm64 11 14 7 7 4 1 ok 7573201930155389176 +macos-arm64 11 14 8 8 2 1 ok 1580814293182404478 +macos-arm64 11 14 8 8 4 1 ok 6132248727033081699 +macos-arm64 11 14 8 8 4 2 ok 17571897192944793353 +macos-arm64 11 14 8 8 4 3 ok 17321560833037622047 +macos-arm64 11 14 8 8 8 1 ok 9101533112797905055 +macos-arm64 11 14 9 8 4 1 ok 8608092470811997215 +macos-arm64 11 14 9 9 4 1 ok 5365458970422917083 macos-arm64 12 11 7 7 4 1 ok 7388571519173228055 macos-arm64 12 11 8 8 2 1 ok 15699276765293966482 macos-arm64 12 11 8 8 4 1 ok 11621882014804934120 @@ -352,14 +352,14 @@ macos-arm64 13 1 8 8 4 3 ok 13134881442491555389 macos-arm64 13 1 8 8 8 1 ok 16484172863350979963 macos-arm64 13 1 9 8 4 1 ok 7377329900378929112 macos-arm64 13 1 9 9 4 1 ok 9971144189844359399 -macos-arm64 14 3 7 7 4 1 ok 17202733506137778788 -macos-arm64 14 3 8 8 2 1 ok 16088282592338295379 -macos-arm64 14 3 8 8 4 1 ok 11925006621409592431 -macos-arm64 14 3 8 8 4 2 ok 414349330464996680 -macos-arm64 14 3 8 8 4 3 ok 760189253045661613 -macos-arm64 14 3 8 8 8 1 ok 3322640947906419646 -macos-arm64 14 3 9 8 4 1 ok 2403928672037668991 -macos-arm64 14 3 9 9 4 1 ok 1552916736750568752 +macos-arm64 14 4 7 7 4 1 ok 17202733506137778788 +macos-arm64 14 4 8 8 2 1 ok 16088282592338295379 +macos-arm64 14 4 8 8 4 1 ok 11925006621409592431 +macos-arm64 14 4 8 8 4 2 ok 414349330464996680 +macos-arm64 14 4 8 8 4 3 ok 760189253045661613 +macos-arm64 14 4 8 8 8 1 ok 3322640947906419646 +macos-arm64 14 4 9 8 4 1 ok 2403928672037668991 +macos-arm64 14 4 9 9 4 1 ok 1552916736750568752 macos-arm64 15 1 7 7 4 1 ok 18061507907613243474 macos-arm64 15 1 8 8 2 1 ok 14205129108449246087 macos-arm64 15 1 8 8 4 1 ok 1822623745155517541 @@ -376,22 +376,22 @@ macos-arm64 16 1 8 8 4 3 ok 4396075304858481954 macos-arm64 16 1 8 8 8 1 ok 699982766957804005 macos-arm64 16 1 9 8 4 1 ok 8928204734545682645 macos-arm64 16 1 9 9 4 1 ok 12471630520837885284 -macos-arm64 17 7 7 7 4 1 ok 8389792356515183180 -macos-arm64 17 7 8 8 2 1 ok 14675185244106174055 -macos-arm64 17 7 8 8 4 1 ok 15876097599295168520 -macos-arm64 17 7 8 8 4 2 ok 7336089206298276893 -macos-arm64 17 7 8 8 4 3 ok 7241957065654810466 -macos-arm64 17 7 8 8 8 1 ok 4371133845777740965 -macos-arm64 17 7 9 8 4 1 ok 10114259367286371870 -macos-arm64 17 7 9 9 4 1 ok 18004714215950592062 -macos-arm64 18 5 7 7 4 1 ok 2131984367767490485 -macos-arm64 18 5 8 8 2 1 ok 3078796380374205351 -macos-arm64 18 5 8 8 4 1 ok 7736833362775075996 -macos-arm64 18 5 8 8 4 2 ok 3733351741626345607 -macos-arm64 18 5 8 8 4 3 ok 4515656875628782478 -macos-arm64 18 5 8 8 8 1 ok 12075523445877924400 -macos-arm64 18 5 9 8 4 1 ok 2230785934352114637 -macos-arm64 18 5 9 9 4 1 ok 10761736745352955182 +macos-arm64 17 8 7 7 4 1 ok 11333632813302390233 +macos-arm64 17 8 8 8 2 1 ok 12138044986439500568 +macos-arm64 17 8 8 8 4 1 ok 14890751749472676683 +macos-arm64 17 8 8 8 4 2 ok 7964695710715966079 +macos-arm64 17 8 8 8 4 3 ok 6508559991005734301 +macos-arm64 17 8 8 8 8 1 ok 15449496265995446758 +macos-arm64 17 8 9 8 4 1 ok 16043875660102039530 +macos-arm64 17 8 9 9 4 1 ok 975597308833091864 +macos-arm64 18 6 7 7 4 1 ok 13941455206050809724 +macos-arm64 18 6 8 8 2 1 ok 5172958575872259794 +macos-arm64 18 6 8 8 4 1 ok 685234629062878118 +macos-arm64 18 6 8 8 4 2 ok 5218743790797528810 +macos-arm64 18 6 8 8 4 3 ok 130655471980274306 +macos-arm64 18 6 8 8 8 1 ok 16548719928209852767 +macos-arm64 18 6 9 8 4 1 ok 311217146217050958 +macos-arm64 18 6 9 9 4 1 ok 11233943223771623871 macos-arm64 19 2 7 7 4 1 ok 2704824689328339712 macos-arm64 19 2 8 8 2 1 ok 18374433395111261761 macos-arm64 19 2 8 8 4 1 ok 9505660943407981793 @@ -416,30 +416,30 @@ macos-arm64 21 1 8 8 4 3 ok 14159020535153467794 macos-arm64 21 1 8 8 8 1 ok 3788158320130144245 macos-arm64 21 1 9 8 4 1 ok 14597284168628427777 macos-arm64 21 1 9 9 4 1 ok 7348842460505219783 -macos-arm64 22 1 7 7 4 1 ok 8819869978616031071 -macos-arm64 22 1 8 8 2 1 ok 730574824765298860 -macos-arm64 22 1 8 8 4 1 ok 9691963763756535666 -macos-arm64 22 1 8 8 4 2 ok 12280328850293820652 -macos-arm64 22 1 8 8 4 3 ok 16156515469674000849 -macos-arm64 22 1 8 8 8 1 ok 12707724408115629147 -macos-arm64 22 1 9 8 4 1 ok 16181991639738054527 -macos-arm64 22 1 9 9 4 1 ok 18170981972027515569 -macos-arm64 23 2 7 7 4 1 ok 549496627108919023 -macos-arm64 23 2 8 8 2 1 ok 10411914600145665080 -macos-arm64 23 2 8 8 4 1 ok 10630205222293629395 -macos-arm64 23 2 8 8 4 2 ok 3769887846353985102 -macos-arm64 23 2 8 8 4 3 ok 14438238323646079445 -macos-arm64 23 2 8 8 8 1 ok 7486873941404335262 -macos-arm64 23 2 9 8 4 1 ok 9501696698924728828 -macos-arm64 23 2 9 9 4 1 ok 8928207243182979823 -macos-arm64 24 3 7 7 4 1 ok 8766269131759353289 -macos-arm64 24 3 8 8 2 1 ok 11694144927456181775 -macos-arm64 24 3 8 8 4 1 ok 12461145917554129793 -macos-arm64 24 3 8 8 4 2 ok 14956981799443403869 -macos-arm64 24 3 8 8 4 3 ok 16634610244346170796 -macos-arm64 24 3 8 8 8 1 ok 14595537376914576811 -macos-arm64 24 3 9 8 4 1 ok 16587253345581904688 -macos-arm64 24 3 9 9 4 1 ok 12750052710567335197 +macos-arm64 22 2 7 7 4 1 ok 3655866758271062779 +macos-arm64 22 2 8 8 2 1 ok 12138492238448374580 +macos-arm64 22 2 8 8 4 1 ok 1284471235474139044 +macos-arm64 22 2 8 8 4 2 ok 8478284340363876001 +macos-arm64 22 2 8 8 4 3 ok 3651272843185213508 +macos-arm64 22 2 8 8 8 1 ok 12614988246288899091 +macos-arm64 22 2 9 8 4 1 ok 17425447918742743653 +macos-arm64 22 2 9 9 4 1 ok 9365457891684218114 +macos-arm64 23 3 7 7 4 1 ok 16284233831079059960 +macos-arm64 23 3 8 8 2 1 ok 13363436405671293892 +macos-arm64 23 3 8 8 4 1 ok 17739866559139208687 +macos-arm64 23 3 8 8 4 2 ok 15066220726718427366 +macos-arm64 23 3 8 8 4 3 ok 6306903179004779394 +macos-arm64 23 3 8 8 8 1 ok 8993378800483085102 +macos-arm64 23 3 9 8 4 1 ok 12830180604398138241 +macos-arm64 23 3 9 9 4 1 ok 13154479288816036136 +macos-arm64 24 4 7 7 4 1 ok 15335564520221051714 +macos-arm64 24 4 8 8 2 1 ok 1330458027073369597 +macos-arm64 24 4 8 8 4 1 ok 11900636304707015638 +macos-arm64 24 4 8 8 4 2 ok 15045971539824213999 +macos-arm64 24 4 8 8 4 3 ok 14226682360002951214 +macos-arm64 24 4 8 8 8 1 ok 3556977709737021092 +macos-arm64 24 4 9 8 4 1 ok 7641797400793793992 +macos-arm64 24 4 9 9 4 1 ok 8801641067316080015 macos-arm64 26 3 7 7 4 1 ok 7596233670176169203 macos-arm64 26 3 8 8 2 1 ok 10412697564682542969 macos-arm64 26 3 8 8 4 1 ok 15850274609968439542 @@ -448,14 +448,14 @@ macos-arm64 26 3 8 8 4 3 ok 12664969190182808656 macos-arm64 26 3 8 8 8 1 ok 696076906107120864 macos-arm64 26 3 9 8 4 1 ok 6547976804033402803 macos-arm64 26 3 9 9 4 1 ok 11162413902762714846 -macos-arm64 27 3 7 7 4 1 ok 3004732659855785781 -macos-arm64 27 3 8 8 2 1 ok 12106426376107521520 -macos-arm64 27 3 8 8 4 1 ok 241424920386510085 -macos-arm64 27 3 8 8 4 2 ok 13721009075482023062 -macos-arm64 27 3 8 8 4 3 ok 1999890532377512024 -macos-arm64 27 3 8 8 8 1 ok 1936688929129444838 -macos-arm64 27 3 9 8 4 1 ok 18057613840189663773 -macos-arm64 27 3 9 9 4 1 ok 17117909255655369195 +macos-arm64 27 4 7 7 4 1 ok 9592663086717577494 +macos-arm64 27 4 8 8 2 1 ok 11526286817626729205 +macos-arm64 27 4 8 8 4 1 ok 11687969261943259926 +macos-arm64 27 4 8 8 4 2 ok 9278341790741263348 +macos-arm64 27 4 8 8 4 3 ok 16994554207723317914 +macos-arm64 27 4 8 8 8 1 ok 14068689398045735615 +macos-arm64 27 4 9 8 4 1 ok 1214087987765198835 +macos-arm64 27 4 9 9 4 1 ok 1374002861262449661 macos-arm64 28 4 7 7 4 1 ok 10897682792262896502 macos-arm64 28 4 8 8 2 1 ok 13829565247877238314 macos-arm64 28 4 8 8 4 1 ok 4320506443997572230 @@ -464,30 +464,30 @@ macos-arm64 28 4 8 8 4 3 ok 13102395041856171672 macos-arm64 28 4 8 8 8 1 ok 14091742024861043395 macos-arm64 28 4 9 8 4 1 ok 8120863798265800623 macos-arm64 28 4 9 9 4 1 ok 4370189412371078302 -macos-arm64 29 4 7 7 4 1 ok 4232995500160826297 -macos-arm64 29 4 8 8 2 1 ok 7399419839946439559 -macos-arm64 29 4 8 8 4 1 ok 9472114427354905765 -macos-arm64 29 4 8 8 4 2 ok 15267437393931662669 -macos-arm64 29 4 8 8 4 3 ok 16695674954699155816 -macos-arm64 29 4 8 8 8 1 ok 13911889789453582543 -macos-arm64 29 4 9 8 4 1 ok 7057480006238666143 -macos-arm64 29 4 9 9 4 1 ok 6519639175542525008 -macos-arm64 30 3 7 7 4 1 ok 6823664725837608399 -macos-arm64 30 3 8 8 2 1 ok 18262511519283505011 -macos-arm64 30 3 8 8 4 1 ok 18435202348722167438 -macos-arm64 30 3 8 8 4 2 ok 15449658255269471781 -macos-arm64 30 3 8 8 4 3 ok 7877433232038019115 -macos-arm64 30 3 8 8 8 1 ok 10268677231018964684 -macos-arm64 30 3 9 8 4 1 ok 15407699937027208274 -macos-arm64 30 3 9 9 4 1 ok 1129976864654011214 -macos-arm64 31 6 7 7 4 1 ok 7688111426140491122 -macos-arm64 31 6 8 8 2 1 ok 11763462524905175676 -macos-arm64 31 6 8 8 4 1 ok 10294467211899142010 -macos-arm64 31 6 8 8 4 2 ok 2468014486729326948 -macos-arm64 31 6 8 8 4 3 ok 13708832934710116111 -macos-arm64 31 6 8 8 8 1 ok 6715384199869579049 -macos-arm64 31 6 9 8 4 1 ok 10414113953351534244 -macos-arm64 31 6 9 9 4 1 ok 10377664767817836693 +macos-arm64 29 5 7 7 4 1 ok 12200772585279551772 +macos-arm64 29 5 8 8 2 1 ok 16251445089057044280 +macos-arm64 29 5 8 8 4 1 ok 16622643961647829752 +macos-arm64 29 5 8 8 4 2 ok 902254425595264091 +macos-arm64 29 5 8 8 4 3 ok 15069842375605218424 +macos-arm64 29 5 8 8 8 1 ok 11813498569711113796 +macos-arm64 29 5 9 8 4 1 ok 92662381611664645 +macos-arm64 29 5 9 9 4 1 ok 7969632618175026300 +macos-arm64 30 4 7 7 4 1 ok 9915816056599072298 +macos-arm64 30 4 8 8 2 1 ok 7596154628303916297 +macos-arm64 30 4 8 8 4 1 ok 17767901218841587993 +macos-arm64 30 4 8 8 4 2 ok 6901459681767284395 +macos-arm64 30 4 8 8 4 3 ok 13677287723078906169 +macos-arm64 30 4 8 8 8 1 ok 3355014859431778632 +macos-arm64 30 4 9 8 4 1 ok 6344437938599436843 +macos-arm64 30 4 9 9 4 1 ok 7697697006014909532 +macos-arm64 31 7 7 7 4 1 ok 3556994827907664176 +macos-arm64 31 7 8 8 2 1 ok 1671530153995168572 +macos-arm64 31 7 8 8 4 1 ok 4227638302785670886 +macos-arm64 31 7 8 8 4 2 ok 5227958771095877535 +macos-arm64 31 7 8 8 4 3 ok 858939263087336773 +macos-arm64 31 7 8 8 8 1 ok 15917102255954610647 +macos-arm64 31 7 9 8 4 1 ok 4704259247486630826 +macos-arm64 31 7 9 9 4 1 ok 15362997919148692705 macos-arm64 32 4 7 7 4 1 ok 7540126588246018722 macos-arm64 32 4 8 8 2 1 ok 8633803889491191707 macos-arm64 32 4 8 8 4 1 ok 17633894656724945162 From 65ae972627eb9280a5fb948f59b0a253d6c00adc Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 10:54:56 -0400 Subject: [PATCH 134/136] Lobby: random maps by default, random parameters, start quality breakdown - The custom game screen opens on a random map at four colonies when nothing is saved; the premade library preselects FourSquares1 the first time it is opened. A saved lobby still restores every setting, generator options included (verified by the preferences harness modes). - Reset to defaults moves to the top of the map column, right under the landscape chooser, with Random parameters beside it: every one of the landscape's controls drawn at random (GenerationRequest::randomizeControls), a set the generator refuses redrawn on the spot and one the world refuses redrawn when the preview's candidates come back empty, up to six times, so the first set that generates a valid map is the one shown. - The landscape picker gains Randomize parameters beside Regenerate all, with the same redraw rule per tile (LandscapePreviewer::reroll), and Use hands the lobby the parameters the shown map was rolled with as well as its seed. - Under a generated map's preview: its fairness and score, and an (i) that opens StartQualityScreen, the per-colony breakdown of what was measured, how each factor scored, the weights and the totals. - Labels that do not fit a compact button take the small font. - 13 new UI strings in all 32 catalogs; MenuColonyHarness drives the preview before reading the map header; CustomGameSetupHarness covers the new buttons, the breakdown screen and the picker's randomize. Docs updated in docs/custom-game-setup/README.md. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- data/texts.ar.txt | 28 +++++ data/texts.br.txt | 28 +++++ data/texts.ca.txt | 28 +++++ data/texts.cz.txt | 28 +++++ data/texts.de.txt | 28 +++++ data/texts.dk.txt | 28 +++++ data/texts.en.txt | 28 +++++ data/texts.eo.txt | 28 +++++ data/texts.es.txt | 28 +++++ data/texts.eu.txt | 28 +++++ data/texts.fa.txt | 28 +++++ data/texts.fi.txt | 28 +++++ data/texts.fr.txt | 28 +++++ data/texts.gr.txt | 28 +++++ data/texts.hu.txt | 28 +++++ data/texts.id.txt | 28 +++++ data/texts.it.txt | 28 +++++ data/texts.ja.txt | 28 +++++ data/texts.keys.txt | 14 +++ data/texts.ko.txt | 28 +++++ data/texts.nl.txt | 28 +++++ data/texts.pl.txt | 28 +++++ data/texts.pt.txt | 28 +++++ data/texts.ro.txt | 28 +++++ data/texts.ru.txt | 28 +++++ data/texts.si.txt | 28 +++++ data/texts.sk.txt | 28 +++++ data/texts.sr.txt | 28 +++++ data/texts.sv.txt | 28 +++++ data/texts.tr.txt | 28 +++++ data/texts.uk.txt | 28 +++++ data/texts.vi.txt | 28 +++++ data/texts.zh-cn.txt | 28 +++++ data/texts.zh-tw.txt | 28 +++++ docs/custom-game-setup/README.md | 27 ++++- src/CustomGameScreen.cpp | 191 ++++++++++++++++++++++++------- src/CustomGameScreen.h | 15 ++- src/LandscapePickerScreen.cpp | 60 +++++++++- src/LandscapePickerScreen.h | 14 ++- src/LandscapePreviewer.cpp | 43 ++++++- src/LandscapePreviewer.h | 10 +- src/SConscript | 1 + src/StartQualityScreen.cpp | 135 ++++++++++++++++++++++ src/StartQualityScreen.h | 35 ++++++ test/CustomGameSetupHarness.cpp | 133 ++++++++++++++++++++- tools/MenuColonyHarness.cpp | 3 + 46 files changed, 1543 insertions(+), 62 deletions(-) create mode 100644 src/StartQualityScreen.cpp create mode 100644 src/StartQualityScreen.h diff --git a/data/texts.ar.txt b/data/texts.ar.txt index f9eba2c61..fc987b54a 100644 --- a/data/texts.ar.txt +++ b/data/texts.ar.txt @@ -2154,3 +2154,31 @@ OpenGL غير متوفر في هذا الإصدار. أفقي [Inland lakes] بحيرات داخلية +[Block shape] +شكل الكتل +[Random parameters] +معلمات عشوائية +[Randomize parameters] +اختر معلمات عشوائية +[Start quality] +جودة البداية +[Fairness] +الإنصاف +[Score] +النتيجة +[Weight] +الوزن +[Worst] +الأدنى +[Best] +الأعلى +[Fertility] +الخصوبة +[Deposits] +المخزون +[Room] +المساحة +[Isolation] +العزلة +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +يُقاس موقع بداية كل مستعمرة على الخريطة النهائية: المسافة إلى القمح وإلى الخشب، ومدى تجدد نمو أرضها، والمخزون ومواقع البناء في متناولها، وبُعدها عن المنافسين. يُقيَّم كل عامل من 0 إلى 1 مقابل حد ثابت، ويجمعها المجموع بأوزانها، والإنصاف هو مجموع أضعف مستعمرة مقسوماً على مجموع أفضلها، والنتيجة هي أضعف مجموع مضروباً في الإنصاف. diff --git a/data/texts.br.txt b/data/texts.br.txt index b65e15f83..42b0a2ba7 100644 --- a/data/texts.br.txt +++ b/data/texts.br.txt @@ -2152,3 +2152,31 @@ Verticais Horizontais [Inland lakes] Lagos interiores +[Block shape] +Forma dos quarteirões +[Random parameters] +Parâmetros aleatórios +[Randomize parameters] +Sortear parâmetros +[Start quality] +Qualidade do início +[Fairness] +Equidade +[Score] +Pontuação +[Weight] +Peso +[Worst] +Pior +[Best] +Melhor +[Fertility] +Fertilidade +[Deposits] +Depósitos +[Room] +Espaço +[Isolation] +Isolamento +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +O início de cada colônia é medido no mapa pronto: o caminho até o trigo e a madeira, quão bem seu terreno volta a crescer, os depósitos e locais de construção ao alcance, e a distância até os rivais. Cada fator pontua de 0 a 1 contra uma barra fixa, o total os pondera, a equidade é o total da pior colônia dividido pelo da melhor, e a pontuação é o pior total multiplicado pela equidade. diff --git a/data/texts.ca.txt b/data/texts.ca.txt index 5d4ccac39..90e101ca0 100644 --- a/data/texts.ca.txt +++ b/data/texts.ca.txt @@ -2164,3 +2164,31 @@ Verticals Horitzontals [Inland lakes] Llacs interiors +[Block shape] +Forma de les illes de cases +[Random parameters] +Paràmetres aleatoris +[Randomize parameters] +Aleatoritzar paràmetres +[Start quality] +Qualitat de l'inici +[Fairness] +Equitat +[Score] +Puntuació +[Weight] +Pes +[Worst] +Pitjor +[Best] +Millor +[Fertility] +Fertilitat +[Deposits] +Dipòsits +[Room] +Espai +[Isolation] +Aïllament +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +L'inici de cada colònia es mesura sobre el mapa acabat: el camí fins al blat i fins a la fusta, com de bé rebrota el seu terreny, els dipòsits i els llocs de construcció a l'abast, i la distància als rivals. Cada factor puntua de 0 a 1 respecte a un llindar fix, el total els pondera, l'equitat és el total de la pitjor colònia dividit pel de la millor, i la puntuació és el pitjor total multiplicat per l'equitat. diff --git a/data/texts.cz.txt b/data/texts.cz.txt index c37107938..399682e1b 100644 --- a/data/texts.cz.txt +++ b/data/texts.cz.txt @@ -2156,3 +2156,31 @@ Svisle Vodorovně [Inland lakes] Vnitrozemská jezera +[Block shape] +Tvar bloků +[Random parameters] +Náhodné parametry +[Randomize parameters] +Náhodně zvolit parametry +[Start quality] +Kvalita startu +[Fairness] +Spravedlnost +[Score] +Skóre +[Weight] +Váha +[Worst] +Nejhorší +[Best] +Nejlepší +[Fertility] +Úrodnost +[Deposits] +Zásoby +[Room] +Prostor +[Isolation] +Odloučení +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Start každé kolonie se měří na hotové mapě: cesta k obilí a ke dřevu, jak dobře její půda dorůstá, zásoby a stavební místa v dosahu a vzdálenost od soupeřů. Každý činitel dostane skóre od 0 do 1 vůči pevné laťce, celek je váží, spravedlnost je celek nejhorší kolonie dělený celkem nejlepší a skóre je nejhorší celek násobený spravedlností. diff --git a/data/texts.de.txt b/data/texts.de.txt index 33f551bdb..c63354d30 100644 --- a/data/texts.de.txt +++ b/data/texts.de.txt @@ -2156,3 +2156,31 @@ Senkrecht Waagerecht [Inland lakes] Binnenseen +[Block shape] +Blockform +[Random parameters] +Zufällige Parameter +[Randomize parameters] +Parameter zufällig wählen +[Start quality] +Startqualität +[Fairness] +Ausgeglichenheit +[Score] +Wertung +[Weight] +Gewicht +[Worst] +Schlechteste +[Best] +Beste +[Fertility] +Fruchtbarkeit +[Deposits] +Vorkommen +[Room] +Platz +[Isolation] +Abgeschiedenheit +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Der Start jeder Kolonie wird auf der fertigen Karte gemessen: der Weg zu Weizen und zu Holz, wie gut ihr Boden nachwächst, die Vorkommen und Bauplätze in Reichweite und ihr Abstand zu Rivalen. Jeder Faktor erhält 0 bis 1 gegenüber einer festen Messlatte, die Summe gewichtet sie, die Ausgeglichenheit ist die Summe der schlechtesten Kolonie geteilt durch die der besten, und die Wertung ist die schlechteste Summe mal der Ausgeglichenheit. diff --git a/data/texts.dk.txt b/data/texts.dk.txt index 841d6d5d8..2c0db1c8e 100644 --- a/data/texts.dk.txt +++ b/data/texts.dk.txt @@ -2232,3 +2232,31 @@ Lodret Vandret [Inland lakes] Indsøer +[Block shape] +Karréform +[Random parameters] +Tilfældige parametre +[Randomize parameters] +Vælg tilfældige parametre +[Start quality] +Startkvalitet +[Fairness] +Retfærdighed +[Score] +Pointtal +[Weight] +Vægt +[Worst] +Dårligst +[Best] +Bedst +[Fertility] +Frugtbarhed +[Deposits] +Forekomster +[Room] +Plads +[Isolation] +Afsondring +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Hver kolonis start måles på det færdige kort: vejen til hvede og til træ, hvor godt dens jord vokser igen, forekomsterne og byggepladserne inden for rækkevidde og afstanden til rivalerne. Hver faktor scorer fra 0 til 1 mod en fast målestok, totalen vægter dem, retfærdigheden er den dårligste kolonis total delt med den bedstes, og pointtallet er den dårligste total gange retfærdigheden. diff --git a/data/texts.en.txt b/data/texts.en.txt index 9068b8759..6cb664202 100644 --- a/data/texts.en.txt +++ b/data/texts.en.txt @@ -2154,3 +2154,31 @@ Vertical Horizontal [Inland lakes] Inland lakes +[Block shape] +Block shape +[Random parameters] +Random parameters +[Randomize parameters] +Randomize parameters +[Start quality] +Start quality +[Fairness] +Fairness +[Score] +Score +[Weight] +Weight +[Worst] +Worst +[Best] +Best +[Fertility] +Fertility +[Deposits] +Deposits +[Room] +Room +[Isolation] +Isolation +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness. diff --git a/data/texts.eo.txt b/data/texts.eo.txt index bb7b41cd1..feba7bd8f 100644 --- a/data/texts.eo.txt +++ b/data/texts.eo.txt @@ -2154,3 +2154,31 @@ Vertikala Horizontala [Inland lakes] Enlandaj lagoj +[Block shape] +Formo de kvartaloj +[Random parameters] +Hazardaj parametroj +[Randomize parameters] +Hazardigi parametrojn +[Start quality] +Kvalito de la komenco +[Fairness] +Justeco +[Score] +Poentaro +[Weight] +Pezo +[Worst] +Plej malbona +[Best] +Plej bona +[Fertility] +Fekundeco +[Deposits] +Rezervoj +[Room] +Spaco +[Isolation] +Izoliteco +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +La komenco de ĉiu kolonio estas mezurata sur la finita mapo: la vojo al tritiko kaj al ligno, kiel bone ĝia tero rekreskas, la rezervoj kaj konstruejoj atingeblaj, kaj ĝia distanco de rivaloj. Ĉiu faktoro ricevas poenton de 0 ĝis 1 kontraŭ fiksa mezurilo, la sumo pezas ilin, la justeco estas la sumo de la plej malbona kolonio dividita per tiu de la plej bona, kaj la poentaro estas la plej malbona sumo multiplikita per la justeco. diff --git a/data/texts.es.txt b/data/texts.es.txt index a665fdff5..3fb14a076 100644 --- a/data/texts.es.txt +++ b/data/texts.es.txt @@ -2156,3 +2156,31 @@ Verticales Horizontales [Inland lakes] Lagos interiores +[Block shape] +Forma de las manzanas +[Random parameters] +Parámetros aleatorios +[Randomize parameters] +Aleatorizar parámetros +[Start quality] +Calidad del inicio +[Fairness] +Equidad +[Score] +Puntuación +[Weight] +Peso +[Worst] +Peor +[Best] +Mejor +[Fertility] +Fertilidad +[Deposits] +Depósitos +[Room] +Espacio +[Isolation] +Aislamiento +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +El inicio de cada colonia se mide sobre el mapa terminado: el camino hasta el trigo y la madera, lo bien que rebrota su terreno, los depósitos y sitios de construcción a su alcance, y su distancia a los rivales. Cada factor puntúa de 0 a 1 frente a un umbral fijo, el total los pondera, la equidad es el total de la peor colonia dividido por el de la mejor, y la puntuación es el peor total multiplicado por la equidad. diff --git a/data/texts.eu.txt b/data/texts.eu.txt index ecb53638a..522c74ba0 100644 --- a/data/texts.eu.txt +++ b/data/texts.eu.txt @@ -2166,3 +2166,31 @@ Bertikala Horizontala [Inland lakes] Barnealdeko aintzirak +[Block shape] +Etxadien forma +[Random parameters] +Ausazko parametroak +[Randomize parameters] +Parametroak ausaz aukeratu +[Start quality] +Hasieraren kalitatea +[Fairness] +Zuzentasuna +[Score] +Puntuazioa +[Weight] +Pisua +[Worst] +Okerrena +[Best] +Onena +[Fertility] +Emankortasuna +[Deposits] +Hobiak +[Room] +Lekua +[Isolation] +Isolamendua +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Kolonia bakoitzaren hasiera amaitutako mapan neurtzen da: gariraino eta egurreraino dagoen bidea, bere lurra zein ondo berritzen den, eskura dituen hobiak eta eraikitzeko lekuak, eta aurkariekiko distantzia. Faktore bakoitzak 0tik 1era puntuatzen du neurri finko baten aurrean, guztizkoak haztatzen ditu, zuzentasuna koloniarik okerrenaren guztizkoa onenarenaz zatituta da, eta puntuazioa guztizko okerrena zuzentasunaz biderkatuta. diff --git a/data/texts.fa.txt b/data/texts.fa.txt index 4c96ac6fe..39cf8d96d 100644 --- a/data/texts.fa.txt +++ b/data/texts.fa.txt @@ -2154,3 +2154,31 @@ OpenGL در این بیلد موجود نیست. افقی [Inland lakes] دریاچه‌های داخلی +[Block shape] +شکل بلوک‌ها +[Random parameters] +پارامترهای تصادفی +[Randomize parameters] +تصادفی‌سازی پارامترها +[Start quality] +کیفیت شروع +[Fairness] +انصاف +[Score] +امتیاز +[Weight] +وزن +[Worst] +بدترین +[Best] +بهترین +[Fertility] +باروری +[Deposits] +ذخایر +[Room] +فضا +[Isolation] +انزوا +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +شروع هر مستعمره روی نقشهٔ نهایی سنجیده می‌شود: مسیر تا گندم و تا چوب، اینکه زمینش چقدر خوب دوباره می‌روید، ذخایر و جای ساخت‌وساز در دسترس، و فاصله‌اش از رقیبان. هر عامل از ۰ تا ۱ در برابر معیاری ثابت امتیاز می‌گیرد، مجموع آن‌ها را وزن می‌دهد، انصاف مجموع بدترین مستعمره تقسیم بر بهترین است، و امتیاز بدترین مجموع ضرب در انصاف است. diff --git a/data/texts.fi.txt b/data/texts.fi.txt index 4e286f292..d56213ca0 100644 --- a/data/texts.fi.txt +++ b/data/texts.fi.txt @@ -2156,3 +2156,31 @@ Pystysuoraan Vaakasuoraan [Inland lakes] Sisämaan järvet +[Block shape] +Korttelin muoto +[Random parameters] +Satunnaiset parametrit +[Randomize parameters] +Satunnaista parametrit +[Start quality] +Aloituksen laatu +[Fairness] +Tasapuolisuus +[Score] +Pisteet +[Weight] +Paino +[Worst] +Heikoin +[Best] +Paras +[Fertility] +Hedelmällisyys +[Deposits] +Esiintymät +[Room] +Tila +[Isolation] +Eristyneisyys +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Jokaisen yhdyskunnan aloitus mitataan valmiilta kartalta: matka vehnän ja puun luo, kuinka hyvin sen maa kasvaa takaisin, ulottuvilla olevat esiintymät ja rakennuspaikat sekä etäisyys kilpailijoihin. Jokainen tekijä saa pisteet 0–1 kiinteää mittapuuta vasten, kokonaisarvo painottaa ne, tasapuolisuus on heikoimman yhdyskunnan kokonaisarvo jaettuna parhaan arvolla, ja pisteet ovat heikoin kokonaisarvo kerrottuna tasapuolisuudella. diff --git a/data/texts.fr.txt b/data/texts.fr.txt index 938a3d5f3..0edd0635d 100644 --- a/data/texts.fr.txt +++ b/data/texts.fr.txt @@ -2166,3 +2166,31 @@ Verticales Horizontales [Inland lakes] Lacs intérieurs +[Block shape] +Forme des pâtés de maisons +[Random parameters] +Paramètres aléatoires +[Randomize parameters] +Paramètres au hasard +[Start quality] +Qualité du départ +[Fairness] +Équité +[Score] +Note +[Weight] +Poids +[Worst] +Pire +[Best] +Meilleur +[Fertility] +Fertilité +[Deposits] +Gisements +[Room] +Espace +[Isolation] +Isolement +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Le départ de chaque colonie est mesuré sur la carte terminée : le trajet jusqu'au blé et au bois, la repousse de son sol, les gisements et emplacements de construction à portée, et sa distance aux rivaux. Chaque facteur est noté de 0 à 1 face à une barre fixe, le total les pondère, l'équité est le total de la pire colonie divisé par celui de la meilleure, et la note est le pire total multiplié par l'équité. diff --git a/data/texts.gr.txt b/data/texts.gr.txt index 3e7ef23f8..ba29680a0 100644 --- a/data/texts.gr.txt +++ b/data/texts.gr.txt @@ -2226,3 +2226,31 @@ Cortex Οριζόντια [Inland lakes] Εσωτερικές λίμνες +[Block shape] +Σχήμα οικοδομικών τετραγώνων +[Random parameters] +Τυχαίες παράμετροι +[Randomize parameters] +Τυχαία επιλογή παραμέτρων +[Start quality] +Ποιότητα εκκίνησης +[Fairness] +Δικαιοσύνη +[Score] +Βαθμολογία +[Weight] +Βάρος +[Worst] +Χειρότερο +[Best] +Καλύτερο +[Fertility] +Γονιμότητα +[Deposits] +Αποθέματα +[Room] +Χώρος +[Isolation] +Απομόνωση +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Η εκκίνηση κάθε αποικίας μετριέται στον τελικό χάρτη: η διαδρομή ως το σιτάρι και ως το ξύλο, πόσο καλά ξαναφυτρώνει το έδαφός της, τα αποθέματα και οι θέσεις δόμησης σε εμβέλεια, και η απόστασή της από τους αντιπάλους. Κάθε παράγοντας βαθμολογείται από 0 έως 1 απέναντι σε σταθερό όριο, το σύνολο τους σταθμίζει, η δικαιοσύνη είναι το σύνολο της χειρότερης αποικίας προς εκείνο της καλύτερης, και η βαθμολογία είναι το χειρότερο σύνολο επί τη δικαιοσύνη. diff --git a/data/texts.hu.txt b/data/texts.hu.txt index 74d9c66ac..a3f88a4b7 100644 --- a/data/texts.hu.txt +++ b/data/texts.hu.txt @@ -2156,3 +2156,31 @@ Függőleges Vízszintes [Inland lakes] Belső tavak +[Block shape] +Háztömbök alakja +[Random parameters] +Véletlen paraméterek +[Randomize parameters] +Paraméterek véletlenszerűen +[Start quality] +Kezdés minősége +[Fairness] +Méltányosság +[Score] +Pontszám +[Weight] +Súly +[Worst] +Leggyengébb +[Best] +Legjobb +[Fertility] +Termékenység +[Deposits] +Készletek +[Room] +Hely +[Isolation] +Elszigeteltség +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Minden kolónia kezdését a kész térképen mérjük: az út a búzáig és a fáig, mennyire nő vissza a talaja, az elérhető készletek és építési helyek, valamint a távolság a riválisoktól. Minden tényező 0-tól 1-ig pontoz egy rögzített mércéhez képest, az összeg súlyozza őket, a méltányosság a leggyengébb kolónia összege osztva a legjobbéval, a pontszám pedig a leggyengébb összeg szorozva a méltányossággal. diff --git a/data/texts.id.txt b/data/texts.id.txt index a84a3dc84..cc0ec4aaf 100644 --- a/data/texts.id.txt +++ b/data/texts.id.txt @@ -2152,3 +2152,31 @@ Tegak Mendatar [Inland lakes] Danau pedalaman +[Block shape] +Bentuk blok +[Random parameters] +Parameter acak +[Randomize parameters] +Acak parameter +[Start quality] +Kualitas awal +[Fairness] +Keadilan +[Score] +Skor +[Weight] +Bobot +[Worst] +Terburuk +[Best] +Terbaik +[Fertility] +Kesuburan +[Deposits] +Cadangan +[Room] +Ruang +[Isolation] +Keterpencilan +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Awal setiap koloni diukur pada peta yang sudah jadi: jarak tempuh ke gandum dan ke kayu, seberapa baik tanahnya tumbuh kembali, cadangan dan lokasi bangunan yang terjangkau, serta jaraknya dari pesaing. Setiap faktor dinilai dari 0 sampai 1 terhadap patokan tetap, totalnya membobotinya, keadilan adalah total koloni terburuk dibagi total yang terbaik, dan skor adalah total terburuk dikali keadilan. diff --git a/data/texts.it.txt b/data/texts.it.txt index 54ee99d05..ed287de27 100644 --- a/data/texts.it.txt +++ b/data/texts.it.txt @@ -2218,3 +2218,31 @@ Verticali Orizzontali [Inland lakes] Laghi interni +[Block shape] +Forma degli isolati +[Random parameters] +Parametri casuali +[Randomize parameters] +Parametri a caso +[Start quality] +Qualità della partenza +[Fairness] +Equità +[Score] +Punteggio +[Weight] +Peso +[Worst] +Peggiore +[Best] +Migliore +[Fertility] +Fertilità +[Deposits] +Giacimenti +[Room] +Spazio +[Isolation] +Isolamento +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +La partenza di ogni colonia è misurata sulla mappa finita: il cammino fino al grano e al legno, quanto bene ricresce il suo terreno, i giacimenti e i siti edificabili a portata, e la distanza dai rivali. Ogni fattore ottiene un punteggio da 0 a 1 rispetto a un'asticella fissa, il totale li pondera, l'equità è il totale della colonia peggiore diviso per quello della migliore, e il punteggio è il totale peggiore moltiplicato per l'equità. diff --git a/data/texts.ja.txt b/data/texts.ja.txt index 65c2d8f9a..07c878742 100644 --- a/data/texts.ja.txt +++ b/data/texts.ja.txt @@ -2152,3 +2152,31 @@ Globulation 2 のカーソルを表示します。 横 [Inland lakes] 内陸の湖 +[Block shape] +街区の形 +[Random parameters] +ランダムなパラメータ +[Randomize parameters] +パラメータをランダム化 +[Start quality] +開始地点の質 +[Fairness] +公平性 +[Score] +スコア +[Weight] +重み +[Worst] +最低 +[Best] +最高 +[Fertility] +肥沃度 +[Deposits] +資源量 +[Room] +建設余地 +[Isolation] +隔離度 +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +各コロニーの開始地点は完成したマップ上で測定されます: 小麦と木材までの道のり、地面の再生の良さ、手の届く範囲の資源量と建設地、そしてライバルまでの距離です。各要素は固定の基準に対して0から1で採点され、合計はそれらを重み付けし、公平性は最低のコロニーの合計を最高のもので割った値、スコアは最低の合計に公平性を掛けた値です。 diff --git a/data/texts.keys.txt b/data/texts.keys.txt index 61ba419ad..13f30198e 100644 --- a/data/texts.keys.txt +++ b/data/texts.keys.txt @@ -1075,3 +1075,17 @@ [Vertical] [Horizontal] [Inland lakes] +[Block shape] +[Random parameters] +[Randomize parameters] +[Start quality] +[Fairness] +[Score] +[Weight] +[Worst] +[Best] +[Fertility] +[Deposits] +[Room] +[Isolation] +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] diff --git a/data/texts.ko.txt b/data/texts.ko.txt index 6e98534d6..6563bdef0 100644 --- a/data/texts.ko.txt +++ b/data/texts.ko.txt @@ -2152,3 +2152,31 @@ Globulation 2 커서를 표시합니다. 가로 [Inland lakes] 내륙 호수 +[Block shape] +블록 모양 +[Random parameters] +무작위 매개변수 +[Randomize parameters] +매개변수 무작위화 +[Start quality] +시작 품질 +[Fairness] +공정성 +[Score] +점수 +[Weight] +가중치 +[Worst] +최저 +[Best] +최고 +[Fertility] +비옥도 +[Deposits] +매장량 +[Room] +공간 +[Isolation] +격리 +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +각 식민지의 시작 지점은 완성된 지도에서 측정됩니다: 밀과 목재까지의 거리, 땅이 얼마나 잘 다시 자라는지, 닿을 수 있는 매장량과 건설 부지, 그리고 경쟁자와의 거리입니다. 각 요소는 고정된 기준에 대해 0에서 1로 점수가 매겨지고, 합계는 이를 가중하며, 공정성은 최저 식민지의 합계를 최고 식민지의 합계로 나눈 값이고, 점수는 최저 합계에 공정성을 곱한 값입니다. diff --git a/data/texts.nl.txt b/data/texts.nl.txt index 9096b46a1..a8cf28190 100644 --- a/data/texts.nl.txt +++ b/data/texts.nl.txt @@ -2180,3 +2180,31 @@ Verticaal Horizontaal [Inland lakes] Binnenmeren +[Block shape] +Vorm van de bouwblokken +[Random parameters] +Willekeurige parameters +[Randomize parameters] +Parameters willekeurig kiezen +[Start quality] +Startkwaliteit +[Fairness] +Eerlijkheid +[Score] +Waardering +[Weight] +Gewicht +[Worst] +Slechtste +[Best] +Beste +[Fertility] +Vruchtbaarheid +[Deposits] +Voorraden +[Room] +Ruimte +[Isolation] +Afzondering +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +De start van elke kolonie wordt gemeten op de voltooide kaart: de loop naar tarwe en naar hout, hoe goed haar grond teruggroeit, de voorraden en bouwplaatsen binnen bereik, en haar afstand tot rivalen. Elke factor scoort van 0 tot 1 tegen een vaste maatstaf, het totaal weegt ze, de eerlijkheid is het totaal van de slechtste kolonie gedeeld door dat van de beste, en de waardering is het slechtste totaal maal de eerlijkheid. diff --git a/data/texts.pl.txt b/data/texts.pl.txt index 5cd04a776..802f9f8dd 100644 --- a/data/texts.pl.txt +++ b/data/texts.pl.txt @@ -2156,3 +2156,31 @@ Pionowe Poziome [Inland lakes] Jeziora śródlądowe +[Block shape] +Kształt kwartałów +[Random parameters] +Losowe parametry +[Randomize parameters] +Losuj parametry +[Start quality] +Jakość startu +[Fairness] +Sprawiedliwość +[Score] +Wynik +[Weight] +Waga +[Worst] +Najgorszy +[Best] +Najlepszy +[Fertility] +Żyzność +[Deposits] +Zasoby +[Room] +Miejsce +[Isolation] +Odosobnienie +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Start każdej kolonii mierzony jest na gotowej mapie: droga do zboża i do drewna, jak dobrze odrasta jej grunt, zasoby i miejsca pod budowę w zasięgu oraz odległość od rywali. Każdy czynnik otrzymuje od 0 do 1 względem stałej miary, suma je waży, sprawiedliwość to suma najgorszej kolonii podzielona przez sumę najlepszej, a wynik to najgorsza suma pomnożona przez sprawiedliwość. diff --git a/data/texts.pt.txt b/data/texts.pt.txt index c5ef41b2e..a9306a06e 100644 --- a/data/texts.pt.txt +++ b/data/texts.pt.txt @@ -2162,3 +2162,31 @@ Verticais Horizontais [Inland lakes] Lagos interiores +[Block shape] +Forma dos quarteirões +[Random parameters] +Parâmetros aleatórios +[Randomize parameters] +Aleatorizar parâmetros +[Start quality] +Qualidade do início +[Fairness] +Equidade +[Score] +Pontuação +[Weight] +Peso +[Worst] +Pior +[Best] +Melhor +[Fertility] +Fertilidade +[Deposits] +Depósitos +[Room] +Espaço +[Isolation] +Isolamento +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +O início de cada colónia é medido no mapa terminado: o caminho até ao trigo e à madeira, quão bem o seu terreno volta a crescer, os depósitos e locais de construção ao alcance, e a distância aos rivais. Cada fator pontua de 0 a 1 face a uma barra fixa, o total pondera-os, a equidade é o total da pior colónia dividido pelo da melhor, e a pontuação é o pior total multiplicado pela equidade. diff --git a/data/texts.ro.txt b/data/texts.ro.txt index 5212b59b1..c37652f7e 100644 --- a/data/texts.ro.txt +++ b/data/texts.ro.txt @@ -2156,3 +2156,31 @@ Verticale Orizontale [Inland lakes] Lacuri interioare +[Block shape] +Forma cvartalelor +[Random parameters] +Parametri aleatori +[Randomize parameters] +Parametri la întâmplare +[Start quality] +Calitatea startului +[Fairness] +Echitate +[Score] +Scor +[Weight] +Pondere +[Worst] +Cel mai slab +[Best] +Cel mai bun +[Fertility] +Fertilitate +[Deposits] +Zăcăminte +[Room] +Spațiu +[Isolation] +Izolare +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Startul fiecărei colonii se măsoară pe harta terminată: drumul până la grâu și la lemn, cât de bine își reface terenul, zăcămintele și locurile de construcție la îndemână și distanța față de rivali. Fiecare factor primește de la 0 la 1 față de un prag fix, totalul îi ponderează, echitatea este totalul celei mai slabe colonii împărțit la cel al celei mai bune, iar scorul este cel mai slab total înmulțit cu echitatea. diff --git a/data/texts.ru.txt b/data/texts.ru.txt index 44899b2b8..99ff856f6 100644 --- a/data/texts.ru.txt +++ b/data/texts.ru.txt @@ -2154,3 +2154,31 @@ OpenGL недоступен в этой сборке. Горизонтально [Inland lakes] Внутренние озёра +[Block shape] +Форма кварталов +[Random parameters] +Случайные параметры +[Randomize parameters] +Подобрать случайные параметры +[Start quality] +Качество старта +[Fairness] +Справедливость +[Score] +Оценка +[Weight] +Вес +[Worst] +Худший +[Best] +Лучший +[Fertility] +Плодородие +[Deposits] +Запасы +[Room] +Простор +[Isolation] +Удалённость +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Старт каждой колонии измеряется на готовой карте: путь до пшеницы и до леса, насколько хорошо восстанавливается её земля, запасы и места для строительства в пределах досягаемости и расстояние до соперников. Каждый фактор оценивается от 0 до 1 относительно фиксированной планки, итог их взвешивает, справедливость — это итог худшей колонии, делённый на итог лучшей, а оценка — худший итог, умноженный на справедливость. diff --git a/data/texts.si.txt b/data/texts.si.txt index e9b7b067c..18bbd5d98 100644 --- a/data/texts.si.txt +++ b/data/texts.si.txt @@ -2156,3 +2156,31 @@ Navpično Vodoravno [Inland lakes] Celinska jezera +[Block shape] +Oblika blokov +[Random parameters] +Naključni parametri +[Randomize parameters] +Naključno izberi parametre +[Start quality] +Kakovost začetka +[Fairness] +Pravičnost +[Score] +Ocena +[Weight] +Utež +[Worst] +Najslabši +[Best] +Najboljši +[Fertility] +Rodovitnost +[Deposits] +Zaloge +[Room] +Prostor +[Isolation] +Odmaknjenost +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Začetek vsake kolonije se meri na dokončanem zemljevidu: pot do pšenice in do lesa, kako dobro se njena zemlja obnavlja, zaloge in gradbena mesta v dosegu ter razdalja do tekmecev. Vsak dejavnik dobi oceno od 0 do 1 glede na stalno merilo, skupni seštevek jih uteži, pravičnost je seštevek najslabše kolonije deljen s seštevkom najboljše, ocena pa je najslabši seštevek pomnožen s pravičnostjo. diff --git a/data/texts.sk.txt b/data/texts.sk.txt index 11fdc9fa0..5918f99de 100644 --- a/data/texts.sk.txt +++ b/data/texts.sk.txt @@ -2164,3 +2164,31 @@ Zvisle Vodorovne [Inland lakes] Vnútrozemské jazerá +[Block shape] +Tvar blokov +[Random parameters] +Náhodné parametre +[Randomize parameters] +Náhodne zvoliť parametre +[Start quality] +Kvalita štartu +[Fairness] +Spravodlivosť +[Score] +Skóre +[Weight] +Váha +[Worst] +Najhorší +[Best] +Najlepší +[Fertility] +Úrodnosť +[Deposits] +Zásoby +[Room] +Priestor +[Isolation] +Odlúčenosť +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Štart každej kolónie sa meria na hotovej mape: cesta k obiliu a k drevu, ako dobre jej pôda dorastá, zásoby a stavebné miesta v dosahu a vzdialenosť od súperov. Každý činiteľ dostane od 0 do 1 voči pevnej latke, celok ich váži, spravodlivosť je celok najhoršej kolónie delený celkom najlepšej a skóre je najhorší celok násobený spravodlivosťou. diff --git a/data/texts.sr.txt b/data/texts.sr.txt index 1cdfe52b6..8dc14b42e 100644 --- a/data/texts.sr.txt +++ b/data/texts.sr.txt @@ -2156,3 +2156,31 @@ OpenGL није доступан у овој верзији. Водоравно [Inland lakes] Унутрашња језера +[Block shape] +Облик блокова +[Random parameters] +Насумични параметри +[Randomize parameters] +Насумично изабери параметре +[Start quality] +Квалитет почетка +[Fairness] +Правичност +[Score] +Оцена +[Weight] +Тежина +[Worst] +Најслабији +[Best] +Најбољи +[Fertility] +Плодност +[Deposits] +Залихе +[Room] +Простор +[Isolation] +Удаљеност +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Почетак сваке колоније мери се на завршеној мапи: пут до жита и до дрвета, колико добро њено тло поново расте, залихе и места за градњу на дохват руке и удаљеност од супарника. Сваки чинилац добија оцену од 0 до 1 према сталној мери, укупан збир их одмерава, правичност је збир најслабије колоније подељен збиром најбоље, а оцена је најслабији збир помножен правичношћу. diff --git a/data/texts.sv.txt b/data/texts.sv.txt index f75c1f5b6..1c57c2a70 100644 --- a/data/texts.sv.txt +++ b/data/texts.sv.txt @@ -2166,3 +2166,31 @@ Lodrätt Vågrätt [Inland lakes] Insjöar +[Block shape] +Kvartersform +[Random parameters] +Slumpmässiga parametrar +[Randomize parameters] +Slumpa parametrar +[Start quality] +Startkvalitet +[Fairness] +Rättvisa +[Score] +Poäng +[Weight] +Vikt +[Worst] +Sämst +[Best] +Bäst +[Fertility] +Bördighet +[Deposits] +Fyndigheter +[Room] +Utrymme +[Isolation] +Avskildhet +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Varje kolonis start mäts på den färdiga kartan: vägen till vete och till trä, hur väl dess mark växer tillbaka, fyndigheterna och byggplatserna inom räckhåll samt avståndet till rivalerna. Varje faktor får 0 till 1 mot ett fast riktmärke, totalen väger dem, rättvisan är den sämsta kolonins total delad med den bästas, och poängen är den sämsta totalen gånger rättvisan. diff --git a/data/texts.tr.txt b/data/texts.tr.txt index 20d86775d..67946086e 100644 --- a/data/texts.tr.txt +++ b/data/texts.tr.txt @@ -2164,3 +2164,31 @@ Dikey Yatay [Inland lakes] İç göller +[Block shape] +Blok şekli +[Random parameters] +Rastgele parametreler +[Randomize parameters] +Parametreleri rastgele seç +[Start quality] +Başlangıç kalitesi +[Fairness] +Adalet +[Score] +Puan +[Weight] +Ağırlık +[Worst] +En kötü +[Best] +En iyi +[Fertility] +Verimlilik +[Deposits] +Yataklar +[Room] +Alan +[Isolation] +Uzaklık +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Her koloninin başlangıcı bitmiş haritada ölçülür: buğdaya ve oduna yürüme mesafesi, toprağının ne kadar iyi yeniden yeşerdiği, erişilebilir yataklar ve inşaat alanları ile rakiplere olan uzaklığı. Her etken sabit bir ölçüte göre 0 ile 1 arasında puanlanır, toplam bunları ağırlıklandırır, adalet en kötü koloninin toplamının en iyisine bölümüdür ve puan en kötü toplam çarpı adalettir. diff --git a/data/texts.uk.txt b/data/texts.uk.txt index 5fe8e8302..9c9c893bf 100644 --- a/data/texts.uk.txt +++ b/data/texts.uk.txt @@ -2152,3 +2152,31 @@ OpenGL недоступний у цій збірці. Горизонтально [Inland lakes] Внутрішні озера +[Block shape] +Форма кварталів +[Random parameters] +Випадкові параметри +[Randomize parameters] +Випадково обрати параметри +[Start quality] +Якість старту +[Fairness] +Справедливість +[Score] +Оцінка +[Weight] +Вага +[Worst] +Найгірший +[Best] +Найкращий +[Fertility] +Родючість +[Deposits] +Запаси +[Room] +Простір +[Isolation] +Віддаленість +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Старт кожної колонії вимірюється на готовій карті: шлях до пшениці та до лісу, наскільки добре відновлюється її земля, запаси та місця для будівництва в межах досяжності й відстань до суперників. Кожен чинник оцінюється від 0 до 1 відносно сталої планки, підсумок їх зважує, справедливість — це підсумок найгіршої колонії, поділений на підсумок найкращої, а оцінка — найгірший підсумок, помножений на справедливість. diff --git a/data/texts.vi.txt b/data/texts.vi.txt index 777fe2fc0..022584f22 100644 --- a/data/texts.vi.txt +++ b/data/texts.vi.txt @@ -2152,3 +2152,31 @@ Dọc Ngang [Inland lakes] Hồ nội địa +[Block shape] +Hình dạng khu phố +[Random parameters] +Tham số ngẫu nhiên +[Randomize parameters] +Chọn tham số ngẫu nhiên +[Start quality] +Chất lượng khởi đầu +[Fairness] +Công bằng +[Score] +Điểm +[Weight] +Trọng số +[Worst] +Kém nhất +[Best] +Tốt nhất +[Fertility] +Độ phì nhiêu +[Deposits] +Trữ lượng +[Room] +Không gian +[Isolation] +Cách biệt +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +Khởi đầu của mỗi thuộc địa được đo trên bản đồ hoàn chỉnh: đường đi tới lúa mì và tới gỗ, đất của nó mọc lại tốt đến đâu, trữ lượng và vị trí xây dựng trong tầm với, cùng khoảng cách tới các đối thủ. Mỗi yếu tố được chấm từ 0 đến 1 so với một mức cố định, tổng điểm cân nhắc chúng theo trọng số, công bằng là tổng của thuộc địa kém nhất chia cho tổng của thuộc địa tốt nhất, và điểm là tổng kém nhất nhân với công bằng. diff --git a/data/texts.zh-cn.txt b/data/texts.zh-cn.txt index 71947b4e6..b9aa70923 100644 --- a/data/texts.zh-cn.txt +++ b/data/texts.zh-cn.txt @@ -2152,3 +2152,31 @@ OpenGL 在此版本中不可用。 横向 [Inland lakes] 内陆湖 +[Block shape] +街区形状 +[Random parameters] +随机参数 +[Randomize parameters] +随机选取参数 +[Start quality] +开局质量 +[Fairness] +公平度 +[Score] +评分 +[Weight] +权重 +[Worst] +最低 +[Best] +最高 +[Fertility] +肥沃度 +[Deposits] +储量 +[Room] +空间 +[Isolation] +隔离度 +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +每个殖民地的开局都在完成的地图上测量:到小麦和木材的路程、土地的再生能力、可及范围内的储量和建筑用地,以及与对手的距离。每项因素按固定标准计 0 到 1 分,总分对其加权,公平度是最低殖民地的总分除以最高者,评分是最低总分乘以公平度。 diff --git a/data/texts.zh-tw.txt b/data/texts.zh-tw.txt index 4d656cbbf..5a63e891a 100644 --- a/data/texts.zh-tw.txt +++ b/data/texts.zh-tw.txt @@ -2228,3 +2228,31 @@ OpenGL 在此版本中不可用。 橫向 [Inland lakes] 內陸湖 +[Block shape] +街區形狀 +[Random parameters] +隨機參數 +[Randomize parameters] +隨機選取參數 +[Start quality] +開局品質 +[Fairness] +公平度 +[Score] +評分 +[Weight] +權重 +[Worst] +最低 +[Best] +最高 +[Fertility] +肥沃度 +[Deposits] +儲量 +[Room] +空間 +[Isolation] +隔離度 +[Each colony's start is measured on the finished map: the walk to wheat and to wood, how well its ground regrows, the deposits and building sites within reach, and its distance from rivals. Every factor scores from 0 to 1 against a fixed bar, the total weighs them, fairness is the worst colony's total over the best, and the score is the worst total times the fairness.] +每個殖民地的開局都在完成的地圖上測量:到小麥和木材的路程、土地的再生能力、可及範圍內的儲量和建築用地,以及與對手的距離。每項因素按固定標準計 0 到 1 分,總分對其加權,公平度是最低殖民地的總分除以最高者,評分是最低總分乘以公平度。 diff --git a/docs/custom-game-setup/README.md b/docs/custom-game-setup/README.md index 56026f078..32b23feeb 100644 --- a/docs/custom-game-setup/README.md +++ b/docs/custom-game-setup/README.md @@ -5,7 +5,9 @@ keeps the match summary and launch action available while dense content scrolls. ## Behavior -- Start with FourSquares1 and four-player FFA: you plus three Numbi AIs. +- Start on a random map with four colonies in a free-for-all: you plus three Numbi AIs + (since 2026-09-14; the premade library, a tab away, preselects FourSquares1 the first time + it is opened). A saved lobby restores whichever mode it was left in. - Unix map libraries separate installed and user roots. Windows/shared-root installations show one combined library so shipped maps remain accessible. - Select a premade map or generate a random world. Random maps start at @@ -13,12 +15,29 @@ keeps the match summary and launch action available while dense content scrolls. automatically after a 500 ms edit debounce, with generation deferred during a drag or open choice menu. The displayed snapshot is the map that launches. Randomize, under the preview, rolls the same settings again with a new seed. +- Landscape, at the top of the map column, opens a sheet showing every landscape as + a real map at the current size and colony count. Regenerate all rolls the sheet + again with fresh seeds; Randomize parameters rolls it with every landscape's own + controls drawn at random, redrawing any set the world refuses. Use plays exactly + the map shown: its seed, and the parameters it was rolled with, come back to the + lobby. +- Right under the landscape chooser, Reset to defaults returns width, height, colony + count and the landscape's own controls to their defaults, keeping the landscape + itself; Random parameters beside it draws every one of the landscape's controls at + random (size, colony count and workers stay). Some combinations make no map: a + draw the generator refuses is redrawn on the spot, and one the world refuses is + redrawn when the preview fails, up to six times, so the first set that generates a + valid map is the one shown. - Expand Terrain, Resources and Layout to tune the applicable generator controls. Resource amounts are percentages of the landscape's own default (100). On/off switches are checkbox rows: click one, or press Space or Return while it has - focus. Reset to defaults, below them, returns width, height, colony count and - the landscape's own controls to their defaults, keeping the landscape itself. - Starting workers belong to Game Rules; premade maps retain authored units. + focus. Starting workers belong to Game Rules; premade maps retain authored units. +- Under a generated map's preview the lobby shows its start quality: the fairness + (the worst colony's start over the best) and the score it ranked its candidate + rolls by, with an (i) that opens the breakdown, one row per colony with what was + measured (the walk to wheat and to wood, the ground's fertility, deposits and + building sites within reach, distance from rivals), each factor's score and the + weights. - Each colony has a numbered color swatch, controller, AI/difficulty and team. You, AI, shared You + AI, and Closed are explicit choices. Shared control uses two of the twelve controller records; the UI explains unavailable combinations. diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 6bfee0cf1..7087ba3a0 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -14,11 +14,13 @@ #include "LobbyControls.h" #include "LobbyMapCatalog.h" #include "Player.h" +#include "StartQualityScreen.h" #include #include #include #include #include +#include #include #include #include @@ -218,21 +220,14 @@ CustomGameScreen::CustomGameScreen() : Glob2TabScreen(false, true) } else { + // No saved lobby: a random map at four colonies (FEEDBACK 2026-09-14: random maps are the + // default, not the premade library). listMaps(); - for (const auto &p : mapPaths) - if (std::filesystem::path(p).filename() == "FourSquares1.map") - { - loadMap(p); - break; - } - if (!validMap || setup.capacity != 4) - { - setup.random = true; - setup.setCapacity(4); - validMap = false; - source.clear(); - invalidate(); - } + setup.random = true; + setup.setCapacity(4); + validMap = false; + source.clear(); + invalidate(); } activateGroup(groups[0]); @@ -313,20 +308,90 @@ void CustomGameScreen::chooseLandscape() if (result == QUIT_APPLICATION) endExecute(QUIT_APPLICATION); else if (result >= 0 && result < int(entries.size())) - applyLandscape(entries[result].first, picker.chosenSeed()); + { + const GenerationRequest request = picker.chosenRequest(); + applyLandscape(entries[result].first, picker.chosenSeed(), &request); + } } -void CustomGameScreen::applyLandscape(int method, std::optional seed) +void CustomGameScreen::applyLandscape(int method, std::optional seed, + const GenerationRequest *shown) { setup.generatorHistory.select(setup.generator, method); + // The parameters the picker rolled the map with come along, so the lobby shows the map it + // showed: the landscape's own, or a random set from "Randomize parameters". + if (shown && shown->method == method) + setup.generator.options = shown->options; chosenSeed = seed; + randomAttempts = 0; ++setup.mapRevision; invalidate(); // An explicit choice, not an edit in progress: preview it now rather than after the debounce. previewDue = SDL_GetTicks(); } +// Reset keeps the chosen landscape and the Game Rules tab's starting workers, and returns every +// other field in the map column to the landscape's registered defaults. +void CustomGameScreen::resetParameters() +{ + GenerationRequest reset; + reset.setMethodDefaults(setup.generator.method); + reset.nbWorkers = setup.generator.nbWorkers; + reset.terrainType = setup.generator.terrainType; + reset.seed = setup.generator.seed; + setup.generator = reset; + setup.setCapacity(reset.nbTeams); + chosenSeed.reset(); + randomAttempts = 0; + ++setup.mapRevision; + invalidate(); +} +// Random parameters (FEEDBACK 2026-09-14): every one of the landscape's own controls drawn at +// random, keeping the size, colony count and workers the player chose. Some combinations make no +// map: a draw the generator refuses up front is redrawn on the spot (randomizeControls), and one +// the world refuses is redrawn when the preview's candidates come back empty (collectCandidates), +// up to kRandomAttempts times, so the first set that generates a valid map is the one kept. +void CustomGameScreen::randomizeParameters() +{ + randomAttempts = kRandomAttempts; + if (!drawRandomParameters()) + { + randomAttempts = 0; + message = tr("Generation failed. Adjust settings or press Start to retry."); + return; + } + chosenSeed.reset(); + ++setup.mapRevision; + invalidate(); + previewDue = SDL_GetTicks(); +} +bool CustomGameScreen::drawRandomParameters() +{ + auto draft = setup.generator; + draft.nbTeams = setup.capacity; + if (!draft.randomizeControls(GenerationContext::randomSeed())) + return false; + setup.generator = draft; + return true; +} +void CustomGameScreen::showStartQuality() +{ + if (!quality.measured) + return; + std::vector labels; + std::vector colors; + for (size_t i = 0; i < quality.colonies.size(); ++i) + { + labels.push_back(colonyLabel(int(i))); + colors.push_back(i < preview->starts.size() ? preview->starts[i].color + : Color(160, 172, 149)); + } + StartQualityScreen screen(quality, labels, colors); + if (screen.execute(globalContainer->gfx, 40) == QUIT_APPLICATION) + endExecute(QUIT_APPLICATION); +} void CustomGameScreen::invalidate() { validMap = false; + quality = {}; previewRevision = ~0u; previewPending = setup.random; previewDue = SDL_GetTicks() + 500; @@ -481,6 +546,8 @@ bool CustomGameScreen::generateMap() std::filesystem::remove_all(std::filesystem::path(snapshot).parent_path()); snapshot = candidate; previewRevision = setup.mapRevision; + quality = generationResult.quality; + randomAttempts = 0; message = tr("Preview ready. Start plays this exact map."); return true; } @@ -614,6 +681,21 @@ bool CustomGameScreen::collectCandidates() return false; // an edit meanwhile already asked for a new preview if (!best) { + // A random set of parameters the world refused on every seed: draw another while the + // draws last (randomizeParameters), rather than leave the player a failure to fix by hand. + if (randomAttempts > 0) + { + --randomAttempts; + if (drawRandomParameters()) + { + chosenSeed.reset(); + ++setup.mapRevision; + invalidate(); + previewDue = SDL_GetTicks(); + return false; + } + } + randomAttempts = 0; validMap = false; message = tr("Generation failed. Adjust settings or press Start to retry."); return false; @@ -644,6 +726,21 @@ void CustomGameScreen::setMapMode(bool random) } else if (!setup.premadeMap.empty()) loadMap(setup.premadeMap); + else + { + // The first visit to the library with nothing chosen yet (the lobby now opens on a random + // map): FourSquares1, the lobby's old opening map, or failing that the first map listed. + listMaps(); + std::string first = mapPaths.empty() ? "" : mapPaths.front(); + for (const auto &p : mapPaths) + if (std::filesystem::path(p).filename() == "FourSquares1.map") + { + first = p; + break; + } + if (!first.empty()) + loadMap(first); + } } void CustomGameScreen::showAIProfile(int colony) { @@ -980,7 +1077,31 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) yy += 18; ui.chooser("generator/landscape", {x, yy, leftW - 16, 30}, tr(GenerationRequest::methodName(g.method)), [this] { chooseLandscape(); }); - yy += 40; + yy += 36; + // Right under the landscape, before its controls (FEEDBACK 2026-09-14): Reset to defaults, + // and beside it Random parameters, which draws every control below at random. + { + GenerationRequest defaults; + defaults.setMethodDefaults(g.method); + const bool atDefaults = g.options == defaults.options && g.wDec == defaults.wDec && + g.hDec == defaults.hDec && setup.capacity == defaults.nbTeams; + const int half = (leftW - 16 - 6) / 2; + // The narrow column of the compact layout: a label the standard font cannot fit in + // its half takes the small one rather than being cut off. + const auto fitting = [half](const std::string &label) + { + return Toolkit::getFont("standard")->getStringWidth(label) + 16 <= half ? "standard" + : "little"; + }; + const std::string reset = tr("Reset to defaults"), random = tr("Random parameters"); + ui.button( + "generator/reset", {x, yy, half, 30}, reset, [this] { resetParameters(); }, false, + !atDefaults, true, fitting(reset)); + ui.button( + "generator/random", {x + half + 6, yy, half, 30}, random, + [this] { randomizeParameters(); }, false, true, true, fitting(random)); + yy += 38; + } auto discrete = [&](const GenerationRequest::Control &c, const std::string &id) { ui.text(x + 4, yy + 7, tr(c.label), "little", 110, true); @@ -1071,29 +1192,6 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) : "Dimensions and colony count are set above.")) + 8; } - // Reset keeps the chosen landscape and the Game Rules tab's starting workers, and returns - // every other field in this column to the landscape's registered defaults. - GenerationRequest defaults; - defaults.setMethodDefaults(g.method); - const bool atDefaults = g.options == defaults.options && g.wDec == defaults.wDec && - g.hDec == defaults.hDec && setup.capacity == defaults.nbTeams; - ui.button( - "generator/reset", {x, yy, std::min(230, leftW - 16), 30}, tr("Reset to defaults"), - [this] - { - GenerationRequest reset; - reset.setMethodDefaults(setup.generator.method); - reset.nbWorkers = setup.generator.nbWorkers; - reset.terrainType = setup.generator.terrainType; - reset.seed = setup.generator.seed; - setup.generator = reset; - setup.setCapacity(reset.nbTeams); - chosenSeed.reset(); - ++setup.mapRevision; - invalidate(); - }, - false, !atDefaults, true); - yy += 38; ui.endRegion(yy - startY + 8); } ui.text(rightX, top, @@ -1108,6 +1206,19 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) ui.text(rightX, top + 29, dimensions + " / " + std::to_string(setup.capacity) + " " + tr("colonies"), "little", rightW, true); + // The generated map's start quality (FEEDBACK 2026-09-14): the fairness the lobby ranked its + // candidate rolls by, and a small (i) that opens the breakdown behind it. + if (setup.random && validMap && quality.measured) + { + char summary[96]; + std::snprintf(summary, sizeof summary, "%s %.2f / %s %.2f", tr("Fairness").c_str(), + quality.fairness, tr("Score").c_str(), quality.score); + const int sw = Toolkit::getFont("little")->getStringWidth(summary); + ui.text(rightX + rightW - sw - 30, top + 29, summary, "little", sw + 2, true); + ui.button( + "quality/info", {rightX + rightW - 24, top + 26, 22, 20}, "i", + [this] { showStartQuality(); }, false, true, false, "little"); + } // A random map keeps a row under its preview for the Randomize button. const int previewLimit = h - (setup.random ? 162 : 126); int size = std::min(rightW, previewLimit); diff --git a/src/CustomGameScreen.h b/src/CustomGameScreen.h index d1fea2970..52c9e2abf 100644 --- a/src/CustomGameScreen.h +++ b/src/CustomGameScreen.h @@ -3,6 +3,7 @@ #include "CustomGameSetup.h" #include "Glob2Screen.h" #include "MapHeader.h" +#include "StartQuality.h" #include #include #include @@ -67,6 +68,13 @@ class CustomGameScreen : public Glob2TabScreen Uint32 previewDue = 0; // A seed the landscape picker showed a map for: the next preview reproduces that map. std::optional chosenSeed; + // How good a start each colony got on the map in the preview, as GenerationService scored + // it; measured only while the preview is a generated map. + MapGeneration::StartQualityReport quality; + // Random parameters (see randomizeParameters): draws still allowed when the world refuses a + // set, so the preview keeps redrawing until it shows a map. + int randomAttempts = 0; + static constexpr int kRandomAttempts = 6; // The preview's candidate rolls run on the previewer's workers; the best-scoring one is then // rolled again on this thread for the snapshot. candidateRevision is the draft they were // rolled for, so an edit made meanwhile discards them. @@ -96,7 +104,12 @@ class CustomGameScreen : public Glob2TabScreen bool generateMap(); std::vector> landscapeEntries() const; void chooseLandscape(); - void applyLandscape(int method, std::optional seed); + void applyLandscape(int method, std::optional seed, + const GenerationRequest *shown = nullptr); + void resetParameters(); + void randomizeParameters(); + bool drawRandomParameters(); + void showStartQuality(); int choose(const std::string &, const std::vector &, int, bool profiles = false, const std::vector &enabled = {}); void invalidate(); diff --git a/src/LandscapePickerScreen.cpp b/src/LandscapePickerScreen.cpp index 121a4e600..3a845907c 100644 --- a/src/LandscapePickerScreen.cpp +++ b/src/LandscapePickerScreen.cpp @@ -2,6 +2,7 @@ #include "LandscapePickerScreen.h" #include "GlobalContainer.h" #include "LobbyControls.h" +#include "GenerationContext.h" #include #include #include @@ -25,6 +26,7 @@ std::vector LandscapePickerScreen::requestsOf(const std::vect LandscapePickerScreen::LandscapePickerScreen(const std::string &title, std::vector entries, int selected) : title(title), entries(std::move(entries)), tiles(this->entries.size()), + redraws(this->entries.size(), 0), selected(std::clamp(selected, 0, std::max(0, int(this->entries.size()) - 1))), previewer(requestsOf(this->entries)) { @@ -49,6 +51,30 @@ std::optional LandscapePickerScreen::chosenSeed() const return preview.seed; } +GenerationRequest LandscapePickerScreen::chosenRequest() const +{ + if (selected < 0 || selected >= int(entries.size())) + return GenerationRequest(); + return previewer.request(std::size_t(selected)); +} + +void LandscapePickerScreen::randomizeParameters() +{ + std::vector requests; + for (std::size_t i = 0; i < entries.size(); ++i) + { + GenerationRequest request = entries[i].request; + // Each landscape draws from its own stream; a draw the generator refuses up front is + // redrawn inside randomizeControls, so what goes to the workers is at least a valid + // request. Should no draw at all be accepted, the entry keeps its own parameters. + request.randomizeControls( + GenerationContext::deriveSeed(GenerationContext::randomSeed(), "random/" + std::to_string(i))); + redraws[i] = kRandomDraws; + requests.push_back(request); + } + previewer.restart(std::move(requests)); +} + void LandscapePickerScreen::select(int index) { if (index < 0 || index >= int(entries.size())) @@ -80,6 +106,17 @@ void LandscapePickerScreen::refresh() tile.revision = revision; tile.preview = previewer.preview(i); tile.surface.reset(); + // A random set of parameters the world refused (every seed failed) is drawn again while + // draws remain: the sheet should show maps, not failures, and the user asked for variety. + if (tile.preview.state == LandscapePreviewer::State::Failed && redraws[i] > 0) + { + --redraws[i]; + GenerationRequest request = entries[i].request; + request.randomizeControls(GenerationContext::deriveSeed( + GenerationContext::randomSeed(), "redraw/" + std::to_string(i))); + previewer.reroll(i, request); + continue; + } if (tile.preview.state == LandscapePreviewer::State::Ready) { // Surfaces belong to the UI thread; the worker only hands over pixels. @@ -219,10 +256,25 @@ void LandscapePickerScreen::render() ui.button( "landscape/back", {x, height - 55, 100, 34}, tr("Back"), [this] { endExecute(CANCEL); }, false, true, true); - const int regenerateW = compact ? 150 : 190; - ui.button("landscape/regenerate", {x + 110, height - 55, regenerateW, 34}, tr("Regenerate all"), - [this] { previewer.regenerate(); }); - const int useX = x + 110 + regenerateW + 10; + // A label the standard font cannot fit in its button on a compact screen takes the small one + // rather than being cut off. + const auto fitting = [](const std::string &label, int width) + { + return Toolkit::getFont("standard")->getStringWidth(label) + 16 <= width ? "standard" + : "little"; + }; + const int regenerateW = compact ? 130 : 170; + const std::string regenerate = tr("Regenerate all"), randomize = tr("Randomize parameters"); + ui.button( + "landscape/regenerate", {x + 110, height - 55, regenerateW, 34}, regenerate, + [this] { previewer.regenerate(); }, false, true, false, fitting(regenerate, regenerateW)); + // Beside it, the same sheet with every landscape's parameters drawn at random (the size and + // colony count stay), for an even wider spread of maps to pick from. + const int randomW = compact ? 150 : 190; + ui.button( + "landscape/randomize", {x + 110 + regenerateW + 10, height - 55, randomW, 34}, randomize, + [this] { randomizeParameters(); }, false, true, false, fitting(randomize, randomW)); + const int useX = x + 110 + regenerateW + 10 + randomW + 10; const bool valid = selected >= 0 && selected < int(entries.size()); ui.button( "landscape/use", {useX, height - 55, x + w - useX, 34}, diff --git a/src/LandscapePickerScreen.h b/src/LandscapePickerScreen.h index eca634c56..26b4dc90d 100644 --- a/src/LandscapePickerScreen.h +++ b/src/LandscapePickerScreen.h @@ -12,7 +12,9 @@ class LobbyControls; /// A full-window modal showing every landscape as a freshly generated map, for picking one by /// sight. execute() returns the chosen entry's index, CANCEL, or QUIT_APPLICATION; chosenSeed() -/// then reproduces the map that was shown for it. Entries are just a localized name and the +/// then reproduces the map that was shown for it, and chosenRequest() carries the parameters it +/// was rolled with - the entry's own, or a random set when "Randomize parameters" was used, so +/// the lobby can take over exactly what was shown. Entries are just a localized name and the /// request to preview, so any screen that selects a landscape can run it. class LandscapePickerScreen : public Glob2Screen { @@ -36,6 +38,13 @@ class LandscapePickerScreen : public Glob2Screen int selection() const { return selected; } /// The seed behind the preview shown for the selection, once that preview is ready. std::optional chosenSeed() const; + /// The request the selection's preview was rolled with (see randomizeParameters). + GenerationRequest chosenRequest() const; + /// Draws every landscape's own controls at random (GenerationRequest::randomizeControls) and + /// rolls them all again. A random set the world then refuses is drawn again, up to + /// kRandomDraws times per landscape, so the sheet fills with maps that exist. + void randomizeParameters(); + static constexpr int kRandomDraws = 6; bool busy() const { return previewer.busy(); } private: @@ -53,6 +62,9 @@ class LandscapePickerScreen : public Glob2Screen std::string title; std::vector entries; std::vector tiles; + /// Per entry, random draws still allowed after a refused set (0 when the parameters are the + /// entry's own or the draws are spent). + std::vector redraws; int selected, columns = 1; bool reveal = true; LobbyControls *controls; diff --git a/src/LandscapePreviewer.cpp b/src/LandscapePreviewer.cpp index 434f6eccf..63a81a404 100644 --- a/src/LandscapePreviewer.cpp +++ b/src/LandscapePreviewer.cpp @@ -11,6 +11,7 @@ LandscapePreviewer::LandscapePreviewer(std::vector requests, { slots.resize(this->requests.size()); seeds.resize(this->requests.size()); + passes.resize(this->requests.size()); if (threads <= 0) { const unsigned cores = std::thread::hardware_concurrency(); @@ -37,13 +38,15 @@ LandscapePreviewer::~LandscapePreviewer() void LandscapePreviewer::beginPass() { ++pass; - next = 0; + queue.clear(); const auto root = GenerationContext::randomSeed(); for (std::size_t i = 0; i < slots.size(); ++i) { seeds[i] = GenerationContext::deriveSeed(root, "landscape/" + std::to_string(i)); + passes[i] = pass; slots[i].state = State::Pending; ++slots[i].revision; + queue.push_back(i); } } @@ -63,11 +66,38 @@ void LandscapePreviewer::restart(std::vector fresh) requests = std::move(fresh); slots.resize(requests.size()); seeds.resize(requests.size()); + passes.resize(requests.size()); beginPass(); } wake.notify_all(); } +void LandscapePreviewer::reroll(std::size_t index, GenerationRequest request) +{ + { + std::lock_guard lock(mutex); + if (index >= slots.size()) + return; + // A pass of its own, so a roll of this slot still under way is dropped when it lands. + ++pass; + requests[index] = std::move(request); + seeds[index] = GenerationContext::deriveSeed(GenerationContext::randomSeed(), + "landscape/" + std::to_string(index)); + passes[index] = pass; + slots[index].state = State::Pending; + ++slots[index].revision; + queue.erase(std::remove(queue.begin(), queue.end(), index), queue.end()); + queue.push_back(index); + } + wake.notify_all(); +} + +GenerationRequest LandscapePreviewer::request(std::size_t index) const +{ + std::lock_guard lock(mutex); + return requests.at(index); +} + unsigned LandscapePreviewer::revision(std::size_t index) const { std::lock_guard lock(mutex); @@ -135,11 +165,12 @@ void LandscapePreviewer::work() std::unique_lock lock(mutex); for (;;) { - wake.wait(lock, [this] { return stopping || next < requests.size(); }); + wake.wait(lock, [this] { return stopping || !queue.empty(); }); if (stopping) return; - const std::size_t index = next++; - const unsigned myPass = pass; + const std::size_t index = queue.front(); + queue.erase(queue.begin()); + const unsigned myPass = passes[index]; slots[index].state = State::Generating; ++slots[index].revision; const GenerationRequest request = requests[index]; @@ -147,8 +178,8 @@ void LandscapePreviewer::work() lock.unlock(); Preview result = roll(request, seed); lock.lock(); - if (pass != myPass) - continue; // regenerate() superseded this roll while it ran + if (passes[index] != myPass) + continue; // regenerate(), restart() or reroll() superseded this roll while it ran result.revision = slots[index].revision + 1; slots[index] = std::move(result); } diff --git a/src/LandscapePreviewer.h b/src/LandscapePreviewer.h index 724a4068b..cc9377092 100644 --- a/src/LandscapePreviewer.h +++ b/src/LandscapePreviewer.h @@ -51,6 +51,11 @@ class LandscapePreviewer /// Replaces the requests and rolls them with fresh seeds, on the same workers; rolls of the /// old requests already under way finish, then are dropped. void restart(std::vector requests); + /// Replaces one slot's request and rolls that slot again with a fresh seed, leaving the others + /// as they are: how a picker redraws a randomised landscape whose parameters the world refused. + void reroll(std::size_t index, GenerationRequest request); + /// The request a slot is currently showing (or rolling). + GenerationRequest request(std::size_t index) const; std::size_t size() const { return requests.size(); } unsigned revision(std::size_t index) const; Preview preview(std::size_t index) const; @@ -68,8 +73,11 @@ class LandscapePreviewer std::vector requests; std::vector slots; std::vector seeds; + /// Per slot, the pass its pending roll belongs to: a roll finished for an older pass is dropped. + std::vector passes; unsigned pass = 0; - std::size_t next = 0; + /// Slots waiting for a worker, in order. + std::vector queue; bool stopping = false; mutable std::mutex mutex; std::condition_variable wake; diff --git a/src/SConscript b/src/SConscript index 71de437bd..19a5468f1 100644 --- a/src/SConscript +++ b/src/SConscript @@ -171,6 +171,7 @@ LANGameInformation.cpp LANMenuScreen.cpp LandscapePickerScreen.cpp LandscapePreviewer.cpp +StartQualityScreen.cpp MainMenuScreen.cpp map/Map.cpp map/generator/compatibility/LegacyGenerationDescriptor.cpp diff --git a/src/StartQualityScreen.cpp b/src/StartQualityScreen.cpp new file mode 100644 index 000000000..1d394499c --- /dev/null +++ b/src/StartQualityScreen.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "StartQualityScreen.h" +#include "GlobalContainer.h" +#include "LobbyControls.h" +#include +#include +#include +#include + +namespace +{ +std::string tr(const std::string &s) +{ + return Toolkit::getStringTable()->getString("[" + s + "]"); +} +std::string fixed(double v, int decimals = 2) +{ + char text[32]; + std::snprintf(text, sizeof text, "%.*f", decimals, v); + return text; +} +} // namespace + +StartQualityScreen::StartQualityScreen(const MapGeneration::StartQualityReport &report, + std::vector colonyLabels, + std::vector colonyColors) + : report(report), labels(std::move(colonyLabels)), colors(std::move(colonyColors)) +{ + gfx = globalContainer->gfx; + controls = new LobbyControls(); + controls->render = [this] { render(); }; + addWidget(controls); +} + +StartQualityScreen::~StartQualityScreen() = default; + +void StartQualityScreen::onAction(Widget *, Action, int, int) {} + +void StartQualityScreen::onSDLEvent(SDL_Event *event) +{ + if (event->type == SDL_KEYDOWN && !controls->popup.open && + (event->key.keysym.sym == SDLK_ESCAPE || event->key.keysym.sym == SDLK_RETURN)) + { + endExecute(BACK); + return; + } + controls->handle(event); +} + +// The table: what each colony was measured to have (walking steps to wheat and wood, the mean +// growth chance of its ground, the deposits within its catchment, its building sites, steps to the +// nearest rival) and how every factor scored between 0 and 1, then its weighted total; under it the +// map's worst and best totals, the fairness (worst over best) and the score (worst times fairness) +// the lobby keeps the best candidate roll by. The factor weights come from StartQualityWeights. +void StartQualityScreen::render() +{ + auto &ui = *controls; + const int width = gfx->getW(), height = gfx->getH(); + const int w = std::min(width - 32, 1000), x = (width - w) / 2; + const bool compact = width < 800; + ui.setScreenPosition(0, 0); + ui.setDimensions(width, height); + ui.box({x - 8, 8, w + 16, height - 16}, Color(232, 237, 218), 8); + ui.text(x + 8, 20, tr("Start quality"), compact ? "standard" : "menu", w - 16); + const int subtitleY = compact ? 46 : 56; + int yy = subtitleY + 8 + + ui.paragraph(x + 8, subtitleY, w - 16, + tr("Each colony's start is measured on the finished map: the walk to " + "wheat and to wood, how well its ground regrows, the deposits and " + "building sites within reach, and its distance from rivals. Every " + "factor scores from 0 to 1 against a fixed bar, the total weighs " + "them, fairness is the worst colony's total over the best, and the " + "score is the worst total times the fairness.")); + // Columns: the label, then a measured value with its score under it for each factor, then the + // total. Widths share the row; the label column gets a little more. + const char *heads[] = {"Colony", "Wheat", "Wood", "Fertility", "Deposits", "Room", "Isolation", + "Total"}; + const int columns = 8; + const int labelW = compact ? 90 : 130; + const int cellW = (w - 16 - labelW) / (columns - 1); + const MapGeneration::StartQualityWeights weights; + ui.beginRegion(40, {x, yy, w, height - yy - 70}); + int rowY = yy - ui.regions[40].offset; + const int startY = rowY; + for (int c = 0; c < columns; ++c) + ui.text(x + 8 + (c == 0 ? 0 : labelW + (c - 1) * cellW), rowY, tr(heads[c]), "standard", + c == 0 ? labelW - 8 : cellW - 6); + rowY += 24; + // The weights, so the reader sees what a factor counts for in the total. + const double factorWeights[] = {weights.wheat, weights.wood, weights.fertility, + weights.depth, weights.room, weights.isolation}; + ui.text(x + 8, rowY, tr("Weight"), "little", labelW - 8, true); + for (int c = 0; c < 6; ++c) + ui.text(x + 8 + labelW + c * cellW, rowY, fixed(factorWeights[c]), "little", cellW - 6, + true); + rowY += 22; + const int rowH = 44; + for (size_t i = 0; i < report.colonies.size(); ++i) + { + const auto &q = report.colonies[i]; + ui.box({x, rowY - 4, w - 12, rowH - 4}, ui.panel); + if (i < colors.size()) + ui.box({x + 6, rowY + 2, 14, 14}, colors[i], 3); + ui.text(x + 26, rowY, i < labels.size() ? labels[i] : std::to_string(i + 1), "little", + labelW - 30); + // Measured on the top line, the factor's score under it. + const std::string measured[] = { + q.wheatDistance < 0 ? "-" : std::to_string(q.wheatDistance), + q.woodDistance < 0 ? "-" : std::to_string(q.woodDistance), + fixed(q.meanFertility, 0), + std::to_string(q.resourceAmount), + std::to_string(q.buildSites), + q.rivalDistance < 0 ? "-" : std::to_string(q.rivalDistance)}; + const double scores[] = {q.wheat, q.wood, q.fertility, q.depth, q.room, q.isolation}; + for (int c = 0; c < 6; ++c) + { + const int cx = x + 8 + labelW + c * cellW; + ui.text(cx, rowY, measured[c], "little", cellW - 6); + ui.text(cx, rowY + 18, fixed(scores[c]), "little", cellW - 6, true); + } + ui.text(x + 8 + labelW + 6 * cellW, rowY + 6, fixed(q.total), "standard", cellW - 6); + rowY += rowH; + } + rowY += 8; + const std::string totals = tr("Worst") + " " + fixed(report.worst) + " / " + tr("Best") + + " " + fixed(report.best) + " / " + tr("Fairness") + " " + + fixed(report.fairness) + " / " + tr("Score") + " " + + fixed(report.score); + ui.text(x + 8, rowY, totals, "standard", w - 16); + rowY += 30; + ui.endRegion(rowY - startY); + ui.button( + "quality/back", {x, height - 55, 100, 34}, tr("Back"), [this] { endExecute(BACK); }, false, + true, true); +} diff --git a/src/StartQualityScreen.h b/src/StartQualityScreen.h new file mode 100644 index 000000000..4087b5e79 --- /dev/null +++ b/src/StartQualityScreen.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Glob2Screen.h" +#include "StartQuality.h" +#include +#include +class LobbyControls; + +/// The breakdown behind a generated map's start quality (FEEDBACK 2026-09-14: "a little (i) icon +/// that i can click on which shows me the breakdown of all the different things analyzed which +/// went into the fairness score"): one row per colony with what was measured on the finished map +/// and how each factor scored, then the totals the lobby ranks its candidate rolls by. Read-only; +/// Back or Escape returns. +class StartQualityScreen : public Glob2Screen +{ + friend struct CustomGameSetupHarness; + + public: + enum + { + BACK = -2 + }; + StartQualityScreen(const MapGeneration::StartQualityReport &report, + std::vector colonyLabels, std::vector colonyColors); + ~StartQualityScreen() override; + void onAction(Widget *, Action, int, int) override; + void onSDLEvent(SDL_Event *) override; + + private: + void render(); + MapGeneration::StartQualityReport report; + std::vector labels; + std::vector colors; + LobbyControls *controls; +}; diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index 84f9a6974..711abc187 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -9,6 +9,7 @@ #include "FrontendTheme.h" #include "GlobalContainer.h" #include "LandscapePickerScreen.h" +#include "StartQualityScreen.h" #include "LobbyControls.h" #include "LobbyMapCatalog.h" #include "LobbyMapPreview.h" @@ -137,7 +138,12 @@ struct CustomGameSetupHarness // Every control's whole range survives, whether it lives in a legacy field or not. for (int method : GeneratorRegistry::builtins().methods(false)) for (const auto &c : GenerationRequest::controls(method)) + { + if (!roundTrip(method, {{c.id, c.minimum}}) || !roundTrip(method, {{c.id, c.maximum}})) + std::cerr << "control " << c.id << " of " << GeneratorRegistry::builtins().at(method).id + << " does not survive its range " << c.minimum << ".." << c.maximum << "\n"; assert(roundTrip(method, {{c.id, c.minimum}}) && roundTrip(method, {{c.id, c.maximum}})); + } CustomGamePreferences original; original.setup.random = true; @@ -186,7 +192,11 @@ struct CustomGameSetupHarness if (write) { CustomGameScreen screen; - assert(screen.validMap && screen.setup.capacity == 4); + // Nothing saved: a random map (FEEDBACK 2026-09-14). The premade library is what this + // file is written with, so switch to it first. + assert(screen.setup.random && screen.previewPending && screen.setup.capacity == 4); + screen.setMapMode(false); + assert(!screen.setup.random && screen.validMap); assert(screen.setup.setController(2, CustomGameSetup::Shared)); screen.setup.colonies[2].ai = AI::CORTEX; screen.setup.colonies[0].alliance = 2; @@ -250,8 +260,11 @@ struct CustomGameSetupHarness out.write(truncated.data(), truncated.size(), "broken preferences"); }); { + // A file the lobby cannot read is the same as none: a random map at four colonies, + // its preview pending (FEEDBACK 2026-09-14: random maps are the default tab). CustomGameScreen screen; - assert(screen.validMap && screen.setup.capacity == 4 && screen.setup.speed == 0); + assert(screen.setup.random && screen.previewPending && !screen.validMap && + screen.setup.capacity == 4 && screen.setup.speed == 0); } files->remove(CustomGamePreferences::filename); } @@ -433,7 +446,14 @@ struct CustomGameSetupHarness CustomGameScreen screen; screen.gfx = globalContainer->gfx; screen.dispatchInit(); - assert(screen.validMap && screen.setup.capacity == 4); + // With nothing saved the lobby opens on a random map (FEEDBACK 2026-09-14); the premade + // library is a click away and is what the catalog checks below exercise. + assert(screen.setup.random && screen.previewPending && !screen.validMap && + screen.setup.capacity == 4); + // The first visit to the library preselects FourSquares1, the old opening map. + screen.setMapMode(false); + assert(!screen.setup.random && screen.validMap && !screen.previewPending && + std::filesystem::path(screen.setup.premadeMap).filename() == "FourSquares1.map"); screen.separateMapLibraries = false; screen.listMaps(); assert(std::any_of( @@ -574,10 +594,11 @@ struct CustomGameSetupHarness globalContainer->gfx->printScreen(output + "/" + name + ".bmp"); }; capture("map-640"); - // Randomize and Reset to defaults only apply to random maps. + // Randomize, Reset to defaults and Random parameters only apply to random maps. assert(std::none_of(screen.controls->hits.begin(), screen.controls->hits.end(), [](const auto &h) { - return h.id == "map/randomize" || h.id == "generator/reset"; + return h.id == "map/randomize" || h.id == "generator/reset" || + h.id == "generator/random" || h.id == "quality/info"; })); screen.activateGroup(screen.groups[1]); capture("players-640"); @@ -673,10 +694,78 @@ struct CustomGameSetupHarness screen.controls->hits.begin(), screen.controls->hits.end(), [](const auto &h) { return h.id == "generator/reset"; }); assert(reset != screen.controls->hits.end() && !reset->enabled); + // Reset to defaults sits at the top of the column, right under the landscape chooser, + // with Random parameters beside it (FEEDBACK 2026-09-14). + const auto landscape = std::find_if( + screen.controls->hits.begin(), screen.controls->hits.end(), + [](const auto &h) { return h.id == "generator/landscape"; }); + const auto random = std::find_if( + screen.controls->hits.begin(), screen.controls->hits.end(), + [](const auto &h) { return h.id == "generator/random"; }); + assert(landscape != screen.controls->hits.end() && random != screen.controls->hits.end()); + assert(reset->box.y > landscape->box.y && reset->box.y < landscape->box.y + 80 && + random->box.y == reset->box.y && random->box.x > reset->box.x && random->enabled); + for (const auto &h : screen.controls->hits) + if (h.id.rfind("generator/", 0) == 0 && h.id != "generator/landscape" && + h.id != "generator/reset" && h.id != "generator/random") + assert(h.box.y >= reset->box.y); preview(); assert(screen.validMap); capture("reset-640"); } + // Random parameters draws every one of the landscape's controls at random, keeping the size, + // colony count and workers, and yields a request the generator accepts; the preview then + // shows a map for it. Reset returns to the defaults from there. + { + const auto before = screen.setup.generator; + const int capacity = screen.setup.capacity; + const GeneratorDefinition &definition = + GeneratorRegistry::builtins().at(before.method); + bool changed = false; + for (int attempt = 0; attempt < 4 && !changed; ++attempt) { + clickControl("generator/random"); + changed = screen.setup.generator.options != before.options; + } + assert(changed && !screen.validMap && screen.previewPending && !screen.chosenSeed); + assert(screen.setup.generator.method == before.method && + screen.setup.generator.nbWorkers == before.nbWorkers && + screen.setup.generator.wDec == before.wDec && + screen.setup.generator.hDec == before.hDec && screen.setup.capacity == capacity); + assert(validateGenerationRequest(screen.setup.generator, definition).empty()); + preview(); + assert(screen.validMap && screen.quality.measured && + screen.quality.colonies.size() == size_t(capacity)); + paint(); + // The start quality line under the preview: fairness and score, and its (i). + assert(std::any_of(screen.controls->hits.begin(), screen.controls->hits.end(), + [](const auto &h) { return h.id == "quality/info" && h.enabled; })); + capture("random-parameters-640"); + { + std::vector labels; + std::vector colors; + for (size_t i = 0; i < screen.quality.colonies.size(); ++i) { + labels.push_back(screen.colonyLabel(int(i))); + colors.push_back(screen.preview->starts[i].color); + } + StartQualityScreen breakdown(screen.quality, labels, colors); + breakdown.dispatchInit(); + breakdown.dispatchPaint(false); + globalContainer->gfx->printScreen(output + "/start-quality.bmp"); + assert(std::any_of(breakdown.controls->hits.begin(), breakdown.controls->hits.end(), + [](const auto &h) { return h.id == "quality/back"; })); + SDL_Event e = {}; + e.type = SDL_KEYDOWN; + e.key.keysym.sym = SDLK_ESCAPE; + breakdown.dispatchEvents(&e); + assert(breakdown.returnCode == StartQualityScreen::BACK); + } + clickControl("generator/reset"); + GenerationRequest expected; + expected.setMethodDefaults(before.method); + assert(screen.setup.generator.options == expected.options && !screen.quality.measured); + preview(); + assert(screen.validMap); + } screen.setup.presetRules(1); screen.invalidate(); assert(!screen.validMap); @@ -930,6 +1019,40 @@ struct CustomGameSetupHarness const auto second = seedsShown(); for (size_t i = 0; i < first.size(); ++i) assert(first[i] != second[i]); + // Randomize parameters rolls every landscape with its controls drawn at random: the sheet + // still fills with maps (a refused set is redrawn), and each tile's request is one its + // generator accepts, at the lobby's size and colony count. + pick("landscape/randomize"); + assert(picker.busy()); + settle(); + seedsShown(); + { + bool anyDiffer = false; + for (size_t i = 0; i < shown.size(); ++i) { + const GenerationRequest rolled = picker.previewer.request(i); + assert(rolled.method == shown[i].request.method && rolled.nbTeams == 4 && + rolled.wDec == 8 && rolled.hDec == 8); + assert(validateGenerationRequest(rolled, GeneratorRegistry::builtins().at(rolled.method)) + .empty()); + anyDiffer = anyDiffer || rolled.options != shown[i].request.options; + } + assert(anyDiffer); + } + globalContainer->gfx->printScreen(output + "/landscape-picker-randomized.bmp"); + // Using a randomized landscape hands the lobby the parameters it was shown with. + { + pick("landscape/" + std::to_string(other)); + const GenerationRequest rolled = picker.chosenRequest(); + assert(rolled.method == entries[other].first); + screen.applyLandscape(entries[other].first, picker.chosenSeed(), &rolled); + assert(screen.setup.generator.method == entries[other].first && + screen.setup.generator.options == rolled.options && + screen.setup.generator.nbTeams == 4); + // Back to the landscapes' own parameters for the checks below. + pick("landscape/regenerate"); + settle(); + seedsShown(); + } // Return confirms the selection, as does clicking the selected tile. pick("landscape/" + std::to_string(other)); pickerKey(SDLK_RETURN); diff --git a/tools/MenuColonyHarness.cpp b/tools/MenuColonyHarness.cpp index 7e799a688..08c0823b3 100644 --- a/tools/MenuColonyHarness.cpp +++ b/tools/MenuColonyHarness.cpp @@ -451,6 +451,9 @@ std::cout << "global_assets_ms=" << std::chrono::duration(std key.key.keysym.sym=SDLK_ESCAPE; dialog.dispatchEvents(&key); require(dialog.endValue==LoadSaveScreen::CANCEL,"replay dialog cancellation"); Preview custom; custom.selectFirstListItem(); custom.render(); + // The lobby opens on a random map (2026-09-14) and previews it on worker threads; drive + // its timer as the event loop would until the preview's map is loaded. + for(Uint32 start=SDL_GetTicks(); custom.getMapHeader().getNumberOfTeams()==0 && SDL_GetTicks()-start<120000;) { SDL_Delay(10); custom.dispatchTimer(SDL_GetTicks()); } require(custom.getMapHeader().getNumberOfTeams()>0,"map selection loads teams"); key.key.keysym.sym=SDLK_ESCAPE; custom.dispatchEvents(&key); require(custom.result()==CustomGameScreen::CANCEL,"custom game cancellation"); From 459093f3d1d619b12735a8c604d90b70a80cf261 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 11:27:48 -0400 Subject: [PATCH 135/136] City states: the archipelago in the sea outside the circle; picker Reset to defaults - City states: the islets move out of the strait into the sea beyond the design circle, where the torus wraps (FEEDBACK 2026-09-14: "in the area outside the circle ... in like the empty space where the torus wraps", and bigger, since the plot "is taking up too much of the space"). One islet on the wrap point, then `islands` rings of eight, sixteen and so on at equal angles with the map's own four-fold symmetry (a rectangle's two points across the wrap on each axis get the same rings), radius 11, each with its 10x4 plot and a small prize; an islet whose disc and three-tile moat are not all sea is left out with its mirror images. The commons and strait defaults return to 55 and 4 percent now that the strait holds nothing. Revision 9; golden rows regenerated for both platforms, --sweep 0 failing combinations. - Landscape picker: Reset to defaults beside Randomize parameters puts every landscape back on its registered controls at the sheet's size and colony count and rolls the sheet again; harness coverage and docs. - Lobby: switching to the premade library drops random-map candidates still rolling, which would otherwise come back and replace the premade choice now that the lobby opens on a random map. - Harness: the random-parameters check gives the preview its redraw rounds (a refused random set aborted the visual run on ubuntu 24.04), and the SDL flow starts from the premade library as it was written to. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- docs/custom-game-setup/README.md | 6 +- .../map-generators/MAP_GENERATOR_FRAMEWORK.md | 36 +-- src/CustomGameScreen.cpp | 4 + src/LandscapePickerScreen.cpp | 57 ++-- src/LandscapePickerScreen.h | 3 + .../generators/CityStatesGenerator.cpp | 255 +++++++----------- test/CustomGameSetupHarness.cpp | 30 ++- test/map-generator-golden.txt | 32 +-- 8 files changed, 221 insertions(+), 202 deletions(-) diff --git a/docs/custom-game-setup/README.md b/docs/custom-game-setup/README.md index 32b23feeb..ea9462e7d 100644 --- a/docs/custom-game-setup/README.md +++ b/docs/custom-game-setup/README.md @@ -18,9 +18,9 @@ keeps the match summary and launch action available while dense content scrolls. - Landscape, at the top of the map column, opens a sheet showing every landscape as a real map at the current size and colony count. Regenerate all rolls the sheet again with fresh seeds; Randomize parameters rolls it with every landscape's own - controls drawn at random, redrawing any set the world refuses. Use plays exactly - the map shown: its seed, and the parameters it was rolled with, come back to the - lobby. + controls drawn at random, redrawing any set the world refuses; Reset to defaults + puts every landscape back on its registered controls. Use plays exactly the map + shown: its seed, and the parameters it was rolled with, come back to the lobby. - Right under the landscape chooser, Reset to defaults returns width, height, colony count and the landscape's own controls to their defaults, keeping the landscape itself; Random parameters beside it draws every one of the landscape's controls at diff --git a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md index e92c2236a..ac6ad023a 100644 --- a/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md +++ b/docs/map-generators/MAP_GENERATOR_FRAMEWORK.md @@ -566,9 +566,9 @@ fairness falls from about 0.97 to 0.65–0.8, because a home's fields and fertil shape. - **Geometry.** A pure function of the request (`geometryFor`). The commons' radius is - `commons-size` percent of half the shorter side (45 since 2026-09-14, from 55), the strait - `strait-width` percent of the shorter side (11, from 4: room for the islets), and the homes reach - out to the map's half side less a rim of sea, so opposite homes never meet across the wrap. Both coasts are radial shapes (`coast-roughness`); the strait follows the + `commons-size` percent of half the shorter side, the strait `strait-width` percent of the shorter + side, and the homes reach out to the map's half side less a rim of sea, so opposite homes never + meet across the wrap. Both coasts are radial shapes (`coast-roughness`); the strait follows the commons' coast. Every home is the same wedge turned round the centre, so the layout is fair for any colony count. `validateRequest` needs at least 20 tiles of home between the strait and the sea, and at each home's inner coast at least the causeway plus ten tiles of arc beyond its @@ -616,19 +616,23 @@ shape. walls' stone protected, causeways and approaches are cleared, and a cheapest-walk pass keeps a way open from every swarm to its causeway and from every landing to the heart. - **The archipelago** (2026-09-14: "WAY more islands out filling the no mans land", each with "the - 10x4 little sand building plots"). Round islets of radius 9 dot the strait and the channels: - `islands` (0-4, 2) either side of every causeway on a four-colony 256 map's arc, proportionally - more on a longer arc (a two-colony map gets four a side) and never more than fit at three tiles - apart, spread evenly along the strait's midline between the causeway's clear stretch and the - channel, and one in every channel between two homes, half way along it. Their middles are designed in the wedge frame, so every colony has the same islets - at the same distances, and each is drawn as a round disc on the map about its middle, so a bowed - channel does not shear it. Every islet carries the farms' 10x4 building plot (`stampFarmPlot`, a - clearing of grass in a two-vertex ring of sand) at its middle, kept clear of everything, and a - small prize on its grass beyond the ring, stone, fruit and wheat in turn round the wedge. An islet - keeps three tiles of water from every coast, causeway and other islet, so it is only ever reached - by swimming: a forward post, not a stepping stone. An islet whose moat the strait's bulge at a - causeway or a channel's bow would break is left out of every wedge alike. A 128 map's strait is - too narrow for any. + 10x4 little sand building plots", "in the area outside the circle ... where the torus wraps", and + bigger, since the plot "is taking up too much of the space"). The design circle sits on the + shorter side, so the sea outside it - a square's four corners, which the torus joins into one + ocean round the point across the map from the centre, and a rectangle's bands - held nothing. + Round islets of radius 11 now dot it: one on the wrap point, then `islands` (0-4, 2) rings of + eight, sixteen and so on at equal angles round it, at least two radii plus the moat apart, so the + set has the map's own four-fold symmetry with mirrors; on a rectangle the two points across the + wrap on each axis get the same rings. An islet whose disc and three tiles of water round it do not + lie wholly in the sea - too near a home's outer coast, or another islet - is left out with its + mirror images, so the symmetry holds. Every islet carries the farms' 10x4 building plot + (`stampFarmPlot`, a clearing of grass in a two-vertex ring of sand) at its middle, kept clear of + everything, and a small prize on its grass beyond the ring, stone, fruit and wheat in turn. An + islet is only ever reached by swimming: a forward post, not a stepping stone. The archipelago + cannot be turned round the centre for every colony like the rest of the design, so with three, + five or six colonies it lies nearer some homes than others, which the lobby's best-of-five rolls + cover, as on Canals. A 128 map's corners hold only the islet on the wrap point; a 256 map holds + thirteen at the defaults. - **Checked, not assumed.** `validateWorld` rebuilds the design and requires every causeway road and ford walkable and every designed stone tile present (shoulders, walls, ridges and crag), every colony and the heart reachable on foot from colony 0, every islet's plot buildable and no islet reachable on foot, no colony reachable from any beach with the roads shut, no home able to reach the diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 7087ba3a0..ea3e2ff50 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -719,6 +719,10 @@ void CustomGameScreen::setMapMode(bool random) setup.random = random; previewPending = false; validMap = false; + // Candidates still rolling for the random map are dropped with it, or they would come back + // and replace the premade choice (the lobby now opens on a random map, so a player can reach + // the library while its first preview is still rolling). + candidates.reset(); if (random) { setup.setCapacity(setup.generator.nbTeams); diff --git a/src/LandscapePickerScreen.cpp b/src/LandscapePickerScreen.cpp index 3a845907c..ee0060b0e 100644 --- a/src/LandscapePickerScreen.cpp +++ b/src/LandscapePickerScreen.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace { @@ -75,6 +76,28 @@ void LandscapePickerScreen::randomizeParameters() previewer.restart(std::move(requests)); } +void LandscapePickerScreen::resetParameters() +{ + // FEEDBACK 2026-09-14: "we will also need a 'reset to defaults' button beside the 'randomize + // parameters'". Every landscape on its registered controls at the sheet's size, colony count + // and workers; nothing to redraw, since the defaults are known to make maps. + std::vector requests; + for (std::size_t i = 0; i < entries.size(); ++i) + { + const GenerationRequest &shown = entries[i].request; + GenerationRequest request; + request.setMethodDefaults(shown.method); + request.wDec = shown.wDec; + request.hDec = shown.hDec; + request.nbTeams = shown.nbTeams; + request.nbWorkers = shown.nbWorkers; + request.terrainType = shown.terrainType; + redraws[i] = 0; + requests.push_back(request); + } + previewer.restart(std::move(requests)); +} + void LandscapePickerScreen::select(int index) { if (index < 0 || index >= int(entries.size())) @@ -253,9 +276,6 @@ void LandscapePickerScreen::render() ui.text(r.x + 8, r.y + 8 + image + 8 + nameH + 4, note, "little", tileW - 16, true); } ui.endRegion(rows * stride - gap + 4); - ui.button( - "landscape/back", {x, height - 55, 100, 34}, tr("Back"), [this] { endExecute(CANCEL); }, - false, true, true); // A label the standard font cannot fit in its button on a compact screen takes the small one // rather than being cut off. const auto fitting = [](const std::string &label, int width) @@ -263,18 +283,25 @@ void LandscapePickerScreen::render() return Toolkit::getFont("standard")->getStringWidth(label) + 16 <= width ? "standard" : "little"; }; - const int regenerateW = compact ? 130 : 170; - const std::string regenerate = tr("Regenerate all"), randomize = tr("Randomize parameters"); - ui.button( - "landscape/regenerate", {x + 110, height - 55, regenerateW, 34}, regenerate, - [this] { previewer.regenerate(); }, false, true, false, fitting(regenerate, regenerateW)); - // Beside it, the same sheet with every landscape's parameters drawn at random (the size and - // colony count stay), for an even wider spread of maps to pick from. - const int randomW = compact ? 150 : 190; - ui.button( - "landscape/randomize", {x + 110 + regenerateW + 10, height - 55, randomW, 34}, randomize, - [this] { randomizeParameters(); }, false, true, false, fitting(randomize, randomW)); - const int useX = x + 110 + regenerateW + 10 + randomW + 10; + // The row: Back; Regenerate all; beside it Randomize parameters, the same sheet with every + // landscape's parameters drawn at random (the size and colony count stay), for an even wider + // spread of maps to pick from; Reset to defaults, the way back; and Use, taking what is left. + int bx = x; + const auto place = [&](const std::string &id, int width, const std::string &label, + std::function action) + { + ui.button(id, {bx, height - 55, width, 34}, label, std::move(action), false, true, false, + fitting(label, width)); + bx += width + 10; + }; + place("landscape/back", compact ? 80 : 100, tr("Back"), [this] { endExecute(CANCEL); }); + place("landscape/regenerate", compact ? 110 : 170, tr("Regenerate all"), + [this] { previewer.regenerate(); }); + place("landscape/randomize", compact ? 130 : 190, tr("Randomize parameters"), + [this] { randomizeParameters(); }); + place("landscape/reset", compact ? 110 : 150, tr("Reset to defaults"), + [this] { resetParameters(); }); + const int useX = bx; const bool valid = selected >= 0 && selected < int(entries.size()); ui.button( "landscape/use", {useX, height - 55, x + w - useX, 34}, diff --git a/src/LandscapePickerScreen.h b/src/LandscapePickerScreen.h index 26b4dc90d..33f27456a 100644 --- a/src/LandscapePickerScreen.h +++ b/src/LandscapePickerScreen.h @@ -44,6 +44,9 @@ class LandscapePickerScreen : public Glob2Screen /// rolls them all again. A random set the world then refuses is drawn again, up to /// kRandomDraws times per landscape, so the sheet fills with maps that exist. void randomizeParameters(); + /// Puts every landscape back on its registered controls, at the sheet's size and colony + /// count, and rolls them all again (the way back from randomizeParameters). + void resetParameters(); static constexpr int kRandomDraws = 6; bool busy() const { return previewer.busy(); } diff --git a/src/map/generator/generators/CityStatesGenerator.cpp b/src/map/generator/generators/CityStatesGenerator.cpp index 0ec5530fc..56312cb49 100644 --- a/src/map/generator/generators/CityStatesGenerator.cpp +++ b/src/map/generator/generators/CityStatesGenerator.cpp @@ -51,20 +51,23 @@ using namespace MapGeneration; // THE ARCHIPELAGO (FEEDBACK 2026-09-14: "WAY more islands out filling the no mans land where there // is currently only ocean. it should be like a mini archipelago dotting the landscape. Additionally // I want those islands to all contain the 10x4 little sand building plots with no resources in -// them so that users have a spot to place buildings"). The strait and the channels carry islets: -// round, kIsletRadius across, each with an axis-aligned 10x4 clearing of grass in a ring of sand -// at its middle - the same building plot the farms use (Farmland.h) - and a small prize on its -// grass beyond the ring. Their middles are designed in the wedge frame like everything else: -// `islands` of them either side of every causeway in the strait, evenly spread over the arc between -// the causeway's clear stretch and the channel, and one in every channel between two homes, half -// way along it, so every colony has the same islets at the same distances; each islet is then -// drawn as a round disc on the map about its middle, so a bowed channel does not shear it. An -// islet keeps kIsletMoat of water from every coast and every other islet, so it is only ever -// reached by swimming: a forward post for whoever swims first, not a stepping stone. A slot whose -// islet would break that moat in any wedge is left out of every wedge (rasterize), so the colonies -// always match. The strait (11% of the side) and the commons (45% of the half side) are sized so -// the islets fit on a 256 map; a 128 map's strait is too narrow for any, and has none. The former -// random "resource islands" went with this. +// them so that users have a spot to place buildings"; and later that day: they belong "in the area +// outside the circle ... in like the empty space where the torus wraps", and bigger, since the +// plot "is taking up too much of the space"). The design circle sits on the map's shorter side, so +// the sea outside it - a square's four corners, which the torus joins into one ocean round the +// point across the map from the centre, and a rectangle's bands - held nothing. It now holds round +// islets, kIsletRadius in radius, each with an axis-aligned 10x4 clearing of grass in a ring of +// sand at its middle - the same building plot the farms use (Farmland.h) - and a small prize on its +// grass beyond the ring. Their middles lie on rings round each wrap point (rasterize): the point +// itself, then `islands` rings of eight, sixteen and so on at equal angles, so the set has the +// map's own four-fold symmetry with mirrors; an islet whose disc and kIsletMoat of water round it +// do not lie wholly in the sea - too near a home's outer coast, or another islet - is left out +// together with its mirror images, which keeps the symmetry. An islet is only ever reached by +// swimming: a forward post for whoever swims first, not a stepping stone. The archipelago cannot +// be turned round the centre for every colony like the rest of the design, so with three, five or +// six colonies it lies nearer some homes than others; the lobby's choice of the fairest of several +// rolls covers that, as on Canals. A 128 map's corners hold only the islet on the wrap point. The +// former random "resource islands" went with this. // // The Barrens home layout - both flanks of the home under sand - went the same day ("that one // just feels lame when you receive it"), so a home is one of four kinds. @@ -86,10 +89,10 @@ using namespace MapGeneration; // have different shapes at different angles, and the fairness score fell from 0.97 to between // 0.65 and 0.8. Equal homes matter more here than filling the map. // -// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a commons 58 tiles in radius, a strait 28 wide, -// homes 36 tiles deep from the strait out to a 6-tile rim of sea at the wrap, a home lake about 4 -// tiles in radius, and twenty islets of radius 9: two either side of every causeway and one in -// every channel (a two-colony map's longer strait gets four either side). +// THE SIZES AT THE DEFAULTS (256x256, 4 colonies): a commons 70 tiles in radius, a strait 10 wide, +// homes 42 tiles deep from the strait out to a 6-tile rim of sea at the wrap, a home lake about 4 +// tiles in radius, and an archipelago of thirteen islets of radius 11 round the wrap point: one on +// it, a ring of eight round that, and four more out along the axes. namespace { @@ -148,13 +151,13 @@ constexpr double kRoadDepthShare = 0.5; constexpr double kStreetInset = 7.0; // Sand vertices across a road. constexpr int kRoadWidth = 2; -// The islets (see the header): radius and roughness (nearly round, so an axis-aligned 10x4 plot of -// grass, whose far corner is 5.4 tiles out, sits well inside the beach: the grass reaches -// 8.55 - 1 beach = 7.5 at the roughest, and the plot's sand ring may lie on the beach), the water -// kept between an islet and -// any coast or other islet, and how far from the middle the prize stands, clear of the ring. -constexpr double kIsletRadius = 9.0, kIsletRoughness = 0.05; -constexpr int kIsletMoat = 3, kIsletPrizeOut = 6; +// The islets (see the header): radius (11 since the plot "is taking up too much of the space" at 9) +// and roughness (nearly round, so the axis-aligned 10x4 plot of grass, whose far corner is 5.4 tiles +// out, sits well inside the beach with room to build round it: the grass reaches 10.45 - 1 beach = +// 9.5 at the roughest), the water kept between an islet and any coast or other islet, and how far +// from the middle the prize stands, clear of the ring. +constexpr double kIsletRadius = 11.0, kIsletRoughness = 0.05; +constexpr int kIsletMoat = 3, kIsletPrizeOut = 7; // What a home is made of, and what the heart of the commons is. One of each per map. enum HomeKind @@ -249,9 +252,7 @@ Geometry geometryFor(const GenerationRequest &r) g.rim = std::max(kRimMinimum, int(std::lround(kRimShare * g.half))); g.commonsRadius = std::lround(o.commonsSize / 100.0 * g.half); g.outerRadius = g.half - g.rim; - // The strait is a share of the side, but never so wide that the homes lose the depth a home needs: - // on a 128 map the default 11% would leave 14 tiles, so the strait shrinks to 8 there (and the - // islets, which need 27, are left out). + // The strait is a share of the side, but never so wide that the homes lose the depth a home needs. g.strait = std::max(4, int(std::lround(o.straitWidth / 100.0 * side))); g.strait = std::max(4, std::min(g.strait, int(g.outerRadius - g.commonsRadius) - kMinimumDepth)); g.causeway = o.causewayWidth; @@ -409,11 +410,9 @@ struct Features double deltaFordR = 0; RadialShape heart; std::vector commonsSand; - // The archipelago: the strait's islets in the wedge frame, all of one outline; and the radius at - // which the channel islet sits (0 for none). + // The archipelago: every islet's outline, and how many rings of them round each wrap point. RadialShape islet; - std::vector islets; - double channelIsletR = 0; + int isletRings = 0; Features(RadialShape heart, RadialShape islet, double deltaFordR) : deltaFordR(deltaFordR), heart(heart), islet(islet) { @@ -553,62 +552,8 @@ static Features rollFeatures(const GenerationRequest &request, GenerationContext t.y(L.cy + int(std::lround(r0 * std::sin(a)))), shape}); } - // The archipelago (see the header). Strait islets: `islands` of them either side of the causeway, - // spread evenly between the causeway's clear stretch and the channel, each on the strait's - // midline at its own arc position, and dropped - in every wedge alike, since the coasts are - // periodic in the wedge - where the strait there is too narrow for it with its moat on both - // sides. The islet's outline is one shape for all, so every islet is the same islet. - const double reach = f.islet.maximumRadius(); - const double room = 2 * reach + 2 * kIsletMoat; - if (o.islands > 0) - { - const double dMid = g.commonsRadius + g.strait / 2.0; - const double first = c.flatArc + kIsletMoat + reach; - const double last = g.arcHalf(dMid) - kIsletMoat - reach; - if (last >= first) - { - // `islands` is the count either side on a four-colony 256 map's arc; a longer arc - // (fewer colonies, a bigger map) gets proportionally more, a shorter one fewer, and - // never more than fit at the least spacing. - const double spacing = room - kIsletMoat; - const int roomFit = 1 + int((last - first) / spacing); - const int wanted = std::max( - 1, int(std::lround(o.islands * (last - first + spacing) / (2 * spacing)))); - const int fit = std::min(roomFit, wanted); - for (int k = 0; k < fit; ++k) - { - const double s = fit == 1 ? (first + last) / 2 : first + (last - first) * k / (fit - 1); - for (const double sign : {-1.0, 1.0}) - { - const double s0 = sign * s; - const double u0 = 0.5 + s0 / (wedge * dMid); - const double lo = c.coastAt(u0, dMid), hi = c.innerAt(u0, dMid); - if (hi - lo < room) - continue; - // Centred in the strait at its middle, the islet must also clear both coasts - // with its moat where its sides lie: the coasts bulge towards the causeway just - // where the first islet's near side is, and the strait follows the commons' - // coast, so both may lean across the islet's span. - const double r0 = (lo + hi) / 2; - bool clear = true; - for (const double off : {-0.7 * reach, 0.7 * reach}) - { - const double u = 0.5 + (s0 + off) / (wedge * dMid); - const double half = std::sqrt(reach * reach - off * off); - clear = clear && r0 - half >= c.coastAt(u, dMid) + kIsletMoat && - r0 + half <= c.innerAt(u, dMid) - kIsletMoat; - } - if (!clear) - continue; - f.islets.push_back({s0, r0, 1.0, 0.0, f.islet}); - } - } - } - // The channel islet: on the boundary between two homes, half way from the strait to the outer - // sea, when the channel is wide enough and long enough for it. - if (g.teams >= 2 && g.strait >= room && depth >= room) - f.channelIsletR = inner + depth / 2; - } + // The archipelago is laid on the map in rasterize; only how many rings is decided here. + f.isletRings = o.islands; return f; } @@ -812,69 +757,79 @@ static void rasterize(Layout &L, const Geometry &g, const Coasts &coasts, const } } } - // The archipelago (see the header). Every islet's middle in every wedge - a strait islet's at - // the home's angle plus its arc offset, the channel's on the bent boundary clockwise of the home - // (the frame bends a tile's angle by bendAt(d) / d, so the boundary sits that far the other way) - // - and the islet drawn round that middle on the map itself, not in the frame, so it is a round - // islet however the channel bows. A slot whose islet would come within kIsletMoat of a coast or - // a causeway in any wedge (the bow narrows a channel beside its islet) is left out of every - // wedge, so every colony still has the same islets. Three is the least moat that leaves a tile + // The archipelago (see the header): islets on rings round every wrap point - the point across + // the torus from the centre, which is a square map's four corners at once, and on a rectangle + // also the point across the wrap on each axis, where the circle on the shorter side leaves a + // band of sea (on a square those two lie on the circle and seat nothing). Ring k, for k from 1 + // to `islands`, carries 8k candidates at equal angles, a pitch of two radii and the moat out + // from the ring before and far enough out for its neighbours to keep that pitch too. A + // candidate's whole disc with kIsletMoat of water round it must be sea, which keeps it off + // every home's outer coast and clear of the islets already raised, and its images under the + // map's symmetries are raised or dropped with it. Three is the least moat that leaves a tile // of pure water: the beach pass sands the land vertices beside water, and a tile with a sand - // corner is no longer water to walk on. The plots and prizes stand at the middles kept; the - // prize kind goes round by the islet's place among its wedge's. + // corner is no longer water to walk on. The plots and prizes stand at every middle raised; the + // prize kind goes round by the islet's place. { - struct Centre + const double reach = f.islet.maximumRadius(); + const double pitch = 2 * reach + kIsletMoat + 1; + const int moat = kIsletMoat, span = int(std::ceil(reach)) + moat; + const auto fits = [&](int x, int y) { - int x, y, slot; + for (int dy = -span; dy <= span; ++dy) + for (int dx = -span; dx <= span; ++dx) + if (std::hypot(dx, dy) < f.islet.radiusAt(std::atan2(dy, dx)) + moat && + L.region[t.at(x + dx, y + dy)] != Sea) + return false; + return true; }; - std::vector centres; - const int slots = int(f.islets.size()) + 1, channelSlot = slots - 1; - for (int k = 0; k < teams; ++k) + const auto raise = [&](int x, int y) { - const Home &h = L.homes[k]; - for (size_t j = 0; j < f.islets.size(); ++j) - { - const Blob &b = f.islets[j]; - const double a = h.angle + b.s / b.r; - centres.push_back({t.x(L.cx + int(std::lround(b.r * std::cos(a)))), - t.y(L.cy + int(std::lround(b.r * std::sin(a)))), int(j)}); - } - if (f.channelIsletR > 0 && teams >= 2) + for (int dy = -span; dy <= span; ++dy) + for (int dx = -span; dx <= span; ++dx) + if (std::hypot(dx, dy) < f.islet.radiusAt(std::atan2(dy, dx))) + L.region[t.at(x + dx, y + dy)] = Islet; + L.plots.push_back({x, y, int(L.plots.size())}); + }; + // A candidate offset from a focus and its images under the square's symmetries, each once. + const auto orbit = [&](int fx, int fy, double u, double v) + { + const std::pair images[] = {{u, v}, {-u, v}, {u, -v}, {-u, -v}, + {v, u}, {-v, u}, {v, -u}, {-v, -u}}; + std::vector> points; + for (const auto &[a, b] : images) { - const double r = f.channelIsletR; - const double a = L.phase + wedge * (k + 1) + coasts.bendAt(r) / r; - centres.push_back({t.x(L.cx + int(std::lround(r * std::cos(a)))), - t.y(L.cy + int(std::lround(r * std::sin(a)))), channelSlot}); + const std::pair point{t.x(fx + int(std::lround(a))), + t.y(fy + int(std::lround(b)))}; + if (std::find(points.begin(), points.end(), point) == points.end()) + points.push_back(point); } - } - const int moat = kIsletMoat; - const int span = int(std::ceil(f.islet.maximumRadius())) + moat; - std::vector slotOk(slots, 1); - for (const Centre &c : centres) - for (int dy = -span; dy <= span && slotOk[c.slot]; ++dy) - for (int dx = -span; dx <= span && slotOk[c.slot]; ++dx) - if (std::hypot(dx, dy) < f.islet.radiusAt(std::atan2(dy, dx)) + moat && - L.region[t.at(c.x + dx, c.y + dy)] != Sea) - slotOk[c.slot] = 0; - for (const Centre &c : centres) - if (slotOk[c.slot]) - for (int dy = -span; dy <= span; ++dy) - for (int dx = -span; dx <= span; ++dx) - if (std::hypot(dx, dy) < f.islet.radiusAt(std::atan2(dy, dx))) - L.region[t.at(c.x + dx, c.y + dy)] = Islet; + for (const auto &[x, y] : points) + if (!fits(x, y)) + return; + for (const auto &[x, y] : points) + raise(x, y); + }; L.plots.clear(); - int place = 0, wedgeOf = -1; - for (size_t index = 0; index < centres.size(); ++index) + const int cx = int(L.cx), cy = int(L.cy); + std::vector> foci = {{t.x(cx + t.w / 2), t.y(cy + t.h / 2)}}; + if (t.w != t.h) + { + foci.push_back({t.x(cx + t.w / 2), t.y(cy)}); + foci.push_back({t.x(cx), t.y(cy + t.h / 2)}); + } + for (const auto &[fx, fy] : foci) { - const Centre &c = centres[index]; - const int k = int(index) * teams / int(centres.size()); - if (k != wedgeOf) + orbit(fx, fy, 0, 0); + double rho = 0; + for (int k = 1; k <= f.isletRings; ++k) { - wedgeOf = k; - place = 0; + rho = std::max(rho + pitch, pitch / (2 * std::sin(kPi / (8 * k)))); + for (int j = 0; j <= k; ++j) + { + const double a = j * (kPi / 4) / k; + orbit(fx, fy, rho * std::cos(a), rho * std::sin(a)); + } } - if (slotOk[c.slot]) - L.plots.push_back({c.x, c.y, place++}); } } // The commons-side end is the end nearest the centre; the home-side end the farthest. @@ -1386,9 +1341,9 @@ void stockCommons(Map &map, const Layout &L, GenerationContext &context, const C } // Every islet's prize: one small clump on its grass beyond the plot's ring, kIsletPrizeOut tiles -// below the middle (the same side on every islet, so every wedge's islets match), of a kind that -// goes round by the islet's place in its wedge - stone, then a fruit (the three fruits in turn), -// then wheat - each scaled by its amount. Small, since the islet is a place to build, not a mine. +// below the middle (the same side on every islet), of a kind that goes round by the islet's place +// - stone, then a fruit (the three fruits in turn), then wheat - each scaled by its amount. Small, +// since the islet is a place to build, not a mine. void stockIslets(Map &map, const Layout &L, GenerationContext &context, const CityStatesOptions &o, const Farm &plots) { @@ -1657,15 +1612,13 @@ GeneratorDefinition cityStatesDefinition() "city-states", 17, "City states", - 8, + 9, false, - // The commons' radius as a share of half the shorter side (45 since 2026-09-14, from 55), - // the strait's width as a share of the shorter side (11, from 4: room for the islets), - // the causeway's road in tiles; valleys per 128x128 of commons; islets either side of - // every causeway in the strait on a four-colony 256 map, more on a longer arc (the - // channels get one each whenever this is above 0). - {{"commons-size", "Commons size", 30, 65, 5, 45, ControlGroup::Terrain}, - {"strait-width", "Strait width", 3, 14, 1, 11, ControlGroup::Terrain}, + // The commons' radius as a share of half the shorter side, the strait's width as a share of + // the shorter side, the causeway's road in tiles; valleys per 128x128 of commons; rings of + // islets round the map's wrap point (see the header), 0 for the one on the point alone. + {{"commons-size", "Commons size", 30, 65, 5, 55, ControlGroup::Terrain}, + {"strait-width", "Strait width", 3, 14, 1, 4, ControlGroup::Terrain}, {"causeway-width", "Causeway width", 5, 11, 2, 7, ControlGroup::Layout}, // Bays and headlands on every coast and the bow in the channels. {"coast-roughness", "Coast roughness", 0, 100, 5, 50, ControlGroup::Terrain}, diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index 711abc187..e01f0bd16 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -337,6 +337,9 @@ struct CustomGameSetupHarness }, 220}); }; + // The lobby opens on a random map (2026-09-14); the flow was written from the premade + // library, so start there and let the later Random mode click switch as it always did. + click("Premade maps", 90, 100); click("Players tab", 320, 30); click("Inline controller", 170, 143); key(SDLK_DOWN); @@ -732,7 +735,16 @@ struct CustomGameSetupHarness screen.setup.generator.wDec == before.wDec && screen.setup.generator.hDec == before.hDec && screen.setup.capacity == capacity); assert(validateGenerationRequest(screen.setup.generator, definition).empty()); - preview(); + // A random set the world refuses on every seed is redrawn by the preview itself, one + // more round of candidates per draw; give it those rounds, and a fresh click should even + // the last draw fail, so the check does not hang on one unlucky stream. + for (int click = 0; click < 6 && !screen.validMap; ++click) { + if (click > 0) + clickControl("generator/random"); + for (int round = 0; round < CustomGameScreen::kRandomAttempts + 1 && !screen.validMap; + ++round) + preview(); + } assert(screen.validMap && screen.quality.measured && screen.quality.colonies.size() == size_t(capacity)); paint(); @@ -1039,6 +1051,22 @@ struct CustomGameSetupHarness assert(anyDiffer); } globalContainer->gfx->printScreen(output + "/landscape-picker-randomized.bmp"); + // Reset to defaults puts every landscape back on its registered controls at the sheet's + // size and colony count, and rolls the sheet again. + pick("landscape/reset"); + assert(picker.busy()); + settle(); + seedsShown(); + for (size_t i = 0; i < shown.size(); ++i) { + const GenerationRequest rolled = picker.previewer.request(i); + GenerationRequest expected; + expected.setMethodDefaults(shown[i].request.method); + assert(rolled.options == expected.options && rolled.nbTeams == 4 && rolled.wDec == 8 && + rolled.hDec == 8); + } + pick("landscape/randomize"); + settle(); + seedsShown(); // Using a randomized landscape hands the lobby the parameters it was shown with. { pick("landscape/" + std::to_string(other)); diff --git a/test/map-generator-golden.txt b/test/map-generator-golden.txt index db218249d..2846e31d9 100644 --- a/test/map-generator-golden.txt +++ b/test/map-generator-golden.txt @@ -128,14 +128,14 @@ linux-x86_64 16 1 8 8 4 3 ok 8018514250963441197 linux-x86_64 16 1 8 8 8 1 ok 11001992185533046958 linux-x86_64 16 1 9 8 4 1 ok 12695777392728758889 linux-x86_64 16 1 9 9 4 1 ok 15361103174928577925 -linux-x86_64 17 8 7 7 4 1 ok 2202071218543666908 -linux-x86_64 17 8 8 8 2 1 ok 4428719747623839556 -linux-x86_64 17 8 8 8 4 1 ok 2965302471633099017 -linux-x86_64 17 8 8 8 4 2 ok 6136422521341729105 -linux-x86_64 17 8 8 8 4 3 ok 15470226854365351642 -linux-x86_64 17 8 8 8 8 1 ok 3694942298190354927 -linux-x86_64 17 8 9 8 4 1 ok 10736492416954691629 -linux-x86_64 17 8 9 9 4 1 ok 16271468635570942766 +linux-x86_64 17 9 7 7 4 1 ok 17034604800497937195 +linux-x86_64 17 9 8 8 2 1 ok 2291695230428227643 +linux-x86_64 17 9 8 8 4 1 ok 7677815441073057233 +linux-x86_64 17 9 8 8 4 2 ok 9828838947824432417 +linux-x86_64 17 9 8 8 4 3 ok 18357922235407957319 +linux-x86_64 17 9 8 8 8 1 ok 3145723734224646597 +linux-x86_64 17 9 9 8 4 1 ok 8624455383694366557 +linux-x86_64 17 9 9 9 4 1 ok 8229687630579337407 linux-x86_64 18 6 7 7 4 1 ok 13941455206050809724 linux-x86_64 18 6 8 8 2 1 ok 5172958575872259794 linux-x86_64 18 6 8 8 4 1 ok 685234629062878118 @@ -376,14 +376,14 @@ macos-arm64 16 1 8 8 4 3 ok 4396075304858481954 macos-arm64 16 1 8 8 8 1 ok 699982766957804005 macos-arm64 16 1 9 8 4 1 ok 8928204734545682645 macos-arm64 16 1 9 9 4 1 ok 12471630520837885284 -macos-arm64 17 8 7 7 4 1 ok 11333632813302390233 -macos-arm64 17 8 8 8 2 1 ok 12138044986439500568 -macos-arm64 17 8 8 8 4 1 ok 14890751749472676683 -macos-arm64 17 8 8 8 4 2 ok 7964695710715966079 -macos-arm64 17 8 8 8 4 3 ok 6508559991005734301 -macos-arm64 17 8 8 8 8 1 ok 15449496265995446758 -macos-arm64 17 8 9 8 4 1 ok 16043875660102039530 -macos-arm64 17 8 9 9 4 1 ok 975597308833091864 +macos-arm64 17 9 7 7 4 1 ok 8967836227717015855 +macos-arm64 17 9 8 8 2 1 ok 5611667628395191960 +macos-arm64 17 9 8 8 4 1 ok 18355967033291476603 +macos-arm64 17 9 8 8 4 2 ok 4823509492741169680 +macos-arm64 17 9 8 8 4 3 ok 3780786382713317496 +macos-arm64 17 9 8 8 8 1 ok 3762748597928127409 +macos-arm64 17 9 9 8 4 1 ok 11583458392115039545 +macos-arm64 17 9 9 9 4 1 ok 10370544929694661202 macos-arm64 18 6 7 7 4 1 ok 13941455206050809724 macos-arm64 18 6 8 8 2 1 ok 5172958575872259794 macos-arm64 18 6 8 8 4 1 ok 685234629062878118 From 342bbc10e3dbe607cc960dcf4376afb653530b63 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Mon, 14 Sep 2026 11:50:27 -0400 Subject: [PATCH 136/136] CI: show the visual lobby harness's output in the job log The xvfb run wrote only to artifacts/custom-game-ci/ui.log, which the job does not upload, so an assertion there left nothing to read but the exit code. The generation chatter is filtered and the rest goes to both places; the shell's pipefail keeps a failure failing. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GpUW6Ny43aUA1EzThn6cYE --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3cd92811c..b5aebe34b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,7 +117,7 @@ jobs: timeout 600s ./build/src/MapGeneratorGoldenTest glob2-golden-ci --sweep timeout 180s ./build/src/CustomGameSetupHarness mkdir -p artifacts/custom-game-ci - timeout 180s xvfb-run -a -s '-screen 0 1024x768x24' ./build/src/CustomGameSetupHarness artifacts/custom-game-ci > artifacts/custom-game-ci/ui.log 2>&1 + timeout 180s xvfb-run -a -s '-screen 0 1024x768x24' ./build/src/CustomGameSetupHarness artifacts/custom-game-ci 2>&1 | grep -v '^Map generation\|^Landscape preview\|^Settings::load' | tee artifacts/custom-game-ci/ui.log timeout 60s xvfb-run -a ./build/src/CustomGameSetupHarness preferences-write timeout 60s xvfb-run -a ./build/src/CustomGameSetupHarness preferences-read cat artifacts/custom-game-ci/ui.log

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 UFnqx!7Ta{%_H*4v{z$AChbVRSFxftOE!mxtq- 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

_BK#G8mCaufIAXWBroC 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⪻Q(F&`eQW_El2`Y{ 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 literal 0 HcmV?d00001 diff --git a/src/CustomGameScreen.cpp b/src/CustomGameScreen.cpp index 7ef14b0a3..c3d1b1ea0 100644 --- a/src/CustomGameScreen.cpp +++ b/src/CustomGameScreen.cpp @@ -1,13 +1,16 @@ +#include "GenerationContext.h" +#include "GeneratorRegistry.h" // SPDX-License-Identifier: GPL-3.0-or-later #include "CustomGameScreen.h" #include "CustomGamePreferences.h" #include "AINames.h" +#include "CustomGameScreen.h" #include "GUIMapPreview.h" #include "Game.h" +#include "GenerationService.h" #include "GlobalContainer.h" #include "LobbyControls.h" #include "LobbyMapCatalog.h" -#include "MapGenerator.h" #include "Player.h" #include #include @@ -25,9 +28,8 @@ const Uint32 A = ALIGN_SCREEN_CENTERED; const std::vector methods = [] { std::vector result; - for (int m = MapGenerationDescriptor::eSWAMP; m <= MapGenerationDescriptor::eOLDISLANDS; ++m) - result.emplace_back( - MapGenerationDescriptor::methodName(static_cast(m))); + for (int m : GeneratorRegistry::builtins().methods(false)) + result.emplace_back(GenerationRequest::methodName(m)); return result; }(); std::vector localized(std::vector v) @@ -420,21 +422,28 @@ bool CustomGameScreen::generateMap() throw std::runtime_error("snapshot directory"); candidate = (directory / "preview.map").string(); std::unique_ptr game; - MapGenerator generator; + GenerationService generator; + const auto rootSeed = GenerationContext::randomSeed(); + GenerationResult generationResult; setup.generator.nbTeams = setup.capacity; // Some rolls cannot fit every starting colony. Retry those rolls before // reporting failure; only the successful world becomes the preview. for (int attempt = 0; attempt < 5; ++attempt) { game = std::make_unique(nullptr); - if (generator.generateMap(*game, setup.generator) && - game->teamsCount() == setup.capacity) + auto request = setup.generator; + request.seed = + GenerationContext::deriveSeed(rootSeed, "attempt/" + std::to_string(attempt)); + generationResult = generator.generate(*game, request); + std::cout << "Map generation: " << generationResult.diagnostic() << std::endl; + if (generationResult && game->teamsCount() == setup.capacity) break; game.reset(); } if (!game) - throw std::runtime_error("generation"); + throw std::runtime_error(generationResult.diagnostic()); GameHeader initial; + initial.setRandomSeed(generationResult.seed); setup.writeHeader(initial, username); game->setGameHeader(initial); { @@ -454,8 +463,9 @@ bool CustomGameScreen::generateMap() message = tr("Preview ready. Start plays this exact map."); return true; } - catch (const std::exception &) + catch (const std::exception &error) { + std::cerr << "Map preview: " << error.what() << std::endl; if (!candidate.empty()) std::filesystem::remove_all(std::filesystem::path(candidate).parent_path()); validMap = false; @@ -573,7 +583,8 @@ void CustomGameScreen::renderLobby() speed.gameSpeed = setup.speed; std::vector titles = localized({"Map", "Players & Teams", "Game Rules"}); std::vector details = { - setup.random ? tr(methods[setup.generator.method - 1]) : mapHeader.getMapName(), + setup.random ? tr(GenerationRequest::methodName(setup.generator.method)) + : mapHeader.getMapName(), std::to_string(setup.activeColonies()) + " " + tr("colonies") + " / " + tr(setup.format), tr(setup.ruleset)}; for (int i = 0; i < 3; ++i) @@ -798,19 +809,16 @@ void CustomGameScreen::renderRules(int x, int y, int w, int h) else { if (setup.random) - ui.stepper( - "rule/workers", {fieldX, yy + 5, 130, 29}, setup.generator.nbWorkers, - MapGenerationDescriptor::control(setup.generator.method, "Starting workers") - .minimum, - MapGenerationDescriptor::control(setup.generator.method, "Starting workers") - .maximum, - [this](int v) - { - setup.generator.nbWorkers = v; - ++setup.mapRevision; - setup.ruleset = "Custom"; - invalidate(); - }); + ui.stepper("rule/workers", {fieldX, yy + 5, 130, 29}, setup.generator.nbWorkers, + GenerationRequest::control(setup.generator.method, "workers").minimum, + GenerationRequest::control(setup.generator.method, "workers").maximum, + [this](int v) + { + setup.generator.nbWorkers = v; + ++setup.mapRevision; + setup.ruleset = "Custom"; + invalidate(); + }); else ui.text(fieldX + 9, yy + 12, tr("Map-defined starting units"), "standard", fieldW); help = tr(setup.random ? "More workers jump-start colony growth. Changes the " @@ -886,40 +894,40 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) }; ui.text(x + 4, yy, tr("Landscape"), "little", leftW - 20, true); yy += 18; - ui.dropdown( - "generator/landscape", {x, yy, leftW - 16, 30}, localized(methods), g.method - 1, - [this, changed](int value) - { - setup.generatorHistory.select( - setup.generator, static_cast(value + 1)); - changed(); - }); + ui.dropdown("generator/landscape", {x, yy, leftW - 16, 30}, localized(methods), + GeneratorRegistry::builtins().selectionIndex(g.method, false), + [this, changed](int value) + { + setup.generatorHistory.select( + setup.generator, + GeneratorRegistry::builtins().methods(false).at(value)); + changed(); + }); yy += 40; - auto discrete = [&](const MapGenerationDescriptor::Control &c, const std::string &id) + auto discrete = [&](const GenerationRequest::Control &c, const std::string &id) { ui.text(x + 4, yy + 7, tr(c.label), "little", 110, true); std::vector options; - for (int v = c.minimum; v <= c.maximum; v += c.step) - options.push_back(std::to_string(c.powerOfTwo ? (1 << v) : v)); - ui.dropdown(id, {x + 112, yy, leftW - 128, 28}, options, - (c.get(g) - c.minimum) / c.step, + for (int v : c.values()) + options.push_back(std::to_string(c.displayValue(v))); + ui.dropdown(id, {x + 112, yy, leftW - 128, 28}, options, c.indexOf(c.get(g)), [this, c, changed](int i) { - c.set(setup.generator, c.minimum + i * c.step); - if (c.field == &MapGenerationDescriptor::nbTeams) + c.set(setup.generator, c.valueAt(i)); + if (c.id == "teams") setup.setCapacity(setup.generator.nbTeams); changed(); }); yy += 36; }; - for (const auto &c : MapGenerationDescriptor::sharedControls()) - if (c.field != &MapGenerationDescriptor::nbWorkers) + for (const auto &c : GenerationRequest::sharedControls()) + if (c.id != "workers") { - if (c.field == &MapGenerationDescriptor::nbTeams) + if (c.id == "teams") g.nbTeams = setup.capacity; - const char *id = c.field == &MapGenerationDescriptor::wDec ? "generator/width" - : c.field == &MapGenerationDescriptor::hDec ? "generator/height" - : "generator/colonies"; + const char *id = c.id == "width" ? "generator/width" + : c.id == "height" ? "generator/height" + : "generator/colonies"; discrete(c, id); } @@ -936,15 +944,15 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) if (!expanded[section]) continue; bool any = false; - for (const auto &c : MapGenerationDescriptor::controls(g.method)) + for (const auto &c : GenerationRequest::controls(g.method)) { if (static_cast(c.group) != section) continue; any = true; - std::string id = "generator/" + std::string(c.label); - if (c.powerOfTwo) + std::string id = "generator/" + c.id; + if (c.powerOfTwo || !c.allowedValues.empty()) { - discrete(c, "generator/repeat"); + discrete(c, id); continue; } ui.text(x + 5, yy, tr(c.label), "little", leftW - 55, true); @@ -977,10 +985,11 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h) ui.endRegion(yy - startY + 8); } ui.text(rightX, top, - setup.random ? tr(methods[setup.generator.method - 1]) : mapHeader.getMapName(), + setup.random ? tr(GenerationRequest::methodName(setup.generator.method)) + : mapHeader.getMapName(), w < 800 ? "standard" : "menu", rightW); - std::string dimensions = validMap ? std::to_string(preview->getLastWidth()) + " x " + - std::to_string(preview->getLastHeight()) + std::string dimensions = validMap ? std::to_string(preview->getLastWidth()) + " x " + + std::to_string(preview->getLastHeight()) : setup.random ? std::to_string(1 << setup.generator.wDec) + " x " + std::to_string(1 << setup.generator.hDec) : ""; diff --git a/src/CustomGameSetup.h b/src/CustomGameSetup.h index 1c13c4109..900618999 100644 --- a/src/CustomGameSetup.h +++ b/src/CustomGameSetup.h @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "GameHeader.h" -#include "MapGenerationDescriptor.h" +#include "GenerationRequest.h" +#include "GenerationValidation.h" +#include "GeneratorRegistry.h" #include #include #include @@ -40,9 +42,9 @@ struct CustomGameSetup case 3: return speed != 0; case 4: - return random && generator.nbWorkers != MapGenerationDescriptor::control( - generator.method, "Starting workers") - .defaultValue; + return random && + generator.nbWorkers != + GenerationRequest::control(generator.method, "workers").defaultValue; default: return false; } @@ -54,8 +56,8 @@ struct CustomGameSetup int alliance = 0; }; std::array colonies; - MapGenerationDescriptor generator; - MapGenerationHistory generatorHistory; + GenerationRequest generator; + GenerationHistory generatorHistory; int capacity; bool random = false, prestige = true, revealed = false, locked = true; int speed = 0; @@ -64,7 +66,7 @@ struct CustomGameSetup unsigned mapRevision = 0; CustomGameSetup() { - generator.setMethodDefaults(MapGenerationDescriptor::eRIVER); + generator.setMethodDefaults(GenerationRequest::eRIVER); capacity = generator.nbTeams; for (int i = 0; i < Team::MAX_COUNT; ++i) colonies[i].alliance = i; @@ -143,8 +145,7 @@ struct CustomGameSetup revealed = preset == 2; locked = true; speed = preset == 1 ? 3 : 0; - const auto &workerControl = - MapGenerationDescriptor::control(generator.method, "Starting workers"); + const auto &workerControl = GenerationRequest::control(generator.method, "workers"); int workers = preset == 1 ? workerControl.maximum : workerControl.defaultValue; if (generator.nbWorkers != workers) { @@ -160,8 +161,17 @@ struct CustomGameSetup { if (capacity < 1 || capacity > Team::MAX_COUNT) return "Invalid colony count."; - if (random && !generator.hasTerrainWeight()) - return "Give at least one terrain type a nonzero weight."; + if (random) + { + const auto *definition = GeneratorRegistry::builtins().find(generator.method); + if (!definition) + return "Unknown generator"; + auto request = generator; + request.nbTeams = capacity; + const auto error = validateGenerationRequest(request, *definition); + if (!error.empty()) + return error; + } if (controllerCount() > Team::MAX_COUNT) return "Shared control needs a free controller slot (maximum 12)."; if (activeColonies() < 1) diff --git a/src/EditorMainMenu.cpp b/src/EditorMainMenu.cpp index 22d591942..0bc28df1e 100644 --- a/src/EditorMainMenu.cpp +++ b/src/EditorMainMenu.cpp @@ -2,127 +2,134 @@ // Copyright (C) 2008 Bradley Arsenault // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +#include "EditorMainMenu.h" #include "CampaignEditor.h" #include "CampaignSelectorScreen.h" #include "ChooseMapScreen.h" -#include "EditorMainMenu.h" #include "FrontendTheme.h" +#include "GenerationContext.h" +#include "GenerationService.h" #include "GlobalContainer.h" -#include -#include #include "MapEdit.h" -#include "MapGenerator.h" #include "NewMapScreen.h" +#include "Utilities.h" +#include +#include +#include #include #include -#include "Utilities.h" - - using namespace GAGGUI; EditorMainMenu::EditorMainMenu() { - addWidget(new TextButton(0, 70, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[new map]"), NEWMAP, 13)); - addWidget(new TextButton(0, 130, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[load map]"), LOADMAP)); - addWidget(new TextButton(0, 190, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[new campaign]"), NEWCAMPAIGN)); - addWidget(new TextButton(0, 250, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[load campaign]"), LOADCAMPAIGN)); - addWidget(new TextButton(0, 415, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[goto main menu]"), CANCEL, 27)); - addWidget(new Text(0, 18, ALIGN_FILL, ALIGN_SCREEN_CENTERED, "menu", Toolkit::getStringTable()->getString("[editor]"))); + addWidget(new TextButton(0, 70, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", + Toolkit::getStringTable()->getString("[new map]"), NEWMAP, 13)); + addWidget(new TextButton(0, 130, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", + Toolkit::getStringTable()->getString("[load map]"), LOADMAP)); + addWidget(new TextButton(0, 190, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", + Toolkit::getStringTable()->getString("[new campaign]"), NEWCAMPAIGN)); + addWidget(new TextButton(0, 250, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", + Toolkit::getStringTable()->getString("[load campaign]"), + LOADCAMPAIGN)); + addWidget(new TextButton(0, 415, 300, 40, ALIGN_CENTERED, ALIGN_SCREEN_CENTERED, "menu", + Toolkit::getStringTable()->getString("[goto main menu]"), CANCEL, 27)); + addWidget(new Text(0, 18, ALIGN_FILL, ALIGN_SCREEN_CENTERED, "menu", + Toolkit::getStringTable()->getString("[editor]"))); } void EditorMainMenu::onAction(Widget *source, Action action, int par1, int par2) { - if ((action==BUTTON_RELEASED) || (action==BUTTON_SHORTCUT)) + if ((action == BUTTON_RELEASED) || (action == BUTTON_SHORTCUT)) { - if (par1==NEWMAP) + if (par1 == NEWMAP) { - bool retryNewMapScreen=true; + bool retryNewMapScreen = true; while (retryNewMapScreen) { NewMapScreen newMapScreen; int rc_nms = newMapScreen.execute(globalContainer->gfx, 40); - if (rc_nms==NewMapScreen::OK) + if (rc_nms == NewMapScreen::OK) { MapEdit mapEdit; - MapGenerator generator; - setRandomSyncRandSeed(); - if (generator.generateMap(mapEdit.game, newMapScreen.descriptor)) + GenerationService generator; + newMapScreen.descriptor.seed = GenerationContext::randomSeed(); + auto result = generator.generate(mapEdit.game, newMapScreen.descriptor); + if (result) { mapEdit.mapHasBeenModified(); // make all map as modified by default mapEdit.regenerateGameHeader(); - if (mapEdit.run()==-1) + if (mapEdit.run() == -1) endExecute(-1); - retryNewMapScreen=false; + retryNewMapScreen = false; } else { - //TODO: popup a widow to explain that the generateMap() has failed. - retryNewMapScreen=true; + GAGGUI::MessageBox(globalContainer->gfx, "standard", GAGGUI::MB_ONEBUTTON, + result.diagnostic(), + Toolkit::getStringTable()->getString("[ok]")); + retryNewMapScreen = true; } } - else if(rc_nms == -1) + else if (rc_nms == -1) { endExecute(-1); - retryNewMapScreen=false; + retryNewMapScreen = false; } else { - retryNewMapScreen=false; + retryNewMapScreen = false; } } } - else if (par1==LOADMAP) + else if (par1 == LOADMAP) { ChooseMapScreen chooseMapScreen("maps", "map", false, "games", "game", false); - int rc=chooseMapScreen.execute(globalContainer->gfx, 40); - if (rc==ChooseMapScreen::OK) + int rc = chooseMapScreen.execute(globalContainer->gfx, 40); + if (rc == ChooseMapScreen::OK) { MapEdit mapEdit; std::string filename = chooseMapScreen.getMapHeader().getFileName(); mapEdit.load(filename.c_str()); - if (mapEdit.run()==-1) + if (mapEdit.run() == -1) endExecute(-1); } - else if (rc==-1) + else if (rc == -1) endExecute(-1); } - else if (par1==NEWCAMPAIGN) + else if (par1 == NEWCAMPAIGN) { FrontendScope editor(false); CampaignEditor ce(""); - int rc=ce.execute(globalContainer->gfx, 40); - if(rc == -1) + int rc = ce.execute(globalContainer->gfx, 40); + if (rc == -1) endExecute(-1); - } - else if (par1==LOADCAMPAIGN) + else if (par1 == LOADCAMPAIGN) { CampaignSelectorScreen css; - int rc_css=css.execute(globalContainer->gfx, 40); - if(rc_css==CampaignSelectorScreen::OK) + int rc_css = css.execute(globalContainer->gfx, 40); + if (rc_css == CampaignSelectorScreen::OK) { FrontendScope editor(false); CampaignEditor ce(css.getCampaignName()); - int rc_ce=ce.execute(globalContainer->gfx, 40); - if(rc_ce == -1) + int rc_ce = ce.execute(globalContainer->gfx, 40); + if (rc_ce == -1) { endExecute(-1); } } - else if(rc_css==CampaignSelectorScreen::CANCEL) + else if (rc_css == CampaignSelectorScreen::CANCEL) { } - else if(rc_css == -1) + else if (rc_css == -1) { endExecute(-1); } - } - else if(par1 == CANCEL) - { - endExecute(CANCEL); - } + } + else if (par1 == CANCEL) + { + endExecute(CANCEL); + } } } - - diff --git a/src/GUIMapPreview.h b/src/GUIMapPreview.h index a3c39189e..e99947f03 100644 --- a/src/GUIMapPreview.h +++ b/src/GUIMapPreview.h @@ -4,7 +4,6 @@ #pragma once #include -#include "MapGenerationDescriptor.h" #include #include "MapThumbnail.h" diff --git a/src/Game.h b/src/Game.h index 8eced3411..c753ecdc1 100644 --- a/src/Game.h +++ b/src/Game.h @@ -25,7 +25,6 @@ namespace GAGCore class OutputStream; } using namespace GAGCore; -class MapGenerationDescriptor; class GameGUI; class MapEdit; @@ -413,11 +412,6 @@ class Game Team *getTeamWithMostPrestige(void); bool isPrestigeWinCondition(void); -public: - bool oldMakeIslandsMap(MapGenerationDescriptor &descriptor); - bool makeRandomMap(MapGenerationDescriptor &descriptor); - bool generateMap(MapGenerationDescriptor &descriptor); - protected: int ticksGameSum[TICK_PROFILE_BUF_LEN]; }; diff --git a/src/Glob2.cpp b/src/Glob2.cpp index de9f0bc2a..05c64297f 100644 --- a/src/Glob2.cpp +++ b/src/Glob2.cpp @@ -1,3 +1,5 @@ +#include "GeneratorRegistry.h" +#include "GenerationContext.h" // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière @@ -15,7 +17,7 @@ #include "Game.h" #include "LANMenuScreen.h" #include "MainMenuScreen.h" -#include "MapGenerator.h" +#include "GenerationService.h" #include "SettingsScreen.h" #include #include "Utilities.h" @@ -166,10 +168,11 @@ int Glob2::runTestMapGeneration() setSyncRandSeed(t); while(true) { - MapGenerationDescriptor descriptor; + GenerationRequest descriptor; - using D = MapGenerationDescriptor; - auto method = static_cast(D::eSWAMP + syncRand() % (D::METHOD_COUNT - 1)); + using D = GenerationRequest; + const auto methods=GeneratorRegistry::builtins().methods(false); + auto method=methods[syncRand()%methods.size()]; descriptor.setMethodDefaults(method); auto controls = D::sharedControls(); const auto& specific = D::controls(method); @@ -183,9 +186,11 @@ int Glob2::runTestMapGeneration() continue; std::cout<<"Generating Map"< -#include -#include -#include +#include "NewMapScreen.h" #include #include #include +#include +#include +#include +#include using namespace GAGCore; using namespace GAGGUI; @@ -22,13 +25,12 @@ std::string tr(const char *label) constexpr Uint32 A = ALIGN_SCREEN_CENTERED; } // namespace -NewMapScreen::NewMapScreen() +NewMapScreen::NewMapScreen(const GeneratorRegistry ®istry) : registry(registry) { methods = new List(20, 100, 280, 300, A, A, "menu"); - for (int m = 0; m <= MapGenerationDescriptor::eOLDISLANDS; ++m) - methods->addText(tr( - MapGenerationDescriptor::methodName(static_cast(m)))); - methods->setSelectionIndex(descriptor.method); + for (int m : registry.methods()) + methods->addText(tr(registry.at(m).nameKey)); + methods->setSelectionIndex(registry.selectionIndex(descriptor.method)); addWidget(methods); terrains = new List(340, 100, 280, 300, A, A, "menu"); for (const char *name : {"water", "sand", "grass"}) @@ -36,36 +38,29 @@ NewMapScreen::NewMapScreen() terrains->setSelectionIndex(descriptor.terrainType); addWidget(terrains); - auto addControl = [&](const MapGenerationDescriptor::Control &c, int method, int x, int y) + auto addControl = [&](const GenerationRequest::Control &c, int method, int x, int y) { auto *number = new Number(x, y, 114, 18, A, A, 18, "menu"); - for (int value = c.minimum; value <= c.maximum; value += c.step) - number->add(c.powerOfTwo ? (1 << value) : value); + for (int value : c.values()) + number->add(c.displayValue(value)); addWidget(number); auto *label = new Text(x + 120, y, A, A, "standard", tr(c.label)); addWidget(label); controlWidgets.push_back({c, method, number, label}); }; - for (const auto &c : MapGenerationDescriptor::sharedControls()) + for (const auto &c : GenerationRequest::sharedControls()) { - bool size = - c.field == &MapGenerationDescriptor::wDec || c.field == &MapGenerationDescriptor::hDec; - int y = c.field == &MapGenerationDescriptor::wDec ? 50 - : c.field == &MapGenerationDescriptor::hDec ? 75 - : c.field == &MapGenerationDescriptor::nbTeams ? 100 - : 125; + bool size = c.id == "width" || c.id == "height"; + int y = c.id == "width" ? 50 : c.id == "height" ? 75 : c.id == "teams" ? 100 : 125; addControl(c, -1, size ? 20 : 310, y); } - for (int m = 0; m <= MapGenerationDescriptor::eOLDISLANDS; ++m) + for (int m : registry.methods()) { int y = 160; - for (const auto &c : - MapGenerationDescriptor::controls(static_cast(m))) + for (const auto &c : registry.at(m).controls) { - bool repeat = c.group == MapGenerationDescriptor::ControlGroup::Layout; - addControl(c, m, 310, repeat ? 75 : y); - if (!repeat) - y += 20; + addControl(c, m, 310, y); + y += 20; } } updateControls(); @@ -76,17 +71,16 @@ NewMapScreen::NewMapScreen() void NewMapScreen::updateControls() { - terrains->visible = descriptor.method == MapGenerationDescriptor::eUNIFORM; + terrains->visible = descriptor.method == GenerationRequest::eUNIFORM; for (auto &widget : controlWidgets) { const auto &c = widget.definition; - bool size = - c.field == &MapGenerationDescriptor::wDec || c.field == &MapGenerationDescriptor::hDec; + bool size = c.id == "width" || c.id == "height"; bool visible = widget.method == descriptor.method || (widget.method == -1 && (size || !terrains->visible)); widget.number->visible = widget.label->visible = visible; if (visible) - widget.number->setNth((c.get(descriptor) - c.minimum) / c.step); + widget.number->setNth(c.indexOf(c.get(descriptor))); } } @@ -94,23 +88,24 @@ void NewMapScreen::onAction(Widget *source, Action action, int par1, int par2) { if (action == BUTTON_RELEASED || action == BUTTON_SHORTCUT) { - if (par1 == OK && !descriptor.hasTerrainWeight()) - MessageBox(globalContainer->gfx, "standard", MB_ONEBUTTON, - tr("Give at least one terrain type a nonzero weight."), tr("ok")); + const auto error = + par1 == OK ? validateGenerationRequest(descriptor, registry.at(descriptor.method)) + : std::string{}; + if (!error.empty()) + MessageBox(globalContainer->gfx, "standard", MB_ONEBUTTON, error, tr("ok")); else if (par1 == OK || par1 == CANCEL) endExecute(par1); } - else if (action==NUMBER_ELEMENT_SELECTED) + else if (action == NUMBER_ELEMENT_SELECTED) { for (const auto &widget : controlWidgets) if (source == widget.number && widget.number->visible) widget.definition.set(descriptor, - widget.definition.minimum + - widget.number->getNth() * widget.definition.step); + widget.definition.valueAt(widget.number->getNth())); } - else if (action==LIST_ELEMENT_SELECTED) + else if (action == LIST_ELEMENT_SELECTED) { - if (source==terrains) + if (source == terrains) { if (auto selection = terrains->selection()) descriptor.terrainType = static_cast(*selection); @@ -119,8 +114,7 @@ void NewMapScreen::onAction(Widget *source, Action action, int par1, int par2) { if (auto selection = methods->selection()) { - history.select(descriptor, - static_cast(*selection)); + history.select(descriptor, registry.methods().at(*selection), registry); updateControls(); } } diff --git a/src/NewMapScreen.h b/src/NewMapScreen.h index 211e9086d..cc76b8436 100644 --- a/src/NewMapScreen.h +++ b/src/NewMapScreen.h @@ -4,48 +4,50 @@ #pragma once #include "Glob2Screen.h" -#include "MapGenerationDescriptor.h" +#include "GenerationRequest.h" +#include "GeneratorRegistry.h" namespace GAGGUI { - class Number; - class Text; - class Ratio; - class List; -} +class Number; +class Text; +class Ratio; +class List; +} // namespace GAGGUI //! This screen allows to choose the size of the map and the default background class NewMapScreen : public Glob2Screen { -public: + public: enum { OK = 1, CANCEL = 2 }; -public: - MapGenerationDescriptor descriptor; -private: - friend class MapGeneratorDefaultsTest; - struct ControlWidget - { - MapGenerationDescriptor::Control definition; - int method; // -1 for shared controls - Number *number; - Text *label; - }; - std::vector controlWidgets; - MapGenerationHistory history; - List *methods, *terrains; - void updateControls(); + public: + GenerationRequest descriptor; -public: + private: + friend class MapGeneratorDefaultsTest; + struct ControlWidget + { + GenerationRequest::Control definition; + int method; // -1 for shared controls + Number *number; + Text *label; + }; + std::vector controlWidgets; + GenerationHistory history; + const GeneratorRegistry ®istry; + List *methods, *terrains; + void updateControls(); + + public: //! Constructor - NewMapScreen(); + explicit NewMapScreen(const GeneratorRegistry ®istry = GeneratorRegistry::builtins()); //! Destructor - virtual ~NewMapScreen() { }; + virtual ~NewMapScreen() {}; //! Action handler void onAction(Widget *source, Action action, int par1, int par2); }; - diff --git a/src/PerlinNoise.cpp b/src/PerlinNoise.cpp deleted file mode 100644 index 8d6fb30fc..000000000 --- a/src/PerlinNoise.cpp +++ /dev/null @@ -1,287 +0,0 @@ -// perlinNoise.cpp -// Class to implement coherent noise over 1, 2, or 3 dimensions. -// Implementation based on the Perlin noise function. Thanks to -// Ken Perlin of NYU for publishing his algorithm online. -// Copyright (c) 2001, Matt Zucker -// You may use this source code as you wish, but it is provided -// with no warranty. Please email me at mazucker@vassar.edu if -// you find it useful. - -#include -#include -#include -#include "PerlinNoise.h" - -// TODO: change these preprocessor macros into inline functions - -// S curve is (3x^2 - 2x^3) because it's quick to calculate -// though -cos(x * PI) * 0.5 + 0.5 would work too - -#define easeCurve(t)( t * t * (3.0 - 2.0 * t) ) -#define linearInterp(t, a, b)( a + t * (b - a) ) -#define dot2(rx, ry, q)( rx * q[0] + ry * q[1] ) -#define dot3(rx, ry, rz, q) ( rx * q[0] + ry * q[1] + rz * q[2] ) - - -#define setupValues(t, axis, g0, g1, d0, d1, pos) \ - t = pos[axis] + NOISE_LARGE_PWR2; \ - g0 = ((int)t) & NOISE_MOD_MASK; \ - g1 = (g0 + 1) & NOISE_MOD_MASK; \ - d0 = t - (int)t; \ - d1 = d0 - 1.0; \ - - -// Constructor -PerlinNoise::PerlinNoise() { - -} -// Destructor -PerlinNoise::~PerlinNoise() { -} - -// initialize static variables - -unsigned PerlinNoise::initialized = 0; -unsigned PerlinNoise::permutationTable[ NOISE_WRAP_INDEX*2 + 2 ] = { 0 }; -float PerlinNoise::gradientTable1d[ NOISE_WRAP_INDEX*2 + 2 ] = { 0 }; -float PerlinNoise::gradientTable2d[ NOISE_WRAP_INDEX*2 + 2 ][ 2 ] = { { 0 } }; -float PerlinNoise::gradientTable3d[ NOISE_WRAP_INDEX*2 + 2 ][ 3 ] = { { 0 } }; - -// return a random float in [-1,1] - -inline float PerlinNoise::randNoiseFloat() { - return ( float ) ( ( rand() % ( NOISE_WRAP_INDEX + NOISE_WRAP_INDEX ) ) - - NOISE_WRAP_INDEX ) / NOISE_WRAP_INDEX; -}; - -// convert a 2D vector into unit length - -void PerlinNoise::normalize2d( float vector[ 2 ] ) { - float length = sqrt( ( vector[ 0 ] * vector[ 0 ] ) + ( vector[ 1 ] * vector[ 1 ] ) ); - vector[ 0 ] /= length; - vector[ 1 ] /= length; -} - -// convert a 3D vector into unit length - -void PerlinNoise::normalize3d( float vector[ 3 ] ) { - float length = sqrt( ( vector[ 0 ] * vector[ 0 ] ) + - ( vector[ 1 ] * vector[ 1 ] ) + - ( vector[ 2 ] * vector[ 2 ] ) ); - vector[ 0 ] /= length; - vector[ 1 ] /= length; - vector[ 2 ] /= length; -} - -// -// Mnemonics used in the following 3 functions: -// L = left(-X direction) -// R = right (+X direction) -// D = down (-Y direction) -// U = up (+Y direction) -// B = backwards(-Z direction) -// F = forwards (+Z direction) -// -// Not that it matters to the math, but a reader might want to know. -// -// noise1d - create 1-dimensional coherent noise -// if you want to learn about how noise works, look at this -// and then look at noise2d. - -float PerlinNoise::Noise1d( float pos[ 1 ] ) { - int gridPointL, gridPointR; - float distFromL, distFromR, sX, t, u, v; - - if ( ! initialized ) { reseed(); } - - // find out neighboring grid points to pos and signed distances from pos to them. - setupValues( t, 0, gridPointL, gridPointR, distFromL, distFromR, pos ); - - sX = easeCurve( distFromL ); - - // u, v, are the vectors from the grid pts. times the random gradients for the grid points - // they are actually dot products, but this looks like scalar multiplication - u = distFromL * gradientTable1d[ permutationTable[ gridPointL ] ]; - v = distFromR * gradientTable1d[ permutationTable[ gridPointR ] ]; - - // return the linear interpretation between u and v (0 = u, 1 = v) at sX. - return linearInterp( sX, u, v ); -} - -// create 2d coherent noise - -float PerlinNoise::Noise2d( float pos[ 2 ] ) { - int gridPointL, gridPointR, gridPointD, gridPointU; - int indexLD, indexRD, indexLU, indexRU; - float distFromL, distFromR, distFromD, distFromU; - float *q, sX, sY, a, b, t, u, v; - int indexL, indexR; - - if ( ! initialized ) { reseed(); } - - // find out neighboring grid points to pos and signed distances from pos to them. - setupValues( t, 0, gridPointL, gridPointR, distFromL, distFromR, pos ); - setupValues( t, 1, gridPointD, gridPointU, distFromD, distFromU, pos ); - - // Generate some temporary indexes associated with the left and right grid values - indexL = permutationTable[ gridPointL ]; - indexR = permutationTable[ gridPointR ]; - - // Generate indexes in the permutation table for all 4 corners - indexLD = permutationTable[ indexL + gridPointD ]; - indexRD = permutationTable[ indexR + gridPointD ]; - indexLU = permutationTable[ indexL + gridPointU ]; - indexRU = permutationTable[ indexR + gridPointU ]; - - // Get the s curves at the proper values - sX = easeCurve( distFromL ); - sY = easeCurve( distFromD ); - - // Do the dot products for the lower left corner and lower right corners. - // Interpolate between those dot products value sX to get a. - q = gradientTable2d[ indexLD ]; u = dot2( distFromL, distFromD, q ); - q = gradientTable2d[ indexRD ]; v = dot2( distFromR, distFromD, q ); - a = linearInterp( sX, u, v ); - - // Do the dot products for the upper left corner and upper right corners. - // Interpolate between those dot products at value sX to get b. - q = gradientTable2d[ indexLU ]; u = dot2( distFromL, distFromU, q ); - q = gradientTable2d[ indexRU ]; v = dot2( distFromR, distFromU, q ); - b = linearInterp( sX, u, v ); - - // Interpolate between a and b at value sY to get the noise return value. - return linearInterp( sY, a, b ); -} - -// you guessed it -- 3D coherent noise - -float PerlinNoise::Noise3d( float pos[ 3 ] ) { - int gridPointL, gridPointR, gridPointD, gridPointU, gridPointB, gridPointF; - int indexLD, indexLU, indexRD, indexRU; - float distFromL, distFromR, distFromD, distFromU, distFromB, distFromF; - float *q, sX, sY, sZ, a, b, c, d, t, u, v; - int indexL, indexR; - - if ( ! initialized ) { reseed(); } - - // find out neighboring grid points to pos and signed distances from pos to them. - setupValues( t, 0, gridPointL, gridPointR, distFromL, distFromR, pos ); - setupValues( t, 1, gridPointD, gridPointU, distFromD, distFromU, pos ); - setupValues( t, 2, gridPointB, gridPointF, distFromB, distFromF, pos ); - - indexL = permutationTable[ gridPointL ]; - indexR = permutationTable[ gridPointR ]; - - indexLD = permutationTable[ indexL + gridPointD ]; - indexRD = permutationTable[ indexR + gridPointD ]; - indexLU = permutationTable[ indexL + gridPointU ]; - indexRU = permutationTable[ indexR + gridPointU ]; - - sX = easeCurve( distFromL ); - sY = easeCurve( distFromD ); - sZ = easeCurve( distFromB ); - - q = gradientTable3d[ indexLD + gridPointB ]; u = dot3( distFromL, distFromD, distFromB, q ); - q = gradientTable3d[ indexRD + gridPointB ]; v = dot3( distFromR, distFromD, distFromB, q ); - a = linearInterp( sX, u, v ); - - q = gradientTable3d[ indexLU + gridPointB ]; u = dot3( distFromL, distFromU, distFromB, q ); - q = gradientTable3d[ indexRU + gridPointB ]; v = dot3( distFromR, distFromU, distFromB, q ); - b = linearInterp( sX, u, v ); - - c = linearInterp( sY, a, b ); - - q = gradientTable3d[ indexLD + gridPointF ]; u = dot3( distFromL, distFromD, distFromF, q ); - q = gradientTable3d[ indexRD + gridPointF ]; v = dot3( distFromR, distFromD, distFromF, q ); - a = linearInterp( sX, u, v ); - - q = gradientTable3d[ indexLU + gridPointF ]; u = dot3( distFromL, distFromU, distFromF, q ); - q = gradientTable3d[ indexRU + gridPointF ]; v = dot3( distFromR, distFromU, distFromF, q ); - b = linearInterp( sX, u, v ); - - d = linearInterp( sY, a, b ); - - return linearInterp( sZ, c, d ); -} - -// you can call noise component-wise, too. - -float PerlinNoise::Noise( float x ) { - return Noise1d( &x ); -} - -float PerlinNoise::Noise( float x, float y ) { - float p[ 2 ] = { x, y }; - return Noise2d( p ); -} - -float PerlinNoise::Noise( float x, float y, float z ) { - float p[ 3 ] = { x, y, z }; - return Noise3d( p ); -} - -// reinitialize with new, random values. - -void PerlinNoise::reseed() { - srand( ( unsigned int ) ( time( NULL ) + rand() ) ); - generateLookupTables(); -} - -// reinitialize using a user-specified random seed. - -void PerlinNoise::reseed( unsigned int rSeed ) { - srand( rSeed ); - generateLookupTables(); -} - -// initialize everything during constructor or reseed -- note -// that space was already allocated for the gradientTable -// during the constructor - -void PerlinNoise::generateLookupTables() { - unsigned i, j, temp; - - for ( i = 0; i < NOISE_WRAP_INDEX; i++ ) { - // put index into permutationTable[index], we will shuffle later - permutationTable[ i ] = i; - - gradientTable1d[ i ] = randNoiseFloat(); - - for ( j = 0; j < 2; j++ ) { gradientTable2d[ i ][ j ] = randNoiseFloat(); } - normalize2d( gradientTable2d[ i ] ); - - for ( j = 0; j < 3; j++ ) { gradientTable3d[ i ][ j ] = randNoiseFloat(); } - normalize3d( gradientTable3d[ i ] ); - } - - // Shuffle permutation table up to NOISE_WRAP_INDEX - for ( i = 0; i < NOISE_WRAP_INDEX; i++ ) { - j = rand() & NOISE_MOD_MASK; - temp = permutationTable[ i ]; - permutationTable[ i ] = permutationTable[ j ]; - permutationTable[ j ] = temp; - } - - // Add the rest of the table entries in, duplicating - // indices and entries so that they can effectively be indexed - // by unsigned chars. I think. Ask Perlin what this is really doing. - // - // This is the only part of the algorithm that I don't understand 100%. - - for ( i = 0; i < NOISE_WRAP_INDEX + 2; i++ ) { - permutationTable[ NOISE_WRAP_INDEX + i ] = permutationTable[ i ]; - - gradientTable1d[ NOISE_WRAP_INDEX + i ] = gradientTable1d[ i ]; - - for ( j = 0; j < 2; j++ ) { - gradientTable2d[ NOISE_WRAP_INDEX + i ][ j ] = gradientTable2d[ i ][ j ]; - } - - for ( j = 0; j < 3; j++ ) { - gradientTable3d[ NOISE_WRAP_INDEX + i ][ j ] = gradientTable3d[ i ][ j ]; - } - } - - // And we're done. Set initialized to true - initialized = 1; -} diff --git a/src/PerlinNoise.h b/src/PerlinNoise.h deleted file mode 100644 index ce9618b00..000000000 --- a/src/PerlinNoise.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include - -// It must be true that (x % NOISE_WRAP_INDEX) == (x & NOISE_MOD_MASK) -// so NOISE_WRAP_INDEX must be a power of two, and NOISE_MOD_MASK must be -// that power of 2 - 1. as indices are implemented, as unsigned chars, -// NOISE_WRAP_INDEX should be less than or equal to 256. -// There's no good reason to change it from 256, really. - -#define NOISE_WRAP_INDEX 256 -#define NOISE_MOD_MASK 255 - - -#define NOISE_LARGE_PWR2 4096 - -class PerlinNoise { -private: - static unsigned initialized; - - static unsigned permutationTable[ NOISE_WRAP_INDEX*2 + 2 ]; - static float gradientTable1d[ NOISE_WRAP_INDEX*2 + 2 ]; - static float gradientTable2d[ NOISE_WRAP_INDEX*2 + 2 ][ 2 ]; - static float gradientTable3d[ NOISE_WRAP_INDEX*2 + 2 ][ 3 ]; - - static float randNoiseFloat(); - static void normalize2d( float vector[ 2 ] ); - static void normalize3d( float vector[ 3 ] ); - static void generateLookupTables(); - -public: - - PerlinNoise(); - PerlinNoise( unsigned int rSeed ) { reseed(rSeed); } - ~PerlinNoise(); - - static void reseed(); - static void reseed( unsigned int rSeed ); - - float Noise1d( float pos[ 1 ] ); - float Noise2d( float pos[ 2 ] ); - float Noise3d( float pos[ 3 ] ); - - float Noise( float ); - float Noise( float, float ); - float Noise( float, float, float ); - -}; - diff --git a/src/SConscript b/src/SConscript index 4960aa7a1..3b1ec2e71 100644 --- a/src/SConscript +++ b/src/SConscript @@ -158,7 +158,6 @@ GlobalContainer.cpp GlobalContainerArgs.cpp GUIGlob2FileList.cpp GUIMapPreview.cpp -map/generator/HeightMapGenerator.cpp building/IntBuildingType.cpp net/irc/IRC.cpp net/irc/IRCTextMessageHandler.cpp @@ -170,6 +169,35 @@ LANGameInformation.cpp LANMenuScreen.cpp MainMenuScreen.cpp map/Map.cpp +map/generator/compatibility/LegacyGenerationDescriptor.cpp +map/generator/core/GenerationContext.cpp +map/generator/core/GenerationService.cpp +map/generator/core/GenerationValidation.cpp +map/generator/core/GeneratorControls.cpp +map/generator/core/GeneratorRegistry.cpp +map/generator/generators/ConcreteIslandsGenerator.cpp +map/generator/generators/ContestedCommonsGenerator.cpp +map/generator/generators/MazeGenerator.cpp +map/generator/generators/CraterLakesGenerator.cpp +map/generator/generators/FjordContinentGenerator.cpp +map/generator/generators/IslandsGenerator.cpp +map/generator/generators/IslesGenerator.cpp +map/generator/generators/LatticeGenerator.cpp +map/generator/generators/RiverGenerator.cpp +map/generator/generators/RuggedArchipelagoGenerator.cpp +map/generator/generators/ShatteredCoastGenerator.cpp +map/generator/generators/SwampGenerator.cpp +map/generator/generators/UniformGenerator.cpp +map/generator/shared/Distances.cpp +map/generator/shared/HeightMap.cpp +map/generator/shared/Noise.cpp +map/generator/shared/Geometry.cpp +map/generator/shared/Topology.cpp +map/generator/shared/Settlements.cpp +map/generator/shared/Regions.cpp +map/generator/shared/Resources.cpp +map/generator/shared/StartingPositions.cpp +map/generator/shared/Terrain.cpp map/gradient/MapGradientArea.cpp map/gradient/MapGradientBuilding.cpp map/gradient/MapGradientGlobal.cpp @@ -202,19 +230,6 @@ map/edit/MapEditDelegate.cpp map/edit/MapEditClicks.cpp map/edit/MapEditDialog.cpp map/edit/MapEditKeyActions.cpp -map/generator/MapGenerationDescriptor.cpp -map/generator/Generator.cpp -map/generator/GeneratorDivide.cpp -map/generator/GeneratorSplit.cpp -map/generator/GeneratorPoints.cpp -map/generator/GeneratorHeightmap.cpp -map/generator/MapHomogen.cpp -map/generator/MapOldRandom.cpp -map/generator/MapOldRandomRessources.cpp -map/generator/MapRandom.cpp -map/generator/MapOldIslands.cpp -map/generator/MapOldIslandsRessources.cpp -map/generator/GameMaps.cpp map/io/MapHeader.cpp MapScript.cpp MapScriptError.cpp @@ -259,7 +274,6 @@ OrderModify.cpp OrderMisc.cpp OverlayAreas.cpp OverlayFill.cpp -PerlinNoise.cpp Player.cpp game/entities/Race.cpp ReplayReader.cpp diff --git a/src/map/Map.h b/src/map/Map.h index 2835d73fc..985526422 100644 --- a/src/map/Map.h +++ b/src/map/Map.h @@ -25,7 +25,6 @@ class Unit; class Map; class Game; -class MapGenerationDescriptor; class SessionGame; class MapHeader; @@ -693,6 +692,8 @@ class Map Uint16 lookup(Uint8 tl, Uint8 tr, Uint8 bl, Uint8 br) const; public: + // Rebuild rendered terrain after bulk undermap edits. + void rebuildTerrain() { regenerateMap(0, 0, w, h); } // here we handle terrain // mapDiscovered bool arraysBuilt; // if true, the next pointers(arrays) have to be valid and filled. @@ -818,11 +819,6 @@ class Map void makeHomogenMap(TerrainType terrainType); void controlSand(void); void smoothResources(int times); - bool makeRandomMap(MapGenerationDescriptor &descriptor); - bool oldMakeRandomMap(MapGenerationDescriptor &descriptor); - bool oldMakeIslandsMap(MapGenerationDescriptor &descriptor); - void oldAddResourcesRandomMap(MapGenerationDescriptor &descriptor); - void oldAddResourcesIslandsMap(MapGenerationDescriptor &descriptor); }; diff --git a/src/map/MapTerrain.cpp b/src/map/MapTerrain.cpp index c52610cb6..b2f53caf3 100644 --- a/src/map/MapTerrain.cpp +++ b/src/map/MapTerrain.cpp @@ -132,7 +132,7 @@ Uint16 Map::lookup(Uint8 tl, Uint8 tr, Uint8 bl, Uint8 br) const The index in the following table is : val[0] + val[1]*k + val[2]*k^2 + val[3]*k^3 where k is the number of different possibilities. - + H = grass S = sand E = water @@ -234,3 +234,81 @@ Uint16 Map::lookup(Uint8 tl, Uint8 tr, Uint8 bl, Uint8 br) const } + +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière + +#include +#include +#include +#include + +//also the Perlin Noise stuff uses random that is not based on syncRand +#include "Map.h" +#include "Unit.h" +#include "Utilities.h" + +///generates a map that is of one terrain type only +void Map::makeHomogenMap(TerrainType terrainType) +{ + for (int y=0; y=0)&&(d<40)&&(syncRand()&4)) + { + if (l<=(int)(syncRand()&3)) + setTerrain(x, y, d+273); + else + { + // we extend resource: + int dx, dy; + Unit::dxDyFromDirection(syncRand()&7, &dx, &dy); + int nx=x+dx; + int ny=y+dy; + if (getGroundUnit(nx, ny)==NOGUID) + if (((r==WOOD||r==CORN||r==STONE)&&isGrass(nx, ny))||((r==ALGA)&&isWater(nx, ny))) + setTerrain(nx, ny, 272+(r*10)+((syncRand()&1)*5)); + } + } + } +} + diff --git a/src/map/generator/GameMaps.cpp b/src/map/generator/GameMaps.cpp deleted file mode 100644 index 41c420f0f..000000000 --- a/src/map/generator/GameMaps.cpp +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "GlobalContainer.h" -#include "MapGenerationDescriptor.h" -#include "MapGenerator.h" -#include "Map.h" -#include "Unit.h" -#include "Utilities.h" - -bool Game::oldMakeIslandsMap(MapGenerationDescriptor &descriptor) -{ - for (int s=0; sbuildingsTypes.getTypeNum("swarm", 0, false); - if (!checkRoomForBuilding(descriptor.bootX[s], descriptor.bootY[s], globalContainer->buildingsTypes.get(typeNum), s, false)) - { - if (verbose) - printf("Failed to add swarm of team %d\n", s); - return false; - } - teams[s]->startPosX=descriptor.bootX[s]; - teams[s]->startPosY=descriptor.bootY[s]; - Building *b=addBuilding(descriptor.bootX[s], descriptor.bootY[s], typeNum, s); - assert(b); - for (int i=0; icreateLists(); - } - map.smoothResources(descriptor.oldIslandSize/10); - return true; -} - -bool Game::makeRandomMap(MapGenerationDescriptor &descriptor) -{ - for (int s=0; sbuildingsTypes.getTypeNum("swarm", 0, false); - if (!checkRoomForBuilding(descriptor.bootX[s], descriptor.bootY[s], globalContainer->buildingsTypes.get(typeNum), s, false)) - { - if (verbose) - printf("Failed to add swarm of team %d\n", s); - return false; - } - teams[s]->startPosX=descriptor.bootX[s]; - teams[s]->startPosY=descriptor.bootY[s]; - Building *b=addBuilding(descriptor.bootX[s], descriptor.bootY[s], typeNum, s); - assert(b); - for (int i=0; icreateLists(); - } - return true; -} diff --git a/src/map/generator/Generator.cpp b/src/map/generator/Generator.cpp deleted file mode 100644 index 2a271c0f6..000000000 --- a/src/map/generator/Generator.cpp +++ /dev/null @@ -1,450 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "MapGenerationDescriptor.h" -#include "MapGenerator.h" -#include "Map.h" -#include "Unit.h" -#include "Utilities.h" - -bool MapGenerator::generateMap(Game& game, MapGenerationDescriptor &descriptor, std::optional syncSeed) -{ - if (verbose) - printf("Generating map, please wait ....\n"); - game.map.setSize(descriptor.wDec, descriptor.hDec); - game.map.setGame(&game); - if (syncSeed) setSyncRandSeed(*syncSeed); - else setRandomSyncRandSeed(); - - switch (descriptor.method) - { - case MapGenerationDescriptor::eUNIFORM: - game.map.makeHomogenMap(descriptor.terrainType); - game.addTeam(); - break; - case MapGenerationDescriptor::eSWAMP: - case MapGenerationDescriptor::eISLANDS: - case MapGenerationDescriptor::eRIVER: - case MapGenerationDescriptor::eCRATERLAKES: - if (!game.map.makeRandomMap(descriptor)) - return false; - if (!game.makeRandomMap(descriptor)) - return false; - break; - case MapGenerationDescriptor::eCONCRETEISLANDS: - if (!computeConcreteIslands(game, descriptor)) - return false; - break; - case MapGenerationDescriptor::eISLES: - if (!computeIsles(game, descriptor)) - return false; - break; - case MapGenerationDescriptor::eOLDRANDOM: - if (!game.map.oldMakeRandomMap(descriptor)) - return false; - if (!game.makeRandomMap(descriptor)) - return false; - game.map.oldAddResourcesRandomMap(descriptor); - break; - case MapGenerationDescriptor::eOLDISLANDS: - if (!game.map.oldMakeIslandsMap(descriptor)) - return false; - if (!game.oldMakeIslandsMap(descriptor)) - return false; - game.map.oldAddResourcesIslandsMap(descriptor); - break; - - default: - assert(false); - } - - // compile script - game.sgslScript.compileScript(&game); - - if (verbose) - printf(".... map generated.\n"); - return true; -} - - -bool MapGenerator::computeConcreteIslands(Game& game, MapGenerationDescriptor& descriptor) -{ - // Reuse the serialized diameter slot: this mode measures channel width. - // Older descriptors stored the unused value 50; retain their original width. - const auto &channelControl = - MapGenerationDescriptor::control(descriptor.method, "Channel width"); - const int channelWidth = - descriptor.riverDiameter == 50 ? 6 : channelControl.normalize(descriptor.riverDiameter); - game.map.makeHomogenMap(descriptor.terrainType); - for(int i=0; i grid(game.map.getW() * game.map.getH(), 0); - std::vector teamPoints; - std::vector weights1; - std::vector weights2; - std::vector teamAreaNumbers; - std::vector islandAreaNumbers; - - //Add in team bases - for(int i=0; i areaNumbers = teamAreaNumbers; - areaNumbers.insert(areaNumbers.end(), islandAreaNumbers.begin(), islandAreaNumbers.end()); - - // Initially divide up the land - splitUpPoints(game, grid, 0, teamPoints, weights1); - splitUpArea(game, grid, 0, teamPoints, weights2, areaNumbers); - - // Create a heightmap that will be used to give the map a rough edge - std::vector heights(game.map.getW() * game.map.getH(), 75); - adjustHeightmapFromPerlinNoise(game, heights, 15); - - // Compute the distance of every square from the border - std::vector sources; - findBorderPoints(game, grid, sources); - std::vector obstacles; - std::vector distances; - computeDistances(game, sources, obstacles, distances); - - // Locations near the border are deaper, thus causing more water - for(int x=0; x=45 && total_height<=55) - game.map.setUMatPos(x, y, SAND, 1); - else - game.map.setUMatPos(x, y, GRASS, 1); - } - } - game.map.controlSand(); - - // Go through the map again and place alga - for(int x=0; x areaWeights; - std::vector areaNumbers; - for(int j=0; j<2; ++j) - { - areaWeights.push_back(1); - areaNumbers.push_back(areaNumber); - areaNumber+=1; - } - - // Divide the area. Its possible the area will be so small it can't be used - if(divideUpArea(game, grid, islandAreaNumbers[i], areaWeights, areaNumbers)) - { - // Fill in wheat - std::vector points; - getAllPoints(game, grid, areaNumbers[0], points); - fillInResource(game, points, CORN, 2); - points.clear(); - - // Place some fruit - int fruit_n = syncRand()%6+1; - getAllPoints(game, grid, areaNumbers[1], points); - chooseRandomPoints(game, points, fruit_n); - for(unsigned int j=0; jcreateLists(); - } - return true; -} - - - -bool MapGenerator::computeIsles(Game& game, MapGenerationDescriptor& descriptor) -{ - const int islandSize = MapGenerationDescriptor::control(descriptor.method, "Island size") - .normalize(descriptor.grassRatio); - const int bridgeWidth = - descriptor.riverDiameter == 50 - ? 4 - : MapGenerationDescriptor::control(descriptor.method, "Land bridge width") - .normalize(descriptor.riverDiameter); - const int bridgeRadius = bridgeWidth - 2; - game.map.makeHomogenMap(descriptor.terrainType); - for(int i=0; i grid(game.map.getW() * game.map.getH(), 0); - - // Do the starting locations of the teams - std::vector teamPoints; - std::vector teamWeights; - std::vector teamAreaNumbers; - for(int i=0; i heightmap(game.map.getW() * game.map.getH(), 50); - std::vector teamAreaPoints; - getAllOtherPoints(game, grid, 0, teamAreaPoints); - std::vector obstacles; - - std::vector distances; - computeDistances(game, teamAreaPoints, obstacles, distances); - - // Stamp out the team areas - for(int x=0; x 1 && d <= 11) - heightmap[y * game.map.getW() + x] += (11-d)*10; - else if(d == 1) - heightmap[y * game.map.getW() + x] += 100; - } - } - - // Connect each teams area to each other players area - std::vector connectorPoints; - int connectorArea = areaNumber; - areaNumber+=1; - for(int i=0; i teamI; - std::vector teamJ; - getAllPoints(game, grid, teamAreaNumbers[i], teamI); - getAllPoints(game, grid, teamAreaNumbers[j], teamJ); - chooseRandomPoints(game, teamI, 1); - chooseRandomPoints(game, teamJ, 1); - - // Traverse between the two points - std::vector linePoints; - getAllPointsLine(game, teamI[0].x, teamI[0].y, teamJ[0].x, teamJ[0].y, linePoints); - // If a connection can be made without going through another teams area, then do it - bool failed=false; - for(unsigned int p=0; p bridgeWidth + 1) - { - grid[ny * game.map.getW() + nx] = connectorArea; - } - } - } - } - } - } - } - computeDistances(game, connectorPoints, obstacles, distances); - - // Stamp out the connectors - for(int x=0; x 1 && d <= bridgeWidth) - heightmap[y * game.map.getW() + x] += (bridgeWidth - d) * (100 / (bridgeWidth - 1)); - else if(d == 1) - heightmap[y * game.map.getW() + x] += 100; - } - } - - - // Use the heightmap to put in water, grass, and sand - adjustHeightmapFromPerlinNoise(game, heightmap, 45); - for(int x=0; x95 && total_height<105) - game.map.setUMatPos(x, y, SAND, 1); - else - game.map.setUMatPos(x, y, GRASS, 1); - } - } - game.map.controlSand(); - - // Reset the grid, and recompute within the boundaries of the various islands - for(int x=0; x connectorDistances = distances; - - // For each team, find a point just off the coast and place algae there - for(int i=0; i sources; - getAllPoints(game, grid, teamAreaNumbers[i], sources); - computeDistances(game, sources, obstacles, distances); - std::vector possible; - for(int x=0; x bridgeWidth) - { - possible.push_back(MapGeneratorPoint(x, y)); - } - } - } - if(possible.size() == 0) - { - return false; - } - int r = syncRand() % possible.size(); - for(int x=-2; x<=2; ++x) - { - int nx = game.map.normalizeX(possible[r].x + x); - for(int y=-2; y<=2; ++y) - { - int ny = game.map.normalizeY(possible[r].y + y); - game.map.setResource(nx, ny, ALGA, 1); - } - } - } - - if(!divideUpPlayerLands(game, descriptor, grid, teamAreaNumbers, areaNumber)) - { - return false; - } - - // Initialize final team info - for(int i=0; icreateLists(); - } - return true; -} diff --git a/src/map/generator/GeneratorDivide.cpp b/src/map/generator/GeneratorDivide.cpp deleted file mode 100644 index f6a094430..000000000 --- a/src/map/generator/GeneratorDivide.cpp +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "GlobalContainer.h" -#include "MapGenerationDescriptor.h" -#include "MapGenerator.h" -#include "Map.h" -#include "Unit.h" -#include "Utilities.h" - -bool MapGenerator::divideUpPlayerLands(Game& game, MapGenerationDescriptor& descriptor, std::vector& grid, std::vector& teamAreaNumbers, int& areaNumber) -{ - int typeNum=globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); - BuildingType *swarm = globalContainer->buildingsTypes.get(typeNum); - - //Compute the distances from water - std::vector sources; - std::vector obstacles; - std::vector distances; - obstacles.clear(); - getAllPoints(game, grid, 0, sources); - computeDistances(game, sources, obstacles, distances); - - //Create a new heightmap from noise and distance to water - std::vector heightmap(game.map.getW() * game.map.getH(), 50); - adjustHeightmapFromPerlinNoise(game, heightmap, 5); - for(int x=0; x areaWeights; - std::vector areaNumbers; - for(int j=0; j<12; ++j) - { - areaWeights.push_back(1); - areaNumbers.push_back(areaNumber); - areaNumber+=1; - } - - // Divide the area. Its possible the area will be so small it can't be used - if(divideUpArea(game, grid, teamAreaNumbers[i], areaWeights, areaNumbers)) - { - // Sort the list of areas based on how close they are to water - std::vector areaDistances(areaNumbers.size()); - std::vector areaIndexes(areaNumbers.size()); - for(unsigned int j=0; j wheatWoodPoints; - std::vector wheatPoints; - getAllPoints(game, grid, areaNumbers[3], wheatWoodPoints); - getAllPoints(game, grid, areaNumbers[4], wheatWoodPoints); - getAllPoints(game, grid, areaNumbers[5], wheatWoodPoints); - adjustHeightmapFromPoints(game, wheatWoodPoints, heightmap, 10); - for(unsigned int j=0; j 50) - { - game.map.setResource(wheatWoodPoints[j].x, wheatWoodPoints[j].y, WOOD, 1); - } - } - wheatWoodPoints.clear(); - - // Place wheat - getAllPoints(game, grid, areaNumbers[0], wheatWoodPoints); - getAllPoints(game, grid, areaNumbers[1], wheatWoodPoints); - getAllPoints(game, grid, areaNumbers[2], wheatWoodPoints); - adjustHeightmapFromPoints(game, wheatWoodPoints, heightmap, 10); - for(unsigned int j=0; j 50) - { - game.map.setResource(wheatWoodPoints[j].x, wheatWoodPoints[j].y, CORN, 1); - wheatPoints.push_back(wheatWoodPoints[j]); - } - } - - - // These are all points in the base - std::vector baseLocations; - getAllPoints(game, grid, areaNumbers[6], baseLocations); - getAllPoints(game, grid, areaNumbers[7], baseLocations); - getAllPoints(game, grid, areaNumbers[8], baseLocations); - getAllPoints(game, grid, areaNumbers[9], baseLocations); - getAllPoints(game, grid, areaNumbers[10], baseLocations); - getAllPoints(game, grid, areaNumbers[11], baseLocations); - - // Place stone - int numberOfStone = 6; - std::vector stoneLocations = baseLocations; - chooseRandomPoints(game, stoneLocations, numberOfStone); - for(unsigned int j=0; j wheatDistance; - computeDistances(game, wheatPoints, obstacles, wheatDistance); - - // Only consider points between 1 and 4 squares from wheat - std::vector startingLocations; - for(unsigned int j=0; j= 1 && minValue <= 2) - { - startingLocations.push_back(baseLocations[j]); - } - } - - // Place swarms - chooseFreeForBuildingSquares(game, startingLocations, swarm, i); - if(startingLocations.size() == 0) - { - return false; - } - int chosen = syncRand()%startingLocations.size(); - Building* b = addBuilding(game, startingLocations[chosen].x, startingLocations[chosen].y, i, IntBuildingType::SWARM_BUILDING, 1, false); - if(b == NULL) - { - return false; - } - - // Set the initial viewport location - game.teams[i]->startPosX=b->posX; - game.teams[i]->startPosY=b->posY; - game.teams[i]->startPosSet=Team::START_POS_FROM_SWARM; - - // Place units around the swarm - std::vector unitLocations = baseLocations; - chooseFreeForGroundUnits(game, unitLocations, i); - chooseTouchingBuilding(game, unitLocations, b); - chooseRandomPoints(game, unitLocations, descriptor.nbWorkers); - for(unsigned int n=0; n& grid, int areaN, std::vector& weights, std::vector& areaNumbers) -{ - std::vector points; - std::vector splitWeights; - for(unsigned int i=0; i& grid, int areaN, int x, int y, int width, int height) -{ - int h2 = (height/2) * (height/2); - int w2 = (width/2) * (width/2); - int t2 = h2 * w2; - for(int px = -(width/2); px < (width/2); ++px) - { - int nx = game.map.normalizeX(x + px); - int px2 = px*px*h2; - for(int py = -(height/2); py < (height/2); ++py) - { - int ny = game.map.normalizeY(y + py); - int py2 = py*py*w2; - if(px2 + py2 < t2) - { - grid[ny * game.map.getW() + nx] = areaN; - } - } - } -} - - - diff --git a/src/map/generator/GeneratorHeightmap.cpp b/src/map/generator/GeneratorHeightmap.cpp deleted file mode 100644 index 5f37f4d1c..000000000 --- a/src/map/generator/GeneratorHeightmap.cpp +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "GlobalContainer.h" -#include "HeightMapGenerator.h" -#include "MapGenerator.h" -#include "Map.h" -#include -#include "Unit.h" -#include "Utilities.h" - -void MapGenerator::adjustHeightmapFromPoints(Game& game, std::vector& points, std::vector& heightmap, int value) -{ - for(unsigned int i=0; i& heights, int spread) -{ - HeightMap noise(game.map.getW(), game.map.getH()); - noise.makePlain(4); - for(int x=0; x& sources, std::vector& obstacles, std::vector& heightmap) -{ - std::queue places; - heightmap.clear(); - heightmap.resize(game.map.getW() * game.map.getH(), 0); - for(unsigned int i=0; i> wDec; // Calculate the coordinates of - size_t x = deltaAddrG & wMask; // the current field and of the - - size_t yu = ((y - 1) & hMask); // fields next to it. - size_t yd = ((y + 1) & hMask); // We live on a torus! If we are on - size_t xl = ((x - 1) & wMask); // the "last line" of the map, the - size_t xr = ((x + 1) & wMask); // next line is the line 0 again. - - int g = heightmap[(y << wDec) | x] + 1; - - size_t deltaAddrC[8]; - int *addr; - int side; - - deltaAddrC[0] = (yu << wDec) | xl; // Calculate the positions of the - deltaAddrC[1] = (yu << wDec) | x ; // 8 fields next to us from their - deltaAddrC[2] = (yu << wDec) | xr; // coordinates. - deltaAddrC[3] = (y << wDec) | xr; - deltaAddrC[4] = (yd << wDec) | xr; - deltaAddrC[5] = (yd << wDec) | x ; - deltaAddrC[6] = (yd << wDec) | xl; - deltaAddrC[7] = (y << wDec) | xl; - for (int ci=0; ci<8; ci++) // Check for each of this fields if we - { // can improve its gradient value - addr = &heightmap[deltaAddrC[ci]]; - side = *addr; - if (side==0) - { - *addr = g; - places.push(deltaAddrC[ci]); - } - } - } -} - - - -int MapGenerator::computeAverageDistance(Game& game, std::vector& grid, int areaN, const std::vector& heightmap) -{ - long total = 0; - int count = 0; - for(int x=0; x 0 ? total/count : 0; -} - - - -Building* MapGenerator::addBuilding(Game& game, int x, int y, int team, int type, int level, bool underConstruction) -{ - std::string name = IntBuildingType::typeFromShortNumber(type); - int typeNum=globalContainer->buildingsTypes.getTypeNum(name, level-1, underConstruction); - BuildingType *bt = globalContainer->buildingsTypes.get(typeNum); - if(bt == NULL) - { - return NULL; - } - - if (game.checkRoomForBuilding(x, y, bt, team, false)) - { - if(bt->maxUnitWorking) - return game.addBuilding(x, y, typeNum, team, 1, 0); - else - return game.addBuilding(x, y, typeNum, team, 0, 0); - } - return NULL; -} - - - diff --git a/src/map/generator/GeneratorPoints.cpp b/src/map/generator/GeneratorPoints.cpp deleted file mode 100644 index b1ae5f083..000000000 --- a/src/map/generator/GeneratorPoints.cpp +++ /dev/null @@ -1,198 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "MapGenerator.h" -#include "Map.h" -#include "Unit.h" -#include "Utilities.h" - -void MapGenerator::getAllPoints(Game& game, std::vector& grid, int areaN, std::vector& points) -{ - for(int x=0; x& grid, int areaN, std::vector& points) -{ - for(int x=0; x& points) -{ - int startX = x1; - int endX = x2; - int startY = y1; - int endY = y2; - - int dirX = (endX > startX ? 1 : -1); - int distX = std::abs(endX - startX); - if(distX > game.map.getW()/2) - { - dirX = -dirX; - distX = game.map.getW() - distX; - } - - int dirY = (endY > startY ? 1 : -1); - int distY = std::abs(endY - startY); - if(distY > game.map.getH()/2) - { - dirY = -dirY; - distY = game.map.getH() - distY; - } - - if(distX > distY) - { - int px = 0; - int py = 0; - int y = startY; - for(int x=startX; x!=endX;) - { - px+=1; - points.push_back(MapGeneratorPoint(x, y)); - if(std::abs(px * distY - py * distX) > std::abs(px * distY - (py+1) * distX)) - { - y=game.map.normalizeY(y+dirY); - points.push_back(MapGeneratorPoint(x, y)); - py+=1; - } - x=game.map.normalizeX(x+dirX); - } - } - else - { - int px = 0; - int py = 0; - int x = startX; - for(int y=startY; y!=endY;) - { - py+=1; - points.push_back(MapGeneratorPoint(x, y)); - if(std::abs(py * distX - px * distY) > std::abs(py * distX - (px+1) * distY)) - { - x=game.map.normalizeX(x+dirX); - points.push_back(MapGeneratorPoint(x, y)); - px+=1; - } - y=game.map.normalizeY(y+dirY); - } - } -} - - - -void MapGenerator::findBorderPoints(Game& game, std::vector& grid, std::vector& points) -{ - for(int x=0; x& points, int resourceType, int maxFillSize) -{ - for(unsigned int n=0; n& points, int n) -{ - n = std::min(int(points.size()), n); - for(int i=0; i& points, BuildingType* type, int team) -{ - std::vector newPoints; - for(unsigned int n=0; n& points, int team) -{ - std::vector newPoints; - for(unsigned int n=0; n& points, Building* building) -{ - std::vector newPoints; - for(unsigned int n=0; ngid)) - { - newPoints.push_back(MapGeneratorPoint(points[n].x, points[n].y)); - } - } - points = newPoints; -} - - - diff --git a/src/map/generator/GeneratorSplit.cpp b/src/map/generator/GeneratorSplit.cpp deleted file mode 100644 index bad31db31..000000000 --- a/src/map/generator/GeneratorSplit.cpp +++ /dev/null @@ -1,262 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include - -#include "boost/integer_traits.hpp" -#include -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "MapGenerator.h" -#include "Map.h" -#include "Utilities.h" - -int MapGenerator::splitUpPoints(Game& game, std::vector& grid, int areaN, std::vector& points, std::vector& weights) -{ - std::vector startingPoints; - for(int x=0; x obstacles; - getAllOtherPoints(game, grid, areaN, obstacles); - std::vector sources; - sources.push_back(startingPoints[n]); - std::vector heights; - computeDistances(game, sources, obstacles, heights); - sources.clear(); - - for(unsigned int i=0; i possible; - for(int x=0; x max) - { - max = h; - possible.clear(); - } - if(h >= max) - { - possible.push_back(MapGeneratorPoint(x, y)); - } - } - } - int n = syncRand() % possible.size(); - points[i] = possible[n]; - sources.push_back(points[i]); - computeDistances(game, sources, obstacles, heights); - } - startingPoints.clear(); - heights.clear(); - sources.clear(); - obstacles.clear(); - - bool cont=true; - int minDist = boost::integer_traits::const_max; - while(cont) - { - minDist = boost::integer_traits::const_max; - bool changed=false; - for(unsigned int i=0; i::const_max; - for(unsigned int j=0; j::const_max; - bool invalid=false; - for(unsigned int j=0; jbest) - { - best = score; - best_x = nx; - best_y = ny; - } - } - } - if(best_x != -1) - { - if(best != orig) - changed=true; - points[i].x = best_x; - points[i].y = best_y; - } - } - if(!changed) - { - cont = false; - } - } - - for(unsigned int i=0; i(0, i)(randomGenerator); - if (i != j) - std::swap(points[i], points[j]); - } - return int(std::sqrt(double(minDist))); -} - - - - -void MapGenerator::splitUpArea(Game& game, std::vector& grid, int areaN, std::vector& points, std::vector& weights, std::vector& areaNumbers, bool grassOnly) -{ - std::vector gradient(game.map.getW() * game.map.getH(), 0); - - Uint32 wDec = game.map.wDec; - Uint32 hMask = game.map.hMask; - Uint32 wMask = game.map.wMask; - - std::vector > squares(points.size()); - std::vector expansion(points.size(), 0); - std::vector current; - std::vector count; - - for(unsigned int i=0; i 0 && !squares[p].empty()) - { - Uint32 deltaAddrG = squares[p].back(); - squares[p].erase(--squares[p].end()); - - size_t y = deltaAddrG >> wDec; // Calculate the coordinates of - size_t x = deltaAddrG & wMask; // the current field and of the - - size_t yu = ((y - 1) & hMask); // fields next to it. - size_t yd = ((y + 1) & hMask); // We live on a torus! If we are on - size_t xl = ((x - 1) & wMask); // the "last line" of the map, the - size_t xr = ((x + 1) & wMask); // next line is the line 0 again. - - - int t = grid[(y << wDec) | x]; - assert(t < (int)points.size()); - int g = gradient[(y << wDec) | x] + 1; - grid[(y << wDec) | x] = areaNumbers[t]; - - size_t deltaAddrC[8]; - int *addr; - int side; - - deltaAddrC[0] = (yu << wDec) | xl; // Calculate the positions of the - deltaAddrC[1] = (yu << wDec) | x ; // 8 fields next to us from their - deltaAddrC[2] = (yu << wDec) | xr; // coordinates. - deltaAddrC[3] = (y << wDec) | xr; - deltaAddrC[4] = (yd << wDec) | xr; - deltaAddrC[5] = (yd << wDec) | x ; - deltaAddrC[6] = (yd << wDec) | xl; - deltaAddrC[7] = (y << wDec) | xl; - - if(g != current[p]) - { - current[p] = g; - count[p] = 0; - } - - for (int ci=0; ci<8; ci++) // Check for each of this fields if we - { // can improve its gradient value - addr = &gradient[deltaAddrC[ci]]; - side = *addr; - if (side==0 && grid[deltaAddrC[ci]]==areaN) - { - if(grassOnly && !game.map.isGrass(deltaAddrC[ci])) - continue; - *addr = g; - grid[deltaAddrC[ci]] = t; - count[p]+=1; - expansion[p]-=1; - - Uint32 randLocation = syncRand() % count[p]; - std::list::iterator i = squares[p].begin(); - std::advance(i, randLocation); - squares[p].insert(i, deltaAddrC[ci]); - } - } - } - } - if(!found) - cont = false; - } -} - - - diff --git a/src/map/generator/HeightMapGenerator.cpp b/src/map/generator/HeightMapGenerator.cpp deleted file mode 100644 index f1c52f4fe..000000000 --- a/src/map/generator/HeightMapGenerator.cpp +++ /dev/null @@ -1,295 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2006 Leo Wandersleb - -#include "GlobalContainer.h" -#include "HeightMapGenerator.h" -#include -#include -#include -#include -#include "PerlinNoise.h" - -/// these faders are factors to be applicable to heightfields. they map (0,0)-(w,h) to [0..1] - -inline float faderCenter (int x, int y, int w, int h) /// to have zero at the borders and 1 in the center -{ - return (1.0-cos(2.0*3.14159265*(float)x/(float)w))*(1.0-cos(2.0*3.14159265*(float)y/(float)h))/4.0; -} -inline float faderLeftRight(int x, int y, int w, int h) /// to have 0 at top and bottom border and 1 at the middle of right and left border -{ - return (faderCenter((x+w/2)%w,y,w,h)); -} -inline float faderTopBottom(int x, int y, int w, int h) /// to have 1 at the middle of top and bottom border and 0 at right and left border -{ - return (faderCenter(x,(y+h/2)%h,w,h)); -} -inline float faderCorner(int x, int y, int w, int h) /// to have 1 in the corners and 0 on a cross going through the center -{ - return (faderCenter((x+w/2)%w,(y+h/2)%h,w,h)); -} - -HeightMap::HeightMap(unsigned int width, unsigned int height) -{ - init(width,height); -} - -HeightMap::~HeightMap() -{ - delete [] _map; - if(_stamp) - delete [] _stamp; -} - -void HeightMap::init(unsigned int width, unsigned int height) -{ - _w=width; _h=height; - _map=new float[_w*_h]; - _stamp=NULL; - _pn.reseed(); -} - -void HeightMap::makeStamp(unsigned int radius) -{ - _r=radius; - if(_stamp) - delete [] _stamp; - _stamp=new float[(2*_r+1)*(2*_r+1)]; - for(unsigned int x=0; x<2*_r+1; x++) - { - for(unsigned int y=0; y<2*_r+1;y++) - { - unsigned int dSquare=(x-_r)*(x-_r)+(y-_r)*(y-_r); - if(dSquare<_r*_r) - _stamp[x+y*(2*_r+1)]=(1.0-cos(sqrt(dSquare)*3.14159265/(float)_r))/2.0; - else - _stamp[x+y*(2*_r+1)]=.9999; - } - } -} -inline void HeightMap::lower(unsigned int coordX, unsigned int coordY) -{ - static unsigned int oldX=(unsigned int)-1; - static unsigned int oldY=(unsigned int)-1; - if((coordX!=oldX) || (coordY!=oldY)) //don't stamp the same spot again. if stamp is moved like in rivermaps this saves a lot of time - { - assert(_stamp); - for(unsigned int x=0; x<2*_r+1;x++) - { - /// this loop can be replaced by a somehow complicated memcpy - for(unsigned int y=0; y<2*_r+1;y++) - { - unsigned int coord1d=(unsigned int)(_w+x-_r+coordX)%_w+((unsigned int)(_h+y-_r+coordY)%_h)*_w; - if(_map[coord1d]>_stamp[x+y*(2*_r+1)]) - _map[coord1d]=_stamp[x+y*(2*_r+1)]; - } - } - oldX=coordX; - oldY=coordY; - } -} - -inline void HeightMap::differenceStamp(unsigned int coordX, unsigned int coordY) -{ - static unsigned int oldX=(unsigned int)-1; - static unsigned int oldY=(unsigned int)-1; - if((coordX!=oldX) || (coordY!=oldY)) //don't stamp the same spot again. if stamp is moved like in rivermaps this saves a lot of time - { - assert(_stamp); - for(unsigned int x=0; x<2*_r+1;x++) - { - for(unsigned int y=0; y<2*_r+1;y++) - { - unsigned int coord1d=(unsigned int)(_w+x-_r+coordX)%_w+((unsigned int)(_h+y-_r+coordY)%_h)*_w; - _map[coord1d]=fabs((1.0-_stamp[x+y*(2*_r+1)])-_map[coord1d]); - } - } - oldX=coordX; - oldY=coordY; - } -} - -inline void HeightMap::addNoise(float weight, float smoothingFactor) -{ - assert((weight>0) && (weight<=1.0)); - for (int x=0; (unsigned int)x<_w; x++) - { - for (int y=0; (unsigned int)y<_h; y++) - { - _map[x+_w*y] = _map[x+_w*y]*(1.0-weight)+ - (faderCenter(x,y,_w,_h) *_pn.Noise((float)( x)/smoothingFactor,(float)( y)/smoothingFactor)+ - faderLeftRight(x,y,_w,_h)*_pn.Noise((float)((x+_w/2)%_w+_w)/smoothingFactor,(float)( y+_h)/smoothingFactor)+ - faderTopBottom(x,y,_w,_h)*_pn.Noise((float)( x+2*_w)/smoothingFactor,(float)((y+_h/2)%_h+2*_h)/smoothingFactor)+ - faderCorner(x,y,_w,_h)*_pn.Noise((float)((x+_w/2)%_w+3*_w)/smoothingFactor,(float)((y+_h/2)%_h+3*4)/smoothingFactor)+ - +4.0)/8.0*weight; - } - } -} - -void HeightMap::makeIslands(unsigned int count, float smoothingFactor) -{ - assert (count); - PerlinNoise pn; - pn.reseed(); - int * centerX = new int[count]; - int * centerY = new int[count]; - float mindist=sqrt(_w*_h/count)/2.0; - assert(mindist>0); - makeStamp((unsigned int)(mindist*2)); - centerX[0]=rand()%_w;centerY[0]=rand()%_h; - /// find spots with distance>min. distance - for (unsigned int i=1; i0?_w:0); - targetPointY=startingPointY+_h-(tmprand%2)*_h; - } - else if (_w>_h) - { - targetPointX=startingPointX+_w; - targetPointY=startingPointY+(rand()%(_w/_h))*_h; - } - else - { - targetPointX=startingPointX+(rand()%(_h/_w))*_w; - targetPointY=startingPointY+_h; - } - float targetDirection=asin((targetPointY-startingPointY)/sqrt(pow(targetPointX-startingPointX,2)+pow(targetPointY-startingPointY,2))); - float targetDirectionX=cos(targetDirection); - float targetDirectionY=sin(targetDirection); - /// length of direct line - float straightRiverLength=sqrt(pow(targetPointX-startingPointX,2)+pow(targetPointY-startingPointY,2)); - for(float t=0; topenOutputStreamBackend(filename); - - for(unsigned int i=0; i<(2*_r+1)*(2*_r+1); i++) - hm2[i]=(char)(_stamp[i]*256); - - stream->write(hm2, (2*_r+1)*(2*_r+1)*sizeof(char)); - delete stream; - delete [] hm2; -} - -void HeightMap::mapOutput(char * filename) -{ - char * hm2=new char[_w*_h]; - StreamBackend * stream = Toolkit::getFileManager()->openOutputStreamBackend(filename); - - for(unsigned int i=0; i<_w*_h; i++) - hm2[i]=(char)(_map[i]*256); - - stream->write(hm2, _w*_h*sizeof(char)); - delete stream; - delete [] hm2; -} - -void HeightMap::makeCraters(unsigned int craterCount, unsigned int craterRadius, float smoothingFactor) -{ - makeStamp(craterRadius); - operator=(1.0); - for(unsigned int t=0; tmax?_map[i]:max; - } - min-=.01; - max+=.01; - float range=max-min; - for(unsigned int i=0; i<_w*_h; i++) - _map[i]=(_map[i]-min)/range; -} diff --git a/src/map/generator/HeightMapGenerator.h b/src/map/generator/HeightMapGenerator.h deleted file mode 100644 index 61524c3df..000000000 --- a/src/map/generator/HeightMapGenerator.h +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2006 Leo Wandersleb - -#pragma once - -#include "PerlinNoise.h" - -class HeightMap /// class to generate heightmaps to decide where to put resources, water, sand and grass later -{ - float * _map; /// height values are always [0,1]. - unsigned int _w, _h; /// map size - float * _stamp; /// smooth 0 to 1 gradient lookup to generate craters, islands and rivers - unsigned int _r; /// radius of the _stamp - PerlinNoise _pn;/// to get reproducible correlated random numbers -public: - enum kindOfMap {SWAMP=0,ISLANDS=1,RIVER=2,CRATERS=3,RANDOM=4}; - - HeightMap(unsigned int width, unsigned int height); - ~HeightMap(); - inline unsigned int uiLevel(unsigned int i, unsigned int scale) { - return (unsigned int)(_map[i]*scale); - } - inline unsigned int uiLevel(unsigned int x, unsigned int y, unsigned int scale) { - return (unsigned int)(_map[x%_w+(y%_h)*_w]*scale); - } - inline float operator() (unsigned int i) { - return _map[i]; - } - inline float operator() (const unsigned int x, const unsigned int y) { - return _map[x%_w+(y%_h)*_w]; - } - void mapOutput(char * filename); /// generates the file ~/.glob2/filename and writes the raw 0..255 values of map to it. to see it, use convert -size [width]x[height] -depth 8 gray:[filename] test.png - - void makePlain(float smoothingFactor); /// a plain perlin height field - void makeSwamp(float smoothingFactor); /// a plain perlin height field - void makeIslands(unsigned int count, float smoothingFactor); /// generates a 'swamp' with count hills - void makeRiver(unsigned int maxDiameter, float smoothingFactor); /// generates a 'swamp' with a river based on a random walk. - void makeCraters(unsigned int craterCount, unsigned int craterRadius, float smoothingFactor); /// generates a 'swamp' with craterCount craters -private: - void operator+(HeightMap hm){for(unsigned int i=0; i<_w*_h; i++)_map[i]+=hm(i);}; - void operator*(const float m){for(unsigned int i=0; i<_w*_h; _map[i++]*=m);}; - void operator/(const float d){for(unsigned int i=0; i<_w*_h; _map[i++]/=d);}; - void operator=(const float h) {for(unsigned int i=0; i<_w*_h; _map[i++]=h);} - void operator=(HeightMap hm) {for(unsigned int i=0; i<_w*_h; i++)_map[i]=hm(i);} - void init(unsigned int width, unsigned int height); - void makeStamp(unsigned int radius); /// generates the stamp (smooth 0 to 1 gradient lookup to generate craters, islands and rivers) - inline void lower(unsigned int coordX, unsigned int coordY); /// lower lowers the region around (coordX,coordY) to min(stamp,map) - inline void differenceStamp(unsigned int coordX, unsigned int coordY); /// multiplyStamp sets the region around (coordX,coordY) to (1-stamp)*map if map>0 and to 1-stamp else to bias away from other hills. - inline void addNoise(float weight, float smoothingFactor); /// adds noise to the map: map=noise*weight+map*(1-weight) - void stampOutput(char * filename); /// generates the file ~/.glob2/filename and writes the raw 0..255 values of stamp to it. to see it, use convert -size [width]x[height] -depth 8 gray:[filename] test.png where with==height as _stamp is always a square - void normalize(); /// fits the values of _map to [0, 1] -}; diff --git a/src/map/generator/MapGenerationDescriptor.cpp b/src/map/generator/MapGenerationDescriptor.cpp deleted file mode 100644 index 76ece7442..000000000 --- a/src/map/generator/MapGenerationDescriptor.cpp +++ /dev/null @@ -1,372 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include -#include -#include - -#include "MapGenerationDescriptor.h" -#include "Marshaling.h" -#include "Utilities.h" - -namespace -{ -using D = MapGenerationDescriptor; -using C = D::Control; -using G = D::ControlGroup; -// Physical descriptor fields have one base definition. Per-method overrides below -// supply the measured recipe, and are also the controls displayed by both UIs. -const std::vector baseControls = { - {"Water weight", &D::waterRatio, 0, 100, 1, 50}, - {"Sand weight", &D::sandRatio, 0, 100, 1, 0}, - {"Grass weight", &D::grassRatio, 0, 100, 1, 50}, - {"Desert weight", &D::desertRatio, 0, 100, 1, 0}, - {"Smoothing", &D::smooth, 1, 8, 1, 4}, - {"River width", &D::riverDiameter, 20, 65, 5, 50}, - {"Extra islands", &D::extraIslands, 0, 8, 1, 0}, - {"Lake density", &D::craterDensity, 10, 50, 5, 50}, - {"Island size", &D::oldIslandSize, 50, 70, 1, 65}, - {"Beach size", &D::oldBeach, 0, 4, 1, 1}, - {"Fruit", &D::fruitRatio, 0, 64, 1, 4, G::Resources}, - {"Repeat landscape", nullptr, 0, 5, 1, 0, G::Layout, true}, -}; -const std::array, D::METHOD_COUNT> methodControls = [] -{ - std::array, D::METHOD_COUNT> result; - for (int m = 1; m < D::METHOD_COUNT; ++m) - { - auto &controls = result[m]; - for (const auto &c : baseControls) - { - bool include = false; - if (m <= D::eCRATERLAKES || m == D::eOLDRANDOM) - { - include = - c.field == &D::waterRatio || c.field == &D::grassRatio || c.field == &D::smooth; - include |= m != D::eSWAMP && c.field == &D::sandRatio; - include |= m >= D::eRIVER && m <= D::eCRATERLAKES && c.field == &D::desertRatio; - include |= - m <= D::eCRATERLAKES && (c.group == G::Resources || c.group == G::Layout); - } - include |= m == D::eRIVER && c.field == &D::riverDiameter; - include |= m == D::eISLANDS && c.field == &D::extraIslands; - include |= m == D::eCRATERLAKES && c.field == &D::craterDensity; - include |= - m == D::eOLDISLANDS && (c.field == &D::oldIslandSize || c.field == &D::oldBeach); - if (include) - controls.push_back(c); - } - } - auto preset = [&](D::Method m, Sint32 D::*field, int value) - { - for (auto &c : result[m]) - if (c.field == field) - c.defaultValue = value; - }; - preset(D::eSWAMP, &D::waterRatio, 35); - preset(D::eSWAMP, &D::grassRatio, 60); - preset(D::eSWAMP, &D::smooth, 6); - preset(D::eRIVER, &D::waterRatio, 45); - preset(D::eRIVER, &D::sandRatio, 3); - preset(D::eRIVER, &D::grassRatio, 75); - preset(D::eRIVER, &D::riverDiameter, 35); - preset(D::eISLANDS, &D::waterRatio, 55); - preset(D::eISLANDS, &D::sandRatio, 3); - preset(D::eISLANDS, &D::grassRatio, 75); - preset(D::eCRATERLAKES, &D::waterRatio, 25); - preset(D::eCRATERLAKES, &D::sandRatio, 3); - preset(D::eCRATERLAKES, &D::grassRatio, 75); - preset(D::eCRATERLAKES, &D::smooth, 6); - preset(D::eCRATERLAKES, &D::craterDensity, 25); - result[D::eCRATERLAKES].push_back({"Lake size", &D::riverDiameter, 10, 40, 5, 25}); - result[D::eCONCRETEISLANDS] = {{"Channel width", &D::riverDiameter, 5, 8, 1, 5}, - {"Extra islands", &D::extraIslands, 0, 6, 1, 3}}; - result[D::eISLES] = {{"Island size", &D::grassRatio, 45, 65, 5, 60}, - {"Land bridge width", &D::riverDiameter, 3, 6, 1, 4}}; - preset(D::eOLDRANDOM, &D::waterRatio, 40); - preset(D::eOLDRANDOM, &D::sandRatio, 4); - preset(D::eOLDRANDOM, &D::grassRatio, 60); - preset(D::eOLDRANDOM, &D::smooth, 3); - for (auto &controls : result) - std::stable_sort(controls.begin(), controls.end(), - [](const C &a, const C &b) { return a.group < b.group; }); - return result; -}(); -} // namespace - -int MapGenerationDescriptor::Control::get(const MapGenerationDescriptor &d) const -{ - return field ? d.*field : static_cast(d.logRepeatAreaTimes); -} -int MapGenerationDescriptor::Control::normalize(int value) const -{ - value = std::clamp(value, minimum, maximum); - return std::min(maximum, minimum + ((value - minimum + step / 2) / step) * step); -} -void MapGenerationDescriptor::Control::set(MapGenerationDescriptor &d, int value) const -{ - if (field) - d.*field = normalize(value); - else - d.logRepeatAreaTimes = normalize(value); -} -const std::vector & -MapGenerationDescriptor::controls(Method method) -{ - assert(method >= eUNIFORM && method <= eOLDISLANDS); - return methodControls[method]; -} -const std::vector &MapGenerationDescriptor::sharedControls() -{ - static const std::vector shared = { - {"Width", &D::wDec, 6, 9, 1, 7, G::Shared, true}, - {"Height", &D::hDec, 6, 9, 1, 7, G::Shared, true}, - {"Colonies", &D::nbTeams, 1, Team::MAX_COUNT, 1, 4, G::Shared}, - {"Starting workers", &D::nbWorkers, 1, 8, 1, 4, G::Shared}}; - return shared; -} -const MapGenerationDescriptor::Control &MapGenerationDescriptor::control(Method method, - const char *label) -{ - for (const auto &c : controls(method)) - if (std::strcmp(c.label, label) == 0) - return c; - for (const auto &c : sharedControls()) - if (std::strcmp(c.label, label) == 0) - return c; - throw std::logic_error("Unknown map generator control"); -} -const char *MapGenerationDescriptor::methodName(Method method) -{ - static const char *names[] = {"uniform terrain", "Swamp", "River", - "Islands", "Crater lakes", "Concrete islands", - "Isles", "Old random", "Old islands"}; - assert(method >= eUNIFORM && method <= eOLDISLANDS); - return names[method]; // Stable translation keys; English values carry the new names. -} -MapGenerationDescriptor::MapGenerationDescriptor() -{ - terrainType=GRASS; - for (const auto &c : sharedControls()) - c.set(*this, c.defaultValue); - setMethodDefaults(eUNIFORM); - for (int i = 0; i < MAX_NB_RESOURCES; ++i) - resource[i] = 7; -} -void MapGenerationDescriptor::setMethodDefaults(Method newMethod) -{ - method = newMethod; - for (const auto &c : baseControls) - c.set(*this, c.defaultValue); - // These legacy serialized fields are unused by resource placement. Do not - // advertise inert sliders; retain their historic values for descriptor compatibility. - wheatRatio = woodRatio = stoneRatio = algaeRatio = 50; - for (const auto &c : controls(method)) - c.set(*this, c.defaultValue); -} -bool MapGenerationDescriptor::hasTerrainWeight() const -{ - if (method == eSWAMP) - return waterRatio + grassRatio > 0; - if (method == eOLDRANDOM) - return waterRatio + sandRatio + grassRatio > 0; - if (method >= eRIVER && method <= eCRATERLAKES) - return waterRatio + sandRatio + grassRatio + desertRatio > 0; - return true; -} -MapGenerationHistory::MapGenerationHistory() -{ - for (int m = 0; m <= MapGenerationDescriptor::eOLDISLANDS; ++m) - settings[m].setMethodDefaults(static_cast(m)); -} -void MapGenerationHistory::select(MapGenerationDescriptor ¤t, - MapGenerationDescriptor::Method method) -{ - if (method == current.method) - return; - assert(method >= 0 && method <= MapGenerationDescriptor::eOLDISLANDS); - settings[current.method] = current; - auto next = settings[method]; - for (const auto &c : MapGenerationDescriptor::sharedControls()) - c.set(next, c.get(current)); - next.terrainType = current.terrainType; - current = next; -} - -MapGenerationDescriptor::~MapGenerationDescriptor() -{ - // Tuut-tuut bom-bom -} - -Uint8 *MapGenerationDescriptor::getData() -{ - assert(DATA_SIZE==100+MAX_NB_RESOURCES*4); - - addSint32(data, wDec, 0); - addSint32(data, hDec, 4); - - addSint32(data, (Sint32)terrainType, 8); - - addSint32(data, (Sint32)method, 12); - addSint32(data, waterRatio, 16); - addSint32(data, sandRatio, 20); - addSint32(data, grassRatio, 24); - addSint32(data, desertRatio, 28); - - addSint32(data, wheatRatio, 32); - addSint32(data, woodRatio, 36); - addSint32(data, algaeRatio, 40); - addSint32(data, stoneRatio, 44); - addSint32(data, riverDiameter, 48); - - addSint32(data, craterDensity, 52); - addSint32(data, extraIslands, 56); - addSint32(data, smooth, 60); - - addUint32(data, nbWorkers, 64); - addUint32(data, nbTeams, 68); - - addUint32(data, oldIslandSize, 72); - addUint32(data, oldBeach, 76); - addUint32(data, fruitRatio, 80); - - addUint32(data, logRepeatAreaTimes, 84); - - for (unsigned i=0; i=32) - good=false; - if (hDec>=32) - good=false; - if (terrainType>GRASS) - good=false; - - return (good); -} - -void MapGenerationDescriptor::save(GAGCore::OutputStream *stream) -{ - stream->writeEnterSection("MapGenerationDescriptor"); - stream->write("MgdB", 4, "signatureStart"); - stream->write(getData(), DATA_SIZE, "data"); - stream->write("MgdE", 4, "signatureEnd"); - stream->writeLeaveSection(); -} - -bool MapGenerationDescriptor::load(GAGCore::InputStream *stream, Sint32 versionMinor) -{ - stream->readEnterSection("MapGenerationDescriptor"); - char signature[4]; - stream->read(signature, 4, "signatureStart"); - if (memcmp(signature, "MgdB", 4) != 0) - { - stream->readLeaveSection(); - return false; - } - stream->read(getData(), DATA_SIZE, "data"); - setData(data, DATA_SIZE); - stream->read(signature, 4, "signatureEnd"); - stream->readLeaveSection(); - if (memcmp(signature, "MgdE", 4) != 0) - return false; - return true; -} - -Uint32 MapGenerationDescriptor::checkSum() -{ - Uint32 cs=0; - - cs^=wDec+(hDec<<16); - cs^=(Sint32)terrainType; - cs=rotr1(cs); - cs^=(Sint32)method; - cs=rotr1(cs); - cs ^= waterRatio; - cs=rotr1(cs); - cs ^= sandRatio; - cs=rotr1(cs); - cs ^= grassRatio; - cs=rotr1(cs); - cs ^= desertRatio; - cs=rotr1(cs); - cs ^= wheatRatio; - cs=rotr1(cs); - cs ^= fruitRatio; - cs=rotr1(cs); - cs ^= woodRatio; - cs=rotr1(cs); - cs ^= algaeRatio; - cs=rotr1(cs); - cs ^= stoneRatio; - cs=rotr1(cs); - cs ^= riverDiameter; - cs=rotr1(cs); - cs ^= craterDensity; - cs=rotr1(cs); - cs ^= extraIslands; - cs=rotr1(cs); - cs ^= smooth; - cs=rotr1(cs); - cs ^= oldIslandSize; - cs=rotr1(cs); - cs ^= oldBeach; - cs=rotr1(cs); - cs ^= logRepeatAreaTimes; - - for (unsigned i=0; i -#include - -namespace GAGCore -{ - class InputStream; - class OutputStream; -} - -class MapGenerationDescriptor -{ -public: - MapGenerationDescriptor(); - virtual ~MapGenerationDescriptor(void); - - Uint8 *getData(); - bool setData(const Uint8 *data, int dataLength); - int getDataLength() {return DATA_SIZE; } - - void save(GAGCore::OutputStream *stream); - bool load(GAGCore::InputStream *stream, Sint32 versionMinor); - Uint32 checkSum(); - -public: - TerrainType terrainType; - enum Method - { - /// No terrain (terrain undefined) - eNONE=-1, - /// Uniform terrain (all of one type. completely unstructured) - eUNIFORM=0, - /// swamp-like terrain with water here and land there - eSWAMP=1, - /// a more or less winding river - eRIVER=2, - /// islands that have organic shape and no passage from one to the next - eISLANDS=3, - /// all connected land with round lakes - eCRATERLAKES=4, - eCONCRETEISLANDS=5, - eISLES=6, - eOLDRANDOM=7, - eOLDISLANDS=8 - }; - static constexpr int METHOD_COUNT = eOLDISLANDS + 1; - /// Set this generator's terrain controls without changing map size or teams. - void setMethodDefaults(Method newMethod); - - // UI-independent definitions shared by the lobby, editor, presets and validation. - enum class ControlGroup - { - Terrain, - Resources, - Layout, - Shared - }; - struct Control - { - const char *label; - Sint32 MapGenerationDescriptor::*field; // null only for unsigned repeat exponent - int minimum, maximum, step, defaultValue; - ControlGroup group = ControlGroup::Terrain; - bool powerOfTwo = false; - int get(const MapGenerationDescriptor &descriptor) const; - void set(MapGenerationDescriptor &descriptor, int value) const; - int normalize(int value) const; - }; - static const std::vector &controls(Method method); - static const std::vector &sharedControls(); - static const Control &control(Method method, const char *label); - static const char *methodName(Method method); - bool hasTerrainWeight() const; - - Method method; - - Sint32 wDec, hDec; - - Sint32 waterRatio, sandRatio, grassRatio, desertRatio, wheatRatio, - woodRatio, fruitRatio, algaeRatio, stoneRatio, riverDiameter, craterDensity, extraIslands; - Sint32 oldIslandSize, oldBeach; - // Mode-specific reuse keeps the existing serialized layout: - // riverDiameter = river diameter / lake size / channel width / bridge width; - // grassRatio = island size for Isles; extraIslands = neutral count for Concrete Islands. - Sint32 smooth; - Sint32 resource[MAX_NB_RESOURCES]; - ///n=2^n-times the same landscape. So 0=all random. - Uint32 logRepeatAreaTimes; - - Sint32 nbTeams, nbWorkers; -public: - // Those may not be in data - Sint32 bootX[Team::MAX_COUNT]{}; - Sint32 bootY[Team::MAX_COUNT]{}; - -public: - enum {DATA_SIZE=100+MAX_NB_RESOURCES*4}; -protected: - //! Serialized form of MapGenerationDescriptor - Uint8 data[DATA_SIZE]{}; -}; - -// Remember terrain edits per method while sharing map size, teams and workers. -class MapGenerationHistory -{ - std::array settings; - - public: - MapGenerationHistory(); - void select(MapGenerationDescriptor ¤t, MapGenerationDescriptor::Method method); -}; +#include "LegacyGenerationDescriptor.h" diff --git a/src/map/generator/MapGenerator.h b/src/map/generator/MapGenerator.h index 57973c4b2..6b2d99380 100644 --- a/src/map/generator/MapGenerator.h +++ b/src/map/generator/MapGenerator.h @@ -1,121 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2008 Bradley Arsenault -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - #pragma once - +#include "GenerationContext.h" +#include "GenerationService.h" +#include "LegacyGenerationDescriptor.h" #include - -#include "MapGenerationDescriptor.h" - -class MapGeneratorPoint; -class Building; - -///This class handles the new algorithms for map generation class MapGenerator { -public: - ///Generates a map from the given generation description - // An explicit sync seed is used by reproducible studies. Normal callers retain - // random_device seeding; height-map generators manage their own RNG streams. - bool generateMap(Game& game, MapGenerationDescriptor& descriptor, - std::optional syncSeed = std::nullopt); - - ///This performs the concrete islands generator - bool computeConcreteIslands(Game& game, MapGenerationDescriptor& descriptor); - - ///This performs the isles generator - bool computeIsles(Game& game, MapGenerationDescriptor& descriptor); - - ///This function divides up the player lands using the standard method - bool divideUpPlayerLands(Game& game, MapGenerationDescriptor& descriptor, std::vector& grid, std::vector& teamAreaNumbers, int& areaNumber); - -private: - ///This is a function that takes an area and divides it up into several smaller areas using the - ///given area numbers and weights. This is basically a combination of splitUpPoints and splitUpArea - bool divideUpArea(Game& game, std::vector& grid, int areaN, std::vector& weights, std::vector& areaNumbers); - - ///This creates an area with the shape of an oval with the given width and height - void createOval(Game& game, std::vector& grid, int areaN, int x, int y, int width, int height); - - ///This function takes the grid, an area number, and places points in it such that the points are spaced - ///as far from each other as possible, bearing in mind weights. Returns 0 if failure, otherwise returns - ///the minimum distance between points that was accomplished - int splitUpPoints(Game& game, std::vector& grid, int areaN, std::vector& points, std::vector& weights); - - ///This function takes a grid and an area number, and divides that area into more areas - void splitUpArea(Game& game, std::vector& grid, int areaN, std::vector& points, std::vector& weights, std::vector& areaNumbers, bool grassOnly=false); - - ///This function fills the vector with all of the points in a specific area - void getAllPoints(Game& game, std::vector& grid, int areaN, std::vector& points); - - ///This function fills the vector with all of points except those in a specific area - void getAllOtherPoints(Game& game, std::vector& grid, int areaN, std::vector& points); - - ///This function gets all of the points in a straight line from x1,y1 to x2,y2 - void getAllPointsLine(Game& game, int x1, int y1, int x2, int y2, std::vector& points); - - ///This function computes all of points that are borders - void findBorderPoints(Game& game, std::vector& grid, std::vector& points); - - ///This function fills all given points area with a certain resource. It will fill in a randomly sized - ///square over each grid space no larger than maxFillSize - void fillInResource(Game& game, std::vector& points, int resourceType, int maxFillSize); - - ///This chooses n-random squares from a the points vector, and eliminates the rest - void chooseRandomPoints(Game& game, std::vector& points, int n); - - ///This function chooses all points that would be free for the building to build on, eliminates the rest - void chooseFreeForBuildingSquares(Game& game, std::vector& points, BuildingType* type, int team); - - ///This function chooses all the points that would be free for ground units - void chooseFreeForGroundUnits(Game& game, std::vector& points, int team); - - ///This function chooses all the points that are bordering on the given building - void chooseTouchingBuilding(Game& game, std::vector& points, Building* building); - - ///This function adjusts the heightmap value of point given by the given value - void adjustHeightmapFromPoints(Game& game, std::vector& points, std::vector& heightmap, int value); - - ///This function adjusts the heightmap values from a standard perlin noise. Spread is how much the value can go up or down - void adjustHeightmapFromPerlinNoise(Game& game, std::vector& heights, int spread); - - ///This function computes the distance of every point from the given points, putting these distances - ///into the given heightmap. The points given as sources are considered to be a distance of 1. The points - ///given as obstacles are considered to be a distance of -1 - void computeDistances(Game& game, std::vector& sources, std::vector& obstacles, std::vector& heightmap); - - ///Computes the average height/distance of a area on a heightmap - int computeAverageDistance(Game& game, std::vector& grid, int areaN, const std::vector& heightmap); - - ///Adds a building to the map with the given typenum, level, under construction, team and location. - ///Returns the pointer if it could, NULL otherwise - Building* addBuilding(Game& game, int x, int y, int team, int typenum, int level, bool underConstruction); - - static const bool verbose=false; -}; - - -class ListComparator -{ -public: - ListComparator(std::vector& list) : list(list) {} - - bool operator()(int lhs, int rhs) + public: + bool generateMap(Game &game, const MapGenerationDescriptor &d, + std::optional seed = std::nullopt) { - return list[lhs] < list[rhs]; + return bool(GenerationService().generate( + game, fromLegacyDescriptor(d, seed ? *seed : GenerationContext::randomSeed()))); + } + bool generateMap(Game &game, const GenerationRequest &r) + { + return bool(GenerationService().generate(game, r)); } - - std::vector& list; -}; - - - -///This is a single point on the map, used by the map generator -class MapGeneratorPoint -{ -public: - MapGeneratorPoint(int x, int y) : x(x), y(y) {} - int x; - int y; }; diff --git a/src/map/generator/MapHomogen.cpp b/src/map/generator/MapHomogen.cpp deleted file mode 100644 index 384f8212e..000000000 --- a/src/map/generator/MapHomogen.cpp +++ /dev/null @@ -1,236 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Map.h" -#include "Unit.h" -#include "Utilities.h" - -///generates a map that is of one terrain type only -void Map::makeHomogenMap(TerrainType terrainType) -{ - for (int y=0; y=0)&&(d<40)&&(syncRand()&4)) - { - if (l<=(int)(syncRand()&3)) - setTerrain(x, y, d+273); - else - { - // we extend resource: - int dx, dy; - Unit::dxDyFromDirection(syncRand()&7, &dx, &dy); - int nx=x+dx; - int ny=y+dy; - if (getGroundUnit(nx, ny)==NOGUID) - if (((r==WOOD||r==CORN||r==STONE)&&isGrass(nx, ny))||((r==ALGA)&&isWater(nx, ny))) - setTerrain(nx, ny, 272+(r*10)+((syncRand()&1)*5)); - } - } - } -} - -void simulateRandomMap(int smooth, double baseWater, double baseSand, double baseGrass, double *finalWater, double *finalSand, double *finalGrass) -{ - int w=32<<(smooth>>2); - int h=w; - int s=w*h; - int m=s-1; - std::vector undermap(w*h); - - int totalRatio=0x7FFF; - int waterRatio=(int)(baseWater*((double)totalRatio)); - int sandRatio =(int)(baseSand *((double)totalRatio)); - int grassRatio=(int)(baseGrass*((double)totalRatio)); - totalRatio=waterRatio+sandRatio+grassRatio; - - if(totalRatio==0) - { - waterRatio = 1; - sandRatio = 1; - grassRatio = 1; - totalRatio = 3; - } - - - /// First, we create a fully random patchwork: - for (int y=0; y -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "MapGenerationDescriptor.h" -#include "Map.h" -#include "Utilities.h" - -bool Map::oldMakeIslandsMap(MapGenerationDescriptor &descriptor) -{ - // First, fill with water: - for (int y=0; y65536) - { - minDistSquare=minDistSquare>>1; - //I think that you need to do this only once, in worst case. - //With a few luck you doesn't need to. - c=0; - - } - } - else - { - bootX[i]=x; - bootY[i]=y; - for (int dx=-1; dx<6; dx++) - for (int dy=0; dy<6; dy++) - setUMTerrain(x+dx, y+dy, GRASS); - } - } - - - - // Three, expands islands - for (int s=0; s - -#include "MapGenerationDescriptor.h" -#include "Map.h" - -void Map::oldAddResourcesIslandsMap(MapGenerationDescriptor &descriptor) -{ - int *bootX=descriptor.bootX; - int *bootY=descriptor.bootY; - - int islandsSize=(int)(((w+h)*descriptor.oldIslandSize)/(400.0*sqrt((double)descriptor.nbTeams))); - if (islandsSize<8) - islandsSize=8; - // let's add resources... - int smoothResources=islandsSize/4; - for (int s=0; s0) - setResource(bootX[s], bootY[s]-p, WOOD, amount); - smallestAmount=amount; - smallestResource=WOOD; - - //WHEAT - for (d=0; d0) - setResource(bootX[s]-p, bootY[s], CORN, amount); - if (amount0) - setResource(bootX[s]+p, bootY[s]+p, smallestResource, amount); - - //ALGAE - for (d=0; d<2*islandsSize; d++) - if (isWater(bootX[s]+d, bootY[s])) - break; - amount=descriptor.resource[ALGA]; - amount=smoothResources; - p=d+smoothResources-1+amount/2; - if (amount>0) - setResource(bootX[s]+p, bootY[s], ALGA, amount); - } - - // Let's smooth resources... - this->smoothResources(smoothResources*2); -} diff --git a/src/map/generator/MapOldRandom.cpp b/src/map/generator/MapOldRandom.cpp deleted file mode 100644 index 90e1fa5db..000000000 --- a/src/map/generator/MapOldRandom.cpp +++ /dev/null @@ -1,454 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière - -#include -#include -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "MapGenerationDescriptor.h" -#include "Map.h" - -void simulateRandomMap(int smooth, double baseWater, double baseSand, double baseGrass, double *finalWater, double *finalSand, double *finalGrass); -#include "Utilities.h" - -bool Map::oldMakeRandomMap(MapGenerationDescriptor &descriptor) -{ - int waterRatio=descriptor.waterRatio; - int sandRatio =descriptor.sandRatio ; - int grassRatio=descriptor.grassRatio; - int totalRatio=waterRatio+sandRatio+grassRatio; - int smooth=descriptor.smooth; - double baseWater, baseSand, baseGrass; - - if (totalRatio == 0) - { - baseWater = baseSand = baseGrass = 1.0/3.0; - } - else - { - baseWater=(float)waterRatio/(float)totalRatio; - baseSand =(float)sandRatio /(float)totalRatio; - baseGrass=(float)grassRatio/(float)totalRatio; - } - //Sorry, the equation is too complex for me. We use a numeric approach: - double alphaWater=baseWater; - double alphaSand =baseSand ; - double alphaGrass=baseGrass; - double alphaSum=alphaWater+alphaSand+alphaGrass; - alphaWater/=alphaSum; - alphaSand /=alphaSum; - alphaGrass/=alphaSum; - - for (int r=1; r<=smooth; r++) - { - for (int prec=0; prec<3; prec++) - { - double finalAlphaWater, finalAlphaSand, finalAlphaGrass; - simulateRandomMap(r, alphaWater, alphaSand, alphaGrass, &finalAlphaWater, &finalAlphaSand, &finalAlphaGrass); - - - double errAlphaWater=finalAlphaWater-baseWater; - double errAlphaSand =finalAlphaSand -baseSand ; - double errAlphaGrass=finalAlphaGrass-baseGrass; - - - double betaWater; - double betaSand ; - double betaGrass; - - if (finalAlphaWater) - betaWater=(alphaWater*baseWater)/finalAlphaWater; - else - betaWater=0; - if (finalAlphaSand) - betaSand =(alphaSand *baseSand )/finalAlphaSand ; - else - betaSand=0; - if (finalAlphaGrass) - betaGrass=(alphaGrass*baseGrass)/finalAlphaGrass; - else - betaGrass=0; - double betaSum=betaWater+betaSand+betaGrass; - betaWater/=betaSum; - betaSand /=betaSum; - betaGrass/=betaSum; - - double finalBetaWater, finalBetaSand, finalBetaGrass; - simulateRandomMap(r, betaWater, betaSand, betaGrass, &finalBetaWater, &finalBetaSand, &finalBetaGrass); - - - double errBetaWater=finalBetaWater-baseWater; - double errBetaSand =finalBetaSand -baseSand ; - double errBetaGrass=finalBetaGrass-baseGrass; - - - double projNom=(errBetaWater*errAlphaWater+errBetaSand*errAlphaSand+errBetaGrass*errAlphaGrass); - double projDen=(errAlphaWater*errAlphaWater+errAlphaSand*errAlphaSand+errAlphaGrass*errAlphaGrass); - if (projDen<=0) - continue; - double proj=projNom/projDen; - - - double minErr=DBL_MAX; - for (double cfi=0.0; cfi<=1.0; cfi+=0.1) - { - double cf=cfi*proj; - - double sumCenter=1.0-cf; - double gammaWater=(-cf*betaWater+1.0*alphaWater)/sumCenter; - double gammaSand =(-cf*betaSand +1.0*alphaSand )/sumCenter; - double gammaGrass=(-cf*betaGrass+1.0*alphaGrass)/sumCenter; - if (gammaWater<0.0) - gammaWater=0.0; - if (gammaSand<0.0) - gammaSand=0.0; - if (gammaGrass<0.0) - gammaGrass=0.0; - double gammaSum=gammaWater+gammaSand+gammaGrass; - if (gammaSum<=0) - continue; - gammaWater/=gammaSum; - gammaSand /=gammaSum; - gammaGrass/=gammaSum; - - double finalGammaWater, finalGammaSand, finalGammaGrass; - simulateRandomMap(r, gammaWater, gammaSand, gammaGrass, &finalGammaWater, &finalGammaSand, &finalGammaGrass); - - - double errGammaWater=finalGammaWater-baseWater; - double errGammaSand =finalGammaSand -baseSand ; - double errGammaGrass=finalGammaGrass-baseGrass; - double errGamma=(errGammaWater*errGammaWater+errGammaSand*errGammaSand+errGammaGrass*errGammaGrass); - - if (errGamma0) - allowed[0]=(Uint32)(pow(errWaterRatioCount, 0.125)*4294967296.0); - else - allowed[0]=0; - if (errSandRatioCount>0) - allowed[1]=(Uint32)(pow(errSandRatioCount , 0.125)*4294967296.0); - else - allowed[1]=0; - if (errGrassRatioCount>0) - allowed[2]=(Uint32)(pow(errGrassRatioCount, 0.125)*4294967296.0); - else - allowed[2]=0; - - assert(allowed[0]<=(Uint32)0xFFFFFFFF); - assert(allowed[1]<=(Uint32)0xFFFFFFFF); - assert(allowed[2]<=(Uint32)0xFFFFFFFF); - - if (i==0) - { - allowed[0]=0; - allowed[1]=0; - allowed[2]=0; - } - - for (int y=0; y0); - int* bootX=descriptor.bootX; - int* bootY=descriptor.bootY; - - //TODO: First pass to find the number of available places. - for (int team=0; team7) - { - int centerX=((x+startX)>>1); - int top, bot; - for (top=0; top0); - - int centerY=y+((bot-top)>>1); - bool farEnough=true; - for (int ti=0; timaxSurface && farEnough) - { - maxSurface=surface; - maxX=centerX; - maxY=centerY; - } - } - width=0; - startX=x; - } - } - } - - if (maxSurface<=0) - return false; - assert(maxSurface); - bootX[team]=maxX; - bootY[team]=maxY; - - for (int dx=-1; dx<6; dx++) - for (int dy=0; dy<6; dy++) - setUMTerrain(maxX+dx, maxY+dy, GRASS); - - - } - - // Let's add some green space for teams: - int squareSize=5+(int)(sqrt((double)minDistSquare)/4.5); - for (int team=0; team - -#include "MapGenerationDescriptor.h" -#include "Map.h" -#include "Unit.h" - -void Map::oldAddResourcesRandomMap(MapGenerationDescriptor &descriptor) -{ - int *bootX=descriptor.bootX; - int *bootY=descriptor.bootY; - int nbTeams=descriptor.nbTeams; - int limitDist=(w+h)/(2*nbTeams); - - // let's add resources to old map generator - for (int team=0; team3) - break; - else - width=1; - - if (dist*distWeight[resI]+width*widthWeight[resI]>maxDist*distWeight[resI]+maxWidth*widthWeight[resI]) - { - maxWidth=width; - maxDist=dist; - maxDir=dir; - } - } - dirUsed[maxDir]=true; - if (maxWidth>1); - dx*=d; - dy*=d; - - int amount=descriptor.resource[res]; - if (amount>0) - setResource(bootX[team]+dx, bootY[team]+dy, res, amount); - } - - if (smallestWidth3) - break; - else - width=1; - - if (dist+width>maxDist+maxWidth) - { - maxWidth=width; - maxDist=dist; - maxDir=dir; - } - } - dirUsed[maxDir]=true; - - int dx, dy; - Unit::dxDyFromDirection(maxDir, &dx, &dy); - int d=maxDist-(maxWidth>>1); - dx*=d; - dy*=d; - - int amount=descriptor.resource[smallestResource]; - if (amount>0) - setResource(bootX[team]+dx, bootY[team]+dy, smallestResource, amount); - } - - int maxDir=0; - int maxWidth=0; - int maxDist=0; - for (int dir=0; dir<8; dir++) - { - int width=0; - int dx, dy, dist; - Unit::dxDyFromDirection(dir, &dx, &dy); - for (dist=0; dist<2*limitDist; dist++) - if (isWater(bootX[team]+dx*dist, bootY[team]+dy*dist)) - width++; - else if (width>3) - break; - else - width=1; - - if (dist+width>width+maxWidth) - { - maxWidth=width; - maxDist=dist; - maxDir=dir; - } - } - - int dx, dy; - Unit::dxDyFromDirection(maxDir, &dx, &dy); - int d=maxDist-(maxWidth>>1); - dx*=d; - dy*=d; - - int amount=descriptor.resource[ALGA]; - if (amount>0) - setResource(bootX[team]+dx, bootY[team]+dy, ALGA, amount); - } - - // Let's smooth resources... - int maxAmount=0; - for (int r=0; r<4; r++) - if (maxAmount -#include -#include - -//also the Perlin Noise stuff uses random that is not based on syncRand -#include "Game.h" -#include "HeightMapGenerator.h" -#include "MapGenerationDescriptor.h" -#include "Map.h" - -/// This random map generator generates a height field and then chooses levels separating water, sand, grass, and desert. -bool Map::makeRandomMap(MapGenerationDescriptor &descriptor) -{ - /// all under waterLevel is water, under sandLevel is beach, under grassLevel is grass and above grasslevel is desert - float waterLevel, sandLevel, grassLevel, wheatWoodLevel, algaeLevel, stoneLevel; - /// to influence the roughness - float smoothingFactor=(float)(descriptor.smooth+4)*3; - /// the proportions requested through the gui can directly be translated into tile counts of the undermap. - unsigned int waterTiles, sandTiles, grassTiles, wheatWoodTiles, algaeTiles; - /// grass + sand + water + desert as from the gui - unsigned int totalGSWFromUI=descriptor.waterRatio+descriptor.sandRatio+descriptor.grassRatio+descriptor.desertRatio+descriptor.fruitRatio; - /// respect symmetry-requirements - unsigned int wPower2Divider=0, hPower2Divider=0; - int power2Divider=descriptor.logRepeatAreaTimes; - for (int i = 0; i> wPower2Divider) > (h >> hPower2Divider)) - wPower2Divider++; - else - hPower2Divider++; - int wRepeat = 1 << wPower2Divider; - int hRepeat = 1 << hPower2Divider; - unsigned int wHeightMap=(unsigned int)(w/wRepeat); - unsigned int hHeightMap=(unsigned int)(h/hRepeat); - /// lets generate a patch of perlin noise. That's a smooth mapping R^2 to ]0;1[ - HeightMap hm(wHeightMap,hHeightMap); - /// 1 to avoid division by zero, - unsigned int tmpTotal=1+descriptor.waterRatio+descriptor.grassRatio; - unsigned int sectionIslandCount=std::max(1u, static_cast((descriptor.nbTeams+descriptor.extraIslands) / (1 << power2Divider))); - switch (descriptor.method) - { - case MapGenerationDescriptor::eSWAMP: - hm.makeSwamp(smoothingFactor); - waterTiles=(unsigned int)((float)descriptor.waterRatio*wHeightMap*hHeightMap/(float)tmpTotal); - sandTiles=0; - grassTiles=wHeightMap*hHeightMap-waterTiles; - break; - case MapGenerationDescriptor::eRIVER: - hm.makeRiver(descriptor.riverDiameter*(wHeightMap+hHeightMap)/2/100,smoothingFactor); - waterTiles=(unsigned int)((float)descriptor.waterRatio/(float)totalGSWFromUI*wHeightMap*hHeightMap); - sandTiles=(unsigned int)((float)descriptor.sandRatio/(float)totalGSWFromUI*wHeightMap*hHeightMap); - grassTiles =(unsigned int)((float)descriptor.grassRatio /(float)totalGSWFromUI*wHeightMap*hHeightMap); - break; - case MapGenerationDescriptor::eCRATERLAKES: - // This mode reuses the diameter slot for the lake stamp radius. - // Preserve old descriptors whose unused diameter was 50. - hm.makeCraters(wHeightMap * hHeightMap * descriptor.craterDensity / 30000, - descriptor.riverDiameter == 50 - ? 30 - : MapGenerationDescriptor::control(descriptor.method, "Lake size") - .normalize(descriptor.riverDiameter), - smoothingFactor); - waterTiles=(unsigned int)((float)descriptor.waterRatio/(float)totalGSWFromUI*wHeightMap*hHeightMap); - sandTiles=(unsigned int)((float)descriptor.sandRatio/(float)totalGSWFromUI*wHeightMap*hHeightMap); - grassTiles =(unsigned int)((float)descriptor.grassRatio /(float)totalGSWFromUI*wHeightMap*hHeightMap); - break; - case MapGenerationDescriptor::eISLANDS: - hm.makeIslands(sectionIslandCount, smoothingFactor); - waterTiles=(unsigned int)((float)descriptor.waterRatio/(float)totalGSWFromUI*wHeightMap*hHeightMap); - sandTiles=(unsigned int)((float)descriptor.sandRatio/(float)totalGSWFromUI*wHeightMap*hHeightMap); - grassTiles =(unsigned int)((float)descriptor.grassRatio /(float)totalGSWFromUI*wHeightMap*hHeightMap); - break; - default: assert(false); - break; - } - /// wheat/wood needs ground to stand on and water. So: - wheatWoodTiles=waterTiles= algaeTiles) - algaeLevel = (float)(i-1)/2048.0; - if (accumulatedHistogram >= waterTiles) - waterLevel = (float)(i-1)/2048.0; - } - while ((sandLevel==0) && (i<2048)) - { - accumulatedHistogram+=histogram[i++]; - if (accumulatedHistogram >= waterTiles+sandTiles) - sandLevel = (float)(i-1)/2048.0; - } - while ((grassLevel==0) && (i<2048)) - { - accumulatedHistogram+=histogram[i++]; - if (wheatWoodLevel==0 && accumulatedHistogram >= waterTiles+sandTiles+wheatWoodTiles) - wheatWoodLevel = (float)(i-1)/2048.0; - if (stoneLevel==0 && accumulatedHistogram >= waterTiles+sandTiles+(wheatWoodTiles / 3)) - stoneLevel = (float)(i-1)/2048.0; - if (accumulatedHistogram >= waterTiles+sandTiles+grassTiles) - grassLevel = (float)(i-1)/2048.0; - } - for (unsigned y=0; y0); - int* bootX=descriptor.bootX; - int* bootY=descriptor.bootY; - - //TODO: First pass to find the number of available places. - for (int team=0; team7) - { - int centerX=((x+startX)>>1); - int top, bot; - for (top=0; top0); - - int centerY=y+((bot-top)>>1); - bool farEnough=true; - for (int ti=0; timaxSurface && farEnough) - { - maxSurface=surface; - maxX=centerX; - maxY=centerY; - } - } - width=0; - startX=x; - } - } - } - - if (maxSurface<=0) - { - return false; - } - assert(maxSurface); - bootX[team]=maxX; - bootY[team]=maxY; - } - - controlSand(); - regenerateMap(0, 0, w, h); - //now to add primary resources for current map generator - for (unsigned y=0; y 0) - { - for (int q1=0; q1 +#include +#include +#include +using D = MapGenerationDescriptor; +namespace +{ +Sint32 D::*legacyField(int method, const std::string &id) +{ + if (id == "water") + return &D::waterRatio; + if (id == "sand") + return &D::sandRatio; + if (id == "grass") + return &D::grassRatio; + if (id == "desert") + return &D::desertRatio; + if (id == "smoothing") + return &D::smooth; + if (id == "fruit") + return &D::fruitRatio; + if (id == "width") + return &D::wDec; + if (id == "height") + return &D::hDec; + if (id == "teams") + return &D::nbTeams; + if (id == "workers") + return &D::nbWorkers; + if (id == "river-width") + return &D::riverDiameter; + if (id == "lake-size") + return &D::riverDiameter; + if (id == "channel-width") + return &D::riverDiameter; + if (id == "bridge-width") + return &D::riverDiameter; + if (id == "lake-density") + return &D::craterDensity; + if (id == "extra-islands") + return &D::extraIslands; + if (id == "beach-size") + return &D::oldBeach; + if (id == "island-size") + return method == D::eISLES ? &D::grassRatio : &D::oldIslandSize; + if (id == "repeat") + return nullptr; + throw std::invalid_argument("Option has no legacy encoding: " + id); +} +} // namespace +int GeneratorControl::get(const D &d) const +{ + auto field = legacyField(d.method, id); + return field ? d.*field : d.logRepeatAreaTimes; +} +void GeneratorControl::set(D &d, int v) const +{ + auto field = legacyField(d.method, id); + if (field) + d.*field = normalize(v); + else + d.logRepeatAreaTimes = normalize(v); +} +const std::vector &D::controls(Method m) { return GenerationRequest::controls(m); } +const std::vector &D::sharedControls() { return sharedGeneratorControls(); } +const D::Control &D::control(Method m, const char *label) +{ + for (const auto &c : controls(m)) + if (std::strcmp(c.label, label) == 0) + return c; + for (const auto &c : sharedControls()) + if (std::strcmp(c.label, label) == 0) + return c; + throw std::invalid_argument("Unknown legacy control"); +} +const char *D::methodName(Method m) { return GenerationRequest::methodName(m); } +D::MapGenerationDescriptor() +{ + terrainType = GRASS; + for (const auto &c : sharedControls()) + c.set(*this, c.defaultValue); + setMethodDefaults(eUNIFORM); + std::fill(std::begin(resource), std::end(resource), 7); +} +D::~MapGenerationDescriptor() = default; +void D::setMethodDefaults(Method m) +{ + method = m; + waterRatio = grassRatio = 50; + sandRatio = desertRatio = 0; + smooth = 4; + riverDiameter = craterDensity = 50; + oldIslandSize = 65; + oldBeach = 1; + fruitRatio = 4; + extraIslands = 0; + logRepeatAreaTimes = 0; + wheatRatio = woodRatio = stoneRatio = algaeRatio = 50; + for (const auto &c : controls(m)) + c.set(*this, c.defaultValue); +} +bool D::hasTerrainWeight() const { return fromLegacyDescriptor(*this, 0).hasTerrainWeight(); } +GenerationRequest fromLegacyDescriptor(const D &d, std::uint32_t seed) +{ + GenerationRequest r; + r.method = d.method; + r.seed = seed; + r.terrainType = d.terrainType; + r.wDec = d.wDec; + r.hDec = d.hDec; + r.nbTeams = d.nbTeams; + r.nbWorkers = d.nbWorkers; + if (!GeneratorRegistry::builtins().find(d.method)) + return r; + for (const auto &c : D::controls(d.method)) + { + int value = c.get(d); + if (d.riverDiameter == 50) + { + if (c.id == "lake-size") + value = 30; + if (c.id == "channel-width") + value = 6; + if (c.id == "bridge-width") + value = 4; + } + // Historical negative neutral count requested a random count. Resolve at + // the adapter boundary from the explicit seed, within the supported range. + if (c.id == "extra-islands" && d.method == D::eCONCRETEISLANDS && value < 0) + value = + int(GenerationContext::deriveSeed(seed, "legacy-neutral-count") % (c.maximum + 1)); + r.options[c.id] = value; + } + std::copy(std::begin(d.resource), std::end(d.resource), r.resourceAmounts.begin()); + return r; +} +D toLegacyDescriptor(const GenerationRequest &r) +{ + D d; + d.setMethodDefaults(static_cast(r.method)); + d.terrainType = r.terrainType; + for (const auto &c : D::sharedControls()) + c.set(d, c.get(r)); + for (const auto &c : D::controls(d.method)) + c.set(d, c.get(r)); + std::copy(r.resourceAmounts.begin(), r.resourceAmounts.end(), d.resource); + return d; +} +Uint8 *MapGenerationDescriptor::getData() +{ + assert(DATA_SIZE == 100 + MAX_NB_RESOURCES * 4); + + addSint32(data, wDec, 0); + addSint32(data, hDec, 4); + + addSint32(data, (Sint32)terrainType, 8); + + addSint32(data, (Sint32)method, 12); + addSint32(data, waterRatio, 16); + addSint32(data, sandRatio, 20); + addSint32(data, grassRatio, 24); + addSint32(data, desertRatio, 28); + + addSint32(data, wheatRatio, 32); + addSint32(data, woodRatio, 36); + addSint32(data, algaeRatio, 40); + addSint32(data, stoneRatio, 44); + addSint32(data, riverDiameter, 48); + + addSint32(data, craterDensity, 52); + addSint32(data, extraIslands, 56); + addSint32(data, smooth, 60); + + addUint32(data, nbWorkers, 64); + addUint32(data, nbTeams, 68); + + addUint32(data, oldIslandSize, 72); + addUint32(data, oldBeach, 76); + addUint32(data, fruitRatio, 80); + + addUint32(data, logRepeatAreaTimes, 84); + + for (unsigned i = 0; i < MAX_NB_RESOURCES; i++) + addSint32(data, resource[i], 88 + i * 4); + + return data; +} + +bool MapGenerationDescriptor::setData(const Uint8 *data, int dataLength) +{ + assert(DATA_SIZE == 100 + MAX_NB_RESOURCES * 4); + assert(getDataLength() == DATA_SIZE); + assert(getDataLength() == dataLength); + + wDec = getSint32(data, 0); + hDec = getSint32(data, 4); + + terrainType = (TerrainType)getSint32(data, 8); + + method = (Method)getSint32(data, 12); + waterRatio = getSint32(data, 16); + sandRatio = getSint32(data, 20); + grassRatio = getSint32(data, 24); + desertRatio = getSint32(data, 28); + + wheatRatio = getSint32(data, 32); + woodRatio = getSint32(data, 36); + algaeRatio = getSint32(data, 40); + stoneRatio = getSint32(data, 44); + riverDiameter = getSint32(data, 48); + + craterDensity = getSint32(data, 52); + extraIslands = getSint32(data, 56); + smooth = getSint32(data, 60); + + nbWorkers = getSint32(data, 64); + nbTeams = getSint32(data, 68); + + oldIslandSize = getSint32(data, 72); + oldBeach = getSint32(data, 76); + + fruitRatio = getSint32(data, 80); + logRepeatAreaTimes = getSint32(data, 84); + + for (unsigned i = 0; i < MAX_NB_RESOURCES; i++) + resource[i] = getSint32(data, 88 + i * 4); + + bool good = true; + if (getDataLength() != dataLength) + good = false; + if (wDec >= 32) + good = false; + if (hDec >= 32) + good = false; + if (terrainType > GRASS) + good = false; + + return (good); +} + +void MapGenerationDescriptor::save(GAGCore::OutputStream *stream) +{ + stream->writeEnterSection("MapGenerationDescriptor"); + stream->write("MgdB", 4, "signatureStart"); + stream->write(getData(), DATA_SIZE, "data"); + stream->write("MgdE", 4, "signatureEnd"); + stream->writeLeaveSection(); +} + +bool MapGenerationDescriptor::load(GAGCore::InputStream *stream, Sint32 versionMinor) +{ + stream->readEnterSection("MapGenerationDescriptor"); + char signature[4]; + stream->read(signature, 4, "signatureStart"); + if (memcmp(signature, "MgdB", 4) != 0) + { + stream->readLeaveSection(); + return false; + } + stream->read(getData(), DATA_SIZE, "data"); + setData(data, DATA_SIZE); + stream->read(signature, 4, "signatureEnd"); + stream->readLeaveSection(); + if (memcmp(signature, "MgdE", 4) != 0) + return false; + return true; +} + +Uint32 MapGenerationDescriptor::checkSum() +{ + Uint32 cs = 0; + + cs ^= wDec + (hDec << 16); + cs ^= (Sint32)terrainType; + cs = rotr1(cs); + cs ^= (Sint32)method; + cs = rotr1(cs); + cs ^= waterRatio; + cs = rotr1(cs); + cs ^= sandRatio; + cs = rotr1(cs); + cs ^= grassRatio; + cs = rotr1(cs); + cs ^= desertRatio; + cs = rotr1(cs); + cs ^= wheatRatio; + cs = rotr1(cs); + cs ^= fruitRatio; + cs = rotr1(cs); + cs ^= woodRatio; + cs = rotr1(cs); + cs ^= algaeRatio; + cs = rotr1(cs); + cs ^= stoneRatio; + cs = rotr1(cs); + cs ^= riverDiameter; + cs = rotr1(cs); + cs ^= craterDensity; + cs = rotr1(cs); + cs ^= extraIslands; + cs = rotr1(cs); + cs ^= smooth; + cs = rotr1(cs); + cs ^= oldIslandSize; + cs = rotr1(cs); + cs ^= oldBeach; + cs = rotr1(cs); + cs ^= logRepeatAreaTimes; + + for (unsigned i = 0; i < MAX_NB_RESOURCES; i++) + cs += static_cast(resource[i]) << ((3 * i) % 32); + + cs = rotr1(cs); + cs ^= nbWorkers; + cs ^= nbTeams << 5; + + return cs; +} diff --git a/src/map/generator/compatibility/LegacyGenerationDescriptor.h b/src/map/generator/compatibility/LegacyGenerationDescriptor.h new file mode 100644 index 000000000..fe2cc565b --- /dev/null +++ b/src/map/generator/compatibility/LegacyGenerationDescriptor.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière + +#pragma once + +#include "GenerationRequest.h" +#include "GeneratorControls.h" +#include "Ressource.h" +#include "Team.h" +#include "TerrainType.h" +#include +#include + +namespace GAGCore +{ +class InputStream; +class OutputStream; +} // namespace GAGCore + +class MapGenerationDescriptor +{ + public: + MapGenerationDescriptor(); + virtual ~MapGenerationDescriptor(void); + + Uint8 *getData(); + bool setData(const Uint8 *data, int dataLength); + int getDataLength() { return DATA_SIZE; } + + void save(GAGCore::OutputStream *stream); + bool load(GAGCore::InputStream *stream, Sint32 versionMinor); + Uint32 checkSum(); + + public: + TerrainType terrainType; + using Method = GenerationRequest::Method; + static constexpr Method eNONE = GenerationRequest::eNONE; + static constexpr Method eUNIFORM = GenerationRequest::eUNIFORM; + static constexpr Method eSWAMP = GenerationRequest::eSWAMP; + static constexpr Method eRIVER = GenerationRequest::eRIVER; + static constexpr Method eISLANDS = GenerationRequest::eISLANDS; + static constexpr Method eCRATERLAKES = GenerationRequest::eCRATERLAKES; + static constexpr Method eCONCRETEISLANDS = GenerationRequest::eCONCRETEISLANDS; + static constexpr Method eISLES = GenerationRequest::eISLES; + static constexpr Method eOLDRANDOM = GenerationRequest::eOLDRANDOM; + static constexpr Method eOLDISLANDS = GenerationRequest::eOLDISLANDS; + static constexpr int METHOD_COUNT = eOLDISLANDS + 1; + /// Set this generator's terrain controls without changing map size or teams. + void setMethodDefaults(Method newMethod); + + using Control = GeneratorControl; + using ControlGroup = ::ControlGroup; + static const std::vector &controls(Method); + static const std::vector &sharedControls(); + static const Control &control(Method, const char *label); + static const char *methodName(Method); + bool hasTerrainWeight() const; + Method method; + + Sint32 wDec, hDec; + + Sint32 waterRatio, sandRatio, grassRatio, desertRatio, wheatRatio, woodRatio, fruitRatio, + algaeRatio, stoneRatio, riverDiameter, craterDensity, extraIslands; + Sint32 oldIslandSize, oldBeach; + // Mode-specific reuse keeps the existing serialized layout: + // riverDiameter = river diameter / lake size / channel width / bridge width; + // grassRatio = island size for Isles; extraIslands = neutral count for Concrete Islands. + Sint32 smooth; + Sint32 resource[MAX_NB_RESOURCES]; + /// n=2^n-times the same landscape. So 0=all random. + Uint32 logRepeatAreaTimes; + + Sint32 nbTeams, nbWorkers; + + public: + // Those may not be in data + Sint32 bootX[Team::MAX_COUNT]{}; + Sint32 bootY[Team::MAX_COUNT]{}; + + public: + enum + { + DATA_SIZE = 100 + MAX_NB_RESOURCES * 4 + }; + + protected: + //! Serialized form of MapGenerationDescriptor + Uint8 data[DATA_SIZE]{}; +}; + +GenerationRequest fromLegacyDescriptor(const MapGenerationDescriptor &, std::uint32_t seed); +MapGenerationDescriptor toLegacyDescriptor(const GenerationRequest &); diff --git a/src/map/generator/core/GenerationContext.cpp b/src/map/generator/core/GenerationContext.cpp new file mode 100644 index 000000000..cd7e3737e --- /dev/null +++ b/src/map/generator/core/GenerationContext.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "GenerationContext.h" +#include +std::uint32_t GenerationContext::deriveSeed(std::uint32_t seed, const std::string &name) +{ + // FNV-1a followed by an unsigned avalanche; independent of std::hash/platform. + std::uint32_t h = 2166136261u ^ seed; + for (unsigned char c : name) + { + h ^= c; + h *= 16777619u; + } + h ^= h >> 16; + h *= 0x7feb352du; + h ^= h >> 15; + h *= 0x846ca68bu; + h ^= h >> 16; + return h; +} +std::mt19937 &GenerationContext::stream(const std::string &name) +{ + return streams.try_emplace(name, deriveSeed(request.seed, name)).first->second; +} +std::uint32_t GenerationContext::randomSeed() { return std::random_device{}(); } + +std::uint32_t GenerationContext::bounded(const std::string &name, std::uint32_t bound) +{ + if (!bound) + throw std::invalid_argument("Empty random choice"); + const std::uint32_t threshold = -bound % bound; + auto &rng = stream(name); + std::uint32_t value; + do + { + value = rng(); + } while (value < threshold); + return value % bound; +} diff --git a/src/map/generator/core/GenerationContext.h b/src/map/generator/core/GenerationContext.h new file mode 100644 index 000000000..1b4b1c074 --- /dev/null +++ b/src/map/generator/core/GenerationContext.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "Team.h" +#include +#include +#include +class Game; +struct GenerationContext +{ + const GenerationRequest &request; + std::array bootX{}, bootY{}; + std::string stage = "terrain"; + std::string detail; + explicit GenerationContext(const GenerationRequest &r) : request(r) {} + std::mt19937 &stream(const std::string &name); + std::uint32_t bounded(const std::string &name, std::uint32_t bound); + static std::uint32_t deriveSeed(std::uint32_t seed, const std::string &name); + static std::uint32_t randomSeed(); + + private: + std::map streams; +}; diff --git a/src/map/generator/core/GenerationRequest.h b/src/map/generator/core/GenerationRequest.h new file mode 100644 index 000000000..b406b9550 --- /dev/null +++ b/src/map/generator/core/GenerationRequest.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GeneratorControls.h" +#include "Ressource.h" +#include "TerrainType.h" +#include +#include +#include +#include +class GeneratorRegistry; +struct GenerationRequest { + enum Method : int { + /// No terrain (terrain undefined) + eNONE = -1, + /// Uniform terrain (all of one type. completely unstructured) + eUNIFORM = 0, + /// swamp-like terrain with water here and land there + eSWAMP = 1, + /// a more or less winding river + eRIVER = 2, + /// islands that have organic shape and no passage from one to the next + eISLANDS = 3, + /// all connected land with round lakes + eCRATERLAKES = 4, + eCONCRETEISLANDS = 5, + eISLES = 6, + eOLDRANDOM = 7, + eOLDISLANDS = 8, + eCONTESTEDCOMMONS = 9, + eLATTICE = 10, + eMAZE = 11, + eFJORDCONTINENT = 12 + }; + + using Control = GeneratorControl; + using ControlGroup = ::ControlGroup; + GenerationRequest(); + int method = eUNIFORM; // stable registry ID, never a selection index + int wDec = 0, hDec = 0, nbTeams = 0, nbWorkers = 0; + TerrainType terrainType = GRASS; + std::uint32_t seed = 0; + std::map options; + // Legacy resource quantities remain supported by the compatibility adapter. + std::array resourceAmounts{}; + int option(const std::string &id) const { return options.at(id); } + void setMethodDefaults(int method); + void setMethodDefaults(int method, const GeneratorRegistry ®istry); + bool hasTerrainWeight() const; + bool hasTerrainWeight(const std::vector &definitions) const; + static const std::vector &controls(int method); + static const std::vector &sharedControls() { + return sharedGeneratorControls(); + } + static const Control &control(int method, const std::string &id); + static const char *methodName(int method); +}; +class GenerationHistory { + std::map settings; + +public: + void select(GenerationRequest ¤t, int method); + void select(GenerationRequest ¤t, int method, + const GeneratorRegistry ®istry); +}; diff --git a/src/map/generator/core/GenerationResult.h b/src/map/generator/core/GenerationResult.h new file mode 100644 index 000000000..2aaa7d73a --- /dev/null +++ b/src/map/generator/core/GenerationResult.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +#include +#include +class GenerationFailure : public std::runtime_error +{ + public: + using std::runtime_error::runtime_error; +}; +enum class GenerationError +{ + None, + InvalidRequest, + NonEmptyTarget, + PlacementFailed, + InvalidWorld +}; +struct GenerationResult +{ + std::string generatorId; + unsigned revision = 0; + std::uint32_t seed = 0; + std::string stage; + GenerationError error = GenerationError::None; + std::string detail; + explicit operator bool() const { return error == GenerationError::None; } + std::string diagnostic() const; +}; diff --git a/src/map/generator/core/GenerationService.cpp b/src/map/generator/core/GenerationService.cpp new file mode 100644 index 000000000..54f2fc113 --- /dev/null +++ b/src/map/generator/core/GenerationService.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "GenerationService.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GenerationValidation.h" +#include "Utilities.h" +std::string GenerationResult::diagnostic() const +{ + return generatorId + " revision " + std::to_string(revision) + " seed " + std::to_string(seed) + + " [" + stage + "]: " + detail; +} +GenerationResult GenerationService::generate(Game &game, const GenerationRequest &request) const +{ + GenerationResult result; + result.seed = request.seed; + result.stage = "validation"; + const auto *definition = registry.find(request.method); + if (!definition) + { + result.error = GenerationError::InvalidRequest; + result.detail = "Unknown generator"; + return result; + } + result.generatorId = definition->id; + result.revision = definition->revision; + result.detail = validateGenerationRequest(request, *definition); + if (!result.detail.empty()) + { + result.error = GenerationError::InvalidRequest; + return result; + } + if (game.mapHeader.getNumberOfTeams() != 0) + { + result.error = GenerationError::NonEmptyTarget; + result.detail = "Generation requires a fresh Game"; + return result; + } + // General engine mutation APIs still use the synchronized gameplay stream. + // This synchronous, scoped bridge is not a concurrency API. + struct EngineRandomScope + { + boost::mt19937 saved = randomGenerator; + ~EngineRandomScope() { randomGenerator = saved; } + } rngScope; + setSyncRandSeed(GenerationContext::deriveSeed(request.seed, "engine")); + GenerationContext context(request); + game.gameHeader.setRandomSeed(request.seed); + game.map.setSize(request.wDec, request.hDec); + game.map.setGame(&game); + bool generated = false; + try + { + generated = definition->generate(game, context); + } + catch (const GenerationFailure &error) + { + context.detail = error.what(); + } + if (!generated) + { + result.error = GenerationError::PlacementFailed; + result.stage = context.stage; + result.detail = + context.detail.empty() ? "Could not place the requested map layout" : context.detail; + return result; + } + result.stage = "validation"; + result.detail = validateGeneratedWorld(game, request, *definition); + if (!result.detail.empty()) + { + result.error = GenerationError::InvalidWorld; + return result; + } + if (definition->validateWorld) + { + result.stage = "generator validation"; + result.detail = definition->validateWorld(game, context); + if (!result.detail.empty()) + { + result.error = GenerationError::InvalidWorld; + return result; + } + } + result.stage = "script"; + const auto script = game.sgslScript.compileScript(&game); + if (script.type != ErrorReport::ET_OK) + { + result.error = GenerationError::InvalidWorld; + result.detail = script.getErrorString(); + return result; + } + result.stage = "complete"; + return result; +} diff --git a/src/map/generator/core/GenerationService.h b/src/map/generator/core/GenerationService.h new file mode 100644 index 000000000..d134eac23 --- /dev/null +++ b/src/map/generator/core/GenerationService.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GenerationResult.h" +#include "GeneratorRegistry.h" +class Game; +class GenerationService +{ + const GeneratorRegistry ®istry; + + public: + explicit GenerationService(const GeneratorRegistry ®istry = GeneratorRegistry::builtins()) + : registry(registry) + { + } + GenerationResult generate(Game &freshGame, const GenerationRequest &request) const; +}; diff --git a/src/map/generator/core/GenerationValidation.cpp b/src/map/generator/core/GenerationValidation.cpp new file mode 100644 index 000000000..22e96ecbc --- /dev/null +++ b/src/map/generator/core/GenerationValidation.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "GenerationValidation.h" +#include "Building.h" +#include "BuildingType.h" +#include "Game.h" +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +#include "IntBuildingType.h" +#include "Unit.h" +std::string validateGenerationRequest(const GenerationRequest &r, const GeneratorDefinition &d) +{ + for (const auto &c : sharedGeneratorControls()) + if (c.get(r) != c.normalize(c.get(r))) + return "Invalid " + c.id; + if (r.terrainType < WATER || r.terrainType > GRASS) + return "Invalid terrain"; + for (const auto &c : d.controls) + { + auto it = r.options.find(c.id); + if (it == r.options.end() || c.normalize(it->second) != it->second) + return "Invalid " + c.id; + } + for (const auto &v : r.options) + { + bool found = false; + for (const auto &c : d.controls) + found |= c.id == v.first; + if (!found) + return "Unknown option " + v.first; + } + for (int amount : r.resourceAmounts) + if (amount < 0 || amount > 100) + return "Invalid legacy resource amount"; + if (!r.hasTerrainWeight(d.controls)) + return "Give at least one terrain type a nonzero weight."; + return d.validateRequest ? d.validateRequest(r) : std::string{}; +} +std::string validateGeneratedWorld(const Game &g, const GenerationRequest &r, + const GeneratorDefinition &d) +{ + if (g.map.getW() != (1 << r.wDec) || g.map.getH() != (1 << r.hDec)) + return "Incorrect map dimensions"; + if (g.mapHeader.getNumberOfTeams() != (d.hasStartingColonies ? r.nbTeams : 1)) + return "Incorrect colony count"; + if (d.hasStartingColonies) + for (int i = 0; i < r.nbTeams; ++i) + { + const Team *team = g.teams[i]; + if (!team) + return "Missing colony"; + int workers = 0; + bool swarm = false; + for (int slot = 0; slot < Unit::MAX_COUNT; ++slot) + if (const auto *unit = team->myUnits[slot]; unit && unit->typeNum == WORKER) + ++workers; + for (int slot = 0; slot < Building::MAX_COUNT; ++slot) + if (const auto *building = team->myBuildings[slot]; + building && building->type->shortTypeNum == IntBuildingType::SWARM_BUILDING) + swarm = true; + if (workers != r.nbWorkers || !swarm) + return "Incomplete starting colony"; + } + return {}; +} diff --git a/src/map/generator/core/GenerationValidation.h b/src/map/generator/core/GenerationValidation.h new file mode 100644 index 000000000..998d28a89 --- /dev/null +++ b/src/map/generator/core/GenerationValidation.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +class Game; +struct GenerationRequest; +struct GeneratorDefinition; +std::string validateGenerationRequest(const GenerationRequest &, const GeneratorDefinition &); +std::string validateGeneratedWorld(const Game &, const GenerationRequest &, + const GeneratorDefinition &); diff --git a/src/map/generator/core/GeneratorControls.cpp b/src/map/generator/core/GeneratorControls.cpp new file mode 100644 index 000000000..b4d45be28 --- /dev/null +++ b/src/map/generator/core/GeneratorControls.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "GeneratorControls.h" +#include "GenerationRequest.h" +#include "GeneratorRegistry.h" +#include "Team.h" +#include +#include +int GeneratorControl::normalize(int v) const +{ + if (!allowedValues.empty()) + { + auto hi = std::lower_bound(allowedValues.begin(), allowedValues.end(), v); + if (hi == allowedValues.begin()) + return *hi; + if (hi == allowedValues.end()) + return allowedValues.back(); + return std::int64_t(v) - *(hi - 1) < std::int64_t(*hi) - v ? *(hi - 1) : *hi; + } + v = std::clamp(v, minimum, maximum); + return int(std::min(std::int64_t(maximum), + minimum + ((std::int64_t(v) - minimum + step / 2) / step) * step)); +} +std::vector GeneratorControl::values() const +{ + if (!allowedValues.empty()) + return allowedValues; + std::vector result; + for (std::int64_t v = minimum; v <= maximum; v += step) + result.push_back(int(v)); + return result; +} +int GeneratorControl::indexOf(int value) const +{ + const auto domain = values(); + return int(std::lower_bound(domain.begin(), domain.end(), normalize(value)) - domain.begin()); +} +int GeneratorControl::valueAt(int index) const { return values().at(index); } +int GeneratorControl::displayValue(int value) const { return powerOfTwo ? (1 << value) : value; } +int GeneratorControl::get(const GenerationRequest &r) const +{ + if (id == "width") + return r.wDec; + if (id == "height") + return r.hDec; + if (id == "teams") + return r.nbTeams; + if (id == "workers") + return r.nbWorkers; + return r.option(id); +} +void GeneratorControl::set(GenerationRequest &r, int value) const +{ + int v = normalize(value); + if (id == "width") + r.wDec = v; + else if (id == "height") + r.hDec = v; + else if (id == "teams") + r.nbTeams = v; + else if (id == "workers") + r.nbWorkers = v; + else + r.options[id] = v; +} +const std::vector &sharedGeneratorControls() +{ + static const std::vector controls = { + {"width", "Width", 6, 9, 1, 7, ControlGroup::Shared, true}, + {"height", "Height", 6, 9, 1, 7, ControlGroup::Shared, true}, + {"teams", "Colonies", 1, Team::MAX_COUNT, 1, 4, ControlGroup::Shared}, + {"workers", "Starting workers", 1, 8, 1, 4, ControlGroup::Shared}}; + return controls; +} +GenerationRequest::GenerationRequest() +{ + resourceAmounts.fill(7); + for (const auto &c : sharedControls()) + c.set(*this, c.defaultValue); + setMethodDefaults(eUNIFORM); +} +void GenerationRequest::setMethodDefaults(int id) +{ + setMethodDefaults(id, GeneratorRegistry::builtins()); +} +void GenerationRequest::setMethodDefaults(int id, const GeneratorRegistry ®istry) +{ + method = id; + options.clear(); + for (const auto &c : registry.at(id).controls) + c.set(*this, c.defaultValue); +} +const std::vector &GenerationRequest::controls(int id) +{ + return GeneratorRegistry::builtins().at(id).controls; +} +const char *GenerationRequest::methodName(int id) +{ + return GeneratorRegistry::builtins().at(id).nameKey; +} +const GeneratorControl &GenerationRequest::control(int method, const std::string &id) +{ + for (const auto &c : controls(method)) + if (c.id == id) + return c; + for (const auto &c : sharedControls()) + if (c.id == id) + return c; + throw std::invalid_argument("Unknown generator control: " + id); +} +bool GenerationRequest::hasTerrainWeight() const { return hasTerrainWeight(controls(method)); } +bool GenerationRequest::hasTerrainWeight(const std::vector &definitions) const +{ + int total = 0; + bool weighted = false; + for (const auto &c : definitions) + if (c.terrainWeight) + { + weighted = true; + total += c.get(*this); + } + return !weighted || total > 0; +} +void GenerationHistory::select(GenerationRequest ¤t, int id) +{ + select(current, id, GeneratorRegistry::builtins()); +} +void GenerationHistory::select(GenerationRequest ¤t, int id, + const GeneratorRegistry ®istry) +{ + if (id == current.method) + return; + settings[current.method] = current; + auto it = settings.find(id); + GenerationRequest next; + if (it != settings.end()) + next = it->second; + else + next.setMethodDefaults(id, registry); + for (const auto &c : sharedGeneratorControls()) + c.set(next, c.get(current)); + next.terrainType = current.terrainType; + next.seed = current.seed; + current = next; +} diff --git a/src/map/generator/core/GeneratorControls.h b/src/map/generator/core/GeneratorControls.h new file mode 100644 index 000000000..e7747615b --- /dev/null +++ b/src/map/generator/core/GeneratorControls.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +#include +struct GenerationRequest; +class MapGenerationDescriptor; +enum class ControlGroup +{ + Terrain, + Resources, + Layout, + Shared +}; +struct GeneratorControl +{ + std::string id; + const char *label; + int minimum, maximum, step, defaultValue; + ControlGroup group = ControlGroup::Terrain; + bool powerOfTwo = false; + bool terrainWeight = false; + // Optional ordered domain, e.g. {4, 8, 16}; values are stored literally. + std::vector allowedValues; + std::vector values() const; + int indexOf(int value) const; + int valueAt(int index) const; + int displayValue(int value) const; + int normalize(int value) const; + int get(const GenerationRequest &) const; + void set(GenerationRequest &, int) const; + int get(const MapGenerationDescriptor &) const; + void set(MapGenerationDescriptor &, int) const; +}; +const std::vector &sharedGeneratorControls(); diff --git a/src/map/generator/core/GeneratorDefinition.h b/src/map/generator/core/GeneratorDefinition.h new file mode 100644 index 000000000..9a9894ff7 --- /dev/null +++ b/src/map/generator/core/GeneratorDefinition.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GeneratorControls.h" +#include +class Game; +struct GenerationContext; +struct GeneratorDefinition +{ + const char *id; + int legacyId; + const char *nameKey; + unsigned revision; + bool editorOnly; + std::vector controls; + bool (*generate)(Game &, GenerationContext &); + bool hasStartingColonies = true; + // Optional pure checks for relationships between controls and generator-specific topology. + std::string (*validateRequest)(const GenerationRequest &) = nullptr; + std::string (*validateWorld)(const Game &, const GenerationContext &) = nullptr; +}; diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp new file mode 100644 index 000000000..557db401f --- /dev/null +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "GeneratorRegistry.h" +#include "ConcreteIslandsGenerator.h" +#include "ContestedCommonsGenerator.h" +#include "CraterLakesGenerator.h" +#include "FjordContinentGenerator.h" +#include "IslandsGenerator.h" +#include "IslesGenerator.h" +#include "LatticeGenerator.h" +#include "MazeGenerator.h" +#include "RiverGenerator.h" +#include "RuggedArchipelagoGenerator.h" +#include "ShatteredCoastGenerator.h" +#include "SwampGenerator.h" +#include "UniformGenerator.h" +#include +#include +#include +GeneratorRegistry::GeneratorRegistry(std::vector values) + : definitions(std::move(values)) { + std::set numbers; + std::set ids; + for (const auto &d : definitions) { + if (!d.id || !*d.id || !d.nameKey || d.legacyId < 0 || !d.generate || + !numbers.insert(d.legacyId).second || !ids.insert(d.id).second) + throw std::invalid_argument("Invalid generator registration"); + std::set controls; + for (const auto &c : sharedGeneratorControls()) + controls.insert(c.id); + for (const auto &c : d.controls) + if (c.id.empty() || !c.label || c.step <= 0 || c.maximum < c.minimum || + (c.allowedValues.empty() && + (std::int64_t(c.maximum) - c.minimum) % c.step) || + (!c.allowedValues.empty() && + (c.allowedValues.front() != c.minimum || + c.allowedValues.back() != c.maximum || + !std::is_sorted(c.allowedValues.begin(), c.allowedValues.end()) || + std::adjacent_find(c.allowedValues.begin(), + c.allowedValues.end()) != + c.allowedValues.end())) || + (c.powerOfTwo && (c.minimum < 0 || c.maximum > 30)) || + c.normalize(c.defaultValue) != c.defaultValue || + !controls.insert(c.id).second) + throw std::invalid_argument("Invalid generator control"); + } +} +const GeneratorDefinition *GeneratorRegistry::find(int id) const { + for (const auto &d : definitions) + if (d.legacyId == id) + return &d; + return nullptr; +} +const GeneratorDefinition &GeneratorRegistry::at(int id) const { + if (auto *d = find(id)) + return *d; + throw std::invalid_argument("Unknown generator ID"); +} +std::vector GeneratorRegistry::methods(bool editor) const { + std::vector result; + for (const auto &d : definitions) + if (editor || !d.editorOnly) + result.push_back(d.legacyId); + return result; +} +int GeneratorRegistry::selectionIndex(int id, bool editor) const { + auto ids = methods(editor); + auto it = std::find(ids.begin(), ids.end(), id); + return it == ids.end() ? -1 : int(it - ids.begin()); +} +const GeneratorRegistry &GeneratorRegistry::builtins() { + static const GeneratorRegistry registry( + {uniformDefinition(), swampDefinition(), riverDefinition(), + islandsDefinition(), craterLakesDefinition(), + concreteIslandsDefinition(), islesDefinition(), + shatteredCoastDefinition(), ruggedArchipelagoDefinition(), + contestedCommonsDefinition(), latticeDefinition(), mazeDefinition(), + fjordContinentDefinition()}); + return registry; +} diff --git a/src/map/generator/core/GeneratorRegistry.h b/src/map/generator/core/GeneratorRegistry.h new file mode 100644 index 000000000..e3d22ac2a --- /dev/null +++ b/src/map/generator/core/GeneratorRegistry.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GeneratorDefinition.h" +class GeneratorRegistry +{ + std::vector definitions; + + public: + explicit GeneratorRegistry(std::vector definitions); + const GeneratorDefinition *find(int id) const; + const GeneratorDefinition &at(int id) const; + std::vector methods(bool editor = true) const; + int selectionIndex(int id, bool editor = true) const; + static const GeneratorRegistry &builtins(); +}; diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.cpp b/src/map/generator/generators/ConcreteIslandsGenerator.cpp new file mode 100644 index 000000000..eda63634f --- /dev/null +++ b/src/map/generator/generators/ConcreteIslandsGenerator.cpp @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GeneratorDefinition.h" +#include "GlobalContainer.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "Resources.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include "Unit.h" +#include +#include +using namespace MapGeneration; +#include "ConcreteIslandsGenerator.h" +static bool generate(Game &game, GenerationContext &context) +{ + context.stage = "layout"; + const ConcreteIslandsOptions options(context.request); + const int channelWidth = options.channel_width; + game.map.makeHomogenMap(context.request.terrainType); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + // This keeps track of the current area number + int areaNumber = 1; + + std::vector grid(game.map.getW() * game.map.getH(), 0); + std::vector teamPoints; + std::vector weights1; + std::vector weights2; + std::vector teamAreaNumbers; + std::vector islandAreaNumbers; + + // Add in team bases + for (int i = 0; i < context.request.nbTeams; ++i) + { + teamPoints.push_back(MapGeneratorPoint(0, 0)); + weights1.push_back(1); + weights2.push_back(10); + teamAreaNumbers.push_back(areaNumber); + areaNumber += 1; + } + + // Add in auxilary islands + // A negative value retains the original random-count mode for comparisons. + int islandsCount = options.extra_islands; + for (int i = 0; i < islandsCount; ++i) + { + teamPoints.push_back(MapGeneratorPoint(0, 0)); + weights1.push_back(1); + weights2.push_back(1 + context.stream("layout")() % 3); + islandAreaNumbers.push_back(areaNumber); + areaNumber += 1; + } + + std::vector areaNumbers = teamAreaNumbers; + areaNumbers.insert(areaNumbers.end(), islandAreaNumbers.begin(), islandAreaNumbers.end()); + + // Initially divide up the land + splitUpPoints(game.map, context, grid, 0, teamPoints, weights1); + splitUpArea(game.map, context, grid, 0, teamPoints, weights2, areaNumbers); + + // Create a heightmap that will be used to give the map a rough edge + std::vector heights(game.map.getW() * game.map.getH(), 75); + adjustHeightmapFromPerlinNoise(game.map, context, heights, 15); + + // Compute the distance of every square from the border + std::vector sources; + findBorderPoints(game.map, grid, sources); + std::vector obstacles; + std::vector distances; + computeDistances(game.map, sources, obstacles, distances); + + // Locations near the border are deaper, thus causing more water + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int d = distances[y * game.map.getW() + x] - 1; + if (d < channelWidth) + { + heights[y * game.map.getW() + x] -= (channelWidth - 1 - d) * 13; + } + } + } + + // Use the heightmap to put in water, grass, and sand + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int total_height = heights[y * game.map.getW() + x]; + if (total_height < 45) + game.map.setUMatPos(x, y, WATER, 1); + else if (total_height >= 45 && total_height <= 55) + game.map.setUMatPos(x, y, SAND, 1); + else + game.map.setUMatPos(x, y, GRASS, 1); + } + } + game.map.controlSand(); + + // Go through the map again and place alga + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int total_height = heights[y * game.map.getW() + x]; + if (total_height <= 10) + { + game.map.setResource(x, y, ALGA, 1); + } + } + } + + // Reset the grid, and recompute within the boundaries of the various islands + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + grid[y * game.map.getW() + x] = 0; + } + } + splitUpArea(game.map, context, grid, 0, teamPoints, weights2, areaNumbers, true); + + // Fill in the auxilary islands + for (int i = 0; i < islandsCount; ++i) + { + // Initialize + std::vector areaWeights; + std::vector areaNumbers; + for (int j = 0; j < 2; ++j) + { + areaWeights.push_back(1); + areaNumbers.push_back(areaNumber); + areaNumber += 1; + } + + // Divide the area. Its possible the area will be so small it can't be used + if (divideUpArea(game.map, context, grid, islandAreaNumbers[i], areaWeights, areaNumbers)) + { + // Fill in wheat + std::vector points; + getAllPoints(game.map, grid, areaNumbers[0], points); + fillInResource(game.map, context, points, CORN, 2); + points.clear(); + + // Place some fruit + int fruit_n = context.stream("layout")() % 6 + 1; + getAllPoints(game.map, grid, areaNumbers[1], points); + chooseRandomPoints(game.map, context, points, fruit_n); + for (unsigned int j = 0; j < points.size(); ++j) + { + game.map.setResource(points[j].x, points[j].y, + CHERRY + context.stream("layout")() % 3, 1); + } + } + } + + if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber)) + return false; + + // Initialize final team info + for (int i = 0; i < context.request.nbTeams; ++i) + { + game.teams[i]->createLists(); + } + return true; +} + +GeneratorDefinition concreteIslandsDefinition() +{ + return {"concrete-islands", + 5, + "Concrete islands", + 1, + false, + {{"channel-width", "Channel width", 5, 8, 1, 5, ControlGroup::Terrain, false}, + {"extra-islands", "Extra islands", 0, 6, 1, 3, ControlGroup::Terrain, false}}, + generate}; +} diff --git a/src/map/generator/generators/ConcreteIslandsGenerator.h b/src/map/generator/generators/ConcreteIslandsGenerator.h new file mode 100644 index 000000000..337ee47ae --- /dev/null +++ b/src/map/generator/generators/ConcreteIslandsGenerator.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct ConcreteIslandsOptions +{ + int channel_width, extra_islands; + explicit ConcreteIslandsOptions(const GenerationRequest &r) + : channel_width(r.option("channel-width")), extra_islands(r.option("extra-islands")) + { + } +}; +GeneratorDefinition concreteIslandsDefinition(); diff --git a/src/map/generator/generators/ContestedCommonsGenerator.cpp b/src/map/generator/generators/ContestedCommonsGenerator.cpp new file mode 100644 index 000000000..0a775cf91 --- /dev/null +++ b/src/map/generator/generators/ContestedCommonsGenerator.cpp @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "ContestedCommonsGenerator.h" +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "GlobalContainer.h" +#include "Regions.h" +#include "Resources.h" +#include "Settlements.h" +#include "Topology.h" +#include "Unit.h" +#include +#include +#include +using namespace MapGeneration; +namespace +{ +constexpr double pi = 3.14159265358979323846; +int wrapDelta(int a, int b, int size) +{ + int d = a - b; + if (d > size / 2) + d -= size; + else if (d < -size / 2) + d += size; + return d; +} +void createJaggedIsland(Map &map, GenerationContext &context, std::vector &grid, int area, + int x, int y, int radius, double roughness) +{ + RadialShape shape(radius, roughness, context, "coast", 1.4); + stampShape(grid, map.getW(), map.getH(), area, ShapeTransform({double(x), double(y)}, 0), shape, + true); +} +static bool generate(Game &game, GenerationContext &context) +{ + context.stage = "layout"; + const ContestedCommonsOptions options(context.request); + game.map.makeHomogenMap(WATER); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + const int W = game.map.getW(); + const int H = game.map.getH(); + + int areaNumber = 1; + std::vector grid(W * H, 0); + + // Spread the team seeds AND the commons seed jointly, as one dispersion problem, instead + // of placing the teams first and hunting for a leftover gap afterward: the commons is an + // (N+1)th point in the exact same "push apart until nobody can move to a better spot" + // search, but with a lighter weight than the teams. In the per-point best-response score + // (nearest-neighbor distance squared, times that neighbor's weight) a heavier neighbor's + // term is easier to satisfy and a lighter one's is the one still binding, so every team + // preferentially keeps its distance from the low-weight commons over from the other + // (high-weight, already-"satisfied") teams. That's a real trade: teams end up a bit + // closer to each other than a teams-only spread would put them, in exchange for a much + // bigger gap around the shared island -- which is the point. + const int teamWeight = 2; + const int commonsWeight = 1; + std::vector allPoints; + std::vector allWeights; + for (int i = 0; i < context.request.nbTeams; ++i) + { + allPoints.push_back(MapGeneratorPoint(0, 0)); + allWeights.push_back(teamWeight); + } + allPoints.push_back(MapGeneratorPoint(0, 0)); // the commons seed + allWeights.push_back(commonsWeight); + + if (splitUpPoints(game.map, context, grid, 0, allPoints, allWeights, + PointSearch::WholeRegion) == 0) + return false; + + // splitUpPoints shuffles the whole array, but weights travel with their point, so the + // one point that kept commonsWeight is still identifiable regardless of where it landed. + std::vector teamPoints; + std::vector teamAreaNumbers; + MapGeneratorPoint commonsCenter(0, 0); + for (unsigned int i = 0; i < allPoints.size(); ++i) + { + if (allWeights[i] == commonsWeight) + { + commonsCenter = allPoints[i]; + } + else + { + teamPoints.push_back(allPoints[i]); + teamAreaNumbers.push_back(areaNumber); + areaNumber += 1; + } + } + + // Home islands are sized off actual team-to-team separation, not the joint figure + // splitUpPoints returns (which is dominated by the deliberately-tighter team/commons gap). + int minDist = std::numeric_limits::max(); + for (unsigned int i = 0; i < teamPoints.size(); ++i) + for (unsigned int j = i + 1; j < teamPoints.size(); ++j) + minDist = std::min( + minDist, (int)std::sqrt((double)game.map.warpDistSquare( + teamPoints[i].x, teamPoints[i].y, teamPoints[j].x, teamPoints[j].y))); + if (minDist == std::numeric_limits::max()) // a single team: no pair to measure + minDist = (int)std::sqrt((double)game.map.warpDistSquare(teamPoints[0].x, teamPoints[0].y, + commonsCenter.x, commonsCenter.y)); + + // Both islands are jagged, which means both can reach noticeably past their nominal + // radius in a lucky direction -- comfortably more than a flat few-tile fudge factor once + // the commons radius is large. Size everything off the worst case each jaggedness value + // can actually produce, not off the ideal circle, so the sizing math and the moat's + // physical enforcement (below) agree on where the real boundary is. + const double teamJaggedness = 0.35; + const double commonsJaggedness = 0.32; + + const int homeRadius = std::max(10, minDist * options.homeSize / 100); + for (unsigned int i = 0; i < teamPoints.size(); ++i) + createJaggedIsland(game.map, context, grid, teamAreaNumbers[i], teamPoints[i].x, + teamPoints[i].y, homeRadius, teamJaggedness); + + // Real Euclidean room at the chosen commons spot: the same metric createOval/ + // createJaggedIsland use to draw the islands, measured straight from the seed points. + int minGapSquared = std::numeric_limits::max(); + for (unsigned int i = 0; i < teamPoints.size(); ++i) + minGapSquared = + std::min(minGapSquared, game.map.warpDistSquare(commonsCenter.x, commonsCenter.y, + teamPoints[i].x, teamPoints[i].y)); + const int centerGap = (int)std::sqrt((double)minGapSquared); + + // Budget the gap as: nearest team's worst-case reach, a real moat, the commons' own + // worst-case reach, and a small flat buffer on top of all that jaggedness math. + const int teamReach = (int)std::ceil(homeRadius * (1.0 + teamJaggedness * 1.4)); + const int moatWidth = std::max(4, homeRadius * options.moatWidth / 100); + const int roomAvailable = std::max(0, centerGap - teamReach - moatWidth - 3); + const int roomLimitedRadius = + std::max(homeRadius, (int)(roomAvailable / (1.0 + commonsJaggedness * 1.4))); + + // The commons is the whole point of the map, so it should use up the room the joint + // dispersion just bought it: aim for 4x a home island's radius (roughly what's actually + // available once teams are pushed off it -- measured, not guessed), and let the min() + // below be the real safety net for whatever a given layout can't quite support. + const int desiredCommonsRadius = homeRadius * options.commonsSize / 100; + const int commonsRadius = std::min(desiredCommonsRadius, roomLimitedRadius); + const int bridgeCount = options.bridgeCount; + + std::vector bridgeAngles; + for (int i = 0; i < bridgeCount; ++i) + bridgeAngles.push_back(context.bounded("layout", 360)); + + int commonsAreaNumber = areaNumber; + areaNumber += 1; + createJaggedIsland(game.map, context, grid, commonsAreaNumber, commonsCenter.x, commonsCenter.y, + commonsRadius, commonsJaggedness); + + // Extend the commons island out across the moat along each bridge angle, so those strips + // get boosted into land by the heightmap pass below just like the island itself. Bridges + // get their own area number, kept separate from the island body, so the resource-field + // split further down only ever touches the round island, never a narrow connecting strip. + int bridgeAreaNumber = areaNumber; + areaNumber += 1; + for (unsigned int b = 0; b < bridgeAngles.size(); ++b) + { + double theta = bridgeAngles[b] * pi / 180.0; + for (int r = commonsRadius - 2; r <= commonsRadius + moatWidth + 2; ++r) + { + int cx = commonsCenter.x + (int)round(r * cos(theta)); + int cy = commonsCenter.y + (int)round(r * sin(theta)); + for (int dx = -1; dx <= 1; ++dx) + { + for (int dy = -1; dy <= 1; ++dy) + { + int nx = game.map.normalizeX(cx + dx); + int ny = game.map.normalizeY(cy + dy); + if (grid[ny * W + nx] == 0) + grid[ny * W + nx] = bridgeAreaNumber; + } + } + } + } + + // Base heightmap: noise everywhere, boosted wherever land has been carved out, so + // every island (and every bridge strip) comes out dry and everything else stays ocean. + std::vector heights(W * H, 40); + adjustHeightmapFromPerlinNoise(game.map, context, heights, 20); + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) + if (grid[y * W + x] != 0) + heights[y * W + x] += 60; + + for (int x = 0; x < W; ++x) + { + for (int y = 0; y < H; ++y) + { + int h = heights[y * W + x]; + if (h < 70) + game.map.setUMatPos(x, y, WATER, 1); + else if (h < 85) + game.map.setUMatPos(x, y, SAND, 1); + else + game.map.setUMatPos(x, y, GRASS, 1); + } + } + + // Force the moat ring: this band is water no matter what the noise -- or a team's own + // jagged coastline -- says, so the commons always reads as an island you have to cross a + // chokepoint to reach. Only the commons body and its bridges are exempt here: a team's + // island is deliberately just as jagged as the commons, so a peninsula can wander into + // this ring by chance, and only checking "is this tile claimed by anything" would let + // that team walk straight onto the commons while everyone else has to swim or use a + // bridge. Flooding it back closes that gap without another team ever being able to tell. + for (int y = 0; y < H; ++y) + { + for (int x = 0; x < W; ++x) + { + if (grid[y * W + x] == commonsAreaNumber || grid[y * W + x] == bridgeAreaNumber) + continue; + int dx = wrapDelta(x, commonsCenter.x, W); + int dy = wrapDelta(y, commonsCenter.y, H); + double r = sqrt((double)(dx * dx + dy * dy)); + if (r >= commonsRadius - 1 && r < commonsRadius + moatWidth) + { + game.map.setUMatPos(x, y, WATER, 1); + if (context.bounded("layout", 4) == 0) + game.map.setResource(x, y, ALGA, 1); + } + } + } + game.map.controlSand(); + + // Stock the commons with real fields, not a scattering of single tiles -- but the commons + // is several times the area of a home island, so splitting it into the same handful of + // zones a home island uses would read as one giant forest next to one giant farm. Scale + // the zone count with its actual size, and shuffle which zone gets which resource, so it + // comes out as a patchwork of many small groves and fields instead of two big blobs. + { + double areaRatio = + (double)(commonsRadius * commonsRadius) / (double)(homeRadius * homeRadius); + int zoneCount = (int)std::round(7.0 * std::sqrt(std::max(1.0, areaRatio))); + zoneCount = std::max(7, std::min(24, zoneCount)); + + std::vector zoneWeights(zoneCount, 1); + std::vector zoneAreas; + const int firstZoneArea = areaNumber; + for (int z = 0; z < zoneCount; ++z) + zoneAreas.push_back(areaNumber++); + if (divideUpArea(game.map, context, grid, commonsAreaNumber, zoneWeights, zoneAreas)) + { + // Which zone borders which: zoneAreas is a contiguous run of area numbers, so a + // tile's zone index is just its area number minus the first one. Two adjacent + // tiles with different zone indices means those two zones share a border. + std::vector> adjacent(zoneCount, std::vector(zoneCount, false)); + for (int y = 0; y < H; ++y) + { + for (int x = 0; x < W; ++x) + { + int a = grid[y * W + x] - firstZoneArea; + if (a < 0 || a >= zoneCount) + continue; + int xr = game.map.normalizeX(x + 1); + int b = grid[y * W + xr] - firstZoneArea; + if (b >= 0 && b < zoneCount && b != a) + adjacent[a][b] = adjacent[b][a] = true; + int yd = game.map.normalizeY(y + 1); + int c = grid[yd * W + x] - firstZoneArea; + if (c >= 0 && c < zoneCount && c != a) + adjacent[a][c] = adjacent[c][a] = true; + } + } + + // Roughly 2/7 of the zones become wood, 2/7 corn, one zone (any zone) becomes the + // single quarry, ~1/7 get a scattered fruit grove, and the rest are left open. Roles + // are assigned zone by zone in random order, each one picking randomly among the + // roles that no already-decided *neighboring* zone already has -- so a big forest + // or farm can never accidentally form just because two same-role zones happened to + // land next to each other; every patch is bordered by something else. + enum + { + ROLE_OPEN = 0, + ROLE_WOOD = 1, + ROLE_CORN = 2, + ROLE_FRUIT = 3, + ROLE_QUARRY = 4 + }; + int woodTarget = std::max(1, zoneCount * 2 / 7); + int cornTarget = std::max(1, zoneCount * 2 / 7); + int fruitTarget = std::max(1, zoneCount / 7); + + std::vector order(zoneCount); + for (int z = 0; z < zoneCount; ++z) + order[z] = z; + for (int z = zoneCount - 1; z > 0; --z) + { + int j = context.bounded("layout", z + 1); + std::swap(order[z], order[j]); + } + + std::vector role(zoneCount, -1); + role[order[0]] = ROLE_QUARRY; + int woodCount = 0, cornCount = 0, fruitCount = 0; + for (int oi = 1; oi < zoneCount; ++oi) + { + int z = order[oi]; + std::vector blocked(4, false); // by role, excluding QUARRY + for (int other = 0; other < zoneCount; ++other) + if (adjacent[z][other] && role[other] >= ROLE_OPEN && role[other] <= ROLE_FRUIT) + blocked[role[other]] = true; + + std::vector candidates; + if (!blocked[ROLE_WOOD] && woodCount < woodTarget) + candidates.push_back(ROLE_WOOD); + if (!blocked[ROLE_CORN] && cornCount < cornTarget) + candidates.push_back(ROLE_CORN); + if (!blocked[ROLE_FRUIT] && fruitCount < fruitTarget) + candidates.push_back(ROLE_FRUIT); + if (!blocked[ROLE_OPEN]) + candidates.push_back(ROLE_OPEN); + if (candidates.empty()) + candidates.push_back(ROLE_OPEN); // every role taken by a neighbor: stay open + + int chosen = candidates[context.bounded("layout", candidates.size())]; + role[z] = chosen; + if (chosen == ROLE_WOOD) + woodCount++; + else if (chosen == ROLE_CORN) + cornCount++; + else if (chosen == ROLE_FRUIT) + fruitCount++; + } + + for (int z = 0; z < zoneCount; ++z) + { + std::vector pts; + getAllPoints(game.map, grid, zoneAreas[z], pts); + if (role[z] == ROLE_QUARRY) + { + // One compact quarry, not a scatter: a single setResource call already + // grows a solid square footprint from its center, which reads as an actual + // deposit and leaves the rest of its zone clear to build on. + if (!pts.empty()) + { + MapGeneratorPoint quarry = pts[context.bounded("layout", pts.size())]; + game.map.setResource(quarry.x, quarry.y, STONE, 5); + } + } + else if (role[z] == ROLE_WOOD) + fillInResource(game.map, context, pts, WOOD, 2); + else if (role[z] == ROLE_CORN) + fillInResource(game.map, context, pts, CORN, 2); + else if (role[z] == ROLE_FRUIT) + { + std::vector fruitPts = pts; + chooseRandomPoints(game.map, context, fruitPts, + std::max(2, (int)pts.size() / 8)); + for (unsigned int i = 0; i < fruitPts.size(); ++i) + game.map.setResource(fruitPts[i].x, fruitPts[i].y, + CHERRY + context.bounded("layout", 3), 1); + } + // else: left open. + } + } + } + + // Give each team a small starter kit, then a swarm and workers, exactly the way the + // other generators do it: enough to bootstrap, not enough to make the commons optional. + for (int i = 0; i < context.request.nbTeams; ++i) + { + // Split the island into zones and fill most of them solid, instead of scattering a + // handful of individual tiles across the whole island: a real wheat field and a real + // treeline, not a light dusting. Two zones are left clear for the swarm and its workers. + std::vector zoneWeights(5, 1); + std::vector zoneAreas; + for (int z = 0; z < 5; ++z) + zoneAreas.push_back(areaNumber++); + std::vector homePoints; + if (divideUpArea(game.map, context, grid, teamAreaNumbers[i], zoneWeights, zoneAreas)) + { + std::vector pts; + getAllPoints(game.map, grid, zoneAreas[0], pts); + fillInResource(game.map, context, pts, WOOD, 2); + homePoints.insert(homePoints.end(), pts.begin(), pts.end()); + pts.clear(); + + getAllPoints(game.map, grid, zoneAreas[1], pts); + fillInResource(game.map, context, pts, CORN, 2); + homePoints.insert(homePoints.end(), pts.begin(), pts.end()); + pts.clear(); + + // One compact deposit, not a scatter across the whole zone -- leaves the rest of + // it clear to build on instead of peppering it with single-tile boulders. + getAllPoints(game.map, grid, zoneAreas[2], pts); + if (!pts.empty()) + { + MapGeneratorPoint quarry = pts[context.bounded("layout", pts.size())]; + game.map.setResource(quarry.x, quarry.y, STONE, 3); + } + homePoints.insert(homePoints.end(), pts.begin(), pts.end()); + pts.clear(); + + getAllPoints(game.map, grid, zoneAreas[3], pts); + homePoints.insert(homePoints.end(), pts.begin(), pts.end()); + pts.clear(); + getAllPoints(game.map, grid, zoneAreas[4], pts); + homePoints.insert(homePoints.end(), pts.begin(), pts.end()); + } + else + { + // The island came out too small to subdivide -- fall back to a light scatter + // over the whole thing rather than failing the map. + getAllPoints(game.map, grid, teamAreaNumbers[i], homePoints); + if (homePoints.empty()) + return false; + std::vector cornPts = homePoints; + chooseRandomPoints(game.map, context, cornPts, 4); + fillInResource(game.map, context, cornPts, CORN, 2); + std::vector woodPts = homePoints; + chooseRandomPoints(game.map, context, woodPts, 4); + fillInResource(game.map, context, woodPts, WOOD, 2); + MapGeneratorPoint quarry = homePoints[context.bounded("layout", homePoints.size())]; + game.map.setResource(quarry.x, quarry.y, STONE, 3); + } + if (homePoints.empty()) + return false; + + std::vector home(size_t(W) * H, 0); + for (const auto &point : homePoints) + home[point.y * W + point.x] = 1; + if (!placeSettlement(game, context, i, home, teamPoints[i], "starts")) + return false; + } + + return true; +} + +} // namespace + +ContestedCommonsOptions::ContestedCommonsOptions(const GenerationRequest &r) + : homeSize(r.option("home-island-size")), commonsSize(r.option("commons-size")), + moatWidth(r.option("moat-width")), bridgeCount(r.option("bridge-count")) +{ +} + +GeneratorDefinition contestedCommonsDefinition() +{ + return {"contested-commons", + 9, + "Contested commons", + 1, + false, + {{"home-island-size", "Home island size", 20, 35, 5, 25, ControlGroup::Terrain}, + {"commons-size", "Commons size", 250, 500, 50, 400, ControlGroup::Terrain}, + {"moat-width", "Moat width", 20, 50, 5, 35, ControlGroup::Terrain}, + {"bridge-count", "Bridge count", 1, 5, 1, 3, ControlGroup::Layout}}, + generate}; +} diff --git a/src/map/generator/generators/ContestedCommonsGenerator.h b/src/map/generator/generators/ContestedCommonsGenerator.h new file mode 100644 index 000000000..50476fab9 --- /dev/null +++ b/src/map/generator/generators/ContestedCommonsGenerator.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct ContestedCommonsOptions +{ + int homeSize, commonsSize, moatWidth, bridgeCount; + explicit ContestedCommonsOptions(const GenerationRequest &r); +}; +GeneratorDefinition contestedCommonsDefinition(); diff --git a/src/map/generator/generators/CraterLakesGenerator.cpp b/src/map/generator/generators/CraterLakesGenerator.cpp new file mode 100644 index 000000000..76fa9e2c0 --- /dev/null +++ b/src/map/generator/generators/CraterLakesGenerator.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CraterLakesGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +using namespace MapGeneration; +static bool generate(Game &game, GenerationContext &context) +{ + const CraterLakesOptions options(context.request); + const HeightFieldOptions terrain{ + options.water, options.sand, options.grass, options.desert, + options.smoothing, options.fruit, options.repeat, false}; + if (!generateHeightField(game, context, terrain, + [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) + { + hm.makeCraters(w * h * options.lake_density / 30000, + options.lake_size, smoothing); + })) + return false; + context.stage = "starts"; + return placeStarts(game, context); +} + +GeneratorDefinition craterLakesDefinition() +{ + return {"crater-lakes", + 4, + "Crater lakes", + 1, + false, + {{"water", "Water weight", 0, 100, 1, 25, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, + {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 6, ControlGroup::Terrain, false}, + {"lake-density", "Lake density", 10, 50, 5, 25, ControlGroup::Terrain, false}, + {"lake-size", "Lake size", 10, 40, 5, 25, ControlGroup::Terrain, false}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, + {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, + generate}; +} diff --git a/src/map/generator/generators/CraterLakesGenerator.h b/src/map/generator/generators/CraterLakesGenerator.h new file mode 100644 index 000000000..783c95bde --- /dev/null +++ b/src/map/generator/generators/CraterLakesGenerator.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct CraterLakesOptions +{ + int water, sand, grass, desert, smoothing, lake_density, lake_size, fruit, repeat; + explicit CraterLakesOptions(const GenerationRequest &r) + : water(r.option("water")), sand(r.option("sand")), grass(r.option("grass")), + desert(r.option("desert")), smoothing(r.option("smoothing")), + lake_density(r.option("lake-density")), lake_size(r.option("lake-size")), + fruit(r.option("fruit")), repeat(r.option("repeat")) + { + } +}; +GeneratorDefinition craterLakesDefinition(); diff --git a/src/map/generator/generators/FjordContinentGenerator.cpp b/src/map/generator/generators/FjordContinentGenerator.cpp new file mode 100644 index 000000000..27d450aec --- /dev/null +++ b/src/map/generator/generators/FjordContinentGenerator.cpp @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "FjordContinentGenerator.h" +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Geometry.h" +#include "GlobalContainer.h" +#include "Regions.h" +#include "Resources.h" +#include "Settlements.h" +#include "Topology.h" +#include "Unit.h" +#include +#include +#include +using namespace MapGeneration; +namespace { +constexpr double pi = 3.14159265358979323846; +double randomAngle(GenerationContext &context) { + return context.bounded("layout", 3600) / 3600.0 * 2 * pi; +} +void createJaggedIsland(Map &map, GenerationContext &context, + std::vector &grid, int area, int x, int y, + int radius, double roughness) { + RadialShape shape(radius, roughness, context, "outliers", 1.4); + stampShape(grid, map.getW(), map.getH(), area, + ShapeTransform({double(x), double(y)}, 0), shape, true); +} + +bool placeBankClump(Map &map, GenerationContext &context, + const std::vector ¢erline, + double progress, int side, int resourceType) { + if (centerline.size() < 3) + return false; + const int target = int(progress * (centerline.size() - 1)); + for (int offset = 0; offset < int(centerline.size()); ++offset) { + const int signedOffset = offset % 2 ? -(offset + 1) / 2 : offset / 2; + const int i = std::max( + 1, std::min(int(centerline.size()) - 2, target + signedOffset)); + int tx = centerline[i + 1].x - centerline[i - 1].x; + int ty = centerline[i + 1].y - centerline[i - 1].y; + if (tx > map.getW() / 2) + tx -= map.getW(); + else if (tx < -map.getW() / 2) + tx += map.getW(); + if (ty > map.getH() / 2) + ty -= map.getH(); + else if (ty < -map.getH() / 2) + ty += map.getH(); + const double length = std::sqrt(double(tx * tx + ty * ty)); + if (length < 0.5) + continue; + const double nx = -ty / length * side, ny = tx / length * side; + for (int distance = 2; distance <= 16; ++distance) { + MapGeneratorPoint anchor( + map.normalizeX(centerline[i].x + int(std::lround(nx * distance))), + map.normalizeY(centerline[i].y + int(std::lround(ny * distance)))); + if (map.isResourceAllowed(anchor.x, anchor.y, resourceType) && + placeResourceClump(map, context, anchor, resourceType, 2) >= 3) + return true; + } + } + return false; +} +static bool generate(Game &game, GenerationContext &context) { + context.stage = "continent"; + const FjordContinentOptions options(context.request); + game.map.makeHomogenMap(WATER); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + const int W = game.map.getW(); + const int H = game.map.getH(); + const int nbTeams = context.request.nbTeams; + if (nbTeams < 1) + return false; + + const double rotation = randomAngle(context) * 0.5; + const double elongation = + 1.0 + context.bounded("layout", 1000) / 1000.0 * 0.3; + ShapeTransform xf({W / 2.0, H / 2.0}, rotation, elongation); + + const double baseR = options.continentSize / 100.0 * std::min(W, H); + RadialShape coast(baseR, options.roughness / 100.0, context, "coast", 1.4); + const double coreR = baseR * 0.19; + const double maxStretch = std::max(elongation, 1.0 / elongation); + + // One angular sector per team, evenly spaced with a little jitter so it + // doesn't look mechanical. Deciding sectors up front (rather than deriving + // neighbor order from wherever seed points happen to land) is what guarantees + // every team gets pushed out to its own tip instead of sometimes landing near + // the shared middle. + std::vector teamTheta(nbTeams); + { + double baseRotation = randomAngle(context); + double slot = 2 * pi / nbTeams; + for (int i = 0; i < nbTeams; ++i) { + double jitter = + ((context.bounded("layout", 2001)) / 1000.0 - 1.0) * 0.2 * slot; + teamTheta[i] = baseRotation + i * slot + jitter; + } + } + + // grid holds area numbers for resource zoning, kept alongside the real + // terrain (which is written straight onto game.map as we go, the same way + // computeIsles and computeContestedCommons do it). + std::vector grid(W * H, 0); + int areaNumber = 1; + + // 1) Stamp the continent. + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + const auto shaped = xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + double theta = atan2(v, u); + double r = sqrt(u * u + v * v); + if (r < coast.radiusAt(theta)) + game.map.setUMatPos(x, y, GRASS, 1); + } + } + + // 2) Carve a fjord between every pair of angularly-neighboring teams: a + // smooth S-curve from just outside the coast at the sector boundary, in to a + // fixed inner radius (coreR) that every fjord stops short of -- so the disc + // r> fjordCenterlines(nbTeams); + if (nbTeams >= 2) { + for (int k = 0; k < nbTeams; ++k) { + int i = k; + int j = (k + 1) % nbTeams; + double ti = teamTheta[i]; + double tj = teamTheta[j]; + double d = fmod(tj - ti, 2 * pi); + if (d < 0) + d += 2 * pi; + double midTheta = ti + d / 2.0; + + double mouthR = coast.radiusAt(midTheta) + 3.0; + double mouthU = mouthR * cos(midTheta), mouthV = mouthR * sin(midTheta); + double tipU = coreR * cos(midTheta), tipV = coreR * sin(midTheta); + double perpU = -sin(midTheta), perpV = cos(midTheta); + + double gap = std::min(d, 2 * pi - d); + double amplitude = gap * mouthR * 0.17; + double phase = randomAngle(context); + double mouthWidth = + options.fjordWidth + 0.6 + context.bounded("layout", 1400) / 1000.0; + double tipWidth = 0.65; + + int steps = std::max(24, (int)(mouthR - coreR)); + for (int s = 0; s <= steps; ++s) { + double t = double(s) / steps; + double baseU = mouthU + t * (tipU - mouthU); + double baseV = mouthV + t * (tipV - mouthV); + double envelope = sin(pi * t); + double lateral = amplitude * sin(2 * pi * t + phase) * envelope; + double u = baseU + lateral * perpU; + double v = baseV + lateral * perpV; + double width = mouthWidth * (1 - t) + tipWidth * t; + + const auto mapped = xf.toMap({u, v}); + double mx = mapped.x, my = mapped.y; + int rad = (int)ceil(width * maxStretch) + 1; + int ix = (int)lround(mx), iy = (int)lround(my); + fjordCenterlines[k].push_back(MapGeneratorPoint( + game.map.normalizeX(ix), game.map.normalizeY(iy))); + for (int dy = -rad; dy <= rad; ++dy) { + for (int dx = -rad; dx <= rad; ++dx) { + int nx = game.map.normalizeX(ix + dx); + int ny = game.map.normalizeY(iy + dy); + const auto shaped = xf.toShape({double(nx), double(ny)}); + double u2 = shaped.x, v2 = shaped.y; + double dd = (u2 - u) * (u2 - u) + (v2 - v) * (v2 - v); + if (dd <= width * width) { + game.map.setUMatPos(nx, ny, WATER, 1); + } + } + } + } + } + } + + // 3) A couple of small, unconnected resource islands out in the open sea -- + // purely a bonus for whoever explores, never touching the mainland or each + // other. + { + double mainlandReach = coast.maximumRadius() * maxStretch; + double halfMapMargin = std::min(W, H) / 2.0 - 6.0; + int outlierCount = std::min(4, options.resourceIslands + + int(context.bounded("layout", 2))); + std::vector outlierCenters; + std::vector outlierRadii; + for (int oi = 0; oi < outlierCount && mainlandReach + 6.0 < halfMapMargin; + ++oi) { + int islandRadius = 5 + context.bounded("layout", 4); + bool placed = false; + for (int attempt = 0; attempt < 40 && !placed; ++attempt) { + double theta = randomAngle(context); + double lo = mainlandReach + 6.0; + double hi = halfMapMargin; + double r = lo + (context.bounded("layout", 1000)) / 1000.0 * (hi - lo); + int cx = game.map.normalizeX((int)lround(W / 2.0 + r * cos(theta))); + int cy = game.map.normalizeY((int)lround(H / 2.0 + r * sin(theta))); + + bool clear = true; + for (unsigned int oc = 0; oc < outlierCenters.size() && clear; ++oc) { + int minSep = islandRadius + outlierRadii[oc] + 8; + if (game.map.warpDistSquare(cx, cy, outlierCenters[oc].x, + outlierCenters[oc].y) < minSep * minSep) + clear = false; + } + if (!clear) + continue; + + int outlierArea = areaNumber++; + createJaggedIsland(game.map, context, grid, outlierArea, cx, cy, + islandRadius, 0.3); + std::vector islandPts; + getAllPoints(game.map, grid, outlierArea, islandPts); + if (islandPts.empty()) + continue; + for (unsigned int p = 0; p < islandPts.size(); ++p) + game.map.setUMatPos(islandPts[p].x, islandPts[p].y, GRASS, 1); + + // Each island leans on one resource theme, so finding one feels like a + // distinct little prize rather than an interchangeable resource dump. + switch (context.bounded("layout", 3)) { + case 0: + placeResourceClump( + game.map, context, + islandPts[context.bounded("resources", islandPts.size())], STONE, + 2); + break; + case 1: + placeResourceClump( + game.map, context, + islandPts[context.bounded("resources", islandPts.size())], + CHERRY + context.bounded("resources", 3), 2); + break; + default: + placeResourceClump( + game.map, context, + islandPts[context.bounded("resources", islandPts.size())], CORN, + 3); + break; + } + + outlierCenters.push_back(MapGeneratorPoint(cx, cy)); + outlierRadii.push_back(islandRadius); + placed = true; + } + } + } + + game.map.controlSand(); + + // 4) Anchor each team out at its own tip: start just inland of the coast at + // the team's angle and only back off toward the core if that exact spot turns + // out to be water (a sharp jaggedness dip, or a fjord belly that swung wider + // than expected) -- the same "compute where we want to be, then confirm the + // grid agrees" shape as chooseFreeForBuildingSquares. + std::vector teamPts; + teamPts.reserve(nbTeams); + for (int i = 0; i < nbTeams; ++i) { + double theta = teamTheta[i]; + double maxR = coast.radiusAt(theta); + double r = std::max(coreR + 6.0, maxR - 8.0); + double stepIn = (maxR - (coreR + 2.0)) / 30.0; + if (stepIn <= 0.0) + stepIn = 1.0; + + int fx = -1, fy = -1; + for (int tries = 0; tries < 30; ++tries) { + const auto mapped = xf.toMap({r * cos(theta), r * sin(theta)}); + double mx = mapped.x, my = mapped.y; + int ix = game.map.normalizeX((int)lround(mx)); + int iy = game.map.normalizeY((int)lround(my)); + if (!game.map.isWater(ix, iy)) { + fx = ix; + fy = iy; + break; + } + r -= stepIn; + } + if (fx < 0) { + const auto mapped = + xf.toMap({(coreR + 6.0) * cos(theta), (coreR + 6.0) * sin(theta)}); + double mx = mapped.x, my = mapped.y; + fx = game.map.normalizeX((int)lround(mx)); + fy = game.map.normalizeY((int)lround(my)); + } + teamPts.push_back(MapGeneratorPoint(fx, fy)); + } + + // 5) Connectivity: verify, don't assume. The untouched core should make this + // unreachable in practice, but every other generator checks its own + // invariants explicitly instead of trusting the construction, so this does + // too. + { + std::vector visited(W * H, false); + std::vector stack; + stack.push_back(teamPts[0]); + visited[teamPts[0].y * W + teamPts[0].x] = true; + while (!stack.empty()) { + MapGeneratorPoint p = stack.back(); + stack.pop_back(); + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + int nx = game.map.normalizeX(p.x + dx); + int ny = game.map.normalizeY(p.y + dy); + if (!visited[ny * W + nx] && !game.map.isWater(nx, ny)) { + visited[ny * W + nx] = true; + stack.push_back(MapGeneratorPoint(nx, ny)); + } + } + } + } + for (int i = 0; i < nbTeams; ++i) + if (!visited[teamPts[i].y * W + teamPts[i].x]) + return false; + } + + // 6) Stone and fruit in the untouched core -- the reward for pushing to the + // middle of the map instead of staying home. + { + int coreArea = areaNumber++; + std::vector corePts; + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + if (game.map.isWater(x, y) || grid[y * W + x] != 0) + continue; + const auto shaped = xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + if (u * u + v * v <= (coreR + 4.0) * (coreR + 4.0)) { + grid[y * W + x] = coreArea; + corePts.push_back(MapGeneratorPoint(x, y)); + } + } + } + if (!corePts.empty()) { + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], + STONE, 3); + placeResourceClump(game.map, context, + corePts[context.bounded("resources", corePts.size())], + CHERRY + context.bounded("resources", 3), 2); + } + } + + // 7) Algae out in the open sea: any water tile clearly beyond the coastline + // (not a fjord, not the moat-ish water right against the shore) gets an + // occasional patch. + std::vector algaeWater; + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + if (!game.map.isWater(x, y)) + continue; + const auto shaped = xf.toShape({double(x), double(y)}); + double u = shaped.x, v = shaped.y; + double theta = atan2(v, u); + double r = sqrt(u * u + v * v); + double shoreR = coast.radiusAt(theta); + if (r > shoreR + 2.0 && r < shoreR + 14.0) + algaeWater.emplace_back(x, y); + } + } + if (!algaeWater.empty()) + for (int i = 0; i < std::max(1, int(algaeWater.size()) / 180); ++i) + placeResourceClump( + game.map, context, + algaeWater[context.bounded("resources", algaeWater.size())], ALGA, 2); + + // 8) A light per-team starter kit so nobody is stuck waiting to reach the + // fjord banks before they can build anything; the banks and the core are the + // map's real economy. + std::vector allWater; + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) + if (game.map.isWater(x, y)) + allWater.push_back(MapGeneratorPoint(x, y)); + + for (int i = 0; i < nbTeams; ++i) { + std::vector sources; + sources.push_back(teamPts[i]); + std::vector distances; + computeDistances(game.map, sources, allWater, distances); + + std::vector homePoints; + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) + if (distances[y * W + x] >= 1 && distances[y * W + x] <= 10) + homePoints.push_back(MapGeneratorPoint(x, y)); + if (homePoints.empty()) + return false; + + placeResourceClump( + game.map, context, + homePoints[context.bounded("resources", homePoints.size())], CORN, 2); + placeResourceClump( + game.map, context, + homePoints[context.bounded("resources", homePoints.size())], WOOD, 2); + + std::vector stonePts = homePoints; + chooseRandomPoints(game.map, context, stonePts, 1); + for (unsigned int j = 0; j < stonePts.size(); ++j) + game.map.setResource(stonePts[j].x, stonePts[j].y, STONE, 1); + + std::vector home(size_t(W) * H, 0); + for (const auto &point : homePoints) + home[point.y * W + point.x] = 1; + if (!placeSettlement(game, context, i, home, teamPts[i], "starts")) + return false; + } + + // 9) Place bank resources last so settlement and regional deposits cannot + // overwrite the guarantee. Every side of every fjord receives both resources + // at distinct points along its length. + context.stage = "fjord bank resources"; + if (nbTeams >= 2) + for (int k = 0; k < nbTeams; ++k) { + for (int side : {-1, 1}) + if (!placeBankClump(game.map, context, fjordCenterlines[k], 0.35, side, + CORN) || + !placeBankClump(game.map, context, fjordCenterlines[k], 0.68, side, + WOOD)) + return false; + } + + return true; +} + +} // namespace + +FjordContinentOptions::FjordContinentOptions(const GenerationRequest &r) + : continentSize(r.option("continent-size")), + roughness(r.option("coast-roughness")), + fjordWidth(r.option("fjord-width")), + resourceIslands(r.option("resource-islands")) {} + +GeneratorDefinition fjordContinentDefinition() { + return {"fjord-continent", + 12, + "Fjord continent", + 1, + false, + {{"continent-size", "Continent size", 28, 40, 2, 34, + ControlGroup::Terrain}, + {"coast-roughness", "Coast roughness", 10, 35, 1, 22, + ControlGroup::Terrain}, + {"fjord-width", "Fjord width", 2, 6, 1, 3, ControlGroup::Terrain}, + {"resource-islands", "Resource islands", 0, 4, 1, 2, + ControlGroup::Resources}}, + generate}; +} diff --git a/src/map/generator/generators/FjordContinentGenerator.h b/src/map/generator/generators/FjordContinentGenerator.h new file mode 100644 index 000000000..b729200ad --- /dev/null +++ b/src/map/generator/generators/FjordContinentGenerator.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct FjordContinentOptions +{ + int continentSize, roughness, fjordWidth, resourceIslands; + explicit FjordContinentOptions(const GenerationRequest &r); +}; +GeneratorDefinition fjordContinentDefinition(); diff --git a/src/map/generator/generators/IslandsGenerator.cpp b/src/map/generator/generators/IslandsGenerator.cpp new file mode 100644 index 000000000..9d9a299cb --- /dev/null +++ b/src/map/generator/generators/IslandsGenerator.cpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "IslandsGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +using namespace MapGeneration; +static bool generate(Game &game, GenerationContext &context) +{ + const IslandsOptions options(context.request); + const HeightFieldOptions terrain{ + options.water, options.sand, options.grass, options.desert, + options.smoothing, options.fruit, options.repeat, false}; + if (!generateHeightField(game, context, terrain, + [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) + { + hm.makeIslands( + std::max(1, (context.request.nbTeams + options.extra_islands) / + (1 << options.repeat)), + smoothing); + })) + return false; + context.stage = "starts"; + return placeStarts(game, context); +} + +GeneratorDefinition islandsDefinition() +{ + return {"islands", + 3, + "Islands", + 1, + false, + {{"water", "Water weight", 0, 100, 1, 55, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, + {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 4, ControlGroup::Terrain, false}, + {"extra-islands", "Extra islands", 0, 8, 1, 0, ControlGroup::Terrain, false}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, + {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, + generate}; +} diff --git a/src/map/generator/generators/IslandsGenerator.h b/src/map/generator/generators/IslandsGenerator.h new file mode 100644 index 000000000..e69478029 --- /dev/null +++ b/src/map/generator/generators/IslandsGenerator.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct IslandsOptions +{ + int water, sand, grass, desert, smoothing, extra_islands, fruit, repeat; + explicit IslandsOptions(const GenerationRequest &r) + : water(r.option("water")), sand(r.option("sand")), grass(r.option("grass")), + desert(r.option("desert")), smoothing(r.option("smoothing")), + extra_islands(r.option("extra-islands")), fruit(r.option("fruit")), + repeat(r.option("repeat")) + { + } +}; +GeneratorDefinition islandsDefinition(); diff --git a/src/map/generator/generators/IslesGenerator.cpp b/src/map/generator/generators/IslesGenerator.cpp new file mode 100644 index 000000000..5655539ec --- /dev/null +++ b/src/map/generator/generators/IslesGenerator.cpp @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GeneratorDefinition.h" +#include "GlobalContainer.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "Resources.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include "Unit.h" +#include +#include +using namespace MapGeneration; +#include "IslesGenerator.h" +static bool generate(Game &game, GenerationContext &context) +{ + context.stage = "layout"; + const IslesOptions options(context.request); + const int islandSize = options.island_size, bridgeWidth = options.bridge_width, + bridgeRadius = bridgeWidth - 2; + game.map.makeHomogenMap(context.request.terrainType); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + int areaNumber = 1; + std::vector grid(game.map.getW() * game.map.getH(), 0); + + // Do the starting locations of the teams + std::vector teamPoints; + std::vector teamWeights; + std::vector teamAreaNumbers; + for (int i = 0; i < context.request.nbTeams; ++i) + { + teamPoints.push_back(MapGeneratorPoint(0, 0)); + teamWeights.push_back(1); + teamAreaNumbers.push_back(areaNumber); + areaNumber += 1; + } + int minDist = splitUpPoints(game.map, context, grid, 0, teamPoints, teamWeights); + + // Construct the areas for the teams + for (int i = 0; i < context.request.nbTeams; ++i) + { + createOval(game.map, grid, teamAreaNumbers[i], teamPoints[i].x, teamPoints[i].y, + minDist * islandSize / 100, minDist * islandSize / 100); + } + + // Construct a heightmap + std::vector heightmap(game.map.getW() * game.map.getH(), 50); + std::vector teamAreaPoints; + getAllOtherPoints(game.map, grid, 0, teamAreaPoints); + std::vector obstacles; + + std::vector distances; + computeDistances(game.map, teamAreaPoints, obstacles, distances); + + // Stamp out the team areas + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int d = distances[y * game.map.getW() + x]; + if (d > 1 && d <= 11) + heightmap[y * game.map.getW() + x] += (11 - d) * 10; + else if (d == 1) + heightmap[y * game.map.getW() + x] += 100; + } + } + + // Connect each teams area to each other players area + std::vector connectorPoints; + int connectorArea = areaNumber; + areaNumber += 1; + for (int i = 0; i < context.request.nbTeams; ++i) + { + for (int j = i + 1; j < context.request.nbTeams; ++j) + { + // Choose one random point from each players area + std::vector teamI; + std::vector teamJ; + getAllPoints(game.map, grid, teamAreaNumbers[i], teamI); + getAllPoints(game.map, grid, teamAreaNumbers[j], teamJ); + chooseRandomPoints(game.map, context, teamI, 1); + chooseRandomPoints(game.map, context, teamJ, 1); + + // Traverse between the two points + std::vector linePoints; + getAllPointsLine(game.map, teamI[0].x, teamI[0].y, teamJ[0].x, teamJ[0].y, linePoints); + // If a connection can be made without going through another teams area, then do it + bool failed = false; + for (unsigned int p = 0; p < linePoints.size() && !failed; ++p) + { + for (int x = -bridgeRadius; x <= bridgeRadius && !failed; ++x) + { + int nx = game.map.normalizeX(linePoints[p].x + x); + for (int y = -bridgeRadius; y <= bridgeRadius && !failed; ++y) + { + int ny = game.map.normalizeY(linePoints[p].y + y); + int g = grid[ny * game.map.getW() + nx]; + if (g != 0 && g != teamAreaNumbers[i] && g != teamAreaNumbers[j] && + g != connectorArea) + { + failed = true; + } + } + } + } + // Make the connection + if (!failed) + { + for (unsigned int p = 0; p < linePoints.size(); ++p) + { + connectorPoints.push_back(linePoints[p]); + for (int x = -bridgeRadius; x <= bridgeRadius; ++x) + { + int nx = game.map.normalizeX(linePoints[p].x + x); + for (int y = -bridgeRadius; y <= bridgeRadius; ++y) + { + int ny = game.map.normalizeY(linePoints[p].y + y); + int d = distances[ny * game.map.getW() + nx]; + if (d > bridgeWidth + 1) + { + grid[ny * game.map.getW() + nx] = connectorArea; + } + } + } + } + } + } + } + computeDistances(game.map, connectorPoints, obstacles, distances); + + // Stamp out the connectors + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int d = distances[y * game.map.getW() + x]; + if (d > 1 && d <= bridgeWidth) + heightmap[y * game.map.getW() + x] += (bridgeWidth - d) * (100 / (bridgeWidth - 1)); + else if (d == 1) + heightmap[y * game.map.getW() + x] += 100; + } + } + + // Use the heightmap to put in water, grass, and sand + adjustHeightmapFromPerlinNoise(game.map, context, heightmap, 45); + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int total_height = heightmap[y * game.map.getW() + x]; + if (total_height < 90) + game.map.setUMatPos(x, y, WATER, 1); + else if (total_height > 95 && total_height < 105) + game.map.setUMatPos(x, y, SAND, 1); + else + game.map.setUMatPos(x, y, GRASS, 1); + } + } + game.map.controlSand(); + + // Reset the grid, and recompute within the boundaries of the various islands + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + if (grid[y * game.map.getW() + x] != connectorArea) + grid[y * game.map.getW() + x] = 0; + } + } + splitUpArea(game.map, context, grid, 0, teamPoints, teamWeights, teamAreaNumbers, true); + + std::vector connectorDistances = distances; + + // For each team, find a point just off the coast and place algae there + for (int i = 0; i < context.request.nbTeams; ++i) + { + std::vector sources; + getAllPoints(game.map, grid, teamAreaNumbers[i], sources); + computeDistances(game.map, sources, obstacles, distances); + std::vector possible; + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int d = distances[y * game.map.getW() + x]; + int d2 = connectorDistances[y * game.map.getW() + x]; + if (d == 8 && d2 > bridgeWidth) + { + possible.push_back(MapGeneratorPoint(x, y)); + } + } + } + if (possible.size() == 0) + { + return false; + } + int r = context.stream("layout")() % possible.size(); + for (int x = -2; x <= 2; ++x) + { + int nx = game.map.normalizeX(possible[r].x + x); + for (int y = -2; y <= 2; ++y) + { + int ny = game.map.normalizeY(possible[r].y + y); + game.map.setResource(nx, ny, ALGA, 1); + } + } + } + + if (!divideUpPlayerLands(game, context, grid, teamAreaNumbers, areaNumber)) + { + return false; + } + + // Initialize final team info + for (int i = 0; i < context.request.nbTeams; ++i) + { + game.teams[i]->createLists(); + } + return true; +} + +GeneratorDefinition islesDefinition() +{ + return {"isles", + 6, + "Isles", + 1, + false, + {{"island-size", "Island size", 45, 65, 5, 60, ControlGroup::Terrain, false}, + {"bridge-width", "Land bridge width", 3, 6, 1, 4, ControlGroup::Terrain, false}}, + generate}; +} diff --git a/src/map/generator/generators/IslesGenerator.h b/src/map/generator/generators/IslesGenerator.h new file mode 100644 index 000000000..e7b2e5da2 --- /dev/null +++ b/src/map/generator/generators/IslesGenerator.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct IslesOptions +{ + int island_size, bridge_width; + explicit IslesOptions(const GenerationRequest &r) + : island_size(r.option("island-size")), bridge_width(r.option("bridge-width")) + { + } +}; +GeneratorDefinition islesDefinition(); diff --git a/src/map/generator/generators/LatticeGenerator.cpp b/src/map/generator/generators/LatticeGenerator.cpp new file mode 100644 index 000000000..137e08b24 --- /dev/null +++ b/src/map/generator/generators/LatticeGenerator.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "LatticeGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Resources.h" +#include "Settlements.h" +#include +using namespace MapGeneration; +namespace { +constexpr double pi = 3.14159265358979323846; + +bool generate(Game &game, GenerationContext &context) { + context.stage = "lattice terrain"; + const LatticeOptions o(context.request); + const int width = game.map.getW(), height = game.map.getH(); + game.map.makeHomogenMap(WATER); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + // controlSand consumes one tile from each edge of both terrain bands. Build + // the undermap two tiles wider so the controls describe the playable result. + const int rawIsletSize = o.isletSize + 2; + const int period = rawIsletSize + o.channelWidth + 2; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + game.map.setUMTerrain( + x, y, + (x % period < rawIsletSize && y % period < rawIsletSize) ? GRASS + : WATER); + + std::vector homes; + const double ringRadius = std::min(width, height) * 0.35; + for (int i = 0; i < context.request.nbTeams; ++i) { + const double angle = 2.0 * pi * i / context.request.nbTeams + 0.4; + const int hx = + game.map.normalizeX(int(width / 2 + ringRadius * std::cos(angle))); + const int hy = + game.map.normalizeY(int(height / 2 + ringRadius * std::sin(angle))); + homes.emplace_back(hx, hy); + for (int dy = -o.homeRadius; dy <= o.homeRadius; ++dy) + for (int dx = -o.homeRadius; dx <= o.homeRadius; ++dx) + if (dx * dx + dy * dy <= o.homeRadius * o.homeRadius) + game.map.setUMTerrain(hx + dx, hy + dy, GRASS); + } + game.map.controlSand(); + game.map.rebuildTerrain(); + + std::vector> teamWheatAreas, teamWoodAreas; + for (int team = 0; team < context.request.nbTeams; ++team) { + std::vector home(size_t(width) * height, 0); + std::vector wheatArea, woodArea; + for (int dy = -o.homeRadius; dy <= o.homeRadius; ++dy) + for (int dx = -o.homeRadius; dx <= o.homeRadius; ++dx) + if (dx * dx + dy * dy <= o.homeRadius * o.homeRadius) { + const int x = game.map.normalizeX(homes[team].x + dx); + const int y = game.map.normalizeY(homes[team].y + dy); + home[y * width + x] = 1; + if (game.map.isGrass(x, y) && dx < -o.homeRadius / 2) + wheatArea.emplace_back(x, y); + if (game.map.isGrass(x, y) && dx > o.homeRadius / 2) + woodArea.emplace_back(x, y); + } + if (!placeSettlement(game, context, team, home, homes[team], "starts")) + return false; + teamWheatAreas.push_back(wheatArea); + teamWoodAreas.push_back(woodArea); + } + context.stage = "resources"; + scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); + for (int team = 0; team < context.request.nbTeams; ++team) + if (!placeResourceClumpInArea(game.map, context, teamWheatAreas[team], CORN, + 2) || + !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, + 2)) + return false; + return true; +} +} // namespace + +LatticeOptions::LatticeOptions(const GenerationRequest &r) + : isletSize(r.option("islet-size")), + channelWidth(r.option("channel-width")), + homeRadius(r.option("home-radius")), corn(r.option("wheat")), + wood(r.option("wood")), stone(r.option("stone")), + algae(r.option("algae")), fruit(r.option("fruit")) {} + +GeneratorDefinition latticeDefinition() { + return { + "lattice", + 10, + "Lattice", + 1, + false, + {{"islet-size", "Islet size", 1, 4, 1, 2, ControlGroup::Terrain}, + {"channel-width", "Channel width", 2, 4, 1, 3, ControlGroup::Terrain}, + {"home-radius", "Home radius", 12, 20, 2, 16, ControlGroup::Layout}, + {"wheat", "Wheat", 0, 64, 1, 50, ControlGroup::Resources}, + {"wood", "Wood", 0, 64, 1, 50, ControlGroup::Resources}, + {"stone", "Stone", 0, 64, 1, 50, ControlGroup::Resources}, + {"algae", "Algae", 0, 64, 1, 50, ControlGroup::Resources}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources}}, + generate}; +} diff --git a/src/map/generator/generators/LatticeGenerator.h b/src/map/generator/generators/LatticeGenerator.h new file mode 100644 index 000000000..ff88ded7e --- /dev/null +++ b/src/map/generator/generators/LatticeGenerator.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct LatticeOptions +{ + int isletSize, channelWidth, homeRadius; + int corn, wood, stone, algae, fruit; + explicit LatticeOptions(const GenerationRequest &r); +}; +GeneratorDefinition latticeDefinition(); diff --git a/src/map/generator/generators/MazeGenerator.cpp b/src/map/generator/generators/MazeGenerator.cpp new file mode 100644 index 000000000..665dfdd8a --- /dev/null +++ b/src/map/generator/generators/MazeGenerator.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "MazeGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "Resources.h" +#include "Settlements.h" +#include "Topology.h" +#include +#include +using namespace MapGeneration; +namespace { +std::string validate(const GenerationRequest &r) { + const MazeOptions o(r); + if (o.roomSize + 2 > o.cellSize) + return "Room size must not exceed cell size."; + if (o.corridorWidth + 2 > o.cellSize) + return "Corridor width must not exceed cell size."; + const int cells = (1 << r.wDec) / o.cellSize * ((1 << r.hDec) / o.cellSize); + return cells < r.nbTeams ? "The map needs at least one maze room per colony." + : ""; +} + +bool generate(Game &game, GenerationContext &context) { + context.stage = "maze layout"; + const MazeOptions o(context.request); + const int width = game.map.getW(), height = game.map.getH(); + const int columns = width / o.cellSize, rows = height / o.cellSize; + const int cellCount = columns * rows; + game.map.makeHomogenMap(WATER); + for (int i = 0; i < context.request.nbTeams; ++i) + game.addTeam(); + + std::vector> edges; + // Shore generation consumes the outer terrain tile. The room and corridor + // controls describe the grass remaining after that shoreline is built. + const int rawRoomSize = o.roomSize + 2; + for (int y = 0; y < rows; ++y) + for (int x = 0; x < columns; ++x) { + const int cell = y * columns + x; + if (x + 1 < columns) + edges.emplace_back(cell, cell + 1); + if (y + 1 < rows) + edges.emplace_back(cell, cell + columns); + } + for (size_t i = edges.size(); i > 1; --i) + std::swap(edges[i - 1], edges[context.bounded("maze", i)]); + + std::vector parent(cellCount); + for (int i = 0; i < cellCount; ++i) + parent[i] = i; + auto root = [&](int a) { + int r = a; + while (parent[r] != r) + r = parent[r]; + while (parent[a] != a) { + const int next = parent[a]; + parent[a] = r; + a = next; + } + return r; + }; + std::vector connected(edges.size(), 0); + int treeEdges = 0; + for (size_t i = 0; i < edges.size() && treeEdges < cellCount - 1; ++i) { + const int a = root(edges[i].first), b = root(edges[i].second); + if (a != b) { + parent[a] = b; + connected[i] = 1; + ++treeEdges; + } + } + int loops = + std::min(cellCount * o.loopiness / 100, int(edges.size()) - treeEdges); + for (size_t i = 0; i < edges.size() && loops; ++i) + if (!connected[i]) { + connected[i] = 1; + --loops; + } + + for (int y = 0; y < rows; ++y) + for (int x = 0; x < columns; ++x) { + const int cx = x * o.cellSize + o.cellSize / 2; + const int cy = y * o.cellSize + o.cellSize / 2; + for (int dy = 0; dy < rawRoomSize; ++dy) + for (int dx = 0; dx < rawRoomSize; ++dx) + game.map.setUMTerrain(cx - rawRoomSize / 2 + dx, + cy - rawRoomSize / 2 + dy, GRASS); + } + for (size_t i = 0; i < edges.size(); ++i) + if (connected[i]) { + const int a = edges[i].first, b = edges[i].second; + const int x0 = (a % columns) * o.cellSize + o.cellSize / 2; + const int y0 = (a / columns) * o.cellSize + o.cellSize / 2; + const int x1 = (b % columns) * o.cellSize + o.cellSize / 2; + const int y1 = (b / columns) * o.cellSize + o.cellSize / 2; + const int half = o.corridorWidth / 2 + 1; + for (int y = std::min(y0, y1) - half; y <= std::max(y0, y1) + half; ++y) + for (int x = std::min(x0, x1) - half; x <= std::max(x0, x1) + half; ++x) + game.map.setUMTerrain(x, y, GRASS); + } + + // Closed cell edges are real maze walls: a grass seam carrying stone, with + // shoreline and water on both sides. Stone blocks ground units as well as + // swimmers; open tree/loop edges remain gaps through the wall grid. + std::vector stoneWall(size_t(width) * height, 0); + for (size_t i = 0; i < edges.size(); ++i) + if (!connected[i]) { + const int a = edges[i].first, b = edges[i].second; + const int ax = (a % columns) * o.cellSize + o.cellSize / 2; + const int ay = (a / columns) * o.cellSize + o.cellSize / 2; + const int bx = (b % columns) * o.cellSize + o.cellSize / 2; + const int by = (b / columns) * o.cellSize + o.cellSize / 2; + if (ay == by) { + const int wallX = (ax + bx) / 2; + for (int y = ay - o.cellSize / 2; y <= ay + o.cellSize / 2; ++y) { + for (int dx = -1; dx <= 1; ++dx) + game.map.setUMTerrain(wallX + dx, y, GRASS); + stoneWall[game.map.normalizeY(y) * width + + game.map.normalizeX(wallX)] = 1; + } + } else { + const int wallY = (ay + by) / 2; + for (int x = ax - o.cellSize / 2; x <= ax + o.cellSize / 2; ++x) { + for (int dy = -1; dy <= 1; ++dy) + game.map.setUMTerrain(x, wallY + dy, GRASS); + stoneWall[game.map.normalizeY(wallY) * width + + game.map.normalizeX(x)] = 1; + } + } + } + game.map.controlSand(); + game.map.rebuildTerrain(); + + RegionGraph graph(cellCount); + for (size_t i = 0; i < edges.size(); ++i) + if (connected[i]) { + graph[edges[i].first].push_back(edges[i].second); + graph[edges[i].second].push_back(edges[i].first); + } + std::vector homes{0}; + std::vector chosen(cellCount, 0); + chosen[0] = 1; + for (int team = 1; team < context.request.nbTeams; ++team) { + const auto distances = graphDistances(graph, homes); + int best = -1; + for (int cell = 0; cell < cellCount; ++cell) + if (!chosen[cell] && (best < 0 || distances[cell] > distances[best])) + best = cell; + if (best < 0) + return false; + chosen[best] = 1; + homes.push_back(best); + } + + std::vector> teamWheatAreas, teamWoodAreas; + for (int team = 0; team < context.request.nbTeams; ++team) { + const int cell = homes[team]; + const int cx = (cell % columns) * o.cellSize + o.cellSize / 2; + const int cy = (cell / columns) * o.cellSize + o.cellSize / 2; + std::vector home(size_t(width) * height, 0); + std::vector wheatArea, woodArea; + for (int dy = -1; dy < rawRoomSize + 1; ++dy) + for (int dx = -1; dx < rawRoomSize + 1; ++dx) { + const int x = game.map.normalizeX(cx - rawRoomSize / 2 + dx); + const int y = game.map.normalizeY(cy - rawRoomSize / 2 + dy); + if (!game.map.isWater(x, y)) { + home[y * width + x] = 1; + if (game.map.isGrass(x, y) && dx < rawRoomSize / 3) + wheatArea.emplace_back(x, y); + if (game.map.isGrass(x, y) && dx > rawRoomSize * 2 / 3) + woodArea.emplace_back(x, y); + } + } + if (!placeSettlement(game, context, team, home, {cx, cy}, "starts")) + return false; + teamWheatAreas.push_back(wheatArea); + teamWoodAreas.push_back(woodArea); + } + context.stage = "resources"; + scatterResources(game, context, {o.corn, o.wood, o.stone, o.algae, o.fruit}); + for (int team = 0; team < context.request.nbTeams; ++team) + if (!placeResourceClumpInArea(game.map, context, teamWheatAreas[team], CORN, + 2) || + !placeResourceClumpInArea(game.map, context, teamWoodAreas[team], WOOD, + 2)) + return false; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + if (stoneWall[y * width + x]) + game.map.setResource(x, y, STONE, 1); + return true; +} +} // namespace + +MazeOptions::MazeOptions(const GenerationRequest &r) + : cellSize(r.option("cell-size")), roomSize(r.option("room-size")), + corridorWidth(r.option("corridor-width")), + loopiness(r.option("loopiness")), corn(r.option("wheat")), + wood(r.option("wood")), stone(r.option("stone")), + algae(r.option("algae")), fruit(r.option("fruit")) {} + +GeneratorDefinition mazeDefinition() { + return {"maze", + 11, + "Maze", + 1, + false, + {{"cell-size", + "Cell size", + 20, + 32, + 1, + 24, + ControlGroup::Layout, + false, + false, + {20, 24, 32}}, + {"room-size", "Room size", 12, 16, 1, 16, ControlGroup::Layout}, + {"corridor-width", + "Corridor width", + 1, + 5, + 1, + 3, + ControlGroup::Layout, + false, + false, + {1, 3, 5}}, + {"loopiness", + "Loopiness", + 0, + 100, + 1, + 25, + ControlGroup::Layout, + false, + false, + {0, 10, 25, 50, 75, 100}}, + {"wheat", "Wheat", 0, 64, 1, 50, ControlGroup::Resources}, + {"wood", "Wood", 0, 64, 1, 50, ControlGroup::Resources}, + {"stone", "Stone", 0, 64, 1, 50, ControlGroup::Resources}, + {"algae", "Algae", 0, 64, 1, 50, ControlGroup::Resources}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources}}, + generate, + true, + validate}; +} diff --git a/src/map/generator/generators/MazeGenerator.h b/src/map/generator/generators/MazeGenerator.h new file mode 100644 index 000000000..4bb3d1008 --- /dev/null +++ b/src/map/generator/generators/MazeGenerator.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct MazeOptions { + int cellSize, roomSize, corridorWidth, loopiness; + int corn, wood, stone, algae, fruit; + explicit MazeOptions(const GenerationRequest &r); +}; +GeneratorDefinition mazeDefinition(); diff --git a/src/map/generator/generators/RiverGenerator.cpp b/src/map/generator/generators/RiverGenerator.cpp new file mode 100644 index 000000000..383fc143e --- /dev/null +++ b/src/map/generator/generators/RiverGenerator.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "RiverGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +using namespace MapGeneration; +static bool generate(Game &game, GenerationContext &context) +{ + const RiverOptions options(context.request); + const HeightFieldOptions terrain{ + options.water, options.sand, options.grass, options.desert, + options.smoothing, options.fruit, options.repeat, false}; + if (!generateHeightField(game, context, terrain, + [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) + { hm.makeRiver(options.river_width * (w + h) / 2 / 100, smoothing); })) + return false; + context.stage = "starts"; + return placeStarts(game, context); +} + +GeneratorDefinition riverDefinition() +{ + return {"river", + 2, + "River", + 1, + false, + {{"water", "Water weight", 0, 100, 1, 45, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 3, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 75, ControlGroup::Terrain, false, true}, + {"desert", "Desert weight", 0, 100, 1, 0, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 4, ControlGroup::Terrain, false}, + {"river-width", "River width", 20, 65, 5, 35, ControlGroup::Terrain, false}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, + {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, + generate}; +} diff --git a/src/map/generator/generators/RiverGenerator.h b/src/map/generator/generators/RiverGenerator.h new file mode 100644 index 000000000..ac8ad491b --- /dev/null +++ b/src/map/generator/generators/RiverGenerator.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct RiverOptions +{ + int water, sand, grass, desert, smoothing, river_width, fruit, repeat; + explicit RiverOptions(const GenerationRequest &r) + : water(r.option("water")), sand(r.option("sand")), grass(r.option("grass")), + desert(r.option("desert")), smoothing(r.option("smoothing")), + river_width(r.option("river-width")), fruit(r.option("fruit")), repeat(r.option("repeat")) + { + } +}; +GeneratorDefinition riverDefinition(); diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.cpp b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp new file mode 100644 index 000000000..025dda25a --- /dev/null +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.cpp @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GeneratorDefinition.h" +#include "GlobalContainer.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "Resources.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include "Unit.h" +#include +#include +using namespace MapGeneration; +#include "RuggedArchipelagoGenerator.h" + +static bool terrain(Game &game, GenerationContext &context, const RuggedArchipelagoOptions &options) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + + // First, fill with water: + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + map.setUMTerrain(x, y, WATER); + + // Two, plants "bootstraps" + int *bootX = context.bootX.data(); + int *bootY = context.bootY.data(); + int nbIslands = context.request.nbTeams; + int islandsSize = (int)(((w + h) * options.island_size) / (400.0 * sqrt((double)nbIslands))); + if (islandsSize < 8) + islandsSize = 8; + int minDistSquare = (w * h) / nbIslands; + + int c = 0; + for (int i = 0; i < nbIslands; i++) + { + int x = context.stream("terrain")() % w; + int y = context.stream("terrain")() % h; + bool failed = false; + int j; + for (j = 0; j < i; j++) + if (map.warpDistSquare(x, y, bootX[j], bootY[j]) < minDistSquare) + { + failed = true; + break; + } + if (failed) + { + + i--; + if (c++ > 65536) + { + minDistSquare = minDistSquare >> 1; + // I think that you need to do this only once, in worst case. + // With a few luck you doesn't need to. + c = 0; + } + } + else + { + bootX[i] = x; + bootY[i] = y; + for (int dx = -1; dx < 6; dx++) + for (int dy = 0; dy < 6; dy++) + map.setUMTerrain(x + dx, y + dy, GRASS); + } + } + + // Three, expands islands + for (int s = 0; s < islandsSize; s++) + { + for (int oddEven = 0; oddEven < 2; oddEven++) + { + for (int y = oddEven; y < h; y += 2) + { + for (int x = oddEven; x < w; x += 2) + { + TerrainType umt = map.getUMTerrain(x, y); + if (umt == GRASS) + continue; + + int a, b; + switch (context.stream("terrain")() & 15) + { + case 0: + a = map.getUMTerrain(x + 1, y); + b = map.getUMTerrain(x - 1, y); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + case 1: + a = map.getUMTerrain(x, y - 1); + b = map.getUMTerrain(x, y + 1); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + case 2: + a = map.getUMTerrain(x + 1, y + 1); + b = map.getUMTerrain(x - 1, y - 1); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + case 3: + a = map.getUMTerrain(x + 1, y - 1); + b = map.getUMTerrain(x - 1, y + 1); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + case 4: + a = map.getUMTerrain(x + 2, y); + b = map.getUMTerrain(x - 2, y); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + case 5: + a = map.getUMTerrain(x, y - 2); + b = map.getUMTerrain(x, y + 2); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + case 6: + a = map.getUMTerrain(x + 2, y + 2); + b = map.getUMTerrain(x - 2, y - 2); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + case 7: + a = map.getUMTerrain(x + 2, y - 2); + b = map.getUMTerrain(x - 2, y + 2); + if ((a == GRASS) || (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + } + break; + default: + break; + } + } + } + } + } + + // Four, avoid too much sand. Let's smooth + for (int s = 0; s < 2; s++) + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + int a, b; + a = map.getUMTerrain(x + 1, y); + b = map.getUMTerrain(x - 1, y); + if ((a == GRASS) && (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + continue; + } + a = map.getUMTerrain(x, y - 1); + b = map.getUMTerrain(x, y + 1); + if ((a == GRASS) && (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + continue; + } + a = map.getUMTerrain(x + 1, y + 1); + b = map.getUMTerrain(x - 1, y - 1); + if ((a == GRASS) && (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + continue; + } + a = map.getUMTerrain(x + 1, y - 1); + b = map.getUMTerrain(x - 1, y + 1); + if ((a == GRASS) && (b == GRASS)) + { + map.setUMTerrain(x, y, GRASS); + continue; + } + } + + map.controlSand(); + + // Five, add some sand + for (int s = 0; s < options.beach_size; s++) + for (int dy = 0; dy < 4; dy++) + for (int dx = 0; dx < 4; dx++) + for (int y = dy; y < h; y += 4) + for (int x = dx; x < w; x += 4) + { + int a, b; + switch (context.stream("terrain")() & 7) + { + case 0: + a = map.getUMTerrain(x + 1, y); + b = map.getUMTerrain(x - 1, y); + if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + case 1: + a = map.getUMTerrain(x, y - 1); + b = map.getUMTerrain(x, y + 1); + if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + case 2: + a = map.getUMTerrain(x + 1, y + 1); + b = map.getUMTerrain(x - 1, y - 1); + if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + case 3: + a = map.getUMTerrain(x + 1, y - 1); + b = map.getUMTerrain(x - 1, y + 1); + if (((a == SAND) && (b == WATER)) || ((a == WATER) && (b == SAND))) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + + case 4: + a = map.getUMTerrain(x + 1, y); + b = map.getUMTerrain(x - 1, y); + if ((a == SAND) && (b == SAND)) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + case 5: + a = map.getUMTerrain(x, y - 1); + b = map.getUMTerrain(x, y + 1); + if ((a == SAND) && (b == SAND)) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + case 6: + a = map.getUMTerrain(x + 1, y + 1); + b = map.getUMTerrain(x - 1, y - 1); + if ((a == SAND) && (b == SAND)) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + case 7: + a = map.getUMTerrain(x + 1, y - 1); + b = map.getUMTerrain(x - 1, y + 1); + if ((a == SAND) && (b == SAND)) + { + map.setUMTerrain(x, y, SAND); + continue; + } + break; + } + } + + // map.controlSand(); + map.rebuildTerrain(); + return true; +} + +static void resources(Game &game, GenerationContext &context, + const RuggedArchipelagoOptions &options) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + + int *bootX = context.bootX.data(); + int *bootY = context.bootY.data(); + + int islandsSize = + (int)(((w + h) * options.island_size) / (400.0 * sqrt((double)context.request.nbTeams))); + if (islandsSize < 8) + islandsSize = 8; + // let's add resources... + int smoothResources = islandsSize / 4; + for (int s = 0; s < context.request.nbTeams; s++) + { + int d, p, amount; + int smallestAmount; + int smallestResource; + + // WOOD + for (d = 0; d < islandsSize; d++) + if (!map.isGrass(bootX[s], bootY[s] - d)) + break; + amount = context.request.resourceAmounts[WOOD]; + amount = d - smoothResources - 2; + if (amount < 1) + amount = 1; + p = d - 1 - amount / 2; + if (amount > 0) + map.setResource(bootX[s], bootY[s] - p, WOOD, amount); + smallestAmount = amount; + smallestResource = WOOD; + + // WHEAT + for (d = 0; d < islandsSize; d++) + if (!map.isGrass(bootX[s] - d, bootY[s])) + break; + amount = context.request.resourceAmounts[CORN]; + amount = d - smoothResources - 0; + if (amount < 1) + amount = 1; + p = d - 1 - amount / 2; + if (amount > 0) + map.setResource(bootX[s] - p, bootY[s], CORN, amount); + if (amount < smallestAmount) + { + smallestAmount = amount; + smallestResource = CORN; + } + + // STONE + for (d = 0; d < islandsSize; d++) + if (!map.isGrass(bootX[s], bootY[s] + d)) + break; + map.setResource(bootX[s], bootY[s] + p, STONE, 1); + + // We add the resource with the smallest amount: + for (d = 0; d < islandsSize; d++) + if (!map.isGrass(bootX[s] + d, bootY[s] + d)) + break; + amount = context.request.resourceAmounts[smallestResource]; + amount = d - smoothResources - 3; + if (amount < 1) + amount = 1; + p = d - 1 - amount / 2; + if (amount > 0) + map.setResource(bootX[s] + p, bootY[s] + p, smallestResource, amount); + + // ALGAE + for (d = 0; d < 2 * islandsSize; d++) + if (map.isWater(bootX[s] + d, bootY[s])) + break; + amount = context.request.resourceAmounts[ALGA]; + amount = smoothResources; + p = d + smoothResources - 1 + amount / 2; + if (amount > 0) + map.setResource(bootX[s] + p, bootY[s], ALGA, amount); + } + + // Let's smooth resources... + map.smoothResources(smoothResources * 2); +} + +static bool generate(Game &game, GenerationContext &context) +{ + const RuggedArchipelagoOptions options(context.request); + if (!terrain(game, context, options)) + return false; + context.stage = "starts"; + if (!placeArchipelagoStarts(game, context, options.island_size)) + return false; + context.stage = "resources"; + resources(game, context, options); + return true; +} + +GeneratorDefinition ruggedArchipelagoDefinition() +{ + return {"rugged-archipelago", + 8, + "Old islands", + 1, + false, + {{"island-size", "Island size", 50, 70, 1, 65, ControlGroup::Terrain, false}, + {"beach-size", "Beach size", 0, 4, 1, 1, ControlGroup::Terrain, false}}, + generate}; +} diff --git a/src/map/generator/generators/RuggedArchipelagoGenerator.h b/src/map/generator/generators/RuggedArchipelagoGenerator.h new file mode 100644 index 000000000..d411aa6db --- /dev/null +++ b/src/map/generator/generators/RuggedArchipelagoGenerator.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct RuggedArchipelagoOptions +{ + int island_size, beach_size; + explicit RuggedArchipelagoOptions(const GenerationRequest &r) + : island_size(r.option("island-size")), beach_size(r.option("beach-size")) + { + } +}; +GeneratorDefinition ruggedArchipelagoDefinition(); diff --git a/src/map/generator/generators/ShatteredCoastGenerator.cpp b/src/map/generator/generators/ShatteredCoastGenerator.cpp new file mode 100644 index 000000000..27c1691ef --- /dev/null +++ b/src/map/generator/generators/ShatteredCoastGenerator.cpp @@ -0,0 +1,817 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GeneratorDefinition.h" +#include "GlobalContainer.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "Resources.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include "Unit.h" +#include +#include +#include +using namespace MapGeneration; +#include "ShatteredCoastGenerator.h" + +static void simulateRandomMap(GenerationContext &context, int smooth, double baseWater, + double baseSand, double baseGrass, double *finalWater, + double *finalSand, double *finalGrass) +{ + int w = 32 << (smooth >> 2); + int h = w; + int s = w * h; + int m = s - 1; + std::vector undermap(w * h); + + int totalRatio = 0x7FFF; + int waterRatio = (int)(baseWater * ((double)totalRatio)); + int sandRatio = (int)(baseSand * ((double)totalRatio)); + int grassRatio = (int)(baseGrass * ((double)totalRatio)); + totalRatio = waterRatio + sandRatio + grassRatio; + + if (totalRatio == 0) + { + waterRatio = 1; + sandRatio = 1; + grassRatio = 1; + totalRatio = 3; + } + + /// First, we create a fully random patchwork: + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + int r = context.stream("simulation")() % totalRatio; + r -= waterRatio; + if (r < 0) + { + undermap[y * w + x] = 0; + continue; + } + r -= sandRatio; + if (r < 0) + { + undermap[y * w + x] = 1; + continue; + } + r -= grassRatio; + if (r < 0) + { + undermap[y * w + x] = 2; + continue; + } + assert(false); // Want's to sing ? + } + + for (int i = 0; i < smooth; i++) + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + if (context.stream("simulation")() & 4) + { + int a = undermap[(y * w + x + 1 + s) & m]; + int b = undermap[(y * w + x - 1 + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + else + { + int a = undermap[(y * w + x + w + s) & m]; + int b = undermap[(y * w + x - w + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + if (context.stream("simulation")() & 4) + { + int a = undermap[(y * w + x + w + 1 + s) & m]; + int b = undermap[(y * w + x - w - 1 + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + else + { + int a = undermap[(y * w + x + w - 1 + s) & m]; + int b = undermap[(y * w + x - w + 1 + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + if (context.stream("simulation")() & 4) + { + int a = undermap[(y * w + x + w - 2 + s) & m]; + int b = undermap[(y * w + x - w + 2 + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + else + { + int a = undermap[(y * w + x + w - (h << 1) + s) & m]; + int b = undermap[(y * w + x - w + (h << 1) + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + if (context.stream("simulation")() & 4) + { + int a = undermap[(y * w + x + w + 2 + (h << 1) + s) & m]; + int b = undermap[(y * w + x - w - 2 - (h << 1) + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + else + { + int a = undermap[(y * w + x + w + 2 - (h << 1) + s) & m]; + int b = undermap[(y * w + x - w - 2 + (h << 1) + s) & m]; + if (a == b) + { + undermap[y * w + x] = a; + continue; + } + } + } + // What's finally in ? + int waterCount = 0; + int sandCount = 0; + int grassCount = 0; + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + switch (undermap[y * w + x]) + { + case 0: + waterCount++; + continue; + case 1: + sandCount++; + continue; + case 2: + grassCount++; + continue; + } + int totalCount = waterCount + sandCount + grassCount; + + *finalWater = ((double)waterCount) / ((double)totalCount); + *finalSand = ((double)sandCount) / ((double)totalCount); + *finalGrass = ((double)grassCount) / ((double)totalCount); +} + +static bool terrain(Game &game, GenerationContext &context, const ShatteredCoastOptions &options) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + + int waterRatio = options.water; + int sandRatio = options.sand; + int grassRatio = options.grass; + int totalRatio = waterRatio + sandRatio + grassRatio; + int smooth = options.smoothing; + double baseWater, baseSand, baseGrass; + + if (totalRatio == 0) + { + baseWater = baseSand = baseGrass = 1.0 / 3.0; + } + else + { + baseWater = (float)waterRatio / (float)totalRatio; + baseSand = (float)sandRatio / (float)totalRatio; + baseGrass = (float)grassRatio / (float)totalRatio; + } + // Sorry, the equation is too complex for me. We use a numeric approach: + double alphaWater = baseWater; + double alphaSand = baseSand; + double alphaGrass = baseGrass; + double alphaSum = alphaWater + alphaSand + alphaGrass; + alphaWater /= alphaSum; + alphaSand /= alphaSum; + alphaGrass /= alphaSum; + + for (int r = 1; r <= smooth; r++) + { + for (int prec = 0; prec < 3; prec++) + { + double finalAlphaWater, finalAlphaSand, finalAlphaGrass; + simulateRandomMap(context, r, alphaWater, alphaSand, alphaGrass, &finalAlphaWater, + &finalAlphaSand, &finalAlphaGrass); + + double errAlphaWater = finalAlphaWater - baseWater; + double errAlphaSand = finalAlphaSand - baseSand; + double errAlphaGrass = finalAlphaGrass - baseGrass; + + double betaWater; + double betaSand; + double betaGrass; + + if (finalAlphaWater) + betaWater = (alphaWater * baseWater) / finalAlphaWater; + else + betaWater = 0; + if (finalAlphaSand) + betaSand = (alphaSand * baseSand) / finalAlphaSand; + else + betaSand = 0; + if (finalAlphaGrass) + betaGrass = (alphaGrass * baseGrass) / finalAlphaGrass; + else + betaGrass = 0; + double betaSum = betaWater + betaSand + betaGrass; + betaWater /= betaSum; + betaSand /= betaSum; + betaGrass /= betaSum; + + double finalBetaWater, finalBetaSand, finalBetaGrass; + simulateRandomMap(context, r, betaWater, betaSand, betaGrass, &finalBetaWater, + &finalBetaSand, &finalBetaGrass); + + double errBetaWater = finalBetaWater - baseWater; + double errBetaSand = finalBetaSand - baseSand; + double errBetaGrass = finalBetaGrass - baseGrass; + + double projNom = (errBetaWater * errAlphaWater + errBetaSand * errAlphaSand + + errBetaGrass * errAlphaGrass); + double projDen = (errAlphaWater * errAlphaWater + errAlphaSand * errAlphaSand + + errAlphaGrass * errAlphaGrass); + if (projDen <= 0) + continue; + double proj = projNom / projDen; + + double minErr = DBL_MAX; + for (double cfi = 0.0; cfi <= 1.0; cfi += 0.1) + { + double cf = cfi * proj; + + double sumCenter = 1.0 - cf; + double gammaWater = (-cf * betaWater + 1.0 * alphaWater) / sumCenter; + double gammaSand = (-cf * betaSand + 1.0 * alphaSand) / sumCenter; + double gammaGrass = (-cf * betaGrass + 1.0 * alphaGrass) / sumCenter; + if (gammaWater < 0.0) + gammaWater = 0.0; + if (gammaSand < 0.0) + gammaSand = 0.0; + if (gammaGrass < 0.0) + gammaGrass = 0.0; + double gammaSum = gammaWater + gammaSand + gammaGrass; + if (gammaSum <= 0) + continue; + gammaWater /= gammaSum; + gammaSand /= gammaSum; + gammaGrass /= gammaSum; + + double finalGammaWater, finalGammaSand, finalGammaGrass; + simulateRandomMap(context, r, gammaWater, gammaSand, gammaGrass, &finalGammaWater, + &finalGammaSand, &finalGammaGrass); + + double errGammaWater = finalGammaWater - baseWater; + double errGammaSand = finalGammaSand - baseSand; + double errGammaGrass = finalGammaGrass - baseGrass; + double errGamma = (errGammaWater * errGammaWater + errGammaSand * errGammaSand + + errGammaGrass * errGammaGrass); + + if (errGamma < minErr) + { + minErr = errGamma; + alphaWater = gammaWater; + alphaSand = gammaSand; + alphaGrass = gammaGrass; + } + } + } + } + + double simWater, simSand, simGrass; + simulateRandomMap(context, smooth, alphaWater, alphaSand, alphaGrass, &simWater, &simSand, + &simGrass); + + totalRatio = 0x7FFF; + waterRatio = (int)(((double)alphaWater) * ((double)totalRatio)); + sandRatio = (int)(((double)alphaSand) * ((double)totalRatio)); + grassRatio = (int)(((double)alphaGrass) * ((double)totalRatio)); + if (waterRatio < 0) + waterRatio = 0; + if (sandRatio < 0) + sandRatio = 0; + if (grassRatio < 0) + grassRatio = 0; + totalRatio = waterRatio + sandRatio + grassRatio; + + // First, we create a fully random patchwork: + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + int r = context.stream("terrain")() % totalRatio; + r -= waterRatio; + if (r < 0) + { + map.setUMTerrain(x, y, WATER); + continue; + } + r -= sandRatio; + if (r < 0) + { + map.setUMTerrain(x, y, SAND); + continue; + } + r -= grassRatio; + if (r < 0) + { + map.setUMTerrain(x, y, GRASS); + continue; + } + assert(false); // Want's to sing ? + } + + // What's finally in ? + int waterCount = 0; + int sandCount = 0; + int grassCount = 0; + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + switch (map.getUMTerrain(x, y)) + { + case WATER: + waterCount++; + continue; + case SAND: + sandCount++; + continue; + case GRASS: + grassCount++; + continue; + } + } + double totalCount = (double)(waterCount + sandCount + grassCount); + + for (int i = 0; i < smooth; i++) + { + // What's in now? + waterCount = 0; + sandCount = 0; + grassCount = 0; + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + switch (map.getUMTerrain(x, y)) + { + case WATER: + waterCount++; + continue; + case SAND: + sandCount++; + continue; + case GRASS: + grassCount++; + continue; + } + } + double totalRatioCount = (double)(waterCount + sandCount + grassCount); + double waterRatioCount = waterCount / totalRatioCount; + double sandRatioCount = sandCount / totalRatioCount; + double grassRatioCount = grassCount / totalRatioCount; + + double errWaterRatioCount = waterRatioCount - baseWater; + double errSandRatioCount = sandRatioCount - baseSand; + double errGrassRatioCount = grassRatioCount - baseGrass; + + Uint32 allowed[3]; + if (errWaterRatioCount > 0) + allowed[0] = (Uint32)(pow(errWaterRatioCount, 0.125) * 4294967296.0); + else + allowed[0] = 0; + if (errSandRatioCount > 0) + allowed[1] = (Uint32)(pow(errSandRatioCount, 0.125) * 4294967296.0); + else + allowed[1] = 0; + if (errGrassRatioCount > 0) + allowed[2] = (Uint32)(pow(errGrassRatioCount, 0.125) * 4294967296.0); + else + allowed[2] = 0; + + assert(allowed[0] <= (Uint32)0xFFFFFFFF); + assert(allowed[1] <= (Uint32)0xFFFFFFFF); + assert(allowed[2] <= (Uint32)0xFFFFFFFF); + + if (i == 0) + { + allowed[0] = 0; + allowed[1] = 0; + allowed[2] = 0; + } + + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + if (context.stream("terrain")() & 4) + { + int a = map.getUMTerrain(x + 1, y); + int b = map.getUMTerrain(x - 1, y); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + else + { + int a = map.getUMTerrain(x, y - 1); + int b = map.getUMTerrain(x, y + 1); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + if (context.stream("terrain")() & 4) + { + int a = map.getUMTerrain(x + 1, y + 1); + int b = map.getUMTerrain(x - 1, y - 1); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + else + { + int a = map.getUMTerrain(x + 1, y - 1); + int b = map.getUMTerrain(x - 1, y + 1); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + if (context.stream("terrain")() & 4) + { + int a = map.getUMTerrain(x + 2, y); + int b = map.getUMTerrain(x - 2, y); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + else + { + int a = map.getUMTerrain(x, y - 2); + int b = map.getUMTerrain(x, y + 2); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + if (context.stream("terrain")() & 4) + { + int a = map.getUMTerrain(x + 2, y + 2); + int b = map.getUMTerrain(x - 2, y - 2); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + else + { + int a = map.getUMTerrain(x + 2, y - 2); + int b = map.getUMTerrain(x - 2, y + 2); + if ((a == b) && (allowed[a] <= context.stream("terrain")())) + { + map.setUMTerrain(x, y, (TerrainType)a); + continue; + } + } + } + } + // What's finally in ? + waterCount = 0; + sandCount = 0; + grassCount = 0; + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + switch (map.getUMTerrain(x, y)) + { + case WATER: + waterCount++; + continue; + case SAND: + sandCount++; + continue; + case GRASS: + grassCount++; + continue; + } + } + totalCount = (double)(waterCount + sandCount + grassCount); + + map.controlSand(); + + // Now, we have to find suitable places for teams: + int nbTeams = context.request.nbTeams; + int minDistSquare = (int)((double)((double)w * (double)h * (double)grassCount) / + (double)((double)nbTeams * (double)totalCount)); + if (minDistSquare <= 0) + return false; + assert(minDistSquare > 0); + int *bootX = context.bootX.data(); + int *bootY = context.bootY.data(); + + // TODO: First pass to find the number of available places. + for (int team = 0; team < nbTeams; team++) + { + int maxSurface = 0; + int maxX = 0; + int maxY = 0; + for (int y = 0; y < h; y++) + { + int width = 0; + int startX = 0; + for (int x = 0; x < w; x++) + { + int a = map.getUMTerrain(x, y); + if (a == GRASS) + width++; + else + { + if (width > 7) + { + int centerX = ((x + startX) >> 1); + int top, bot; + for (top = 0; top < h; top++) + if (map.getUMTerrain(centerX, y - top) != GRASS) + break; + for (bot = 0; bot < h; bot++) + if (map.getUMTerrain(centerX, y + bot) != GRASS) + break; + int height = top + bot - 1; + int surface = height * width; + assert(surface > 0); + + int centerY = y + ((bot - top) >> 1); + bool farEnough = true; + for (int ti = 0; ti < team; ti++) + if (map.warpDistSquare(centerX, centerY, bootX[ti], bootY[ti]) < + minDistSquare) + { + farEnough = false; + break; + } + + if (surface > maxSurface && farEnough) + { + maxSurface = surface; + maxX = centerX; + maxY = centerY; + } + } + width = 0; + startX = x; + } + } + } + + if (maxSurface <= 0) + { + context.stage = "starting locations"; + context.detail = "Cannot space every colony on grass"; + return false; + } + assert(maxSurface); + bootX[team] = maxX; + bootY[team] = maxY; + + for (int dx = -1; dx < 6; dx++) + for (int dy = 0; dy < 6; dy++) + map.setUMTerrain(maxX + dx, maxY + dy, GRASS); + } + + // Let's add some green space for teams: + int squareSize = 5 + (int)(sqrt((double)minDistSquare) / 4.5); + for (int team = 0; team < nbTeams; team++) + { + map.setUMatPos(context.bootX[team] + 2, context.bootY[team] + 0, GRASS, squareSize); + map.setUMatPos(context.bootX[team] + 2, context.bootY[team] + 2, GRASS, squareSize); + } + + map.controlSand(); + map.rebuildTerrain(); + + return true; +} + +static void resources(Game &game, GenerationContext &context, const ShatteredCoastOptions &options) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + + int *bootX = context.bootX.data(); + int *bootY = context.bootY.data(); + int nbTeams = context.request.nbTeams; + int limitDist = (w + h) / (2 * nbTeams); + + // let's add resources to old map generator + for (int team = 0; team < nbTeams; team++) + { + int smallestWidth = limitDist; + int smallestResource = 0; + + bool dirUsed[8]; + for (int i = 0; i < 8; i++) + dirUsed[i] = false; + int resOrder[4]; + resOrder[0] = CORN; + resOrder[1] = WOOD; + resOrder[2] = STONE; + resOrder[3] = CORN; + + int distWeight[4]; + distWeight[0] = 1; + distWeight[1] = 1; + distWeight[2] = 1; + distWeight[3] = 2; + + int widthWeight[4]; + widthWeight[0] = 1; + widthWeight[1] = 1; + widthWeight[2] = 1; + widthWeight[3] = 2; + + for (int resI = 0; resI < 4; resI++) + { + int res = resOrder[resI]; + int maxDir = 0; + int maxWidth = 0; + int maxDist = 0; + for (int dir = 0; dir < 8; dir++) + if (!dirUsed[dir]) + { + int width = 0; + int dx, dy, dist; + Unit::dxDyFromDirection(dir, &dx, &dy); + for (dist = 5; dist < limitDist; dist++) + if (map.isGrass(bootX[team] + dx * dist, bootY[team] + dy * dist)) + width++; + else if (width > 3) + break; + else + width = 1; + + if (dist * distWeight[resI] + width * widthWeight[resI] > + maxDist * distWeight[resI] + maxWidth * widthWeight[resI]) + { + maxWidth = width; + maxDist = dist; + maxDir = dir; + } + } + dirUsed[maxDir] = true; + if (maxWidth < smallestWidth) + { + smallestWidth = maxWidth; + smallestResource = res; + } + + int dx, dy; + Unit::dxDyFromDirection(maxDir, &dx, &dy); + int d = maxDist - (maxWidth >> 1); + dx *= d; + dy *= d; + + int amount = context.request.resourceAmounts[res]; + if (amount > 0) + map.setResource(bootX[team] + dx, bootY[team] + dy, res, amount); + } + + if (smallestWidth < limitDist) + { + int maxDir = 0; + int maxWidth = 0; + int maxDist = 0; + for (int dir = 0; dir < 8; dir++) + if (!dirUsed[dir]) + { + int width = 0; + int dx, dy, dist; + Unit::dxDyFromDirection(dir, &dx, &dy); + for (dist = 0; dist < 2 * limitDist; dist++) + if (map.isGrass(bootX[team] + dx * dist, bootY[team] + dy * dist)) + width++; + else if (width > 3) + break; + else + width = 1; + + if (dist + width > maxDist + maxWidth) + { + maxWidth = width; + maxDist = dist; + maxDir = dir; + } + } + dirUsed[maxDir] = true; + + int dx, dy; + Unit::dxDyFromDirection(maxDir, &dx, &dy); + int d = maxDist - (maxWidth >> 1); + dx *= d; + dy *= d; + + int amount = context.request.resourceAmounts[smallestResource]; + if (amount > 0) + map.setResource(bootX[team] + dx, bootY[team] + dy, smallestResource, amount); + } + + int maxDir = 0; + int maxWidth = 0; + int maxDist = 0; + for (int dir = 0; dir < 8; dir++) + { + int width = 0; + int dx, dy, dist; + Unit::dxDyFromDirection(dir, &dx, &dy); + for (dist = 0; dist < 2 * limitDist; dist++) + if (map.isWater(bootX[team] + dx * dist, bootY[team] + dy * dist)) + width++; + else if (width > 3) + break; + else + width = 1; + + if (dist + width > width + maxWidth) + { + maxWidth = width; + maxDist = dist; + maxDir = dir; + } + } + + int dx, dy; + Unit::dxDyFromDirection(maxDir, &dx, &dy); + int d = maxDist - (maxWidth >> 1); + dx *= d; + dy *= d; + + int amount = context.request.resourceAmounts[ALGA]; + if (amount > 0) + map.setResource(bootX[team] + dx, bootY[team] + dy, ALGA, amount); + } + + // Let's smooth resources... + int maxAmount = 0; + for (int r = 0; r < 4; r++) + if (maxAmount < context.request.resourceAmounts[r]) + maxAmount = context.request.resourceAmounts[r]; + map.smoothResources(maxAmount * 3); +} + +static bool generate(Game &game, GenerationContext &context) +{ + const ShatteredCoastOptions options(context.request); + if (!terrain(game, context, options)) + return false; + context.stage = "starts"; + if (!placeStarts(game, context)) + return false; + context.stage = "resources"; + resources(game, context, options); + return true; +} + +GeneratorDefinition shatteredCoastDefinition() +{ + return {"shattered-coast", + 7, + "Old random", + 1, + false, + {{"water", "Water weight", 0, 100, 1, 40, ControlGroup::Terrain, false, true}, + {"sand", "Sand weight", 0, 100, 1, 4, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 3, ControlGroup::Terrain, false}}, + generate}; +} diff --git a/src/map/generator/generators/ShatteredCoastGenerator.h b/src/map/generator/generators/ShatteredCoastGenerator.h new file mode 100644 index 000000000..f13138660 --- /dev/null +++ b/src/map/generator/generators/ShatteredCoastGenerator.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct ShatteredCoastOptions +{ + int water, sand, grass, smoothing; + explicit ShatteredCoastOptions(const GenerationRequest &r) + : water(r.option("water")), sand(r.option("sand")), grass(r.option("grass")), + smoothing(r.option("smoothing")) + { + } +}; +GeneratorDefinition shatteredCoastDefinition(); diff --git a/src/map/generator/generators/SwampGenerator.cpp b/src/map/generator/generators/SwampGenerator.cpp new file mode 100644 index 000000000..05ae2393a --- /dev/null +++ b/src/map/generator/generators/SwampGenerator.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "SwampGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +using namespace MapGeneration; +static bool generate(Game &game, GenerationContext &context) +{ + const SwampOptions options(context.request); + const HeightFieldOptions terrain{ + options.water, 0, options.grass, 0, options.smoothing, options.fruit, options.repeat, true}; + if (!generateHeightField(game, context, terrain, + [&](HeightMap &hm, unsigned w, unsigned h, float smoothing) + { hm.makeSwamp(smoothing); })) + return false; + context.stage = "starts"; + return placeStarts(game, context); +} + +GeneratorDefinition swampDefinition() +{ + return {"swamp", + 1, + "Swamp", + 1, + false, + {{"water", "Water weight", 0, 100, 1, 35, ControlGroup::Terrain, false, true}, + {"grass", "Grass weight", 0, 100, 1, 60, ControlGroup::Terrain, false, true}, + {"smoothing", "Smoothing", 1, 8, 1, 6, ControlGroup::Terrain, false}, + {"fruit", "Fruit", 0, 64, 1, 4, ControlGroup::Resources, false}, + {"repeat", "Repeat landscape", 0, 5, 1, 0, ControlGroup::Layout, true}}, + generate}; +} diff --git a/src/map/generator/generators/SwampGenerator.h b/src/map/generator/generators/SwampGenerator.h new file mode 100644 index 000000000..e71adc5be --- /dev/null +++ b/src/map/generator/generators/SwampGenerator.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct SwampOptions +{ + int water, grass, smoothing, fruit, repeat; + explicit SwampOptions(const GenerationRequest &r) + : water(r.option("water")), grass(r.option("grass")), smoothing(r.option("smoothing")), + fruit(r.option("fruit")), repeat(r.option("repeat")) + { + } +}; +GeneratorDefinition swampDefinition(); diff --git a/src/map/generator/generators/UniformGenerator.cpp b/src/map/generator/generators/UniformGenerator.cpp new file mode 100644 index 000000000..89c0820b4 --- /dev/null +++ b/src/map/generator/generators/UniformGenerator.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "UniformGenerator.h" +#include "Game.h" +#include "GenerationContext.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +using namespace MapGeneration; +static bool generate(Game &game, GenerationContext &context) +{ + game.map.makeHomogenMap(context.request.terrainType); + game.addTeam(); + return true; +} + +GeneratorDefinition uniformDefinition() +{ + return {"uniform", + 0, + "uniform terrain", + 1, + true, + { + + }, + generate, + false}; +} diff --git a/src/map/generator/generators/UniformGenerator.h b/src/map/generator/generators/UniformGenerator.h new file mode 100644 index 000000000..8a6b5a50c --- /dev/null +++ b/src/map/generator/generators/UniformGenerator.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "GenerationRequest.h" +#include "GeneratorDefinition.h" +struct UniformOptions +{ + explicit UniformOptions(const GenerationRequest &r) {} +}; +GeneratorDefinition uniformDefinition(); diff --git a/src/map/generator/shared/Distances.cpp b/src/map/generator/shared/Distances.cpp new file mode 100644 index 000000000..ec4aa79af --- /dev/null +++ b/src/map/generator/shared/Distances.cpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Distances.h" +#include "GenerationContext.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "Resources.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +#include +#include +using namespace MapGeneration; + +namespace MapGeneration +{ +void adjustHeightmapFromPoints(Map &map, std::vector &points, + std::vector &heightmap, int value) +{ + for (unsigned int i = 0; i < points.size(); ++i) + { + heightmap[points[i].y * map.getW() + points[i].x] += value; + } +} + +void adjustHeightmapFromPerlinNoise(Map &map, GenerationContext &context, std::vector &heights, + int spread) +{ + HeightMap noise(map.getW(), map.getH(), context.stream("noise")); + noise.makePlain(4); + for (int x = 0; x < map.getW(); ++x) + { + for (int y = 0; y < map.getH(); ++y) + { + heights[y * map.getW() + x] += noise.uiLevel(x, y, spread * 2) - spread; + } + } +} + +void computeDistances(Map &map, std::vector &sources, + std::vector &obstacles, std::vector &heightmap) +{ + std::queue places; + heightmap.clear(); + heightmap.resize(map.getW() * map.getH(), 0); + for (unsigned int i = 0; i < sources.size(); ++i) + { + heightmap[sources[i].y * map.getW() + sources[i].x] = 1; + places.push(sources[i].y * map.getW() + sources[i].x); + } + for (unsigned int i = 0; i < obstacles.size(); ++i) + { + heightmap[obstacles[i].y * map.getW() + obstacles[i].x] = -1; + } + + Uint32 wDec = map.wDec; + Uint32 hMask = map.hMask; + Uint32 wMask = map.wMask; + while (!places.empty()) + { + int deltaAddrG = places.front(); + places.pop(); + + size_t y = deltaAddrG >> wDec; // Calculate the coordinates of + size_t x = deltaAddrG & wMask; // the current field and of the + + size_t yu = ((y - 1) & hMask); // fields next to it. + size_t yd = ((y + 1) & hMask); // We live on a torus! If we are on + size_t xl = ((x - 1) & wMask); // the "last line" of the map, the + size_t xr = ((x + 1) & wMask); // next line is the line 0 again. + + int g = heightmap[(y << wDec) | x] + 1; + + size_t deltaAddrC[8]; + int *addr; + int side; + + deltaAddrC[0] = (yu << wDec) | xl; // Calculate the positions of the + deltaAddrC[1] = (yu << wDec) | x; // 8 fields next to us from their + deltaAddrC[2] = (yu << wDec) | xr; // coordinates. + deltaAddrC[3] = (y << wDec) | xr; + deltaAddrC[4] = (yd << wDec) | xr; + deltaAddrC[5] = (yd << wDec) | x; + deltaAddrC[6] = (yd << wDec) | xl; + deltaAddrC[7] = (y << wDec) | xl; + for (int ci = 0; ci < 8; ci++) // Check for each of this fields if we + { // can improve its gradient value + addr = &heightmap[deltaAddrC[ci]]; + side = *addr; + if (side == 0) + { + *addr = g; + places.push(deltaAddrC[ci]); + } + } + } +} + +int computeAverageDistance(Map &map, std::vector &grid, int areaN, + const std::vector &heightmap) +{ + long total = 0; + int count = 0; + for (int x = 0; x < map.getW(); ++x) + { + for (int y = 0; y < map.getH(); ++y) + { + if (grid[y * map.getW() + x] == areaN) + { + total += heightmap[y * map.getW() + x]; + count += 1; + } + } + } + return count > 0 ? total / count : 0; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Distances.h b/src/map/generator/shared/Distances.h new file mode 100644 index 000000000..aa88ea9f6 --- /dev/null +++ b/src/map/generator/shared/Distances.h @@ -0,0 +1,13 @@ +#pragma once +#include "Regions.h" +namespace MapGeneration +{ +void adjustHeightmapFromPoints(Map &map, std::vector &points, + std::vector &heightmap, int value); +void adjustHeightmapFromPerlinNoise(Map &map, GenerationContext &context, std::vector &heights, + int spread); +void computeDistances(Map &map, std::vector &sources, + std::vector &obstacles, std::vector &heightmap); +int computeAverageDistance(Map &map, std::vector &grid, int areaN, + const std::vector &heightmap); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Geometry.cpp b/src/map/generator/shared/Geometry.cpp new file mode 100644 index 000000000..c283cf218 --- /dev/null +++ b/src/map/generator/shared/Geometry.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Geometry.h" +#include "GenerationContext.h" +#include +#include +namespace MapGeneration +{ +namespace +{ +constexpr double pi = 3.14159265358979323846; +} +ShapeTransform::ShapeTransform(ShapePoint c, double angle, double scale) + : center(c), cosine(std::cos(angle)), sine(std::sin(angle)), stretch(scale) +{ + if (!std::isfinite(c.x) || !std::isfinite(c.y) || !std::isfinite(angle) || + !std::isfinite(scale) || scale <= 0) + throw std::invalid_argument("Invalid shape transform"); +} +ShapePoint ShapeTransform::toMap(ShapePoint p) const +{ + return {center.x + p.x * stretch * cosine - p.y / stretch * sine, + center.y + p.x * stretch * sine + p.y / stretch * cosine}; +} +ShapePoint ShapeTransform::toShape(ShapePoint p) const +{ + p.x -= center.x; + p.y -= center.y; + return {(p.x * cosine + p.y * sine) / stretch, (-p.x * sine + p.y * cosine) * stretch}; +} +RadialShape::RadialShape(double r, double roughness, GenerationContext &context, + const std::string &stream, double amplitudeMaximum) + : radius(r) +{ + if (!std::isfinite(r) || r <= 0 || !std::isfinite(roughness) || roughness < 0 || + roughness >= 1 || !std::isfinite(amplitudeMaximum) || amplitudeMaximum < 0.6 || + roughness * amplitudeMaximum >= 1) + throw std::invalid_argument("Invalid radial shape"); + constexpr double falloff[] = {0.42, 0.28, 0.18, 0.12}; + for (size_t i = 0; i < amplitude.size(); ++i) + { + const auto choices = std::uint32_t((amplitudeMaximum - 0.6) * 1000) + 1; + amplitude[i] = roughness * falloff[i] * (0.6 + context.bounded(stream, choices) / 1000.0); + phase[i] = 2 * pi * context.bounded(stream, 65536) / 65536.0; + } +} +double RadialShape::radiusAt(double theta) const +{ + constexpr int harmonics[] = {2, 3, 5, 7}; + double wobble = 0; + for (size_t i = 0; i < amplitude.size(); ++i) + wobble += amplitude[i] * std::cos(harmonics[i] * theta + phase[i]); + return radius * (1 + wobble); +} +double RadialShape::maximumRadius() const +{ + double sum = 0; + for (double a : amplitude) + sum += std::abs(a); + return radius * (1 + sum); +} +void stampShape(std::vector &labels, int width, int height, int label, + const ShapeTransform &transform, const RadialShape &shape, bool wrap) +{ + if (width <= 0 || height <= 0 || labels.size() != size_t(width) * height) + throw std::invalid_argument("Invalid shape grid"); + const auto center = transform.toMap({0, 0}); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + { + ShapePoint p{double(x), double(y)}; + if (wrap) + { + p.x = center.x + std::remainder(p.x - center.x, width); + p.y = center.y + std::remainder(p.y - center.y, height); + } + p = transform.toShape(p); + if (std::hypot(p.x, p.y) < shape.radiusAt(std::atan2(p.y, p.x))) + labels[size_t(y) * width + x] = label; + } +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Geometry.h b/src/map/generator/shared/Geometry.h new file mode 100644 index 000000000..145c12c0a --- /dev/null +++ b/src/map/generator/shared/Geometry.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +#include +#include +struct GenerationContext; +namespace MapGeneration +{ +struct ShapePoint +{ + double x, y; +}; +// Area-preserving stretch and rotation. Geometry stays independent of terrain and wrapping. +class ShapeTransform +{ + public: + ShapeTransform(ShapePoint center, double rotation, double stretch = 1.0); + ShapePoint toMap(ShapePoint p) const; + ShapePoint toShape(ShapePoint p) const; + + private: + ShapePoint center; + double cosine, sine, stretch; +}; +class RadialShape +{ + public: + RadialShape(double radius, double roughness, GenerationContext &, const std::string &stream, + double amplitudeMaximum = 1.0); + double radiusAt(double radians) const; + double maximumRadius() const; + + private: + double radius; + std::array amplitude, phase; +}; +// Rasterizes a shape into a label grid; callers decide what the labels mean. +// Wrapping visits each tile once using its nearest image relative to the shape center. +void stampShape(std::vector &labels, int width, int height, int label, const ShapeTransform &, + const RadialShape &, bool wrap); +} // namespace MapGeneration diff --git a/src/map/generator/shared/HeightMap.cpp b/src/map/generator/shared/HeightMap.cpp new file mode 100644 index 000000000..cf8e77932 --- /dev/null +++ b/src/map/generator/shared/HeightMap.cpp @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2006 Leo Wandersleb + +#include "HeightMap.h" +#include "GenerationResult.h" +#include "GlobalContainer.h" +#include "Noise.h" +#include +#include +#include +#include +#include + +/// these faders are factors to be applicable to heightfields. they map (0,0)-(w,h) to [0..1] + +inline float faderCenter(int x, int y, int w, + int h) /// to have zero at the borders and 1 in the center +{ + return (1.0 - cos(2.0 * 3.14159265 * (float)x / (float)w)) * + (1.0 - cos(2.0 * 3.14159265 * (float)y / (float)h)) / 4.0; +} +inline float faderLeftRight( + int x, int y, int w, + int h) /// to have 0 at top and bottom border and 1 at the middle of right and left border +{ + return (faderCenter((x + w / 2) % w, y, w, h)); +} +inline float faderTopBottom( + int x, int y, int w, + int h) /// to have 1 at the middle of top and bottom border and 0 at right and left border +{ + return (faderCenter(x, (y + h / 2) % h, w, h)); +} +inline float +faderCorner(int x, int y, int w, + int h) /// to have 1 in the corners and 0 on a cross going through the center +{ + return (faderCenter((x + w / 2) % w, (y + h / 2) % h, w, h)); +} + +HeightMap::HeightMap(unsigned int width, unsigned int height, std::mt19937 &rng) + : _pn(rng()), random(rng) +{ + init(width, height); +} + +HeightMap::~HeightMap() +{ + delete[] _map; + if (_stamp) + delete[] _stamp; +} + +void HeightMap::init(unsigned int width, unsigned int height) +{ + _w = width; + _h = height; + _map = new float[_w * _h]; + _stamp = NULL; +} + +void HeightMap::makeStamp(unsigned int radius) +{ + _r = std::max(1u, radius); + oldLowerX = oldLowerY = oldDifferenceX = oldDifferenceY = ~0u; + if (_stamp) + delete[] _stamp; + _stamp = new float[(2 * _r + 1) * (2 * _r + 1)]; + for (unsigned int x = 0; x < 2 * _r + 1; x++) + { + for (unsigned int y = 0; y < 2 * _r + 1; y++) + { + unsigned int dSquare = (x - _r) * (x - _r) + (y - _r) * (y - _r); + if (dSquare < _r * _r) + _stamp[x + y * (2 * _r + 1)] = + (1.0 - cos(sqrt(dSquare) * 3.14159265 / (float)_r)) / 2.0; + else + _stamp[x + y * (2 * _r + 1)] = .9999; + } + } +} +inline void HeightMap::lower(unsigned int coordX, unsigned int coordY) +{ + if ((coordX != oldLowerX) || + (coordY != oldLowerY)) // don't stamp the same spot again. if stamp is moved like in + // rivermaps this saves a lot of time + { + assert(_stamp); + for (unsigned int x = 0; x < 2 * _r + 1; x++) + { + /// this loop can be replaced by a somehow complicated memcpy + for (unsigned int y = 0; y < 2 * _r + 1; y++) + { + unsigned int coord1d = (unsigned int)(_w + x - _r + coordX) % _w + + ((unsigned int)(_h + y - _r + coordY) % _h) * _w; + if (_map[coord1d] > _stamp[x + y * (2 * _r + 1)]) + _map[coord1d] = _stamp[x + y * (2 * _r + 1)]; + } + } + oldLowerX = coordX; + oldLowerY = coordY; + } +} + +inline void HeightMap::differenceStamp(unsigned int coordX, unsigned int coordY) +{ + if ((coordX != oldDifferenceX) || + (coordY != oldDifferenceY)) // don't stamp the same spot again. if stamp is moved like in + // rivermaps this saves a lot of time + { + assert(_stamp); + for (unsigned int x = 0; x < 2 * _r + 1; x++) + { + for (unsigned int y = 0; y < 2 * _r + 1; y++) + { + unsigned int coord1d = (unsigned int)(_w + x - _r + coordX) % _w + + ((unsigned int)(_h + y - _r + coordY) % _h) * _w; + _map[coord1d] = fabs((1.0 - _stamp[x + y * (2 * _r + 1)]) - _map[coord1d]); + } + } + oldDifferenceX = coordX; + oldDifferenceY = coordY; + } +} + +inline void HeightMap::addNoise(float weight, float smoothingFactor) +{ + assert((weight > 0) && (weight <= 1.0)); + for (int x = 0; (unsigned int)x < _w; x++) + { + for (int y = 0; (unsigned int)y < _h; y++) + { + _map[x + _w * y] = + _map[x + _w * y] * (1.0 - weight) + + (faderCenter(x, y, _w, _h) * + _pn.Noise((float)(x) / smoothingFactor, (float)(y) / smoothingFactor) + + faderLeftRight(x, y, _w, _h) * + _pn.Noise((float)((x + _w / 2) % _w + _w) / smoothingFactor, + (float)(y + _h) / smoothingFactor) + + faderTopBottom(x, y, _w, _h) * + _pn.Noise((float)(x + 2 * _w) / smoothingFactor, + (float)((y + _h / 2) % _h + 2 * _h) / smoothingFactor) + + faderCorner(x, y, _w, _h) * + _pn.Noise((float)((x + _w / 2) % _w + 3 * _w) / smoothingFactor, + (float)((y + _h / 2) % _h + 3 * 4) / smoothingFactor) + + +4.0) / + 8.0 * weight; + } + } +} + +void HeightMap::makeIslands(unsigned int count, float smoothingFactor) +{ + assert(count); + GenerationNoise pn(random()); + std::vector centerX(count); + std::vector centerY(count); + float mindist = sqrt(_w * _h / count) / 2.0; + assert(mindist > 0); + makeStamp((unsigned int)(mindist * 2)); + centerX[0] = static_cast(random() & 0x7fffffffu) % _w; + centerY[0] = static_cast(random() & 0x7fffffffu) % _h; + /// find spots with distance>min. distance + for (unsigned int i = 1; i < count; i++) + { + bool foundSpot = false; + unsigned int tries = 0; + int newPosX, newPosY; + do + { + newPosX = static_cast(random() & 0x7fffffffu) % _w; + newPosY = static_cast(random() & 0x7fffffffu) % _h; + tries++; + foundSpot = true; + for (unsigned int j = 0; j < i; j++) + { + int distX = + std::min(abs(newPosX - centerX[j]), (int)_w - abs(newPosX - centerX[j])); + int distY = + std::min(abs(newPosY - centerY[j]), (int)_h - abs(newPosY - centerY[j])); + if (distX < mindist && distY < mindist) + foundSpot = false; + } + } while (!foundSpot && tries < std::min(1000000u, count * count * _w * _h)); + if (!foundSpot) + { + throw GenerationFailure("Cannot space the requested islands"); + } + centerX[i] = newPosX; + centerY[i] = newPosY; + } + /// level the terrain + operator=(0.0); + for (unsigned int i = 0; i < count; i++) + { + differenceStamp(centerX[i], centerY[i]); + } + addNoise(.7, smoothingFactor); + normalize(); +} + +void HeightMap::makeRiver(unsigned int maxDiameter, float smoothingFactor) +{ + /// riverRadius refers to the distance between center of the river and the maximum distance that + /// gets lowered. + makeStamp(maxDiameter / 2); + /// level the map + operator=(1.0); + + /// find start for a random walk + float startingPointX = static_cast(random() & 0x7fffffffu) % _w; + float startingPointY = static_cast(random() & 0x7fffffffu) % _h; + + /// the target=start+(w,h) is set now. tmprand(0,1,2)==position(+h,+w,+w+h) + float targetPointX; + float targetPointY; + if (_w == _h) + { + unsigned int tmprand = static_cast(random() & 0x7fffffffu) % 3; + targetPointX = startingPointX + (tmprand > 0 ? _w : 0); + targetPointY = startingPointY + _h - (tmprand % 2) * _h; + } + else if (_w > _h) + { + targetPointX = startingPointX + _w; + targetPointY = startingPointY + (static_cast(random() & 0x7fffffffu) % (_w / _h)) * _h; + } + else + { + targetPointX = startingPointX + (static_cast(random() & 0x7fffffffu) % (_h / _w)) * _w; + targetPointY = startingPointY + _h; + } + float targetDirection = + asin((targetPointY - startingPointY) / + sqrt(pow(targetPointX - startingPointX, 2) + pow(targetPointY - startingPointY, 2))); + float targetDirectionX = cos(targetDirection); + float targetDirectionY = sin(targetDirection); + /// length of direct line + float straightRiverLength = + sqrt(pow(targetPointX - startingPointX, 2) + pow(targetPointY - startingPointY, 2)); + for (float t = 0; t < straightRiverLength; t += straightRiverLength / 10.0 / (_w + _h)) + { + float offset = (1.0 - cos(t / straightRiverLength * 2 * 3.14159265)) * + (_pn.Noise(t / 153.3) * 300.0 + _pn.Noise(t / 13.3) * 50.0 - 175.0); + if (t < straightRiverLength / 2.0) + offset += (1 + cos(t / straightRiverLength * 2 * 3.14159265)) * + (_pn.Noise(t / 153.3) * 300.0 + _pn.Noise(t / 13.3) * 50.0 - 175.0); + else + offset += (1 + cos(t / straightRiverLength * 2 * 3.14159265)) * + (_pn.Noise((straightRiverLength - t) / 153.3) * 300.0 + + _pn.Noise((straightRiverLength - t) / 13.3) * 50.0 - 175.0); + float reachedPointX = targetDirectionX * t - targetDirectionY * offset / 4; + float reachedPointY = targetDirectionY * t + targetDirectionX * offset / 4; + lower((unsigned int)reachedPointX, (unsigned int)reachedPointY); + } + addNoise(.1, smoothingFactor); + normalize(); +} + +void HeightMap::stampOutput(char *filename) +{ + char *hm2 = new char[(2 * _r + 1) * (2 * _r + 1)]; + StreamBackend *stream = Toolkit::getFileManager()->openOutputStreamBackend(filename); + + for (unsigned int i = 0; i < (2 * _r + 1) * (2 * _r + 1); i++) + hm2[i] = (char)(_stamp[i] * 256); + + stream->write(hm2, (2 * _r + 1) * (2 * _r + 1) * sizeof(char)); + delete stream; + delete[] hm2; +} + +void HeightMap::mapOutput(char *filename) +{ + char *hm2 = new char[_w * _h]; + StreamBackend *stream = Toolkit::getFileManager()->openOutputStreamBackend(filename); + + for (unsigned int i = 0; i < _w * _h; i++) + hm2[i] = (char)(_map[i] * 256); + + stream->write(hm2, _w * _h * sizeof(char)); + delete stream; + delete[] hm2; +} + +void HeightMap::makeCraters(unsigned int craterCount, unsigned int craterRadius, + float smoothingFactor) +{ + makeStamp(craterRadius); + operator=(1.0); + for (unsigned int t = 0; t < craterCount; t++) + lower(static_cast(random() & 0x7fffffffu) % _w, + static_cast(random() & 0x7fffffffu) % _h); + addNoise(.8, smoothingFactor); + normalize(); +} + +void HeightMap::makePlain(float smoothingFactor) +{ + operator=(1.0); + addNoise(.99, smoothingFactor); + normalize(); +} + +void HeightMap::makeSwamp(float smoothingFactor) +{ + operator=(1.0); + addNoise(.99, smoothingFactor); + normalize(); +} + +void HeightMap::normalize() +{ + float min = 100000.0; + float max = -100000.0; + for (unsigned int i = 0; i < _w * _h; i++) + { + min = _map[i] < min ? _map[i] : min; + max = _map[i] > max ? _map[i] : max; + } + min -= .01; + max += .01; + float range = max - min; + for (unsigned int i = 0; i < _w * _h; i++) + _map[i] = (_map[i] - min) / range; +} diff --git a/src/map/generator/shared/HeightMap.h b/src/map/generator/shared/HeightMap.h new file mode 100644 index 000000000..37a4a06d7 --- /dev/null +++ b/src/map/generator/shared/HeightMap.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2006 Leo Wandersleb + +#pragma once + +#include "Noise.h" + +class HeightMap /// class to generate heightmaps to decide where to put resources, water, sand and + /// grass later +{ + float *_map; /// height values are always [0,1]. + unsigned int _w, _h; /// map size + float *_stamp; /// smooth 0 to 1 gradient lookup to generate craters, islands and rivers + unsigned int _r; /// radius of the _stamp + GenerationNoise _pn; + std::mt19937 &random; + unsigned oldLowerX = ~0u, oldLowerY = ~0u, oldDifferenceX = ~0u, + oldDifferenceY = ~0u; /// to get reproducible correlated random numbers + public: + enum kindOfMap + { + SWAMP = 0, + ISLANDS = 1, + RIVER = 2, + CRATERS = 3, + RANDOM = 4 + }; + + HeightMap(unsigned int width, unsigned int height, std::mt19937 &random); + HeightMap(const HeightMap &) = delete; + HeightMap &operator=(const HeightMap &) = delete; + ~HeightMap(); + inline unsigned int uiLevel(unsigned int i, unsigned int scale) + { + return std::min(scale - 1, (unsigned int)(_map[i] * scale)); + } + inline unsigned int uiLevel(unsigned int x, unsigned int y, unsigned int scale) + { + return std::min(scale - 1, (unsigned int)(_map[x % _w + (y % _h) * _w] * scale)); + } + inline float operator()(unsigned int i) { return _map[i]; } + inline float operator()(const unsigned int x, const unsigned int y) + { + return _map[x % _w + (y % _h) * _w]; + } + void mapOutput(char *filename); /// generates the file ~/.glob2/filename and writes the raw + /// 0..255 values of map to it. to see it, use convert -size + /// [width]x[height] -depth 8 gray:[filename] test.png + + void makePlain(float smoothingFactor); /// a plain perlin height field + void makeSwamp(float smoothingFactor); /// a plain perlin height field + void makeIslands(unsigned int count, + float smoothingFactor); /// generates a 'swamp' with count hills + void + makeRiver(unsigned int maxDiameter, + float smoothingFactor); /// generates a 'swamp' with a river based on a random walk. + void makeCraters(unsigned int craterCount, unsigned int craterRadius, + float smoothingFactor); /// generates a 'swamp' with craterCount craters + private: + void operator=(float value) { std::fill(_map, _map + _w * _h, value); } + void init(unsigned int width, unsigned int height); + void makeStamp(unsigned int radius); /// generates the stamp (smooth 0 to 1 gradient lookup to + /// generate craters, islands and rivers) + inline void + lower(unsigned int coordX, + unsigned int coordY); /// lower lowers the region around (coordX,coordY) to min(stamp,map) + inline void + differenceStamp(unsigned int coordX, + unsigned int coordY); /// multiplyStamp sets the region around (coordX,coordY) + /// to (1-stamp)*map if map>0 and to 1-stamp else to bias + /// away from other hills. + inline void + addNoise(float weight, + float smoothingFactor); /// adds noise to the map: map=noise*weight+map*(1-weight) + void stampOutput(char *filename); /// generates the file ~/.glob2/filename and writes the raw + /// 0..255 values of stamp to it. to see it, use convert -size + /// [width]x[height] -depth 8 gray:[filename] test.png where + /// with==height as _stamp is always a square + void normalize(); /// fits the values of _map to [0, 1] +}; diff --git a/src/map/generator/shared/Noise.cpp b/src/map/generator/shared/Noise.cpp new file mode 100644 index 000000000..0c01f2f66 --- /dev/null +++ b/src/map/generator/shared/Noise.cpp @@ -0,0 +1,305 @@ +// perlinNoise.cpp +// Class to implement coherent noise over 1, 2, or 3 dimensions. +// Implementation based on the Perlin noise function. Thanks to +// Ken Perlin of NYU for publishing his algorithm online. +// Copyright (c) 2001, Matt Zucker +// You may use this source code as you wish, but it is provided +// with no warranty. Please email me at mazucker@vassar.edu if +// you find it useful. + +#include + +#include "Noise.h" +#include + +// TODO: change these preprocessor macros into inline functions + +// S curve is (3x^2 - 2x^3) because it's quick to calculate +// though -cos(x * PI) * 0.5 + 0.5 would work too + +#define easeCurve(t) (t * t * (3.0 - 2.0 * t)) +#define linearInterp(t, a, b) (a + t * (b - a)) +#define dot2(rx, ry, q) (rx * q[0] + ry * q[1]) +#define dot3(rx, ry, rz, q) (rx * q[0] + ry * q[1] + rz * q[2]) + +#define setupValues(t, axis, g0, g1, d0, d1, pos) \ + t = pos[axis] + NOISE_LARGE_PWR2; \ + g0 = ((int)t) & NOISE_MOD_MASK; \ + g1 = (g0 + 1) & NOISE_MOD_MASK; \ + d0 = t - (int)t; \ + d1 = d0 - 1.0; + +// Constructor +GenerationNoise::GenerationNoise(unsigned int seed) { reseed(seed); } +// Destructor +GenerationNoise::~GenerationNoise() {} + +// return a random float in [-1,1] + +inline float GenerationNoise::randNoiseFloat() +{ + return (float)((static_cast(random() & 0x7fffffffu) % + (NOISE_WRAP_INDEX + NOISE_WRAP_INDEX)) - + NOISE_WRAP_INDEX) / + NOISE_WRAP_INDEX; +}; + +// convert a 2D vector into unit length + +void GenerationNoise::normalize2d(float vector[2]) +{ + float length = sqrt((vector[0] * vector[0]) + (vector[1] * vector[1])); + if (length == 0) + { + vector[0] = 1; + return; + } + vector[0] /= length; + vector[1] /= length; +} + +// convert a 3D vector into unit length + +void GenerationNoise::normalize3d(float vector[3]) +{ + float length = + sqrt((vector[0] * vector[0]) + (vector[1] * vector[1]) + (vector[2] * vector[2])); + if (length == 0) + { + vector[0] = 1; + return; + } + vector[0] /= length; + vector[1] /= length; + vector[2] /= length; +} + +// +// Mnemonics used in the following 3 functions: +// L = left(-X direction) +// R = right (+X direction) +// D = down (-Y direction) +// U = up (+Y direction) +// B = backwards(-Z direction) +// F = forwards (+Z direction) +// +// Not that it matters to the math, but a reader might want to know. +// +// noise1d - create 1-dimensional coherent noise +// if you want to learn about how noise works, look at this +// and then look at noise2d. + +float GenerationNoise::Noise1d(float pos[1]) +{ + int gridPointL, gridPointR; + float distFromL, distFromR, sX, t, u, v; + + // find out neighboring grid points to pos and signed distances from pos to them. + setupValues(t, 0, gridPointL, gridPointR, distFromL, distFromR, pos); + + sX = easeCurve(distFromL); + + // u, v, are the vectors from the grid pts. times the random gradients for the grid points + // they are actually dot products, but this looks like scalar multiplication + u = distFromL * gradientTable1d[permutationTable[gridPointL]]; + v = distFromR * gradientTable1d[permutationTable[gridPointR]]; + + // return the linear interpretation between u and v (0 = u, 1 = v) at sX. + return linearInterp(sX, u, v); +} + +// create 2d coherent noise + +float GenerationNoise::Noise2d(float pos[2]) +{ + int gridPointL, gridPointR, gridPointD, gridPointU; + int indexLD, indexRD, indexLU, indexRU; + float distFromL, distFromR, distFromD, distFromU; + float *q, sX, sY, a, b, t, u, v; + int indexL, indexR; + + // find out neighboring grid points to pos and signed distances from pos to them. + setupValues(t, 0, gridPointL, gridPointR, distFromL, distFromR, pos); + setupValues(t, 1, gridPointD, gridPointU, distFromD, distFromU, pos); + + // Generate some temporary indexes associated with the left and right grid values + indexL = permutationTable[gridPointL]; + indexR = permutationTable[gridPointR]; + + // Generate indexes in the permutation table for all 4 corners + indexLD = permutationTable[indexL + gridPointD]; + indexRD = permutationTable[indexR + gridPointD]; + indexLU = permutationTable[indexL + gridPointU]; + indexRU = permutationTable[indexR + gridPointU]; + + // Get the s curves at the proper values + sX = easeCurve(distFromL); + sY = easeCurve(distFromD); + + // Do the dot products for the lower left corner and lower right corners. + // Interpolate between those dot products value sX to get a. + q = gradientTable2d[indexLD]; + u = dot2(distFromL, distFromD, q); + q = gradientTable2d[indexRD]; + v = dot2(distFromR, distFromD, q); + a = linearInterp(sX, u, v); + + // Do the dot products for the upper left corner and upper right corners. + // Interpolate between those dot products at value sX to get b. + q = gradientTable2d[indexLU]; + u = dot2(distFromL, distFromU, q); + q = gradientTable2d[indexRU]; + v = dot2(distFromR, distFromU, q); + b = linearInterp(sX, u, v); + + // Interpolate between a and b at value sY to get the noise return value. + return linearInterp(sY, a, b); +} + +// you guessed it -- 3D coherent noise + +float GenerationNoise::Noise3d(float pos[3]) +{ + int gridPointL, gridPointR, gridPointD, gridPointU, gridPointB, gridPointF; + int indexLD, indexLU, indexRD, indexRU; + float distFromL, distFromR, distFromD, distFromU, distFromB, distFromF; + float *q, sX, sY, sZ, a, b, c, d, t, u, v; + int indexL, indexR; + + // find out neighboring grid points to pos and signed distances from pos to them. + setupValues(t, 0, gridPointL, gridPointR, distFromL, distFromR, pos); + setupValues(t, 1, gridPointD, gridPointU, distFromD, distFromU, pos); + setupValues(t, 2, gridPointB, gridPointF, distFromB, distFromF, pos); + + indexL = permutationTable[gridPointL]; + indexR = permutationTable[gridPointR]; + + indexLD = permutationTable[indexL + gridPointD]; + indexRD = permutationTable[indexR + gridPointD]; + indexLU = permutationTable[indexL + gridPointU]; + indexRU = permutationTable[indexR + gridPointU]; + + sX = easeCurve(distFromL); + sY = easeCurve(distFromD); + sZ = easeCurve(distFromB); + + q = gradientTable3d[indexLD + gridPointB]; + u = dot3(distFromL, distFromD, distFromB, q); + q = gradientTable3d[indexRD + gridPointB]; + v = dot3(distFromR, distFromD, distFromB, q); + a = linearInterp(sX, u, v); + + q = gradientTable3d[indexLU + gridPointB]; + u = dot3(distFromL, distFromU, distFromB, q); + q = gradientTable3d[indexRU + gridPointB]; + v = dot3(distFromR, distFromU, distFromB, q); + b = linearInterp(sX, u, v); + + c = linearInterp(sY, a, b); + + q = gradientTable3d[indexLD + gridPointF]; + u = dot3(distFromL, distFromD, distFromF, q); + q = gradientTable3d[indexRD + gridPointF]; + v = dot3(distFromR, distFromD, distFromF, q); + a = linearInterp(sX, u, v); + + q = gradientTable3d[indexLU + gridPointF]; + u = dot3(distFromL, distFromU, distFromF, q); + q = gradientTable3d[indexRU + gridPointF]; + v = dot3(distFromR, distFromU, distFromF, q); + b = linearInterp(sX, u, v); + + d = linearInterp(sY, a, b); + + return linearInterp(sZ, c, d); +} + +// you can call noise component-wise, too. + +float GenerationNoise::Noise(float x) { return Noise1d(&x); } + +float GenerationNoise::Noise(float x, float y) +{ + float p[2] = {x, y}; + return Noise2d(p); +} + +float GenerationNoise::Noise(float x, float y, float z) +{ + float p[3] = {x, y, z}; + return Noise3d(p); +} + +// reinitialize with new, random values. + +// reinitialize using a user-specified random seed. + +void GenerationNoise::reseed(unsigned int rSeed) +{ + random.seed(rSeed); + generateLookupTables(); +} + +// initialize everything during constructor or reseed -- note +// that space was already allocated for the gradientTable +// during the constructor + +void GenerationNoise::generateLookupTables() +{ + unsigned i, j, temp; + + for (i = 0; i < NOISE_WRAP_INDEX; i++) + { + // put index into permutationTable[index], we will shuffle later + permutationTable[i] = i; + + gradientTable1d[i] = randNoiseFloat(); + + for (j = 0; j < 2; j++) + { + gradientTable2d[i][j] = randNoiseFloat(); + } + normalize2d(gradientTable2d[i]); + + for (j = 0; j < 3; j++) + { + gradientTable3d[i][j] = randNoiseFloat(); + } + normalize3d(gradientTable3d[i]); + } + + // Shuffle permutation table up to NOISE_WRAP_INDEX + for (i = 0; i < NOISE_WRAP_INDEX; i++) + { + j = static_cast(random() & 0x7fffffffu) & NOISE_MOD_MASK; + temp = permutationTable[i]; + permutationTable[i] = permutationTable[j]; + permutationTable[j] = temp; + } + + // Add the rest of the table entries in, duplicating + // indices and entries so that they can effectively be indexed + // by unsigned chars. I think. Ask Perlin what this is really doing. + // + // This is the only part of the algorithm that I don't understand 100%. + + for (i = 0; i < NOISE_WRAP_INDEX + 2; i++) + { + permutationTable[NOISE_WRAP_INDEX + i] = permutationTable[i]; + + gradientTable1d[NOISE_WRAP_INDEX + i] = gradientTable1d[i]; + + for (j = 0; j < 2; j++) + { + gradientTable2d[NOISE_WRAP_INDEX + i][j] = gradientTable2d[i][j]; + } + + for (j = 0; j < 3; j++) + { + gradientTable3d[NOISE_WRAP_INDEX + i][j] = gradientTable3d[i][j]; + } + } + + // And we're done. Set initialized to true + initialized = 1; +} diff --git a/src/map/generator/shared/Noise.h b/src/map/generator/shared/Noise.h new file mode 100644 index 000000000..b6f2a7232 --- /dev/null +++ b/src/map/generator/shared/Noise.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +// It must be true that (x % NOISE_WRAP_INDEX) == (x & NOISE_MOD_MASK) +// so NOISE_WRAP_INDEX must be a power of two, and NOISE_MOD_MASK must be +// that power of 2 - 1. as indices are implemented, as unsigned chars, +// NOISE_WRAP_INDEX should be less than or equal to 256. +// There's no good reason to change it from 256, really. + +#define NOISE_WRAP_INDEX 256 +#define NOISE_MOD_MASK 255 + +#define NOISE_LARGE_PWR2 4096 + +class GenerationNoise +{ + private: + unsigned initialized = 0; + std::mt19937 random; + + unsigned permutationTable[NOISE_WRAP_INDEX * 2 + 2]; + float gradientTable1d[NOISE_WRAP_INDEX * 2 + 2]; + float gradientTable2d[NOISE_WRAP_INDEX * 2 + 2][2]; + float gradientTable3d[NOISE_WRAP_INDEX * 2 + 2][3]; + + float randNoiseFloat(); + static void normalize2d(float vector[2]); + static void normalize3d(float vector[3]); + void generateLookupTables(); + + public: + explicit GenerationNoise(unsigned int seed); + + ~GenerationNoise(); + + void reseed(unsigned int rSeed); + + float Noise1d(float pos[1]); + float Noise2d(float pos[2]); + float Noise3d(float pos[3]); + + float Noise(float); + float Noise(float, float); + float Noise(float, float, float); +}; diff --git a/src/map/generator/shared/Regions.cpp b/src/map/generator/shared/Regions.cpp new file mode 100644 index 000000000..d55582ebe --- /dev/null +++ b/src/map/generator/shared/Regions.cpp @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Regions.h" +#include "Distances.h" +#include "GenerationContext.h" +#include "GenerationResult.h" +#include "HeightMap.h" +#include "Map.h" +#include "Resources.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +#include +#include +#include +#include +using namespace MapGeneration; + +namespace MapGeneration +{ +bool divideUpArea(Map &map, GenerationContext &context, std::vector &grid, int areaN, + std::vector &weights, std::vector &areaNumbers) +{ + std::vector points; + std::vector splitWeights; + for (unsigned int i = 0; i < weights.size(); ++i) + { + points.push_back(MapGeneratorPoint(0, 0)); + splitWeights.push_back(1); + } + if (!splitUpPoints(map, context, grid, areaN, points, splitWeights)) + { + return false; + } + splitUpArea(map, context, grid, areaN, points, weights, areaNumbers); + return true; +} + +void createOval(Map &map, std::vector &grid, int areaN, int x, int y, int width, int height) +{ + std::int64_t h2 = std::int64_t(height / 2) * (height / 2); + std::int64_t w2 = std::int64_t(width / 2) * (width / 2); + std::int64_t t2 = h2 * w2; + for (int px = -(width / 2); px < (width / 2); ++px) + { + int nx = map.normalizeX(x + px); + std::int64_t px2 = std::int64_t(px) * px * h2; + for (int py = -(height / 2); py < (height / 2); ++py) + { + int ny = map.normalizeY(y + py); + std::int64_t py2 = std::int64_t(py) * py * w2; + if (px2 + py2 < t2) + { + grid[ny * map.getW() + nx] = areaN; + } + } + } +} + +int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, int areaN, + std::vector &points, std::vector &weights, + PointSearch search, int maxPasses) +{ + if (grid.size() != size_t(map.getW()) * map.getH() || points.empty() || + weights.size() != points.size() || maxPasses <= 0 || + std::any_of(weights.begin(), weights.end(), [](int w) { return w <= 0 || w > 10000; })) + throw GenerationFailure("Invalid point dispersion inputs"); + std::vector startingPoints; + for (int x = 0; x < map.getW(); ++x) + { + for (int y = 0; y < map.getH(); ++y) + { + if (grid[y * map.getW() + x] == areaN) + { + startingPoints.push_back(MapGeneratorPoint(x, y)); + } + } + } + + if (startingPoints.size() < points.size()) + return 0; + + Uint32 n = context.stream("regions")() % startingPoints.size(); + + std::vector obstacles; + getAllOtherPoints(map, grid, areaN, obstacles); + std::vector sources; + sources.push_back(startingPoints[n]); + std::vector heights; + computeDistances(map, sources, obstacles, heights); + sources.clear(); + + for (unsigned int i = 0; i < points.size(); ++i) + { + int max = 0; + std::vector possible; + for (int x = 0; x < map.getW(); ++x) + { + for (int y = 0; y < map.getH(); ++y) + { + int h = heights[y * map.getW() + x]; + if (h > max) + { + max = h; + possible.clear(); + } + if (h >= max) + { + possible.push_back(MapGeneratorPoint(x, y)); + } + } + } + int n = context.stream("regions")() % possible.size(); + points[i] = possible[n]; + sources.push_back(points[i]); + computeDistances(map, sources, obstacles, heights); + } + if (points.size() == 1) + return std::min(map.getW(), map.getH()) / 2; + startingPoints.clear(); + heights.clear(); + sources.clear(); + obstacles.clear(); + + bool cont = true; + std::int64_t minDist = std::numeric_limits::max(); + int passes = 0; + std::uint64_t evaluations = 0; + while (cont) + { + if (++passes > maxPasses) + throw GenerationFailure("Point dispersion did not converge within its pass budget"); + minDist = std::numeric_limits::max(); + bool changed = false; + for (unsigned int i = 0; i < points.size(); ++i) + { + std::int64_t best = std::numeric_limits::max(); + for (unsigned int j = 0; j < points.size(); ++j) + { + if (i == j) + continue; + std::int64_t dist = std::int64_t(map.warpDistSquare(points[i].x, points[i].y, + points[j].x, points[j].y)) * + weights[j]; + best = std::min(dist, best); + } + minDist = std::min(best, minDist); + std::int64_t orig = best; + int best_x = -1; + int best_y = -1; + for (int dx = search == PointSearch::Local ? -3 : 0; + dx <= (search == PointSearch::Local ? 3 : map.getW() - 1); ++dx) + { + for (int dy = search == PointSearch::Local ? -3 : 0; + dy <= (search == PointSearch::Local ? 3 : map.getH() - 1); ++dy) + { + int nx = search == PointSearch::Local ? map.normalizeX(points[i].x + dx) : dx; + int ny = search == PointSearch::Local ? map.normalizeY(points[i].y + dy) : dy; + if (nx == points[i].x && ny == points[i].y) + continue; + if (grid[ny * map.getW() + nx] != areaN) + continue; + std::int64_t score = std::numeric_limits::max(); + bool invalid = false; + for (unsigned int j = 0; j < points.size(); ++j) + { + if (i == j) + continue; + if (nx == points[j].x && ny == points[j].y) + { + invalid = true; + break; + } + if (search == PointSearch::WholeRegion && ++evaluations > 20000000) + throw GenerationFailure( + "Point dispersion exhausted its distance-evaluation budget"); + std::int64_t dist = + std::int64_t(map.warpDistSquare(nx, ny, points[j].x, points[j].y)) * + weights[j]; + score = std::min(dist, score); + } + if (invalid) + continue; + + if (score > best) + { + best = score; + best_x = nx; + best_y = ny; + } + } + } + if (best_x != -1) + { + if (best != orig) + changed = true; + points[i].x = best_x; + points[i].y = best_y; + } + } + if (!changed) + { + cont = false; + } + } + + for (unsigned int i = 0; i < points.size(); ++i) + { + for (unsigned int j = 0; j < points.size(); ++j) + { + if (i != j && points[i].x == points[j].x && points[i].y == points[j].y) + return 0; + } + } + // Fisher-Yates with one draw per element, same as the former + // std::random_shuffle + boost::random_number_generator pairing. + for (size_t i = 1; i < points.size(); ++i) + { + size_t j = context.bounded("regions", i + 1); + if (i != j) + { + std::swap(points[i], points[j]); + std::swap(weights[i], weights[j]); + } + } + return int(std::sqrt(double(minDist))); +} + +void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, int areaN, + std::vector &points, std::vector &weights, + std::vector &areaNumbers, bool grassOnly) +{ + std::vector gradient(map.getW() * map.getH(), 0); + + Uint32 wDec = map.wDec; + Uint32 hMask = map.hMask; + Uint32 wMask = map.wMask; + + std::vector> squares(points.size()); + std::vector expansion(points.size(), 0); + std::vector current; + std::vector count; + + for (unsigned int i = 0; i < points.size(); ++i) + { + grid[points[i].y * map.getW() + points[i].x] = i; + gradient[points[i].y << wDec | points[i].x] = 1; + squares[i].push_back(points[i].y << wDec | points[i].x); + + current.push_back(1); + count.push_back(1); + } + + bool cont = true; + while (cont) + { + bool found = false; + for (unsigned int p = 0; p < points.size(); ++p) + { + expansion[p] += weights[p]; + if (!squares[p].empty()) + found = true; + while (expansion[p] > 0 && !squares[p].empty()) + { + Uint32 deltaAddrG = squares[p].back(); + squares[p].erase(--squares[p].end()); + + size_t y = deltaAddrG >> wDec; // Calculate the coordinates of + size_t x = deltaAddrG & wMask; // the current field and of the + + size_t yu = ((y - 1) & hMask); // fields next to it. + size_t yd = ((y + 1) & hMask); // We live on a torus! If we are on + size_t xl = ((x - 1) & wMask); // the "last line" of the map, the + size_t xr = ((x + 1) & wMask); // next line is the line 0 again. + + int t = grid[(y << wDec) | x]; + assert(t < (int)points.size()); + int g = gradient[(y << wDec) | x] + 1; + grid[(y << wDec) | x] = areaNumbers[t]; + + size_t deltaAddrC[8]; + int *addr; + int side; + + deltaAddrC[0] = (yu << wDec) | xl; // Calculate the positions of the + deltaAddrC[1] = (yu << wDec) | x; // 8 fields next to us from their + deltaAddrC[2] = (yu << wDec) | xr; // coordinates. + deltaAddrC[3] = (y << wDec) | xr; + deltaAddrC[4] = (yd << wDec) | xr; + deltaAddrC[5] = (yd << wDec) | x; + deltaAddrC[6] = (yd << wDec) | xl; + deltaAddrC[7] = (y << wDec) | xl; + + if (g != current[p]) + { + current[p] = g; + count[p] = 0; + } + + for (int ci = 0; ci < 8; ci++) // Check for each of this fields if we + { // can improve its gradient value + addr = &gradient[deltaAddrC[ci]]; + side = *addr; + if (side == 0 && grid[deltaAddrC[ci]] == areaN) + { + if (grassOnly && !map.isGrass(deltaAddrC[ci])) + continue; + *addr = g; + grid[deltaAddrC[ci]] = t; + count[p] += 1; + expansion[p] -= 1; + + Uint32 randLocation = context.stream("regions")() % count[p]; + std::list::iterator i = squares[p].begin(); + std::advance(i, randLocation); + squares[p].insert(i, deltaAddrC[ci]); + } + } + } + } + if (!found) + cont = false; + } +} + +void getAllPoints(Map &map, std::vector &grid, int areaN, + std::vector &points) +{ + for (int x = 0; x < map.getW(); ++x) + { + for (int y = 0; y < map.getH(); ++y) + { + if (grid[y * map.getW() + x] == areaN) + points.push_back(MapGeneratorPoint(x, y)); + } + } +} + +void getAllOtherPoints(Map &map, std::vector &grid, int areaN, + std::vector &points) +{ + for (int x = 0; x < map.getW(); ++x) + { + for (int y = 0; y < map.getH(); ++y) + { + if (grid[y * map.getW() + x] != areaN) + points.push_back(MapGeneratorPoint(x, y)); + } + } +} + +void getAllPointsLine(Map &map, int x1, int y1, int x2, int y2, + std::vector &points) +{ + int startX = x1; + int endX = x2; + int startY = y1; + int endY = y2; + + int dirX = (endX > startX ? 1 : -1); + int distX = std::abs(endX - startX); + if (distX > map.getW() / 2) + { + dirX = -dirX; + distX = map.getW() - distX; + } + + int dirY = (endY > startY ? 1 : -1); + int distY = std::abs(endY - startY); + if (distY > map.getH() / 2) + { + dirY = -dirY; + distY = map.getH() - distY; + } + + if (distX > distY) + { + int px = 0; + int py = 0; + int y = startY; + for (int x = startX; x != endX;) + { + px += 1; + points.push_back(MapGeneratorPoint(x, y)); + if (std::abs(px * distY - py * distX) > std::abs(px * distY - (py + 1) * distX)) + { + y = map.normalizeY(y + dirY); + points.push_back(MapGeneratorPoint(x, y)); + py += 1; + } + x = map.normalizeX(x + dirX); + } + } + else + { + int px = 0; + int py = 0; + int x = startX; + for (int y = startY; y != endY;) + { + py += 1; + points.push_back(MapGeneratorPoint(x, y)); + if (std::abs(py * distX - px * distY) > std::abs(py * distX - (px + 1) * distY)) + { + x = map.normalizeX(x + dirX); + points.push_back(MapGeneratorPoint(x, y)); + px += 1; + } + y = map.normalizeY(y + dirY); + } + } +} + +void findBorderPoints(Map &map, std::vector &grid, std::vector &points) +{ + for (int x = 0; x < map.getW(); ++x) + { + for (int y = 0; y < map.getH(); ++y) + { + bool found = false; + for (int dx = -1; dx <= 1 && !found; ++dx) + { + for (int dy = -1; dy <= 1 && !found; ++dy) + { + if (grid[map.normalizeY(y + dy) * map.getW() + map.normalizeX(x + dx)] != + grid[y * map.getW() + x]) + { + found = true; + } + } + } + if (found) + points.push_back(MapGeneratorPoint(x, y)); + } + } +} + +void chooseRandomPoints(Map &map, GenerationContext &context, + std::vector &points, int n) +{ + n = std::min(int(points.size()), n); + for (int i = 0; i < n; ++i) + { + int r = context.stream("regions")() % (points.size() - i); + std::iter_swap(points.begin() + i, points.begin() + i + r); + } + points.erase(points.begin() + n, points.end()); +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Regions.h b/src/map/generator/shared/Regions.h new file mode 100644 index 000000000..5605a2f58 --- /dev/null +++ b/src/map/generator/shared/Regions.h @@ -0,0 +1,53 @@ +#pragma once +#include +class Game; +class Map; +class Building; +struct BuildingType; +struct GenerationContext; +class ListComparator +{ + public: + ListComparator(std::vector &list) : list(list) {} + + bool operator()(int lhs, int rhs) { return list[lhs] < list[rhs]; } + + std::vector &list; +}; + +/// This is a single point on the map, used by the map generator +class MapGeneratorPoint +{ + public: + MapGeneratorPoint(int x, int y) : x(x), y(y) {} + int x; + int y; +}; + +namespace MapGeneration +{ +bool divideUpArea(Map &map, GenerationContext &context, std::vector &grid, int areaN, + std::vector &weights, std::vector &areaNumbers); +void createOval(Map &map, std::vector &grid, int areaN, int x, int y, int width, int height); +enum class PointSearch +{ + Local, + WholeRegion +}; +// Weights travel with points. WholeRegion is bounded best-response, not an optimum guarantee. +int splitUpPoints(Map &map, GenerationContext &context, std::vector &grid, int areaN, + std::vector &points, std::vector &weights, + PointSearch search = PointSearch::Local, int maxPasses = 200); +void splitUpArea(Map &map, GenerationContext &context, std::vector &grid, int areaN, + std::vector &points, std::vector &weights, + std::vector &areaNumbers, bool grassOnly = false); +void getAllPoints(Map &map, std::vector &grid, int areaN, + std::vector &points); +void getAllOtherPoints(Map &map, std::vector &grid, int areaN, + std::vector &points); +void getAllPointsLine(Map &map, int x1, int y1, int x2, int y2, + std::vector &points); +void findBorderPoints(Map &map, std::vector &grid, std::vector &points); +void chooseRandomPoints(Map &map, GenerationContext &context, + std::vector &points, int n); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Resources.cpp b/src/map/generator/shared/Resources.cpp new file mode 100644 index 000000000..d2873f723 --- /dev/null +++ b/src/map/generator/shared/Resources.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Resources.h" +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GenerationResult.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "StartingPositions.h" +#include "Terrain.h" +#include +#include +using namespace MapGeneration; + +namespace MapGeneration { +int placeResourceClump(Map &map, GenerationContext &context, + MapGeneratorPoint center, int resourceType, int radius) { + int placed = 0; + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) { + const int d2 = dx * dx + dy * dy; + if (d2 > radius * radius || (d2 > (radius - 1) * (radius - 1) && + context.bounded("resources", 4) == 0)) + continue; + const int x = map.normalizeX(center.x + dx), + y = map.normalizeY(center.y + dy); + const int existingType = map.getResource(x, y).type; + if (map.isResourceAllowed(x, y, resourceType) && + (existingType == NO_RES_TYPE || existingType == resourceType)) { + map.setResource(x, y, resourceType, 1); + ++placed; + } + } + return placed; +} + +int placeResourceClumpInArea(Map &map, GenerationContext &context, + const std::vector &points, + int resourceType, int radius) { + if (points.empty()) + return 0; + const size_t first = context.bounded("resources", points.size()); + for (size_t offset = 0; offset < points.size(); ++offset) { + const MapGeneratorPoint ¢er = points[(first + offset) % points.size()]; + if (!map.isResourceAllowed(center.x, center.y, resourceType)) + continue; + const int placed = + placeResourceClump(map, context, center, resourceType, radius); + if (placed) + return placed; + } + return 0; +} + +void scatterResources(Game &game, GenerationContext &context, + const ResourceDensities &density) { + const int width = game.map.getW(), height = game.map.getH(), + area = width * height; + auto scatter = [&](int type, int count) { + const int radius = count < 8 ? 1 : 2; + const int expectedClumpSize = radius == 1 ? 5 : 12; + const int clumps = + std::max(1, (count + expectedClumpSize - 1) / expectedClumpSize); + for (int i = 0; i < clumps; ++i) { + MapGeneratorPoint center(0, 0); + bool found = false; + for (int attempt = 0; attempt < 100 && !found; ++attempt) { + center = {int(context.bounded("resources", width)), + int(context.bounded("resources", height))}; + found = game.map.isResourceAllowed(center.x, center.y, type); + } + if (found) + placeResourceClump(game.map, context, center, type, radius); + } + }; + scatter(CORN, density.corn * area / 1600); + scatter(WOOD, density.wood * area / 1600); + scatter(STONE, density.stone * area / 3000); + scatter(ALGA, density.algae * area / 800); + for (int i = 0; i < density.fruit; ++i) + scatter(CHERRY + context.bounded("resources", 3), 1); +} + +void fillInResource(Map &map, GenerationContext &context, + std::vector &points, int resourceType, + int maxFillSize) { + if (maxFillSize < 1 || maxFillSize >= std::min(map.getW(), map.getH())) + throw GenerationFailure("Invalid resource patch size"); + for (unsigned int n = 0; n < points.size(); ++n) { + map.setResource(points[n].x, points[n].y, resourceType, + 1 + context.stream("regions")() % maxFillSize); + } +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Resources.h b/src/map/generator/shared/Resources.h new file mode 100644 index 000000000..92f075243 --- /dev/null +++ b/src/map/generator/shared/Resources.h @@ -0,0 +1,16 @@ +#pragma once +#include "Regions.h" +namespace MapGeneration { +struct ResourceDensities { + int corn, wood, stone, algae, fruit; +}; +void fillInResource(Map &map, GenerationContext &context, + std::vector &points, int resourceType, + int maxFillSize); +int placeResourceClump(Map &, GenerationContext &, MapGeneratorPoint center, + int resourceType, int radius); +int placeResourceClumpInArea(Map &, GenerationContext &, + const std::vector &, + int resourceType, int radius); +void scatterResources(Game &, GenerationContext &, const ResourceDensities &); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Settlements.cpp b/src/map/generator/shared/Settlements.cpp new file mode 100644 index 000000000..539c7d919 --- /dev/null +++ b/src/map/generator/shared/Settlements.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Settlements.h" +#include "Building.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GlobalContainer.h" +#include "Unit.h" +#include +namespace MapGeneration +{ +bool placeSettlement(Game &game, GenerationContext &context, int team, + const std::vector &home, MapGeneratorPoint anchor, + const std::string &stream) +{ + context.stage = "settlement"; + auto fail = [&](const std::string &detail) + { + context.detail = "Colony " + std::to_string(team) + ": " + detail; + return false; + }; + const int w = game.map.getW(), h = game.map.getH(); + if (team < 0 || team >= game.teamsCount() || !game.teams[team] || + home.size() != size_t(w) * h || context.request.nbWorkers < 1) + return fail("invalid home region or colony"); + int type = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + const auto *buildingType = globalContainer->buildingsTypes.get(type); + if (!buildingType) + return fail("missing swarm type"); + auto inside = [&](int x, int y) + { return home[game.map.normalizeY(y) * w + game.map.normalizeX(x)] != 0; }; + std::vector candidates; + int nearest = std::numeric_limits::max(); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + if (!inside(x, y) || !game.checkRoomForBuilding(x, y, buildingType, team, false)) + continue; + bool fits = true; + for (int dy = 0; dy < buildingType->height && fits; ++dy) + for (int dx = 0; dx < buildingType->width; ++dx) + if (!inside(x + dx, y + dy)) + { + fits = false; + break; + } + if (!fits) + continue; + int distance = game.map.warpDistSquare(x, y, game.map.normalizeX(anchor.x), + game.map.normalizeY(anchor.y)); + if (distance < nearest) + { + nearest = distance; + candidates.clear(); + } + if (distance <= nearest) + candidates.emplace_back(x, y); + } + if (candidates.empty()) + return fail("no swarm footprint fits inside the home region"); + const auto p = candidates[context.bounded(stream, candidates.size())]; + Building *building = game.addBuilding(p.x, p.y, type, team, 1, 0); + if (!building) + return fail("swarm placement failed"); + std::vector workers; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + if (inside(x, y) && game.map.isFreeForGroundUnit(x, y, false, 1u << team) && + game.map.doesPosTouchBuilding(x, y, building->gid)) + workers.emplace_back(x, y); + if (workers.size() < size_t(context.request.nbWorkers)) + return fail("need " + std::to_string(context.request.nbWorkers) + " worker tiles; found " + + std::to_string(workers.size())); + for (int i = 0; i < context.request.nbWorkers; ++i) + { + size_t selected = i + context.bounded(stream, workers.size() - i); + std::swap(workers[i], workers[selected]); + if (!game.addUnit(workers[i].x, workers[i].y, team, WORKER, 0, 0, 0, 0)) + return fail("worker placement failed"); + } + game.teams[team]->startPosX = building->posX; + game.teams[team]->startPosY = building->posY; + game.teams[team]->startPosSet = Team::START_POS_FROM_SWARM; + context.bootX[team] = building->posX; + context.bootY[team] = building->posY; + game.teams[team]->createLists(); + return true; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Settlements.h b/src/map/generator/shared/Settlements.h new file mode 100644 index 000000000..1a59ade89 --- /dev/null +++ b/src/map/generator/shared/Settlements.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Regions.h" +#include +namespace MapGeneration +{ +// The home mask constrains the whole building footprint and worker positions, not +// just the building anchor. Resources remain a generator-owned step before or after this. +// Mutates only the disposable candidate; any failure requires discarding it. +bool placeSettlement(Game &, GenerationContext &, int team, const std::vector &home, + MapGeneratorPoint preferredAnchor, const std::string &stream = "settlements"); +} // namespace MapGeneration diff --git a/src/map/generator/shared/StartingPositions.cpp b/src/map/generator/shared/StartingPositions.cpp new file mode 100644 index 000000000..f05ae3899 --- /dev/null +++ b/src/map/generator/shared/StartingPositions.cpp @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "StartingPositions.h" +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GeneratorDefinition.h" +#include "GlobalContainer.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "Resources.h" +#include "Terrain.h" +#include "Unit.h" +#include +#include +using namespace MapGeneration; + +namespace MapGeneration +{ +bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector &grid, + std::vector &teamAreaNumbers, int &areaNumber) +{ + context.stage = "resources and starts"; + int typeNum = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + BuildingType *swarm = globalContainer->buildingsTypes.get(typeNum); + + // Compute the distances from water + std::vector sources; + std::vector obstacles; + std::vector distances; + obstacles.clear(); + getAllPoints(game.map, grid, 0, sources); + computeDistances(game.map, sources, obstacles, distances); + + // Create a new heightmap from noise and distance to water + std::vector heightmap(game.map.getW() * game.map.getH(), 50); + adjustHeightmapFromPerlinNoise(game.map, context, heightmap, 5); + for (int x = 0; x < game.map.getW(); ++x) + { + for (int y = 0; y < game.map.getH(); ++y) + { + int d = distances[y * game.map.getW() + x]; + heightmap[y * game.map.getW() + x] -= d; + } + } + + for (int i = 0; i < context.request.nbTeams; ++i) + { + // Initialize + std::vector areaWeights; + std::vector areaNumbers; + for (int j = 0; j < 12; ++j) + { + areaWeights.push_back(1); + areaNumbers.push_back(areaNumber); + areaNumber += 1; + } + + // Divide the area. Its possible the area will be so small it can't be used + if (divideUpArea(game.map, context, grid, teamAreaNumbers[i], areaWeights, areaNumbers)) + { + // Sort the list of areas based on how close they are to water + std::vector areaDistances(areaNumbers.size()); + std::vector areaIndexes(areaNumbers.size()); + for (unsigned int j = 0; j < areaNumbers.size(); ++j) + { + areaDistances[j] = + computeAverageDistance(game.map, grid, areaNumbers[j], distances); + areaIndexes[j] = j; + } + ListComparator compare(areaDistances); + std::sort(areaIndexes.begin(), areaIndexes.end(), compare); + for (unsigned int j = 0; j < areaDistances.size(); ++j) + { + areaIndexes[j] = areaNumbers[areaIndexes[j]]; + } + areaNumbers = areaIndexes; + + // Place wood + std::vector wheatWoodPoints; + std::vector wheatPoints; + getAllPoints(game.map, grid, areaNumbers[3], wheatWoodPoints); + getAllPoints(game.map, grid, areaNumbers[4], wheatWoodPoints); + getAllPoints(game.map, grid, areaNumbers[5], wheatWoodPoints); + adjustHeightmapFromPoints(game.map, wheatWoodPoints, heightmap, 10); + for (unsigned int j = 0; j < wheatWoodPoints.size(); ++j) + { + int h = heightmap[wheatWoodPoints[j].y * game.map.getW() + wheatWoodPoints[j].x]; + if (h > 50) + { + game.map.setResource(wheatWoodPoints[j].x, wheatWoodPoints[j].y, WOOD, 1); + } + } + wheatWoodPoints.clear(); + + // Place wheat + getAllPoints(game.map, grid, areaNumbers[0], wheatWoodPoints); + getAllPoints(game.map, grid, areaNumbers[1], wheatWoodPoints); + getAllPoints(game.map, grid, areaNumbers[2], wheatWoodPoints); + adjustHeightmapFromPoints(game.map, wheatWoodPoints, heightmap, 10); + for (unsigned int j = 0; j < wheatWoodPoints.size(); ++j) + { + int h = heightmap[wheatWoodPoints[j].y * game.map.getW() + wheatWoodPoints[j].x]; + if (h > 50) + { + game.map.setResource(wheatWoodPoints[j].x, wheatWoodPoints[j].y, CORN, 1); + wheatPoints.push_back(wheatWoodPoints[j]); + } + } + + // These are all points in the base + std::vector baseLocations; + getAllPoints(game.map, grid, areaNumbers[6], baseLocations); + getAllPoints(game.map, grid, areaNumbers[7], baseLocations); + getAllPoints(game.map, grid, areaNumbers[8], baseLocations); + getAllPoints(game.map, grid, areaNumbers[9], baseLocations); + getAllPoints(game.map, grid, areaNumbers[10], baseLocations); + getAllPoints(game.map, grid, areaNumbers[11], baseLocations); + + // Place stone + int numberOfStone = 6; + std::vector stoneLocations = baseLocations; + chooseRandomPoints(game.map, context, stoneLocations, numberOfStone); + for (unsigned int j = 0; j < stoneLocations.size(); ++j) + { + game.map.setResource(stoneLocations[j].x, stoneLocations[j].y, STONE, 1); + } + + // Concerning starting locations, we also consider points inside the wheat and wood + // areas + getAllPoints(game.map, grid, areaNumbers[0], baseLocations); + getAllPoints(game.map, grid, areaNumbers[1], baseLocations); + getAllPoints(game.map, grid, areaNumbers[2], baseLocations); + getAllPoints(game.map, grid, areaNumbers[3], baseLocations); + getAllPoints(game.map, grid, areaNumbers[4], baseLocations); + getAllPoints(game.map, grid, areaNumbers[5], baseLocations); + + // Compute every points distance from the wheat + std::vector wheatDistance; + computeDistances(game.map, wheatPoints, obstacles, wheatDistance); + + // Only consider points between 1 and 4 squares from wheat + std::vector startingLocations; + for (unsigned int j = 0; j < baseLocations.size(); ++j) + { + int minValue = 100000; + for (int x = 0; x < 4; ++x) + { + for (int y = 0; y < 4; ++y) + { + int nx = game.map.normalizeX(baseLocations[j].x + x); + int ny = game.map.normalizeY(baseLocations[j].y + y); + minValue = std::min(wheatDistance[ny * game.map.getW() + nx], minValue); + } + } + if (minValue >= 1 && minValue <= 2) + { + startingLocations.push_back(baseLocations[j]); + } + } + + // Place swarms + chooseFreeForBuildingSquares(game, startingLocations, swarm, i); + if (startingLocations.size() == 0) + { + return false; + } + int chosen = context.stream("regions")() % startingLocations.size(); + Building *b = + addBuilding(game, startingLocations[chosen].x, startingLocations[chosen].y, i, + IntBuildingType::SWARM_BUILDING, 1, false); + if (b == NULL) + { + return false; + } + + // Set the initial viewport location + game.teams[i]->startPosX = b->posX; + game.teams[i]->startPosY = b->posY; + game.teams[i]->startPosSet = Team::START_POS_FROM_SWARM; + + // Place units around the swarm + std::vector unitLocations = baseLocations; + chooseFreeForGroundUnits(game.map, unitLocations, i); + chooseTouchingBuilding(game.map, unitLocations, b); + chooseRandomPoints(game.map, context, unitLocations, context.request.nbWorkers); + if (unitLocations.size() != static_cast(context.request.nbWorkers)) + { + context.detail = "Not enough worker positions"; + return false; + } + for (unsigned int n = 0; n < unitLocations.size(); ++n) + { + if (!game.addUnit(unitLocations[n].x, unitLocations[n].y, i, WORKER, 0, 0, 0, 0)) + { + context.detail = "Worker placement failed"; + return false; + } + } + } + else + { + return false; + } + } + return true; +} + +void chooseFreeForBuildingSquares(Game &game, std::vector &points, + BuildingType *type, int team) +{ + std::vector newPoints; + for (unsigned int n = 0; n < points.size(); ++n) + { + if (game.checkRoomForBuilding(points[n].x, points[n].y, type, team, false)) + { + newPoints.push_back(MapGeneratorPoint(points[n].x, points[n].y)); + } + } + points = newPoints; +} + +void chooseFreeForGroundUnits(Map &map, std::vector &points, int team) +{ + std::vector newPoints; + for (unsigned int n = 0; n < points.size(); ++n) + { + if (map.isFreeForGroundUnit(points[n].x, points[n].y, false, 1 << team)) + { + newPoints.push_back(MapGeneratorPoint(points[n].x, points[n].y)); + } + } + points = newPoints; +} + +void chooseTouchingBuilding(Map &map, std::vector &points, Building *building) +{ + std::vector newPoints; + for (unsigned int n = 0; n < points.size(); ++n) + { + if (map.doesPosTouchBuilding(points[n].x, points[n].y, building->gid)) + { + newPoints.push_back(MapGeneratorPoint(points[n].x, points[n].y)); + } + } + points = newPoints; +} + +Building *addBuilding(Game &game, int x, int y, int team, int type, int level, + bool underConstruction) +{ + std::string name = IntBuildingType::typeFromShortNumber(type); + int typeNum = globalContainer->buildingsTypes.getTypeNum(name, level - 1, underConstruction); + BuildingType *bt = globalContainer->buildingsTypes.get(typeNum); + if (bt == NULL) + { + return NULL; + } + + if (game.checkRoomForBuilding(x, y, bt, team, false)) + { + if (bt->maxUnitWorking) + return game.addBuilding(x, y, typeNum, team, 1, 0); + else + return game.addBuilding(x, y, typeNum, team, 0, 0); + } + return NULL; +} +} // namespace MapGeneration + +namespace MapGeneration +{ +bool placeArchipelagoStarts(Game &game, GenerationContext &context, int islandSize) +{ + for (int s = 0; s < context.request.nbTeams; s++) + { + if (game.mapHeader.getNumberOfTeams() <= s) + game.addTeam(); + int squareSize = 5 + islandSize / 10; + game.map.setUMatPos(context.bootX[s] + 2, context.bootY[s] + 0, GRASS, squareSize); + game.map.setUMatPos(context.bootX[s] + 2, context.bootY[s] + 2, GRASS, squareSize); + + Sint32 typeNum = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + if (!game.checkRoomForBuilding(context.bootX[s], context.bootY[s], + globalContainer->buildingsTypes.get(typeNum), s, false)) + { + if (false) + printf("Failed to add swarm of team %d\n", s); + return false; + } + game.teams[s]->startPosX = context.bootX[s]; + game.teams[s]->startPosY = context.bootY[s]; + Building *b = game.addBuilding(context.bootX[s], context.bootY[s], typeNum, s); + if (!b) + return false; + for (int i = 0; i < context.request.nbWorkers; i++) + if (game.addUnit(context.bootX[s] + (i % 4), context.bootY[s] - 1 - (i / 4), s, WORKER, + 0, 0, 0, 0) == NULL) + { + if (false) + printf("Failed to add unit %d of team %d\n", i, s); + return false; + } + game.teams[s]->createLists(); + } + game.map.smoothResources(islandSize / 10); + return true; +} + +bool placeStarts(Game &game, GenerationContext &context) +{ + for (int s = 0; s < context.request.nbTeams; s++) + { + assert(game.mapHeader.getNumberOfTeams() == s); + if (game.mapHeader.getNumberOfTeams() <= s) + game.addTeam(); + + game.map.setUMatPos(context.bootX[s] + 2, context.bootY[s] + 0, GRASS, 5); + game.map.setUMatPos(context.bootX[s] + 2, context.bootY[s] + 2, GRASS, 5); + game.map.setNoResource(context.bootX[s] + 2, context.bootY[s] + 0, 5); + game.map.setNoResource(context.bootX[s] + 2, context.bootY[s] + 2, 5); + + Sint32 typeNum = globalContainer->buildingsTypes.getTypeNum("swarm", 0, false); + if (!game.checkRoomForBuilding(context.bootX[s], context.bootY[s], + globalContainer->buildingsTypes.get(typeNum), s, false)) + { + if (false) + printf("Failed to add swarm of team %d\n", s); + return false; + } + game.teams[s]->startPosX = context.bootX[s]; + game.teams[s]->startPosY = context.bootY[s]; + Building *b = game.addBuilding(context.bootX[s], context.bootY[s], typeNum, s); + if (!b) + return false; + for (int i = 0; i < context.request.nbWorkers; i++) + if (game.addUnit(context.bootX[s] + (i % 4), context.bootY[s] - 1 - (i / 4), s, WORKER, + 0, 0, 0, 0) == NULL) + { + if (false) + printf("Failed to add unit %d of team %d\n", i, s); + return false; + } + game.teams[s]->createLists(); + } + return true; +} + +} // namespace MapGeneration diff --git a/src/map/generator/shared/StartingPositions.h b/src/map/generator/shared/StartingPositions.h new file mode 100644 index 000000000..6d75b7d7f --- /dev/null +++ b/src/map/generator/shared/StartingPositions.h @@ -0,0 +1,19 @@ +#pragma once +#include "Regions.h" +namespace MapGeneration +{ +bool divideUpPlayerLands(Game &game, GenerationContext &context, std::vector &grid, + std::vector &teamAreaNumbers, int &areaNumber); +void chooseFreeForBuildingSquares(Game &game, std::vector &points, + BuildingType *type, int team); +void chooseFreeForGroundUnits(Map &map, std::vector &points, int team); +void chooseTouchingBuilding(Map &map, std::vector &points, Building *building); +Building *addBuilding(Game &game, int x, int y, int team, int type, int level, + bool underConstruction); +} // namespace MapGeneration + +namespace MapGeneration +{ +bool placeStarts(Game &, GenerationContext &); +bool placeArchipelagoStarts(Game &, GenerationContext &, int islandSize); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Terrain.cpp b/src/map/generator/shared/Terrain.cpp new file mode 100644 index 000000000..42cb9fd9b --- /dev/null +++ b/src/map/generator/shared/Terrain.cpp @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2001-2004 Stephane Magnenat & Luc-Olivier de Charrière +// Copyright (C) 2008 Bradley Arsenault +#include "Terrain.h" +#include "Distances.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GeneratorDefinition.h" +#include "GlobalContainer.h" +#include "HeightMap.h" +#include "Map.h" +#include "Regions.h" +#include "Resources.h" +#include "StartingPositions.h" +#include "Unit.h" +#include +#include +using namespace MapGeneration; +bool MapGeneration::generateHeightField(Game &game, GenerationContext &context, + const HeightFieldOptions &options, + const HeightFieldBuilder &build) +{ + Map &map = game.map; + const int w = map.getW(), h = map.getH(); + + /// all under waterLevel is water, under sandLevel is beach, under grassLevel is grass and above + /// grasslevel is desert + float waterLevel, sandLevel, grassLevel, wheatWoodLevel, algaeLevel, stoneLevel; + /// to influence the roughness + float smoothingFactor = (float)(options.smoothing + 4) * 3; + /// the proportions requested through the gui can directly be translated into tile counts of the + /// undermap. + unsigned int waterTiles, sandTiles, grassTiles, wheatWoodTiles, algaeTiles; + /// grass + sand + water + desert as from the gui + unsigned int totalGSWFromUI = + options.water + options.sand + options.grass + options.desert + options.fruit; + /// respect symmetry-requirements + unsigned int wPower2Divider = 0, hPower2Divider = 0; + int power2Divider = options.repeat; + for (int i = 0; i < power2Divider; i++) + if ((w >> wPower2Divider) > (h >> hPower2Divider)) + wPower2Divider++; + else + hPower2Divider++; + int wRepeat = 1 << wPower2Divider; + int hRepeat = 1 << hPower2Divider; + unsigned int wHeightMap = (unsigned int)(w / wRepeat); + unsigned int hHeightMap = (unsigned int)(h / hRepeat); + /// lets generate a patch of perlin noise. That's a smooth mapping R^2 to ]0;1[ + HeightMap hm(wHeightMap, hHeightMap, context.stream("heightmap")); + /// 1 to avoid division by zero, + build(hm, wHeightMap, hHeightMap, smoothingFactor); + if (options.swamp) + { + waterTiles = options.water * wHeightMap * hHeightMap / (1u + options.water + options.grass); + sandTiles = 0; + grassTiles = wHeightMap * hHeightMap - waterTiles; + } + else + { + waterTiles = static_cast(options.water) / totalGSWFromUI * wHeightMap * hHeightMap; + sandTiles = static_cast(options.sand) / totalGSWFromUI * wHeightMap * hHeightMap; + grassTiles = static_cast(options.grass) / totalGSWFromUI * wHeightMap * hHeightMap; + } + /// wheat/wood needs ground to stand on and water. So: + wheatWoodTiles = waterTiles < grassTiles ? waterTiles / 2 : grassTiles / 2; + algaeTiles = waterTiles / 6; + + /// histogram[i] collects the count of all terrain levels == i + int histogram[2048]; + memset(histogram, 0, 2048 * sizeof(int)); + + for (unsigned i = 0; i < wHeightMap * hHeightMap; i++) + { + histogram[hm.uiLevel(i, 2048)]++; + } + unsigned int accumulatedHistogram = 0; + int i = 0; + waterLevel = 0; + sandLevel = 0; + grassLevel = 0; + wheatWoodLevel = 0; + stoneLevel = 0; + algaeLevel = 0; + while ((waterLevel == 0) && (i < 2048)) + { + accumulatedHistogram += histogram[i++]; + if (algaeLevel == 0 && accumulatedHistogram >= algaeTiles) + algaeLevel = (float)(i - 1) / 2048.0; + if (accumulatedHistogram >= waterTiles) + waterLevel = (float)(i - 1) / 2048.0; + } + while ((sandLevel == 0) && (i < 2048)) + { + accumulatedHistogram += histogram[i++]; + if (accumulatedHistogram >= waterTiles + sandTiles) + sandLevel = (float)(i - 1) / 2048.0; + } + while ((grassLevel == 0) && (i < 2048)) + { + accumulatedHistogram += histogram[i++]; + if (wheatWoodLevel == 0 && accumulatedHistogram >= waterTiles + sandTiles + wheatWoodTiles) + wheatWoodLevel = (float)(i - 1) / 2048.0; + if (stoneLevel == 0 && + accumulatedHistogram >= waterTiles + sandTiles + (wheatWoodTiles / 3)) + stoneLevel = (float)(i - 1) / 2048.0; + if (accumulatedHistogram >= waterTiles + sandTiles + grassTiles) + grassLevel = (float)(i - 1) / 2048.0; + } + for (unsigned y = 0; y < hHeightMap; y++) + for (unsigned x = 0; x < wHeightMap; x++) + { + int tmpUndermap; + if (hm(y * wHeightMap + x) < waterLevel) + tmpUndermap = WATER; + else if (hm(y * wHeightMap + x) < sandLevel) + tmpUndermap = SAND; + else if (hm(y * wHeightMap + x) < grassLevel) + tmpUndermap = GRASS; + else + tmpUndermap = SAND; + for (int yRepeat = 0; yRepeat < hRepeat; yRepeat++) + for (int xRepeat = 0; xRepeat < wRepeat; xRepeat++) + map.setUMTerrain( + (xRepeat * wHeightMap + x + (yRepeat * hHeightMap + y) * w) % w, + (xRepeat * wHeightMap + x + (yRepeat * hHeightMap + y) * w) / w, + static_cast(tmpUndermap)); + } + map.controlSand(); + + context.stage = "starting locations"; + // Now, we have to find suitable places for teams: + int nbTeams = context.request.nbTeams; + int minDistSquare = (int)((double)w * h / (double)nbTeams / 5); + if (minDistSquare <= 0) + { + return false; + } + assert(minDistSquare > 0); + int *bootX = context.bootX.data(); + int *bootY = context.bootY.data(); + + // TODO: First pass to find the number of available places. + for (int team = 0; team < nbTeams; team++) + { + int maxSurface = 0; + int maxX = 0; + int maxY = 0; + for (int y = 0; y < h; y++) + { + int width = 0; + int startX = 0; + for (int x = 0; x < w; x++) + { + int a = map.getUMTerrain(x, y); + if (a == GRASS) + width++; + else + { + if (width > 7) + { + int centerX = ((x + startX) >> 1); + int top, bot; + for (top = 0; top < h; top++) + if (map.getUMTerrain(centerX, y - top) != GRASS) + break; + for (bot = 0; bot < h; bot++) + if (map.getUMTerrain(centerX, y + bot) != GRASS) + break; + int height = top + bot - 1; + int surface = height * width; + assert(surface > 0); + + int centerY = y + ((bot - top) >> 1); + bool farEnough = true; + for (int ti = 0; ti < team; ti++) + if (map.warpDistSquare(centerX, centerY, bootX[ti], bootY[ti]) < + minDistSquare) + { + farEnough = false; + break; + } + + if (surface > maxSurface && farEnough) + { + maxSurface = surface; + maxX = centerX; + maxY = centerY; + } + } + width = 0; + startX = x; + } + } + } + + if (maxSurface <= 0) + { + return false; + } + assert(maxSurface); + bootX[team] = maxX; + bootY[team] = maxY; + } + + map.controlSand(); + map.rebuildTerrain(); + context.stage = "resources"; + // now to add primary resources for current map generator + for (unsigned y = 0; y < hHeightMap; y++) + { + for (unsigned x = 0; x < wHeightMap; x++) + { + int tmpResource = NO_RES; + if (hm(x + wHeightMap * y) < algaeLevel) + { + tmpResource = ALGA; + // following places stone next to sand & water and keeps wheat & wood more inland + // without clogging up the interior too badly + } + else if (hm(x + wHeightMap * y) < stoneLevel) + { + tmpResource = STONE; + } + else if (hm(x + wHeightMap * y) < wheatWoodLevel) + { + // patch to get smooth areas of wheat and wood: + // if the map is ascending at x+w/2,y set wheat. else set wood + if (hm((x + wHeightMap / 2) % wHeightMap + wHeightMap * y) < + hm((x + wHeightMap / 2 + 1) % wHeightMap + wHeightMap * y)) + { + tmpResource = CORN; + } + else + { + tmpResource = WOOD; + } + } + if (tmpResource != NO_RES) + { + for (int yRepeat = 0; yRepeat < hRepeat; yRepeat++) + { + for (int xRepeat = 0; xRepeat < wRepeat; xRepeat++) + { + map.setResource(xRepeat * wHeightMap + x, yRepeat * hHeightMap + y, + tmpResource, 1); + } + } + } + } + } + + // TODO: count of groves(=options.fruit) does not scale with mapsize. + // so it has to be adjusted higher on bigger maps now. + + // fruit-placement: + if (options.fruit > 0) + { + for (int q1 = 0; q1 < options.fruit; q1++) // counting groves + { + // choose fruit + int fruit; + switch (context.stream("resources")() % 3) + { + case 0: + fruit = CHERRY; + break; + case 1: + fruit = ORANGE; + break; + case 2: + default: + fruit = PRUNE; + break; + } + // choose coordinate where there is grass but no resource yet + int x, y; + int attempts = 0; + do + { + if (++attempts > int(wHeightMap * hHeightMap * 4)) + { + context.detail = "No free grass for fruit"; + return false; + } + x = (context.stream("resources")() % wHeightMap); + y = (context.stream("resources")() % hHeightMap); + } while (map.getUMTerrain(x, y) != GRASS || map.isResource(x, y)); + // choose size of grove (tree count) + int grovesize = (context.stream("resources")() % 10) + 1; + for (int i = 0; i < grovesize; i++) + { + for (int yRepeat = 0; yRepeat < hRepeat; yRepeat++) + for (int xRepeat = 0; xRepeat < wRepeat; xRepeat++) + map.setResource(xRepeat * wHeightMap + x, yRepeat * hHeightMap + y, fruit, + 1); + // find a valid neighbor of actual coordinate + for (int iTry = 0; iTry < 100; iTry++) + { + int xNew = x + context.stream("resources")() % 3 - 1; + int yNew = y + context.stream("resources")() % 3 - 1; + if (map.getUMTerrain(xNew, yNew) == GRASS && !map.isResource(xNew, yNew)) + { + x = xNew; + y = yNew; + break; + } + } + } + } + } + return true; +} diff --git a/src/map/generator/shared/Terrain.h b/src/map/generator/shared/Terrain.h new file mode 100644 index 000000000..4b39fc6c4 --- /dev/null +++ b/src/map/generator/shared/Terrain.h @@ -0,0 +1,16 @@ +#pragma once +#include "HeightMap.h" +#include +class Game; +struct GenerationContext; +namespace MapGeneration +{ +struct HeightFieldOptions +{ + int water, sand, grass, desert, smoothing, fruit, repeat; + bool swamp = false; +}; +using HeightFieldBuilder = std::function; +bool generateHeightField(Game &, GenerationContext &, const HeightFieldOptions &, + const HeightFieldBuilder &); +} // namespace MapGeneration diff --git a/src/map/generator/shared/Topology.cpp b/src/map/generator/shared/Topology.cpp new file mode 100644 index 000000000..6bdbc92b2 --- /dev/null +++ b/src/map/generator/shared/Topology.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "Topology.h" +#include +#include +#include +#include +namespace MapGeneration +{ +namespace +{ +void checkGrid(size_t size, int width, int height) +{ + if (width <= 0 || height <= 0 || size != size_t(width) * height) + throw std::invalid_argument("Invalid topology grid"); +} +template void neighbors(int i, int w, int h, bool wrap, GridNeighbors kind, F visit) +{ + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) + { + if ((!dx && !dy) || (kind == GridNeighbors::Cardinal && dx && dy)) + continue; + int x = i % w + dx, y = i / w + dy; + if (wrap) + { + x = (x + w) % w; + y = (y + h) % h; + } + else if (x < 0 || x >= w || y < 0 || y >= h) + continue; + visit(y * w + x); + } +} +} // namespace +std::vector graphDistances(const RegionGraph &graph, const std::vector &sources) +{ + for (const auto &edges : graph) + for (int v : edges) + if (v < 0 || size_t(v) >= graph.size()) + throw std::invalid_argument("Invalid graph edge"); + std::vector distances(graph.size(), -1); + std::queue pending; + for (int v : sources) + { + if (v < 0 || size_t(v) >= graph.size()) + throw std::invalid_argument("Invalid graph source"); + if (distances[v] < 0) + { + distances[v] = 0; + pending.push(v); + } + } + while (!pending.empty()) + { + int u = pending.front(); + pending.pop(); + for (int v : graph[u]) + if (distances[v] < 0) + { + distances[v] = distances[u] + 1; + pending.push(v); + } + } + return distances; +} +std::vector connectedRegions(const std::vector &passable, int w, int h, + bool wrap, GridNeighbors kind) +{ + checkGrid(passable.size(), w, h); + std::vector regions(passable.size(), -1); + int next = 0; + std::vector pending; + for (size_t i = 0; i < passable.size(); ++i) + { + if (!passable[i] || regions[i] >= 0) + continue; + regions[i] = next; + pending.push_back(int(i)); + while (!pending.empty()) + { + int u = pending.back(); + pending.pop_back(); + neighbors(u, w, h, wrap, kind, + [&](int v) + { + if (passable[v] && regions[v] < 0) + { + regions[v] = next; + pending.push_back(v); + } + }); + } + ++next; + } + return regions; +} +RegionGraph regionAdjacency(const std::vector &labels, int w, int h, + const std::vector &ids, bool wrap) +{ + checkGrid(labels.size(), w, h); + std::map indices; + for (size_t i = 0; i < ids.size(); ++i) + if (!indices.emplace(ids[i], int(i)).second) + throw std::invalid_argument("Duplicate region ID"); + RegionGraph graph(ids.size()); + for (size_t i = 0; i < labels.size(); ++i) + { + auto u = indices.find(labels[i]); + if (u == indices.end()) + continue; + neighbors(int(i), w, h, wrap, GridNeighbors::Cardinal, + [&](int n) + { + auto v = indices.find(labels[n]); + if (v != indices.end() && u->second != v->second) + graph[u->second].push_back(v->second); + }); + } + for (auto &edges : graph) + { + std::sort(edges.begin(), edges.end()); + edges.erase(std::unique(edges.begin(), edges.end()), edges.end()); + } + return graph; +} +} // namespace MapGeneration diff --git a/src/map/generator/shared/Topology.h b/src/map/generator/shared/Topology.h new file mode 100644 index 000000000..f0e36f9a0 --- /dev/null +++ b/src/map/generator/shared/Topology.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +namespace MapGeneration +{ +using RegionGraph = std::vector>; +// Distances in edges, -1 for unreachable vertices; supports multiple sources. +std::vector graphDistances(const RegionGraph &, const std::vector &sources); +// Explicit adjacency policy: geometry connectivity is not necessarily unit pathfinding. +enum class GridNeighbors +{ + Cardinal, + Eight +}; +std::vector connectedRegions(const std::vector &passable, int width, int height, + bool wrap, GridNeighbors neighbors = GridNeighbors::Cardinal); +// Region IDs may be sparse; caller supplies their ordered IDs. Ignores other labels. +RegionGraph regionAdjacency(const std::vector &labels, int width, int height, + const std::vector ®ionIds, bool wrap); +} // namespace MapGeneration diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index 9c673cc85..9629f5a18 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -25,16 +25,14 @@ #include GlobalContainer *globalContainer = nullptr; -struct CountingAI : AIImplementation -{ - int calls = 0; - bool load(GAGCore::InputStream *, Player *, Sint32) override { return true; } - void save(GAGCore::OutputStream *) override {} - std::shared_ptr getOrder() override - { - ++calls; - return std::make_shared(); - } +struct CountingAI : AIImplementation { + int calls = 0; + bool load(GAGCore::InputStream *, Player *, Sint32) override { return true; } + void save(GAGCore::OutputStream *) override {} + std::shared_ptr getOrder() override { + ++calls; + return std::make_shared(); + } }; struct CustomGameSetupHarness { @@ -261,67 +259,67 @@ struct CustomGameSetupHarness } click("Launch", 540, 445); - globalContainer->settings.gameSpeed = 7; - { - Engine engine; - auto timer = SDL_AddTimer(500, Driver::tick, &driver); - assert(timer); - auto watchdog = SDL_AddTimer( - 20000, - [](Uint32, void *) -> Uint32 - { - SDL_Event event = {}; - event.type = SDL_QUIT; - SDL_PushEvent(&event); - return 0; - }, - nullptr); - int result = engine.initCustom(); - SDL_RemoveTimer(timer); - SDL_RemoveTimer(watchdog); - assert(result == Engine::EE_NO_ERROR); - assert(driver.next == driver.steps.size()); - assert(globalContainer->liveSpectating == (control == CustomGameSetup::Computer)); - assert(globalContainer->settings.gameSpeed == 3); - assert(engine.gui.game.gameHeader.getNumberOfPlayers() == - (control == CustomGameSetup::Shared ? 5 : 4)); - assert(engine.gui.game.players[control == CustomGameSetup::Shared ? 2 : 1] - ->ai->implementationID == AI::NICOWAR); - assert(engine.gui.game.gameHeader.getAllyTeamNumber(0) == - engine.gui.game.gameHeader.getAllyTeamNumber(1)); - assert(engine.gui.game.gameHeader.getAllyTeamNumber(0) != - engine.gui.game.gameHeader.getAllyTeamNumber(2)); - if (globalContainer->liveSpectating) - { - SDL_Event pause = {}; - pause.type = SDL_KEYDOWN; - pause.key.keysym.sym = SDLK_p; - engine.gui.processEvent(&pause); - assert(engine.gui.hardPause); - engine.gui.processEvent(&pause); - assert(!engine.gui.hardPause); - } - globalContainer->settings.save(); // Simulate persisting in-game options. - globalContainer->automaticEndingGame = true; - globalContainer->automaticEndingSteps = 30; - globalContainer->automaticGameGlobalEndConditions = true; - engine.run(); - { - FrontendScope gameplay(false); - engine.gui.drawAll(engine.gui.localTeamNo); - globalContainer->gfx->printScreen(output + "/live-control-" + - std::to_string(control) + ".bmp"); - } - } - assert(globalContainer->settings.gameSpeed == 7); - Settings persisted; - persisted.load(); - assert(persisted.gameSpeed == 7); - std::cout << "PASS full SDL UI flow mode " << control - << ": clicks, nested choices, shared control, presets, profiles, " - "random preview, " - "match launch and speed restoration\n"; - } + globalContainer->settings.gameSpeed = 7; + { + Engine engine; + auto timer = SDL_AddTimer(500, Driver::tick, &driver); + assert(timer); + auto watchdog = SDL_AddTimer( + 20000, + [](Uint32, void *) -> Uint32 { + SDL_Event event = {}; + event.type = SDL_QUIT; + SDL_PushEvent(&event); + return 0; + }, + nullptr); + int result = engine.initCustom(); + SDL_RemoveTimer(timer); + SDL_RemoveTimer(watchdog); + assert(result == Engine::EE_NO_ERROR); + assert(driver.next == driver.steps.size()); + assert(globalContainer->liveSpectating == + (control == CustomGameSetup::Computer)); + assert(globalContainer->settings.gameSpeed == 3); + assert(engine.gui.game.gameHeader.getNumberOfPlayers() == + (control == CustomGameSetup::Shared ? 5 : 4)); + assert( + engine.gui.game.players[control == CustomGameSetup::Shared ? 2 : 1] + ->ai->implementationID == AI::NICOWAR); + assert(engine.gui.game.gameHeader.getAllyTeamNumber(0) == + engine.gui.game.gameHeader.getAllyTeamNumber(1)); + assert(engine.gui.game.gameHeader.getAllyTeamNumber(0) != + engine.gui.game.gameHeader.getAllyTeamNumber(2)); + if (globalContainer->liveSpectating) { + SDL_Event pause = {}; + pause.type = SDL_KEYDOWN; + pause.key.keysym.sym = SDLK_p; + engine.gui.processEvent(&pause); + assert(engine.gui.hardPause); + engine.gui.processEvent(&pause); + assert(!engine.gui.hardPause); + } + globalContainer->settings.save(); // Simulate persisting in-game options. + globalContainer->automaticEndingGame = true; + globalContainer->automaticEndingSteps = 30; + globalContainer->automaticGameGlobalEndConditions = true; + engine.run(); + { + FrontendScope gameplay(false); + engine.gui.drawAll(engine.gui.localTeamNo); + globalContainer->gfx->printScreen(output + "/live-control-" + + std::to_string(control) + ".bmp"); + } + } + assert(globalContainer->settings.gameSpeed == 7); + Settings persisted; + persisted.load(); + assert(persisted.gameSpeed == 7); + std::cout << "PASS full SDL UI flow mode " << control + << ": clicks, nested choices, shared control, presets, profiles, " + "random preview, " + "match launch and speed restoration\n"; + } static void visual(const std::string &output) { @@ -340,279 +338,284 @@ struct CustomGameSetupHarness assert(profile.returnCode == AINames::selectionIndex(AI::CORTEX)); } - CustomGameScreen screen; - screen.gfx = globalContainer->gfx; - screen.dispatchInit(); - assert(screen.validMap && screen.setup.capacity == 4); - screen.separateMapLibraries = false; - screen.listMaps(); - assert( - std::any_of(screen.mapPaths.begin(), screen.mapPaths.end(), [](const auto &path) - { return std::filesystem::path(path).filename() == "FourSquares1.map"; })); - screen.separateMapLibraries = true; - screen.listMaps(); + CustomGameScreen screen; + screen.gfx = globalContainer->gfx; + screen.dispatchInit(); + assert(screen.validMap && screen.setup.capacity == 4); + screen.separateMapLibraries = false; + screen.listMaps(); + assert(std::any_of( + screen.mapPaths.begin(), screen.mapPaths.end(), [](const auto &path) { + return std::filesystem::path(path).filename() == "FourSquares1.map"; + })); + screen.separateMapLibraries = true; + screen.listMaps(); - auto paint = [&] { screen.dispatchPaint(false); }; - auto keyEvent = [&](SDL_Keycode key) - { - SDL_Event e = {}; - e.type = SDL_KEYDOWN; - e.key.keysym.sym = key; - screen.dispatchEvents(&e); - paint(); - }; - auto clickControl = [&](const std::string &id) - { - paint(); - auto find = [&] - { - return std::find_if(screen.controls->hits.begin(), screen.controls->hits.end(), - [&](const auto &hit) { return hit.id == id; }); - }; - auto hit = find(); - assert(hit != screen.controls->hits.end()); - if (hit->region >= 0) - { - auto &r = screen.controls->regions[hit->region]; - if (hit->box.y < r.box.y) - screen.controls->scroll(hit->region, hit->box.y - r.box.y); - else if (hit->box.y + hit->box.h > r.box.y + r.box.h) - screen.controls->scroll(hit->region, - hit->box.y + hit->box.h - r.box.y - r.box.h); - paint(); - hit = find(); - } - auto r = hit->box; - for (auto type : {SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP}) - { - SDL_Event e = {}; - e.type = type; - e.button.button = SDL_BUTTON_LEFT; - e.button.x = r.x + r.w / 2; - e.button.y = r.y + r.h / 2; - screen.dispatchEvents(&e); - } - paint(); - }; - // Canonical identity, including repeated roots and symlink aliases. - auto fixture = std::filesystem::temp_directory_path() / - ("glob2-lobby-catalog-test-" + std::to_string(SDL_GetTicks())); - std::filesystem::remove_all(fixture); - std::filesystem::create_directories(fixture / "one"); - std::filesystem::create_directories(fixture / "two"); - for (auto name : {"one/zebra.map", "one/Alpha.map", "two/Alpha.map"}) - { - std::ofstream file(fixture / name); - file << "fixture"; - } - std::filesystem::create_directory_symlink(fixture / "one", fixture / "alias"); - auto catalog = - lobbyMapCatalog({fixture / "one", fixture / "one", fixture / "alias", fixture / "two"}); - assert(catalog.size() == 3 && catalog[0].name.find("Alpha") == 0 && - catalog[1].name.find("Alpha") == 0 && catalog[2].name == "zebra"); - assert(catalog[0].name != catalog[1].name); - std::filesystem::remove_all(fixture); - assert(std::set(screen.mapPaths.begin(), screen.mapPaths.end()).size() == - screen.mapPaths.size()); - paint(); - auto &mapRegion = screen.controls->regions[10]; - mapRegion.offset = std::min(56, mapRegion.maximum); - paint(); - int savedOffset = mapRegion.offset; - int row = savedOffset / 28 + 1; - clickControl("map/entry/" + std::to_string(row)); - assert(mapRegion.offset == savedOffset); - keyEvent(SDLK_DOWN); - assert(mapRegion.offset == savedOffset); - clickControl("map/library/1"); - clickControl("map/library/0"); - assert(mapRegion.offset == savedOffset); - assert(screen.librarySelection[0] == screen.source); - screen.loadMap("maps/FourSquares1.map"); - screen.activateGroup(screen.groups[1]); - paint(); - auto alliances = screen.setup.colonies; - clickControl("colony/0/controller"); - assert(screen.controls->popup.open); - keyEvent(SDLK_DOWN); - keyEvent(SDLK_RETURN); - assert(screen.setup.colonies[0].controller == CustomGameSetup::Computer); - for (int i = 0; i < 4; ++i) - assert(screen.setup.colonies[i].alliance == alliances[i].alliance); - assert(screen.controls->focus == "colony/0/controller"); - clickControl("colony/0/controller"); - keyEvent(SDLK_DOWN); - keyEvent(SDLK_RETURN); - assert(screen.setup.colonies[0].controller == CustomGameSetup::Shared); - clickControl("colony/1/ai"); - keyEvent(SDLK_DOWN); - keyEvent(SDLK_ESCAPE); - assert(screen.setup.colonies[1].ai == AI::NUMBI); - clickControl("colony/1/ai"); - SDL_Event outside = {}; - outside.type = SDL_MOUSEBUTTONDOWN; - outside.button.x = 0; - outside.button.y = 0; - screen.dispatchEvents(&outside); - assert(!screen.controls->popup.open); - clickControl("format/1"); - assert(screen.setup.colonies[0].alliance == screen.setup.colonies[1].alliance); - screen.activateGroup(screen.groups[2]); - paint(); - clickControl("rule/1/1"); - assert(screen.setup.revealed && screen.setup.ruleset == "Custom"); - int rulesOffset = screen.controls->regions[2].offset; - clickControl("rule/1/0"); - assert(!screen.setup.revealed && screen.controls->regions[2].offset == rulesOffset); - clickControl("ruleset/1"); - assert(screen.setup.speed == 3 && screen.setup.generator.nbWorkers == 8); - screen.setup = CustomGameSetup(); - screen.loadMap("maps/FourSquares1.map"); - screen.controls->regions[2].offset = 0; - screen.activateGroup(screen.groups[0]); - screen.controls->regions[10].offset = 0; - std::cout << "PASS canonical map catalog, selection stability, inline " - "controls, popup cancel, focus preservation and rules\n"; - auto capture = [&](const std::string &name) - { - screen.dispatchPaint(false); - globalContainer->gfx->printScreen(output + "/" + name + ".bmp"); - }; - capture("map-640"); - screen.activateGroup(screen.groups[1]); - capture("players-640"); - screen.setup.colonies[1].ai = AI::CORTEX; - capture("cortex-640"); - clickControl("colony/1/ai"); - capture("ai-dropdown"); - keyEvent(SDLK_ESCAPE); + auto paint = [&] { screen.dispatchPaint(false); }; + auto keyEvent = [&](SDL_Keycode key) { + SDL_Event e = {}; + e.type = SDL_KEYDOWN; + e.key.keysym.sym = key; + screen.dispatchEvents(&e); + paint(); + }; + auto clickControl = [&](const std::string &id) { + paint(); + auto find = [&] { + return std::find_if(screen.controls->hits.begin(), + screen.controls->hits.end(), + [&](const auto &hit) { return hit.id == id; }); + }; + auto hit = find(); + assert(hit != screen.controls->hits.end()); + if (hit->region >= 0) { + auto &r = screen.controls->regions[hit->region]; + if (hit->box.y < r.box.y) + screen.controls->scroll(hit->region, hit->box.y - r.box.y); + else if (hit->box.y + hit->box.h > r.box.y + r.box.h) + screen.controls->scroll(hit->region, + hit->box.y + hit->box.h - r.box.y - r.box.h); + paint(); + hit = find(); + } + auto r = hit->box; + for (auto type : {SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP}) { + SDL_Event e = {}; + e.type = type; + e.button.button = SDL_BUTTON_LEFT; + e.button.x = r.x + r.w / 2; + e.button.y = r.y + r.h / 2; + screen.dispatchEvents(&e); + } + paint(); + }; + // Canonical identity, including repeated roots and symlink aliases. + auto fixture = + std::filesystem::temp_directory_path() / + ("glob2-lobby-catalog-test-" + std::to_string(SDL_GetTicks())); + std::filesystem::remove_all(fixture); + std::filesystem::create_directories(fixture / "one"); + std::filesystem::create_directories(fixture / "two"); + for (auto name : {"one/zebra.map", "one/Alpha.map", "two/Alpha.map"}) { + std::ofstream file(fixture / name); + file << "fixture"; + } + std::filesystem::create_directory_symlink(fixture / "one", + fixture / "alias"); + auto catalog = lobbyMapCatalog( + {fixture / "one", fixture / "one", fixture / "alias", fixture / "two"}); + assert(catalog.size() == 3 && catalog[0].name.find("Alpha") == 0 && + catalog[1].name.find("Alpha") == 0 && catalog[2].name == "zebra"); + assert(catalog[0].name != catalog[1].name); + std::filesystem::remove_all(fixture); + assert(std::set(screen.mapPaths.begin(), screen.mapPaths.end()) + .size() == screen.mapPaths.size()); + paint(); + auto &mapRegion = screen.controls->regions[10]; + mapRegion.offset = std::min(56, mapRegion.maximum); + paint(); + int savedOffset = mapRegion.offset; + int row = savedOffset / 28 + 1; + clickControl("map/entry/" + std::to_string(row)); + assert(mapRegion.offset == savedOffset); + keyEvent(SDLK_DOWN); + assert(mapRegion.offset == savedOffset); + clickControl("map/library/1"); + clickControl("map/library/0"); + assert(mapRegion.offset == savedOffset); + assert(screen.librarySelection[0] == screen.source); + screen.loadMap("maps/FourSquares1.map"); + screen.activateGroup(screen.groups[1]); + paint(); + auto alliances = screen.setup.colonies; + clickControl("colony/0/controller"); + assert(screen.controls->popup.open); + keyEvent(SDLK_DOWN); + keyEvent(SDLK_RETURN); + assert(screen.setup.colonies[0].controller == CustomGameSetup::Computer); + for (int i = 0; i < 4; ++i) + assert(screen.setup.colonies[i].alliance == alliances[i].alliance); + assert(screen.controls->focus == "colony/0/controller"); + clickControl("colony/0/controller"); + keyEvent(SDLK_DOWN); + keyEvent(SDLK_RETURN); + assert(screen.setup.colonies[0].controller == CustomGameSetup::Shared); + clickControl("colony/1/ai"); + keyEvent(SDLK_DOWN); + keyEvent(SDLK_ESCAPE); + assert(screen.setup.colonies[1].ai == AI::NUMBI); + clickControl("colony/1/ai"); + SDL_Event outside = {}; + outside.type = SDL_MOUSEBUTTONDOWN; + outside.button.x = 0; + outside.button.y = 0; + screen.dispatchEvents(&outside); + assert(!screen.controls->popup.open); + clickControl("format/1"); + assert(screen.setup.colonies[0].alliance == + screen.setup.colonies[1].alliance); + screen.activateGroup(screen.groups[2]); + paint(); + clickControl("rule/1/1"); + assert(screen.setup.revealed && screen.setup.ruleset == "Custom"); + int rulesOffset = screen.controls->regions[2].offset; + clickControl("rule/1/0"); + assert(!screen.setup.revealed && + screen.controls->regions[2].offset == rulesOffset); + clickControl("ruleset/1"); + assert(screen.setup.speed == 3 && screen.setup.generator.nbWorkers == 8); + screen.setup = CustomGameSetup(); + screen.loadMap("maps/FourSquares1.map"); + screen.controls->regions[2].offset = 0; + screen.activateGroup(screen.groups[0]); + screen.controls->regions[10].offset = 0; + std::cout << "PASS canonical map catalog, selection stability, inline " + "controls, popup cancel, focus preservation and rules\n"; + auto capture = [&](const std::string &name) { + screen.dispatchPaint(false); + globalContainer->gfx->printScreen(output + "/" + name + ".bmp"); + }; + capture("map-640"); + screen.activateGroup(screen.groups[1]); + capture("players-640"); + screen.setup.colonies[1].ai = AI::CORTEX; + capture("cortex-640"); + clickControl("colony/1/ai"); + capture("ai-dropdown"); + keyEvent(SDLK_ESCAPE); - screen.activateGroup(screen.groups[2]); - capture("rules-640"); - screen.setup.random = true; - screen.invalidate(); - screen.activateGroup(screen.groups[0]); - capture("random-controls-640"); - screen.expanded[0] = screen.expanded[1] = screen.expanded[2] = true; - paint(); - screen.controls->regions[3].offset = 180; - capture("generator-expanded"); - screen.controls->regions[3].offset = 0; - // Preview appears from the timer, without a Generate control or click. - assert(std::none_of(screen.controls->hits.begin(), screen.controls->hits.end(), - [](const auto &h) { return h.id == "map/generate"; })); - screen.onTimer(screen.previewDue - 1); - assert(!screen.validMap); - screen.controls->pressed = "generator/Water weight"; - screen.onTimer(screen.previewDue); - assert(!screen.validMap); - screen.controls->pressed.clear(); - screen.onTimer(screen.previewDue); - assert(screen.validMap && !screen.previewPending); - auto first = screen.snapshot; - auto revision = screen.previewRevision; - assert(std::filesystem::exists(first)); - capture("random-preview-640"); - screen.setup.colonies[1].ai = AI::CASTOR; - screen.setup.presetTeams(1); - screen.onTimer(SDL_GetTicks() + 1000); - assert(screen.previewRevision == revision && screen.sourceFile() == first); - clickControl("generator/Water weight"); - assert(!screen.validMap && screen.setup.mapRevision != revision); - auto water = screen.setup.generator.waterRatio; - SDL_Event motion = {}; - motion.type = SDL_MOUSEMOTION; - motion.motion.x = 0; - motion.motion.y = 0; - screen.dispatchEvents(&motion); - assert(screen.setup.generator.waterRatio == water); - keyEvent(SDLK_RIGHT); - assert(screen.setup.generator.waterRatio == water + 1); - assert(screen.setup.colonies[0].alliance == screen.setup.colonies[1].alliance); + screen.activateGroup(screen.groups[2]); + capture("rules-640"); + screen.setup.random = true; + screen.invalidate(); + screen.activateGroup(screen.groups[0]); + capture("random-controls-640"); + screen.expanded[0] = screen.expanded[1] = screen.expanded[2] = true; + paint(); + screen.controls->regions[3].offset = 180; + capture("generator-expanded"); + screen.controls->regions[3].offset = 0; + // Preview appears from the timer, without a Generate control or click. + assert(std::none_of(screen.controls->hits.begin(), + screen.controls->hits.end(), + [](const auto &h) { return h.id == "map/generate"; })); + screen.onTimer(screen.previewDue - 1); + assert(!screen.validMap); + screen.controls->pressed = "generator/water"; + screen.onTimer(screen.previewDue); + assert(!screen.validMap); + screen.controls->pressed.clear(); + screen.onTimer(screen.previewDue); + assert(screen.validMap && !screen.previewPending); + auto first = screen.snapshot; + auto revision = screen.previewRevision; + assert(std::filesystem::exists(first)); + capture("random-preview-640"); + screen.setup.colonies[1].ai = AI::CASTOR; + screen.setup.presetTeams(1); + screen.onTimer(SDL_GetTicks() + 1000); + assert(screen.previewRevision == revision && screen.sourceFile() == first); + clickControl("generator/water"); + assert(!screen.validMap && screen.setup.mapRevision != revision); + auto water = screen.setup.generator.options["water"]; + SDL_Event motion = {}; + motion.type = SDL_MOUSEMOTION; + motion.motion.x = 0; + motion.motion.y = 0; + screen.dispatchEvents(&motion); + assert(screen.setup.generator.options["water"] == water); + keyEvent(SDLK_RIGHT); + assert(screen.setup.generator.options["water"] == water + 1); + assert(screen.setup.colonies[0].alliance == + screen.setup.colonies[1].alliance); - assert(screen.generateMap()); - assert(screen.snapshot != first && !std::filesystem::exists(first)); - screen.setup.presetRules(1); - screen.invalidate(); - assert(!screen.validMap); - auto assignments = screen.setup.colonies; - screen.setup.generator.waterRatio = screen.setup.generator.sandRatio = - screen.setup.generator.grassRatio = screen.setup.generator.desertRatio = 0; - screen.onTimer(screen.previewDue); - assert(!screen.validMap && !screen.previewPending); - for (int i = 0; i < 4; ++i) - assert(screen.setup.colonies[i].alliance == assignments[i].alliance); - screen.setup.generator.waterRatio = screen.setup.generator.sandRatio = - screen.setup.generator.grassRatio = screen.setup.generator.desertRatio = 50; - screen.invalidate(); - screen.onTimer(screen.previewDue); - assert(screen.validMap); + assert(screen.generateMap()); + assert(screen.snapshot != first && !std::filesystem::exists(first)); + screen.setup.presetRules(1); + screen.invalidate(); + assert(!screen.validMap); + auto assignments = screen.setup.colonies; + screen.setup.generator.options["water"] = + screen.setup.generator.options["sand"] = + screen.setup.generator.options["grass"] = + screen.setup.generator.options["desert"] = 0; + screen.onTimer(screen.previewDue); + assert(!screen.validMap && !screen.previewPending); + for (int i = 0; i < 4; ++i) + assert(screen.setup.colonies[i].alliance == assignments[i].alliance); + screen.setup.generator.options["water"] = + screen.setup.generator.options["sand"] = + screen.setup.generator.options["grass"] = + screen.setup.generator.options["desert"] = 50; + screen.invalidate(); + screen.onTimer(screen.previewDue); + assert(screen.validMap); - // Drive the same dropdown/steppers used by players, including measured five-unit steps. - auto landscape = [&](int method) - { - clickControl("generator/landscape"); - for (int i = 0; i < 8; ++i) - keyEvent(SDLK_UP); - for (int i = 1; i < method; ++i) - keyEvent(SDLK_DOWN); - keyEvent(SDLK_RETURN); - assert(screen.setup.generator.method == method); - }; - landscape(MapGenerationDescriptor::eCONCRETEISLANDS); - assert(screen.setup.generator.riverDiameter == 5 && - screen.setup.generator.extraIslands == 3); - clickControl("generator/Channel width/+"); - assert(screen.setup.generator.riverDiameter == 6); - capture("concrete-controls"); - landscape(MapGenerationDescriptor::eISLES); - assert(screen.setup.generator.grassRatio == 60); - clickControl("generator/Island size/+"); - assert(screen.setup.generator.grassRatio == 65); - clickControl("generator/Land bridge width/+"); - assert(screen.setup.generator.riverDiameter == 5); - capture("isles-controls"); - landscape(MapGenerationDescriptor::eCRATERLAKES); - assert(screen.setup.generator.riverDiameter == 25 && - screen.setup.generator.grassRatio == 75); - clickControl("generator/Lake size/+"); - assert(screen.setup.generator.riverDiameter == 30); - capture("crater-controls"); - landscape(MapGenerationDescriptor::eCONCRETEISLANDS); - assert(screen.setup.generator.riverDiameter == 6); - landscape(MapGenerationDescriptor::eOLDISLANDS); - assert(screen.setup.generator.oldIslandSize == 65); - capture("rugged-archipelago-controls"); - landscape(MapGenerationDescriptor::eOLDRANDOM); - capture("shattered-coast-controls"); - landscape(MapGenerationDescriptor::eRIVER); + // Drive the same dropdown/steppers used by players, including measured + // five-unit steps. + auto landscape = [&](int method) { + clickControl("generator/landscape"); + for (int i = 0; i < 8; ++i) + keyEvent(SDLK_UP); + for (int i = 1; i < method; ++i) + keyEvent(SDLK_DOWN); + keyEvent(SDLK_RETURN); + assert(screen.setup.generator.method == method); + }; + landscape(MapGenerationDescriptor::eCONCRETEISLANDS); + assert(screen.setup.generator.options["channel-width"] == 5 && + screen.setup.generator.options["extra-islands"] == 3); + clickControl("generator/channel-width/+"); + assert(screen.setup.generator.options["channel-width"] == 6); + capture("concrete-controls"); + landscape(MapGenerationDescriptor::eISLES); + assert(screen.setup.generator.options["island-size"] == 60); + clickControl("generator/island-size/+"); + assert(screen.setup.generator.options["island-size"] == 65); + clickControl("generator/bridge-width/+"); + assert(screen.setup.generator.options["bridge-width"] == 5); + capture("isles-controls"); + landscape(MapGenerationDescriptor::eCRATERLAKES); + assert(screen.setup.generator.options["lake-size"] == 25 && + screen.setup.generator.options["grass"] == 75); + clickControl("generator/lake-size/+"); + assert(screen.setup.generator.options["lake-size"] == 30); + capture("crater-controls"); + landscape(MapGenerationDescriptor::eCONCRETEISLANDS); + assert(screen.setup.generator.options["channel-width"] == 6); + landscape(MapGenerationDescriptor::eOLDISLANDS); + assert(screen.setup.generator.options["island-size"] == 65); + capture("rugged-archipelago-controls"); + landscape(MapGenerationDescriptor::eOLDRANDOM); + capture("shattered-coast-controls"); + landscape(MapGenerationDescriptor::eRIVER); - screen.activateGroup(screen.groups[0]); - capture("map-1000"); - screen.setup.setCapacity(12); - screen.setup.generator.wDec = screen.setup.generator.hDec = 8; - screen.invalidate(); - assert(screen.generateMap()); - screen.setup.setController(0, CustomGameSetup::Computer); - screen.activateGroup(screen.groups[1]); - capture("players-12-1000"); - clickControl("colony/0/controller"); - assert(!screen.controls->popup.enabled[CustomGameSetup::Shared]); - capture("controller-limit"); - keyEvent(SDLK_DOWN); - keyEvent(SDLK_RETURN); - assert(screen.controls->popup.open && screen.setup.controllerCount() == 12); - keyEvent(SDLK_ESCAPE); - screen.controls->regions[1].offset = screen.controls->regions[1].maximum; - capture("players-12-scrolled"); - clickControl("colony/11/ai"); - int rosterOffset = screen.controls->regions[1].offset; - keyEvent(SDLK_DOWN); - keyEvent(SDLK_RETURN); - assert(screen.controls->regions[1].offset == rosterOffset); - // Sequential keyboard focus scrolls an off-screen control into view. - screen.controls->focus = "colony/11/team"; - keyEvent(SDLK_TAB); + screen.activateGroup(screen.groups[0]); + capture("map-1000"); + screen.setup.setCapacity(12); + screen.setup.generator.wDec = screen.setup.generator.hDec = 8; + screen.invalidate(); + assert(screen.generateMap()); + screen.setup.setController(0, CustomGameSetup::Computer); + screen.activateGroup(screen.groups[1]); + capture("players-12-1000"); + clickControl("colony/0/controller"); + assert(!screen.controls->popup.enabled[CustomGameSetup::Shared]); + capture("controller-limit"); + keyEvent(SDLK_DOWN); + keyEvent(SDLK_RETURN); + assert(screen.controls->popup.open && screen.setup.controllerCount() == 12); + keyEvent(SDLK_ESCAPE); + screen.controls->regions[1].offset = screen.controls->regions[1].maximum; + capture("players-12-scrolled"); + clickControl("colony/11/ai"); + int rosterOffset = screen.controls->regions[1].offset; + keyEvent(SDLK_DOWN); + keyEvent(SDLK_RETURN); + assert(screen.controls->regions[1].offset == rosterOffset); + // Sequential keyboard focus scrolls an off-screen control into view. + screen.controls->focus = "colony/11/team"; + keyEvent(SDLK_TAB); screen.activateGroup(screen.groups[2]); capture("rules-1000"); @@ -796,20 +799,22 @@ int main(int argc, char **argv) return 0; } assert(SDLNet_Init() == 0); - for (int method = 1; method <= 8; ++method) + const auto playableMethods = GeneratorRegistry::builtins().methods(false); + for (int method : playableMethods) { bool generated = false; for (int retry = 0; retry < 5 && !generated; ++retry) { Game game(nullptr); MapGenerator generator; - MapGenerationDescriptor descriptor; - descriptor.setMethodDefaults(static_cast(method)); - generated = generator.generateMap(game, descriptor) && game.teamsCount() == 4; + GenerationRequest request; + request.setMethodDefaults(method); + request.seed = 0x5eed0000u + unsigned(method * 16 + retry); + generated = generator.generateMap(game, request) && game.teamsCount() == 4; } assert(generated); } - std::cout << "PASS all eight playable generator landscapes\n"; + std::cout << "PASS all " << playableMethods.size() << " playable generator landscapes\n"; CustomGameSetupHarness::model(); assert((AINames::selectionOrder() == std::vector{AI::ECONO, AI::NUMBI, AI::WARRUSH, AI::CASTOR, AI::CORTEX, AI::NICOWAR, AI::NONE})); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index 038ff8ccb..c6a92c4f8 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -1,26 +1,37 @@ // SPDX-License-Identifier: GPL-3.0-or-later #define SDL_MAIN_HANDLED +#include "CustomGameSetup.h" +#include "Game.h" +#include "GenerationContext.h" +#include "GenerationService.h" #include "GlobalContainer.h" +#include "IntBuildingType.h" +#include "LegacyGenerationDescriptor.h" +#include "MapGeneratorFrameworkChecks.h" #include "NewMapScreen.h" -#include "CustomGameSetup.h" +#include "Race.h" +#include "StartingPositions.h" +#include "Unit.h" +#include "Utilities.h" #include #include -#include #include +#include +#include #include -#include #include +#include #include GlobalContainer *globalContainer = nullptr; -using D = MapGenerationDescriptor; +using D = GenerationRequest; class MapGeneratorDefaultsTest { public: - static void select(NewMapScreen &s, D::Method method) + static void select(NewMapScreen &s, int method) { - s.methods->setSelectionIndex(method); + s.methods->setSelectionIndex(s.registry.selectionIndex(method)); s.onAction(s.methods, GAGGUI::LIST_ELEMENT_SELECTED, method, 0); } static void sameControls(const D &a, const D &b) @@ -36,15 +47,181 @@ class MapGeneratorDefaultsTest for (auto &w : s.controlWidgets) if (w.number->visible && std::strcmp(w.definition.label, label) == 0) { - w.number->setNth((value - w.definition.minimum) / w.definition.step); + w.number->setNth(w.definition.indexOf(value)); s.onAction(w.number, GAGGUI::NUMBER_ELEMENT_SELECTED, 0, 0); assert(w.definition.get(s.descriptor) == value); return; } assert(false); } + static std::uint64_t fingerprint(const Game &game) + { + std::uint64_t hash = 14695981039346656037ull; + auto add = [&](unsigned v) + { + hash ^= v; + hash *= 1099511628211ull; + }; + for (int y = 0; y < game.map.getH(); ++y) + for (int x = 0; x < game.map.getW(); ++x) + { + add(game.map.getUMTerrain(x, y)); + add(game.map.getTerrain(x, y)); + add(game.map.getResource(x, y).type); + add(game.map.getResource(x, y).amount); + } + for (int i = 0; i < game.teamsCount(); ++i) + { + add(game.teams[i]->startPosX); + add(game.teams[i]->startPosY); + for (int j = 0; j < Unit::MAX_COUNT; ++j) + if (auto *unit = game.teams[i]->myUnits[j]) + { + add(j); + add(unit->posX); + add(unit->posY); + } + } + return hash; + } + static void generationContracts() + { + globalsInit(); + frameworkChecks(); + GenerationService service; + for (int method : GeneratorRegistry::builtins().methods()) + { + D request; + request.setMethodDefaults(method); + request.seed = 22001; + setSyncRandSeed(177); + auto surrounding = randomGenerator; + Game first(nullptr); + auto a = service.generate(first, request); + assert(randomGenerator == surrounding); + auto hash = fingerprint(first); + auto checksum = first.checkSum(nullptr, nullptr, nullptr, true); + D intervening; + intervening.setMethodDefaults(D::eISLANDS); + intervening.seed = 9017; + Game other(nullptr); + service.generate(other, intervening); + Game repeat(nullptr); + auto b = service.generate(repeat, request); + assert(bool(a) == bool(b) && a.stage == b.stage && hash == fingerprint(repeat)); + assert(checksum == repeat.checkSum(nullptr, nullptr, nullptr, true)); + assert(request.seed == 22001 && request.options == DWithDefaults(method).options); + if (a) + { + auto rejected = service.generate(first, request); + assert(rejected.error == GenerationError::NonEmptyTarget); + assert(fingerprint(first) == hash); + } + for (const auto &c : D::controls(method)) + { + D invalid = request; + invalid.options[c.id] = c.maximum + c.step; + Game fresh(nullptr); + auto failure = service.generate(fresh, invalid); + assert(failure.error == GenerationError::InvalidRequest && fresh.teamsCount() == 0); + } + } + // Scoped RNG restoration must also hold when placement fails. + setSyncRandSeed(711); + auto savedFailureRng = randomGenerator; + D invalid; + invalid.method = 99999; + Game fresh(nullptr); + assert(service.generate(fresh, invalid).error == GenerationError::InvalidRequest); + invalid = DWithDefaults(D::eRIVER); + invalid.wDec = 31; + assert(service.generate(fresh, invalid).error == GenerationError::InvalidRequest); + invalid = DWithDefaults(D::eSWAMP); + invalid.options["water"] = 100; + invalid.options["grass"] = 0; + auto failed = service.generate(fresh, invalid); + assert(!failed && failed.error == GenerationError::PlacementFailed && + !failed.stage.empty()); + // Legacy sentinel conversion belongs exclusively to the adapter. + for (auto pair : {std::pair{D::eCRATERLAKES, 30}, std::pair{D::eCONCRETEISLANDS, 6}, + std::pair{D::eISLES, 4}}) + { + MapGenerationDescriptor legacy; + legacy.setMethodDefaults(static_cast(pair.first)); + legacy.riverDiameter = 50; + auto converted = fromLegacyDescriptor(legacy, 42); + const char *key = pair.first == D::eCRATERLAKES ? "lake-size" + : pair.first == D::eISLES ? "bridge-width" + : "channel-width"; + assert(converted.option(key) == pair.second); + } + // New registrations need no ordinal dispatch, UI edits or new descriptor fields. + std::vector definitions; + for (int id : GeneratorRegistry::builtins().methods()) + definitions.push_back(GeneratorRegistry::builtins().at(id)); + definitions.push_back({"test-plateau", + 101, + "uniform terrain", + 1, + false, + {{"test-elevation", "Smoothing", 2, 10, 2, 6}, + {"test-cell", + "Island size", + 4, + 16, + 1, + 8, + ControlGroup::Layout, + false, + false, + {4, 8, 16}}, + {"test-gap", "Channel width", 1, 5, 2, 3, ControlGroup::Layout}}, + [](Game &game, GenerationContext &context) + { + const int elevation = context.request.option("test-elevation"); + assert(elevation == 8); + game.map.makeHomogenMap(GRASS); + for (int team = 0; team < context.request.nbTeams; ++team) + { + context.bootX[team] = elevation + (team % 2) * 40; + context.bootY[team] = elevation + (team / 2) * 40; + } + return MapGeneration::placeStarts(game, context); + }}); + GeneratorRegistry registry(std::move(definitions)); + NewMapScreen screen(registry); + select(screen, 101); + assert(screen.descriptor.option("test-elevation") == 6); + edit(screen, "Island size", 16); + edit(screen, "Channel width", 5); + select(screen, D::eRIVER); + select(screen, 101); + assert(screen.descriptor.option("test-cell") == 16 && + screen.descriptor.option("test-gap") == 5); + edit(screen, "Smoothing", 8); + assert(registry.selectionIndex(101) == 13); + const auto playable = registry.methods(false); + assert(std::find(playable.begin(), playable.end(), 101) != playable.end()); + Game generated(nullptr); + assert(GenerationService(registry).generate(generated, screen.descriptor)); + puts("PASS registration extension, explicit seeds, interleaved repeatability, RNG " + "isolation, errors and legacy sentinels"); + } + static D DWithDefaults(int method) + { + D r; + r.setMethodDefaults(method); + return r; + } + static void globalsInit() + { + globalContainer->buildingsTypes.init(); + IntBuildingType::init(); + Race::loadDefault(); + } static void run(const char *output) { + generationContracts(); NewMapScreen s; s.gfx = globalContainer->gfx; s.dispatchInit(); @@ -52,7 +229,7 @@ class MapGeneratorDefaultsTest D river; river.setMethodDefaults(D::eRIVER); sameControls(lobby.generator, river); - for (int m = 0; m <= D::eOLDISLANDS; ++m) + for (int m : GeneratorRegistry::builtins().methods()) { auto method = static_cast(m); D expected; @@ -68,20 +245,24 @@ class MapGeneratorDefaultsTest { assert(labels.insert(c.label).second); assert(c.step > 0 && c.maximum >= c.minimum); - assert((c.maximum - c.minimum) % c.step == 0); + assert(!c.allowedValues.empty() || (c.maximum - c.minimum) % c.step == 0); assert(c.get(expected) == c.defaultValue); assert(c.normalize(c.defaultValue) == c.defaultValue); assert(c.normalize(c.minimum - 100) == c.minimum); assert(c.normalize(c.maximum + 100) == c.maximum); // The actual editor must expose every value, including 75 grass and 65 size. - for (int v = c.minimum; v <= c.maximum; v += c.step) + for (int v : c.values()) edit(s, c.label, v); edit(s, c.label, c.defaultValue); } sameControls(s.descriptor, expected); - D decoded; - assert(decoded.setData(s.descriptor.getData(), s.descriptor.getDataLength())); - sameControls(decoded, expected); + if (m <= D::eOLDISLANDS) + { + auto encoded = toLegacyDescriptor(s.descriptor); + MapGenerationDescriptor decoded; + assert(decoded.setData(encoded.getData(), encoded.getDataLength())); + sameControls(fromLegacyDescriptor(decoded, 0), expected); + } if (output && m >= 4 && m <= 8) { s.gfx->drawFilledRect(0, 0, 640, 480, GAGCore::Color(34, 55, 42)); @@ -97,7 +278,7 @@ class MapGeneratorDefaultsTest edit(s, "Width", 8); edit(s, "Colonies", 6); lobby.generatorHistory.select(lobby.generator, D::eRIVER); - lobby.generator.waterRatio = 37; + lobby.generator.options["water"] = 37; lobby.generator.wDec = 8; lobby.generator.nbTeams = 6; for (auto method : {D::eISLANDS, D::eISLES, D::eRIVER}) @@ -107,12 +288,12 @@ class MapGeneratorDefaultsTest sameControls(s.descriptor, lobby.generator); assert(s.descriptor.wDec == 8 && s.descriptor.nbTeams == 6); } - assert(s.descriptor.waterRatio == 37); + assert(s.descriptor.options["water"] == 37); lobby.random = true; - lobby.generator.waterRatio = lobby.generator.grassRatio = lobby.generator.sandRatio = - lobby.generator.desertRatio = 0; + lobby.generator.options["water"] = lobby.generator.options["grass"] = + lobby.generator.options["sand"] = lobby.generator.options["desert"] = 0; assert(!lobby.validation().empty()); - lobby.generator.grassRatio = 75; + lobby.generator.options["grass"] = 75; assert(lobby.validation().empty()); puts("PASS shared presets, ranges and steps; all editor values; lobby/editor mode memory; " "serialization and validation"); diff --git a/test/MapGeneratorFrameworkChecks.h b/test/MapGeneratorFrameworkChecks.h new file mode 100644 index 000000000..cad4f85e7 --- /dev/null +++ b/test/MapGeneratorFrameworkChecks.h @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include "Game.h" +#include "GenerationService.h" +#include "GenerationContext.h" +#include "Utilities.h" +#include +#include +#include +#include "GenerationValidation.h" +#include "Geometry.h" +#include "Settlements.h" +#include "Topology.h" +#include + +static void frameworkChecks() +{ + using namespace MapGeneration; + GeneratorControl cells{"cell", "Cell", 4, 16, 1, 8, ControlGroup::Layout, + false, false, {4, 8, 16}}; + assert(cells.normalize(6) == 8 && cells.normalize(11) == 8 && cells.normalize(12) == 16); + assert(cells.normalize(-100) == 4 && cells.normalize(100) == 16); + for (int v : cells.values()) + assert(cells.valueAt(cells.indexOf(v)) == v); + GenerationRequest request; + request.seed = 817; + GenerationContext context(request), repeat(request); + ShapeTransform transform({12, 23}, 0.72, 1.3); + const ShapePoint point{3, -7}; + const auto back = transform.toShape(transform.toMap(point)); + assert(std::abs(back.x - point.x) < 1e-10 && std::abs(back.y - point.y) < 1e-10); + RadialShape shape(12, .7, context, "coast"), same(12, .7, repeat, "coast"); + for (int i = 0; i < 720; ++i) + { + double r = shape.radiusAt(i * .01); + assert(r > 0 && r <= shape.maximumRadius() && r == same.radiusAt(i * .01)); + } + RadialShape circle(3, 0, context, "circle"); + std::vector wrapped(64), clipped(64); + stampShape(wrapped, 8, 8, 7, ShapeTransform({0, 0}, 0), circle, true); + stampShape(clipped, 8, 8, 7, ShapeTransform({0, 0}, 0), circle, false); + assert(wrapped[7] == 7 && clipped[7] == 0); + std::vector mask{1, 0, 1, 0, 0, 0, 0, 1, 0}; + auto cardinal = connectedRegions(mask, 3, 3, false); + auto torus = connectedRegions(mask, 3, 3, true); + assert(cardinal[0] != cardinal[2] && torus[0] == torus[2] && cardinal[1] == -1); + mask = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + assert(connectedRegions(mask, 3, 3, false)[0] != connectedRegions(mask, 3, 3, false)[4]); + assert(connectedRegions(mask, 3, 3, false, GridNeighbors::Eight)[0] == + connectedRegions(mask, 3, 3, false, GridNeighbors::Eight)[8]); + auto graph = regionAdjacency({10, 10, 30, 10, 99, 30}, 3, 2, {10, 30, 99, 400}, false); + auto distances = graphDistances(graph, {0}); + assert(distances == std::vector({0, 1, 1, -1})); + assert(graphDistances({}, {}).empty()); + bool rejected = false; + try + { + graphDistances({{5}}, {0}); + } + catch (const std::invalid_argument &) + { + rejected = true; + } + assert(rejected); + + // A synthetic region, not a playable generator, exercises exact settlement counts + // and callbacks without weakening the production structural validator. + GeneratorDefinition definition{ + "test-home", + 305, + "uniform terrain", + 1, + false, + {cells, {"room", "Room", 4, 8, 1, 6}}, + [](Game &game, GenerationContext &ctx) + { + game.map.makeHomogenMap(GRASS); + for (int i = 0; i < ctx.request.nbTeams; ++i) + game.addTeam(); + std::vector home(size_t(game.map.getW()) * game.map.getH(), 0); + int size = ctx.request.option("room"); + for (int y = 10; y < 10 + size; ++y) + for (int x = 10; x < 10 + size; ++x) + home[y * game.map.getW() + x] = 1; + return placeSettlement(game, ctx, 0, home, {11, 11}); + }, + true, + [](const GenerationRequest &r) -> std::string + { return r.option("room") > r.option("cell") ? "Room must fit inside cell" : ""; }, + [](const Game &, const GenerationContext &ctx) -> std::string + { return ctx.request.seed == 999 ? "Synthetic topology failure" : ""; }}; + GeneratorRegistry registry({definition}); + GenerationService service(registry); + request.setMethodDefaults(305, registry); + request.nbTeams = 1; + auto surrounding = randomGenerator; + { + Game game(nullptr); + assert(service.generate(game, request)); + assert(game.teams[0]->startPosX >= 10 && game.teams[0]->startPosX <= 12); + } + { + Game game(nullptr); + request.options["room"] = 4; + auto failure = service.generate(game, request); + assert(failure.error == GenerationError::PlacementFailed && failure.stage == "settlement"); + assert(failure.detail.find("worker tiles") != std::string::npos); + } + { + Game game(nullptr); + request.options["room"] = 6; + request.options["cell"] = 4; + auto failure = service.generate(game, request); + assert(failure.error == GenerationError::InvalidRequest && game.teamsCount() == 0); + } + { + Game game(nullptr); + request.options["cell"] = 8; + request.seed = 999; + auto failure = service.generate(game, request); + assert(failure.error == GenerationError::InvalidWorld && + failure.stage == "generator validation"); + } + assert(randomGenerator == surrounding); + // Invalid weighted inputs fail explicitly; weighted metadata follows the shuffle. + Game game(nullptr); + game.map.setSize(6, 6); + std::vector grid(64 * 64, 0); + std::vector points(3, {0, 0}); + std::vector weights{1, 2, 2}; + GenerationContext dispersion(request); + assert(splitUpPoints(game.map, dispersion, grid, 0, points, weights, PointSearch::WholeRegion) > + 0); + assert(std::count(weights.begin(), weights.end(), 1) == 1); + for (size_t i = 0; i < points.size(); ++i) + for (size_t j = i + 1; j < points.size(); ++j) + assert(points[i].x != points[j].x || points[i].y != points[j].y); + // A completed whole-region pass must remain a best response with the final + // point/weight pairing. This detects shuffling coordinates without their weights. + for (size_t i = 0; i < points.size(); ++i) + { + int score = 2147483647; + for (size_t j = 0; j < points.size(); ++j) if (i != j) + score = std::min(score, game.map.warpDistSquare(points[i].x, points[i].y, points[j].x, points[j].y) * weights[j]); + for (int y = 0; y < 64; ++y) for (int x = 0; x < 64; ++x) + { + int candidate = 2147483647; + for (size_t j = 0; j < points.size(); ++j) if (i != j) + candidate = std::min(candidate, game.map.warpDistSquare(x, y, points[j].x, points[j].y) * weights[j]); + assert(candidate <= score); + } + } + weights.clear(); + rejected = false; + try + { + splitUpPoints(game.map, dispersion, grid, 0, points, weights); + } + catch (const GenerationFailure &) + { + rejected = true; + } + assert(rejected); + puts("PASS discrete domains, shape bounds, topology, home footprints, exact workers and custom " + "validation"); +} diff --git a/test/MapGeneratorStudy.cpp b/test/MapGeneratorStudy.cpp index 6c643032c..48ecc9ac6 100644 --- a/test/MapGeneratorStudy.cpp +++ b/test/MapGeneratorStudy.cpp @@ -1,46 +1,42 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Dedicated analysis executable; invokes the production generators. #define SDL_MAIN_HANDLED -#include "GlobalContainer.h" #include "Game.h" -#include "MapGenerator.h" +#include "GenerationService.h" +#include "GeneratorRegistry.h" +#include "GlobalContainer.h" #include "IntBuildingType.h" +#include "MapGenerator.h" #include "Race.h" -#include "PerlinNoise.h" +#include "Unit.h" #include "Utilities.h" -#include +#include +#include +#include #include #include -#include -#include -#include +#include #include -#include #include -#include -#include "Unit.h" +#include +#include GlobalContainer *globalContainer = nullptr; -static time_t studyTime = 1700000000; -// Production generation reseeds from time() internally. Override time only in -// this dedicated executable so each sample has a reproducible synthetic clock. -extern "C" time_t time(time_t *out) -{ - if (out) - *out = studyTime; - return studyTime; -} - int main(int argc, char **argv) { if (argc == 2 && std::string(argv[1]) == "--catalog") { - using D = MapGenerationDescriptor; + using D = GenerationRequest; std::puts("["); - for (int m = 0; m <= D::eOLDISLANDS; ++m) + const auto methods = GeneratorRegistry::builtins().methods(); + for (int m : methods) { - auto method = static_cast(m); - std::printf("{\"method\":%d,\"nameKey\":\"%s\",\"controls\":[", m, + const int method = m; + std::printf("{\"method\":%d,\"id\":\"%s\",\"revision\":%u,\"editorOnly\":%s," + "\"nameKey\":\"%s\",\"controls\":[", + m, GeneratorRegistry::builtins().at(m).id, + GeneratorRegistry::builtins().at(m).revision, + GeneratorRegistry::builtins().at(m).editorOnly ? "true" : "false", D::methodName(method)); auto controls = D::sharedControls(); const auto &specific = D::controls(method); @@ -48,12 +44,17 @@ int main(int argc, char **argv) for (size_t i = 0; i < controls.size(); ++i) { const auto &c = controls[i]; - std::printf("%s{\"label\":\"%s\",\"min\":%d,\"max\":%d,\"step\":%d,\"default\":%d," - "\"group\":%d,\"powerOfTwo\":%s}", - i ? "," : "", c.label, c.minimum, c.maximum, c.step, c.defaultValue, - int(c.group), c.powerOfTwo ? "true" : "false"); + std::printf("%s{\"id\":\"%s\",\"label\":\"%s\",\"min\":%d,\"max\":%d,\"step\":%d," + "\"default\":%d," + "\"group\":%d,\"powerOfTwo\":%s,\"values\":[", + i ? "," : "", c.id.c_str(), c.label, c.minimum, c.maximum, c.step, + c.defaultValue, int(c.group), c.powerOfTwo ? "true" : "false"); + const auto domain = c.values(); + for (size_t j = 0; j < domain.size(); ++j) + std::printf("%s%d", j ? "," : "", domain[j]); + std::printf("]}"); } - std::printf("]}%s\n", m == D::eOLDISLANDS ? "" : ","); + std::printf("]}%s\n", m == methods.back() ? "" : ","); } std::puts("]"); return 0; @@ -62,7 +63,7 @@ int main(int argc, char **argv) return 2; // method, seed, disposable profile, [displayed] const int method = std::atoi(argv[1]); const unsigned seed = std::strtoul(argv[2], nullptr, 10); - studyTime += seed; + SDL_SetMainReady(); GlobalContainer globals(argv[3]); globalContainer = &globals; @@ -72,68 +73,63 @@ int main(int argc, char **argv) IntBuildingType::init(); Race::loadDefault(); Game game(nullptr); - MapGenerationDescriptor descriptor; - descriptor.method = static_cast(method); - bool terrainOnly = false; + GenerationRequest descriptor; + if (!GeneratorRegistry::builtins().find(method)) + return 2; + descriptor.setMethodDefaults(method); + descriptor.seed = seed; bool tuning = false; std::string dump; - std::map controls = {{"water", &descriptor.waterRatio}, - {"sand", &descriptor.sandRatio}, - {"grass", &descriptor.grassRatio}, - {"desert", &descriptor.desertRatio}, - {"smooth", &descriptor.smooth}, - {"river", &descriptor.riverDiameter}, - {"craters", &descriptor.craterDensity}, - {"extra", &descriptor.extraIslands}, - {"island", &descriptor.oldIslandSize}, - {"beach", &descriptor.oldBeach}, - {"fruit", &descriptor.fruitRatio}, - {"w", &descriptor.wDec}, - {"h", &descriptor.hDec}, - {"teams", &descriptor.nbTeams}}; + std::map aliases = {{"smooth", "smoothing"}, + {"craters", "lake-density"}, + {"extra", "extra-islands"}, + {"island", "island-size"}, + {"beach", "beach-size"}, + {"w", "width"}, + {"h", "height"}}; + for (const auto &c : GenerationRequest::controls(method)) + if (c.id == "lake-size" || c.id == "channel-width" || c.id == "bridge-width" || + c.id == "river-width") + aliases["river"] = c.id; for (int i = 4; i < argc; ++i) { - if (std::string(argv[i]) == "preset") - descriptor.setMethodDefaults(static_cast(method)); - if (std::string(argv[i]) == "displayed") - { - descriptor.sandRatio = 0; - descriptor.desertRatio = 0; - } - if (std::string(argv[i]) == "terrain-only") - terrainOnly = true; - if (std::string(argv[i]) == "tuning") - tuning = true; std::string arg = argv[i]; - auto eq = arg.find('='); - if (eq != std::string::npos) + if (arg == "tuning") + tuning = true; + else if (arg == "preset") + { + } // All new requests start at registered defaults. + else { - auto key = arg.substr(0, eq); - if (key == "dump") + auto eq = arg.find('='); + if (eq == std::string::npos) + return 2; + std::string id = arg.substr(0, eq); + if (id == "dump") + { dump = arg.substr(eq + 1); - else if (controls.count(key)) - *controls[key] = std::stoi(arg.substr(eq + 1)); + continue; + } + if (aliases.count(id)) + id = aliases[id]; + int value = std::stoi(arg.substr(eq + 1)); + if (id == "width") + descriptor.wDec = value; + else if (id == "height") + descriptor.hDec = value; + else if (id == "teams") + descriptor.nbTeams = value; + else if (id == "workers") + descriptor.nbWorkers = value; else - return 2; + descriptor.options[id] = value; // Service validates; never silently clamp studies. } } - PerlinNoise::reseed(seed); - std::srand(seed); - setSyncRandSeed(seed); const auto start = std::chrono::steady_clock::now(); - MapGenerator generator; - bool success; - if (terrainOnly && method == MapGenerationDescriptor::eOLDISLANDS) - { - game.map.setSize(descriptor.wDec, descriptor.hDec); - game.map.setGame(&game); - setSyncRandSeed(static_cast(studyTime)); - success = game.map.oldMakeIslandsMap(descriptor); - } - else - { - success = generator.generateMap(game, descriptor, static_cast(studyTime)); - } + const auto result = GenerationService().generate(game, descriptor); + const bool success = bool(result); + if (!success) + std::fprintf(stderr, "%s\n", result.diagnostic().c_str()); const double seconds = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); auto &map = game.map; diff --git a/test/PerlinNoiseTest.cpp b/test/PerlinNoiseTest.cpp index 2c404bd9d..1cbe5f310 100644 --- a/test/PerlinNoiseTest.cpp +++ b/test/PerlinNoiseTest.cpp @@ -2,17 +2,17 @@ // Copyright (C) 2010 Leo Wandersleb #include "PerlinNoiseTest.h" -#include "../src/PerlinNoise.h" +#include "../src/map/generator/shared/Noise.h" CPPUNIT_TEST_SUITE_REGISTRATION( PerlinNoiseTest ); const static int SEED = 12; -PerlinNoise * perlinNoise; +GenerationNoise * perlinNoise; float * position; //initialize perlinNoise with always the same seed and setUp a positon-vector. void PerlinNoiseTest::setUp() { - perlinNoise = new PerlinNoise(SEED); + perlinNoise = new GenerationNoise(SEED); position = new float[3]; position[0] = .11111f; position[1] = .21111f; @@ -38,7 +38,7 @@ void PerlinNoiseTest::testNotZeroOne() void PerlinNoiseTest::testReseed() { float a = perlinNoise->Noise1d(position); - perlinNoise->reseed(); + perlinNoise->reseed(SEED + 1); float b = perlinNoise->Noise1d(position); CPPUNIT_ASSERT(a != b); } diff --git a/test/SConstruct b/test/SConstruct index 1f1abb920..c2ad41b84 100644 --- a/test/SConstruct +++ b/test/SConstruct @@ -23,6 +23,8 @@ common_cpppath = ['..', '../src', '../src/ai', '../src/net', '../src/net/irc', '../src/net/message', '../src/yog', '../src/sgsl', '../src/map', '../src/map/edit', '../src/map/generator', + '../src/map/generator/core', '../src/map/generator/shared', + '../src/map/generator/generators', '../src/map/generator/compatibility', '../src/map/gradient', '../src/map/io', '../src/map/pathfind', '../src/render', '../libgag/include', '../libusl/src'] @@ -64,7 +66,7 @@ sources = Split(""" TestsRunner.cpp PerlinNoiseTest.cpp -../src/PerlinNoise.cpp +../src/map/generator/shared/Noise.cpp HelloWorldTest.cpp diff --git a/tools/map_generator_study.py b/tools/map_generator_study.py index 33f7951e5..61a1ce505 100644 --- a/tools/map_generator_study.py +++ b/tools/map_generator_study.py @@ -4,7 +4,7 @@ from pathlib import Path ROOT=Path(__file__).resolve().parents[1] OUT=ROOT/'artifacts/map-generator-validation' -BASE={'water':50,'sand':0,'grass':50,'desert':0,'smooth':4,'river':50,'craters':50,'extra':0,'island':50,'beach':1,'fruit':4,'w':7,'h':7,'teams':4} +BINARY=ROOT/'build/src/MapGeneratorStudy' FIELDS=['method','seed','success','tiles','grass_tiles','sand_tiles','water_tiles','shore','free','fit4','um_grass','um_sand','um_water','seconds','hash'] EXTRA=['min_local_fit4','worst_wheat_distance','worst_wood_distance','viable_teams','wheat_tiles','wood_tiles','stone_tiles','algae_tiles'] PROFILES=set() @@ -13,9 +13,9 @@ def sample(task): config,seed=task p=f'glob2-tuning-{os.getpid()}-{threading.get_ident()}';PROFILES.add(p) - params=config.get('params',{}) if config.get('preset') else BASE|config.get('params',{}) - command=[str(ROOT/'build/src/MapGeneratorStudy'),str(config['method']),str(seed),p,'tuning'] - if config.get('preset'): command += ['preset'] + params=config.get('params',{}) + command=[str(BINARY),str(config['method']),str(seed),p,'tuning'] + command += ['preset'] command += [f'{k}={v}' for k,v in params.items()] if 'dump' in config: command += ['dump='+config['dump']] start=time.monotonic() @@ -26,6 +26,8 @@ def sample(task): b=[x for x in lines if x.startswith('TUNE,')] if result.returncode==0 and len(a)==len(b)==1: row=dict(zip(FIELDS,a[0].split(',')[1:]))|dict(zip(EXTRA,b[0].split(',')[1:])) + if row['success'] != '1': + (OUT/f'failure-{config["id"]}-{seed}.txt').write_text(result.stderr) return {'config':config['id'],**row,'status':'ok' if row['success']=='1' else 'failed'} status=f'exit_{result.returncode}' detail=result.stdout[-1500:]+result.stderr[-1500:] @@ -36,12 +38,14 @@ def sample(task): def run(configs,count,start,label,workers=8): # Fail before a long run if any production RNG stream escaped the seed controls. - repeated=[] - for config in configs: - for seed in sorted({start,start+count//2,start+count-1}): - a,b=sample((config,seed)),sample((config,seed)) - assert {k:v for k,v in a.items() if k!='seconds'} == {k:v for k,v in b.items() if k!='seconds'}, (config['id'],seed,'non-reproducible generation') - repeated.extend([a,b]) + cases=[(config,seed) for config in configs for seed in sorted({start,start+count//2,start+count-1})] + def repeat_case(case): + config,seed=case + a,b=sample(case),sample(case) + assert {k:v for k,v in a.items() if k!='seconds'} == {k:v for k,v in b.items() if k!='seconds'}, (config['id'],seed,'non-reproducible generation') + return a,b + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: + repeated=[row for pair in pool.map(repeat_case,cases) for row in pair] with (OUT/(label+'-reproducibility.csv')).open('w') as f: writer=csv.DictWriter(f,['config']+FIELDS+EXTRA+['status']);writer.writeheader();writer.writerows(repeated) print(f'PASS reproducibility: {len(repeated)//2} independently repeated seed/configuration pairs',flush=True) @@ -56,6 +60,11 @@ def run(configs,count,start,label,workers=8): print(f'DONE {label}: {len(tasks)} attempts, {time.monotonic()-begun:.1f}s',flush=True) if __name__=='__main__': - p=argparse.ArgumentParser();p.add_argument('--configs',required=True);p.add_argument('--output',default=str(OUT));p.add_argument('--count',type=int,default=32);p.add_argument('--start',type=int,default=10001);p.add_argument('--label',default='sweep');p.add_argument('--workers',type=int,default=8) - a=p.parse_args();OUT=Path(a.output);OUT.mkdir(parents=True,exist_ok=True);c=json.loads(Path(a.configs).read_text()) + p=argparse.ArgumentParser();p.add_argument('--configs');p.add_argument('--binary',default=str(BINARY));p.add_argument('--output',default=str(OUT));p.add_argument('--count',type=int,default=32);p.add_argument('--start',type=int,default=10001);p.add_argument('--label',default='sweep');p.add_argument('--workers',type=int,default=8) + a=p.parse_args();OUT=Path(a.output);OUT.mkdir(parents=True,exist_ok=True);BINARY=Path(a.binary).resolve() + if a.configs: + c=json.loads(Path(a.configs).read_text()) + else: + catalog=json.loads(subprocess.check_output([BINARY,'--catalog'],text=True)) + c=[{'id':'modular-'+str(d['method']),'method':d['method'],'preset':True} for d in catalog if not d.get('editorOnly',d['method']==0)] run(c,a.count,a.start,a.label,a.workers) diff --git a/tools/plot_map_generator_refactor.py b/tools/plot_map_generator_refactor.py new file mode 100644 index 000000000..c235ec809 --- /dev/null +++ b/tools/plot_map_generator_refactor.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Compare a refactor study to the frozen #238 cohort and render review maps. + +Requires numpy and matplotlib. The compiled catalog supplies names and generator +membership. Input CSVs are produced by map_generator_study.py. +""" +import argparse +import csv +import gzip +import hashlib +import json +import shutil +import subprocess +from pathlib import Path + +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap +from matplotlib.patches import Patch +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +COLORS = ['#96bb70', '#ddc38e', '#5c9cbc', '#b8c4a3', '#e4c34b', '#285838', '#7f8490', '#292936'] +LABELS = ['Grass', 'Sand', 'Water', 'Shore', 'Wheat', 'Wood', 'Stone', 'Buildings'] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('csv', type=Path) + parser.add_argument('--baseline', type=Path, default=ROOT/'docs/map-generators/study/summary.json') + parser.add_argument('--output', type=Path, default=ROOT/'docs/map-generators/refactor') + args = parser.parse_args() + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + binary = ROOT/'build/src/MapGeneratorStudy' + catalog = json.loads(subprocess.check_output([binary, '--catalog'], text=True)) + methods = [c for c in catalog if not c.get('editorOnly', c['method'] == 0)] + frozen_catalog = args.baseline.parent/'catalog.json' + if frozen_catalog.exists(): + frozen = {d['method']: d for d in json.loads(frozen_catalog.read_text())} + keys = ('label', 'min', 'max', 'step', 'default', 'group', 'powerOfTwo') + for definition in catalog: + if definition['method'] in frozen: + assert [[c[k] for k in keys] for c in definition['controls']] == [[c[k] for k in keys] for c in frozen[definition['method']]['controls']], f"Controls changed for {definition['id']}" + lines = (ROOT/'data/texts.en.txt').read_text().splitlines() + strings = {line[1:-1]: lines[i+1] for i, line in enumerate(lines[:-1]) if line.startswith('[') and line.endswith(']')} + rows = list(csv.DictReader(args.csv.open())) + baseline = {r['method']: r for r in json.loads(args.baseline.read_text()) if r['config'].startswith('tuned-')} + summary = [] + for definition in methods: + method = definition['method'] + cohort = [r for r in rows if int(r['method']) == method] + if not cohort: + continue + ok = [r for r in cohort if r['success'] == '1'] + assert ok, f'No successful maps for {definition["id"]}' + record = dict(method=method, id=definition['id'], revision=definition['revision'], + name=strings[definition['nameKey']], attempts=len(cohort), successes=len(ok), + all_teams_proxy=sum(int(r['viable_teams']) == next(c['default'] for c in definition['controls'] if c['id']=='teams') for r in ok)) + for key in ('grass_tiles', 'free', 'fit4', 'water_tiles', 'sand_tiles', 'shore'): + values = np.array([100*int(r[key])/int(r['tiles']) for r in ok]) + record[key] = float(values.mean()) + record[key+'_p5'], record[key+'_p95'] = map(float, np.percentile(values, [5, 95])) + assert all(int(r['fit4']) <= int(r['free']) <= int(r['grass_tiles']) for r in ok) + assert all(sum(int(r[k]) for k in ('grass_tiles', 'sand_tiles', 'water_tiles', 'shore')) == int(r['tiles']) for r in ok) + record['review_flags'] = [] + if method in baseline: + before = baseline[method] + record['free_delta_pp'] = record['free']-before['free'] + record['failure_delta_pp'] = 100*(1-record['successes']/record['attempts'])-100*(1-before['successes']/before['attempts']) + record['proxy_delta_pp'] = 100*record['all_teams_proxy']/record['attempts']-100*before['all_teams_proxy']/before['attempts'] + for flag, triggered in [('buildable area', abs(record['free_delta_pp']) > 2), + ('generation failures', record['failure_delta_pp'] > 1+1e-9), + ('start proxy', record['proxy_delta_pp'] < -3-1e-9)]: + if triggered: + record['review_flags'].append(flag) + summary.append(record) + (out/'catalog.json').write_text(json.dumps(catalog, indent=2)+'\n') + (out/'summary.json').write_text(json.dumps(summary, indent=2)+'\n') + with args.csv.open('rb') as src, (out/'validation.csv.gz').open('wb') as dest: + with gzip.GzipFile(fileobj=dest, mode='wb', mtime=0, filename='') as zipped: + shutil.copyfileobj(src, zipped) + fig, axes = plt.subplots(1, 2, figsize=(13, 6), sharey=True) + for ax, key, title in zip(axes, ('grass_tiles', 'free'), ('Pure grass', 'Immediately buildable tiles')): + for offset, color, label, values in [(-.18, '#a6b4c4', 'PR #238', [baseline.get(r['method'], {}).get(key, float('nan')) for r in summary]), + (.18, '#38835c', 'Modular generators', [r[key] for r in summary])]: + bars = ax.barh(np.arange(len(summary))+offset, values, .34, color=color, label=label) + ax.bar_label(bars, labels=[f'{v:.1f}%' if np.isfinite(v) else '' for v in values], padding=4, fontsize=8) + ax.set_title(title) + ax.set_xlim(0, 76) + ax.set_xlabel('% of tiles; successful maps') + ax.spines[['top', 'right']].set_visible(False) + ax.grid(axis='x', alpha=.15) + ax.set_axisbelow(True) + axes[0].set_yticks(range(len(summary)), [r['name'] for r in summary]) + axes[0].invert_yaxis() + axes[1].legend(loc='lower right') + fig.suptitle('Map generator refactor — default map character and building space') + fig.tight_layout() + for ext in ('png', 'svg'): + fig.savefig(out/f'coverage.{ext}', dpi=160) + plt.close(fig) + # Three preselected seeds plus the weakest successful map by the start proxy. + preview_records = [] + profile = 'glob2-refactor-gallery' + try: + for record in summary: + method = record['method'] + ok = [r for r in rows if int(r['method']) == method and r['success'] == '1'] + worst = min(ok, key=lambda r: (int(r['viable_teams']), int(r['min_local_fit4']), int(r['free']))) + seeds = [22001, 22002, 22003, int(worst['seed'])] + fig, axes = plt.subplots(1, 4, figsize=(12, 3.7)) + for index, (seed, ax) in enumerate(zip(seeds, axes)): + dump = out/f'.preview-{method}-{seed}.txt' + result = subprocess.run([binary, str(method), str(seed), profile, 'preset', 'tuning', 'dump='+str(dump)], + cwd=ROOT, text=True, capture_output=True, timeout=12, check=True) + study = next(line for line in result.stdout.splitlines() if line.startswith('STUDY,')).split(',') + success = study[3] == '1' + grid = np.loadtxt(dump, skiprows=1, dtype=int) + dump.unlink() + ax.imshow(grid, cmap=ListedColormap(COLORS), vmin=0, vmax=7, interpolation='nearest') + ax.set_title(f'Seed {seed}'+('\nWeak start example' if index == 3 else '')+(' · FAILED' if not success else ''), fontsize=10) + ax.axis('off') + preview_records.append(dict(method=method, seed=seed, success=success, hash=study[-1], role='weak-start' if index == 3 else 'fixed')) + fig.suptitle(record['name'], fontweight='bold') + fig.legend(handles=[Patch(color=c, label=l) for c, l in zip(COLORS, LABELS)], loc='lower center', ncol=8, frameon=False, fontsize=8) + fig.tight_layout(rect=(0, .06, 1, .94)) + fig.savefig(out/f'generator-{method}.png', dpi=145) + plt.close(fig) + finally: + shutil.rmtree(Path.home()/('.'+profile), ignore_errors=True) + (out/'previews.json').write_text(json.dumps(preview_records, indent=2)+'\n') + paths = list((ROOT/'src/map/generator').rglob('*.h')) + list((ROOT/'src/map/generator').rglob('*.cpp')) + paths += [ROOT/'src/map/MapTerrain.cpp', ROOT/'test/MapGeneratorStudy.cpp'] + hashes = {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(paths)} + (out/'source-hashes.json').write_text(json.dumps(hashes, indent=2)+'\n') + report = ['# Modular generator validation', '', + '1,000 seeds per playable generator (20001–21000), 128×128, four colonies and four workers. Failed attempts are retained; tile percentages exclude failures. The baseline is the frozen tuned cohort from PR #238.', '', + '![Coverage](coverage.png)', '', + '| Generator | Free tiles: #238 → refactor | Failures / 1,000 | All-team start proxy / 1,000 | Review flags |', + '|---|---:|---:|---:|---|'] + for r in summary: + b = baseline.get(r['method']) + before_free = f"{b['free']:.2f}%" if b else '—' + before_failed = str(b['attempts']-b['successes']) if b else '—' + before_proxy = str(b['all_teams_proxy']) if b else '—' + report.append(f"| {r['name']} | {before_free} → {r['free']:.2f}% | {before_failed} → {r['attempts']-r['successes']} | {before_proxy} → {r['all_teams_proxy']} | {', '.join(r['review_flags']) or ('None' if b else 'New generator; no baseline')} |") + report += ['', 'Review thresholds: absolute mean free-area shift >2 percentage points; failure-rate increase >1 point; all-team proxy decrease >3 points. Flags require investigation, not automatic retuning. The start proxy measures nearby building anchors and reachable wheat/wood; it is not a fairness guarantee.', '', + '[Investigation and visual assessment](REVIEW.md) · [Raw data](validation.csv.gz) · [Catalog](catalog.json)', '', '## Map previews', '', + 'Each row shows three fixed seeds and a weak successful start. Failed fixed seeds, if any, are labeled rather than replaced.'] + for r in summary: + report += ['', f"### {r['name']}", '', f"![{r['name']}](generator-{r['method']}.png)"] + (out/'RESULTS.md').write_text('\n'.join(report)+'\n') + print(json.dumps({r['name']: r['review_flags'] for r in summary}, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/tools/plot_map_generator_study.py b/tools/plot_map_generator_study.py index edb513986..063b31f23 100644 --- a/tools/plot_map_generator_study.py +++ b/tools/plot_map_generator_study.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 """Publish a reproducible coverage report using the compiled generator catalog. +Historical PR #238 report builder; run from that revision to reproduce its maps. +For the modular framework use plot_map_generator_refactor.py. + Usage: python3 tools/plot_map_generator_study.py path/to/validation.csv Requires numpy and matplotlib. Run after building map-generator-study. """ From 9befd6f5fc77932685867d4d3db380487ea33b71 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Wed, 9 Sep 2026 23:04:23 -0400 Subject: [PATCH 002/136] Sort map generators by quality --- src/map/generator/core/GeneratorRegistry.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 557db401f..a21f84dc8 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -68,12 +68,14 @@ int GeneratorRegistry::selectionIndex(int id, bool editor) const { return it == ids.end() ? -1 : int(it - ids.begin()); } const GeneratorRegistry &GeneratorRegistry::builtins() { + // This is also the product-facing catalog order. Keep the most polished and + // distinctive playable maps first; numeric IDs remain stable compatibility + // identifiers and do not determine presentation order. static const GeneratorRegistry registry( - {uniformDefinition(), swampDefinition(), riverDefinition(), - islandsDefinition(), craterLakesDefinition(), - concreteIslandsDefinition(), islesDefinition(), - shatteredCoastDefinition(), ruggedArchipelagoDefinition(), - contestedCommonsDefinition(), latticeDefinition(), mazeDefinition(), - fjordContinentDefinition()}); + {fjordContinentDefinition(), mazeDefinition(), + contestedCommonsDefinition(), latticeDefinition(), islesDefinition(), + ruggedArchipelagoDefinition(), concreteIslandsDefinition(), + craterLakesDefinition(), islandsDefinition(), swampDefinition(), + riverDefinition(), shatteredCoastDefinition(), uniformDefinition()}); return registry; } From a099035df2808ab3bcef5f3c9cc65783002b4b6e Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Wed, 9 Sep 2026 23:05:56 -0400 Subject: [PATCH 003/136] Refine map generator quality order --- src/map/generator/core/GeneratorRegistry.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index a21f84dc8..5b22379c6 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -72,10 +72,11 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { // distinctive playable maps first; numeric IDs remain stable compatibility // identifiers and do not determine presentation order. static const GeneratorRegistry registry( - {fjordContinentDefinition(), mazeDefinition(), - contestedCommonsDefinition(), latticeDefinition(), islesDefinition(), + {fjordContinentDefinition(), shatteredCoastDefinition(), + contestedCommonsDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), - riverDefinition(), shatteredCoastDefinition(), uniformDefinition()}); + riverDefinition(), mazeDefinition(), latticeDefinition(), + uniformDefinition()}); return registry; } From b77c5c01204771e1ef13453821df807c944ec289 Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Wed, 9 Sep 2026 23:24:30 -0400 Subject: [PATCH 004/136] Make Contested Commons the first and default generator --- docs/map-generators/README.md | 2 ++ src/CustomGameSetup.h | 2 +- src/NewMapScreen.cpp | 1 + src/map/generator/core/GeneratorRegistry.cpp | 4 ++-- test/CustomGameSetupHarness.cpp | 6 ++++-- test/MapGeneratorDefaultsTest.cpp | 9 ++++++--- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index 680b19f3a..82e67b6d7 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -2,6 +2,8 @@ The generators now use a modular registry and per-attempt random state. See the [module and extension guide](ADDING_A_GENERATOR.md) and [refactor measurements and map previews](refactor/RESULTS.md). The tuning study below is the frozen PR #238 baseline. +Contested Commons appears first and is the default generator in the custom-game lobby and map editor. + Random maps now start with generator-specific recipes that leave more grassy building room. The custom-game lobby from PR #237 and the map editor use the same names, available controls, ranges, steps, defaults, and per-method setting memory. Old Random is **Shattered Coast**; Old Islands is **Rugged Archipelago**. The modular catalog also includes **Contested Commons**, **Lattice**, **Maze**, and **Fjord Continent**. ## Development tools diff --git a/src/CustomGameSetup.h b/src/CustomGameSetup.h index 900618999..271e0bd28 100644 --- a/src/CustomGameSetup.h +++ b/src/CustomGameSetup.h @@ -66,7 +66,7 @@ struct CustomGameSetup unsigned mapRevision = 0; CustomGameSetup() { - generator.setMethodDefaults(GenerationRequest::eRIVER); + generator.setMethodDefaults(GeneratorRegistry::builtins().methods(false).front()); capacity = generator.nbTeams; for (int i = 0; i < Team::MAX_COUNT; ++i) colonies[i].alliance = i; diff --git a/src/NewMapScreen.cpp b/src/NewMapScreen.cpp index d5fec744d..e3020a6b1 100644 --- a/src/NewMapScreen.cpp +++ b/src/NewMapScreen.cpp @@ -27,6 +27,7 @@ constexpr Uint32 A = ALIGN_SCREEN_CENTERED; NewMapScreen::NewMapScreen(const GeneratorRegistry ®istry) : registry(registry) { + descriptor.setMethodDefaults(registry.methods().front(), registry); methods = new List(20, 100, 280, 300, A, A, "menu"); for (int m : registry.methods()) methods->addText(tr(registry.at(m).nameKey)); diff --git a/src/map/generator/core/GeneratorRegistry.cpp b/src/map/generator/core/GeneratorRegistry.cpp index 5b22379c6..e358bad8f 100644 --- a/src/map/generator/core/GeneratorRegistry.cpp +++ b/src/map/generator/core/GeneratorRegistry.cpp @@ -72,8 +72,8 @@ const GeneratorRegistry &GeneratorRegistry::builtins() { // distinctive playable maps first; numeric IDs remain stable compatibility // identifiers and do not determine presentation order. static const GeneratorRegistry registry( - {fjordContinentDefinition(), shatteredCoastDefinition(), - contestedCommonsDefinition(), islesDefinition(), + {contestedCommonsDefinition(), fjordContinentDefinition(), + shatteredCoastDefinition(), islesDefinition(), ruggedArchipelagoDefinition(), concreteIslandsDefinition(), craterLakesDefinition(), islandsDefinition(), swampDefinition(), riverDefinition(), mazeDefinition(), latticeDefinition(), diff --git a/test/CustomGameSetupHarness.cpp b/test/CustomGameSetupHarness.cpp index 9629f5a18..fa6c47ef2 100644 --- a/test/CustomGameSetupHarness.cpp +++ b/test/CustomGameSetupHarness.cpp @@ -487,6 +487,8 @@ struct CustomGameSetupHarness screen.activateGroup(screen.groups[2]); capture("rules-640"); screen.setup.random = true; + // The slider/failure checks below exercise River terrain weights explicitly. + screen.setup.generatorHistory.select(screen.setup.generator, MapGenerationDescriptor::eRIVER); screen.invalidate(); screen.activateGroup(screen.groups[0]); capture("random-controls-640"); @@ -555,9 +557,9 @@ struct CustomGameSetupHarness // five-unit steps. auto landscape = [&](int method) { clickControl("generator/landscape"); - for (int i = 0; i < 8; ++i) + for (size_t i = 0; i < GeneratorRegistry::builtins().methods(false).size(); ++i) keyEvent(SDLK_UP); - for (int i = 1; i < method; ++i) + for (int i = 0; i < GeneratorRegistry::builtins().selectionIndex(method, false); ++i) keyEvent(SDLK_DOWN); keyEvent(SDLK_RETURN); assert(screen.setup.generator.method == method); diff --git a/test/MapGeneratorDefaultsTest.cpp b/test/MapGeneratorDefaultsTest.cpp index c6a92c4f8..d011f6b07 100644 --- a/test/MapGeneratorDefaultsTest.cpp +++ b/test/MapGeneratorDefaultsTest.cpp @@ -226,9 +226,12 @@ class MapGeneratorDefaultsTest s.gfx = globalContainer->gfx; s.dispatchInit(); CustomGameSetup lobby; - D river; - river.setMethodDefaults(D::eRIVER); - sameControls(lobby.generator, river); + D commons; + commons.setMethodDefaults(D::eCONTESTEDCOMMONS); + assert(GeneratorRegistry::builtins().methods(false).front() == D::eCONTESTEDCOMMONS); + assert(s.methods->getSelectionIndex() == 0); + sameControls(s.descriptor, commons); + sameControls(lobby.generator, commons); for (int m : GeneratorRegistry::builtins().methods()) { auto method = static_cast(m); From 1da0a158821982d6ca061cfcdc1aaa402f77dc9b Mon Sep 17 00:00:00 2001 From: Bradley Arsenault Date: Wed, 9 Sep 2026 23:36:44 -0400 Subject: [PATCH 005/136] Fix rectangular map previews and wrapped colony start coordinates --- docs/map-generators/README.md | 2 + docs/map-generators/RECTANGULAR_MAP_REVIEW.md | 78 ++++++++++++++++++ docs/map-generators/preview-tall.png | Bin 0 -> 93146 bytes docs/map-generators/preview-wide.png | Bin 0 -> 94124 bytes src/CustomGameScreen.cpp | 51 ++++-------- src/LobbyMapPreview.h | 48 +++++++++++ .../generator/core/GenerationValidation.cpp | 3 + .../generators/ShatteredCoastGenerator.cpp | 2 +- .../generator/shared/StartingPositions.cpp | 8 ++ test/CustomGameSetupHarness.cpp | 39 +++++++++ test/MapGeneratorDefaultsTest.cpp | 17 ++++ 11 files changed, 212 insertions(+), 36 deletions(-) create mode 100644 docs/map-generators/RECTANGULAR_MAP_REVIEW.md create mode 100644 docs/map-generators/preview-tall.png create mode 100644 docs/map-generators/preview-wide.png create mode 100644 src/LobbyMapPreview.h diff --git a/docs/map-generators/README.md b/docs/map-generators/README.md index 82e67b6d7..2876e77d7 100644 --- a/docs/map-generators/README.md +++ b/docs/map-generators/README.md @@ -94,3 +94,5 @@ python3 tools/plot_map_generator_study.py artifacts/map-generator-validation/val The plotting script requires NumPy and Matplotlib. Each sample runs in a fresh process; the dedicated executable overrides its clock to `1700000000 + seed` because the production generators reseed internally. It seeds Perlin noise and `srand`, and supplies the optional synchronized-RNG seed to `MapGenerator::generateMap` so PR #237’s `random_device` reseeding cannot override the study. Production UI calls omit that argument and retain random seeding. Before sampling, the runner checks three seed positions per configuration in independent repeated processes and aborts on any mismatch. Runner profiles are disposable and cleaned up automatically. UI harness profiles use explicit test names. The shared-control regression visits every selectable editor value, checks defaults and step normalization, compares lobby/editor method memory, and verifies serialization round trips. The native lobby harness exercises new dropdown/stepper controls, automatic preview debounce, snapshot ownership, generation recovery, teams, rules, saves and replays. The shared-control test is added to Linux CI alongside the existing lobby tests. Physical macOS mouse delivery is not covered by SDL event injection. + +See [the rectangular-map review](RECTANGULAR_MAP_REVIEW.md) for preview/start-coordinate fixes, rectangular regression coverage, and remaining seeded placement failures. diff --git a/docs/map-generators/RECTANGULAR_MAP_REVIEW.md b/docs/map-generators/RECTANGULAR_MAP_REVIEW.md new file mode 100644 index 000000000..bbf0883e3 --- /dev/null +++ b/docs/map-generators/RECTANGULAR_MAP_REVIEW.md @@ -0,0 +1,78 @@ +# Rectangular map review + +The lobby screenshot showing colony numbers in black space exposed a preview +coordinate bug. Map thumbnails store rectangular terrain centered in a square +128×128 image. The lobby stretched that entire thumbnail but placed colony +markers as if the terrain filled the square. + +## Fixed + +- Crop thumbnail padding and fit the actual map into the available preview area. + Wide maps use the available width; tall maps use the available height. The + frame and colony markers use those same dimensions. +- Wrap marker coordinates for existing maps whose start metadata crosses a torus + seam, rather than pinning those markers to an unrelated edge. +- Normalize generated start coordinates in the shared legacy placement paths. + Shattered Coast seed 31001 at 512×128 previously reported colony 2 at `(474,-6)` + even though the building wraps onto the map. Its start is now `(474,122)`. + Shattered Coast's generator revision is incremented to 2. +- Validate that generated colony start metadata lies inside the map bounds. + +The changes do not alter save/replay encodings or stretch the generated terrain. + +## Validation + +- All twelve playable generators successfully generate the 512×128 and 128×512 + regression cases (seed 31001, four colonies), with starts inside map bounds. +- Native lobby captures at 640×480 and 1000×700 cover 512×128, 128×512, 512×64, + and 64×512 Contested Commons maps. Tests inspect rendered terrain and colored + marker pixels, including equivalent start coordinates outside the base torus + rectangle. They also check the frame's aspect ratio. +- Existing generator contracts, request validation, editor/lobby control tests, + preview ownership, failure recovery and team-preservation checks pass. + +| Wide map | Tall map | +| --- | --- | +| ![512×128 preview](preview-wide.png) | ![128×512 preview](preview-tall.png) | + +## Remaining placement limitations + +A deterministic sample exercised all sixteen combinations of 64, 128, 256 and +512 tiles per axis, with seeds 31001–31005 and four colonies: 960 attempts. There +were 51 reported placement failures both before and after these fixes. These are +failed generation attempts, not missing strips of terrain in successful maps. +This sample does not establish balance or a universal success rate. + +Each generator has 60 rectangular and 20 square attempts: + +| Generator | Rectangular failures | Square failures | Observed failure | +| --- | ---: | ---: | --- | +| River | 3 | 1 | Cannot place starting locations on some small maps | +| Separate Islands | 8 | 1 | Resource/starting-region subdivision cannot place every colony | +| Shattered Coast | 11 | 3 | Cannot space every colony on grass | +| Contested Commons | 1 | 6 | Small home regions; all five 512×512 attempts exhaust the point-search budget | +| Fjord Continent | 15 | 2 | No swarm footprint fits in a home region, mostly with a 64-tile short axis | +| Swamp, Islands, Crater Lakes, Isles, Rugged Archipelago, Lattice, Maze | 0 | 0 | No failures in this sample | + +Fjord Continent sizes its continent from the shorter map axis, so making a +64×64 map longer does not automatically give each home region more room. Its +narrow-map failures need layout tuning; removing the preview padding does not +solve them. Likewise, the large Contested Commons budget failure is a separate +scaling issue, not a rectangular-coordinate error. + +Reproduce representative cases from the repository root: + +```sh +scons release=1 -j8 map-generator-study map-generator-defaults-test custom-setup-test +build/src/MapGeneratorDefaultsTest glob2-rectangular-test +mkdir -p artifacts/rectangular-maps +build/src/CustomGameSetupHarness artifacts/rectangular-maps +build/src/MapGeneratorStudy 9 31001 glob2-rectangular-study width=9 height=7 teams=4 +build/src/MapGeneratorStudy 12 31003 glob2-rectangular-study width=6 height=9 teams=4 +build/src/MapGeneratorStudy 9 31001 glob2-rectangular-study width=9 height=9 teams=4 +``` + +`MapGeneratorStudy` reports generation status in its `STUDY` row and prints a +diagnostic for a failed attempt; its process exit status alone is not a +success check. The full sample iterates method IDs 1–12, width/height exponents +6–9, and seeds 31001–31005 using the same command. diff --git a/docs/map-generators/preview-tall.png b/docs/map-generators/preview-tall.png new file mode 100644 index 0000000000000000000000000000000000000000..aa844504b517de9cc0a3d5cbf2b5cea04e41c9b6 GIT binary patch literal 93146 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≶) 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 n5|0O~Wy*H>`rj!d(C7aH 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 UFnqx!7Ta{%_H*4v{z$AChbVRSFxftOE!mxtq- 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